[PATCH 4/6] automation/qtb: add unit tests for the QTB framework

Baptiste Le Duc <[email protected]>
Newsgroups gmane.comp.emulators.xen.devel
Message-ID <1786378214.8631fc262581453bbf619ec5b2062170.19fec705b96000e099@vates.tech>
The QTB framework is meant to be extended: new test types, new machines and
new device trees aim to be added by other people.

Add pytest coverage of the framework's functions, and of the console-test
type's config validation and expect/retry loop, so that such changes get
immediate feedback and existing behaviour does not silently regress.

The suite covers 100% of the framework's statements, so a new code path
added without a test shows up as a coverage drop.

The tests are meant to be run locally, from the Xen tree root:

    python3 -m pytest automation/scripts/qtb/riscv/unit/

and, with pytest-cov installed, the coverage report is:

    python3 -m pytest --cov=automation/scripts/qtb \
        automation/scripts/qtb/riscv/unit/

The tests drive fakes rather than QEMU, so they need no artifacts to run.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Baptiste Le Duc <[email protected]>
---
 automation/scripts/qtb/riscv/unit/__init__.py |   2 +
 automation/scripts/qtb/riscv/unit/conftest.py |  42 ++++
 .../scripts/qtb/riscv/unit/test_config.py     | 128 +++++++++++
 .../qtb/riscv/unit/test_console_test.py       | 217 ++++++++++++++++++
 automation/scripts/qtb/riscv/unit/test_dt.py  |  99 ++++++++
 .../scripts/qtb/riscv/unit/test_machine.py    |  58 +++++
 .../scripts/qtb/riscv/unit/test_temp_dir.py   |  42 ++++
 .../scripts/qtb/riscv/unit/test_xen_dt.py     |  46 ++++
 8 files changed, 634 insertions(+)
 create mode 100644 automation/scripts/qtb/riscv/unit/__init__.py
 create mode 100644 automation/scripts/qtb/riscv/unit/conftest.py
 create mode 100644 automation/scripts/qtb/riscv/unit/test_config.py
 create mode 100644 automation/scripts/qtb/riscv/unit/test_console_test.py
 create mode 100644 automation/scripts/qtb/riscv/unit/test_dt.py
 create mode 100644 automation/scripts/qtb/riscv/unit/test_machine.py
 create mode 100644 automation/scripts/qtb/riscv/unit/test_temp_dir.py
 create mode 100644 automation/scripts/qtb/riscv/unit/test_xen_dt.py

diff --git a/automation/scripts/qtb/riscv/unit/__init__.py b/automation/scripts/qtb/riscv/unit/__init__.py
new file mode 100644
index 0000000000..b234dc5303
--- /dev/null
+++ b/automation/scripts/qtb/riscv/unit/__init__.py
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+"""pytest tests of the framework's own logic."""
diff --git a/automation/scripts/qtb/riscv/unit/conftest.py b/automation/scripts/qtb/riscv/unit/conftest.py
new file mode 100644
index 0000000000..576eaf13b1
--- /dev/null
+++ b/automation/scripts/qtb/riscv/unit/conftest.py
@@ -0,0 +1,42 @@
+# SPDX-License-Identifier: GPL-2.0-only
+"""Shared pytest fixtures for the qtb unit tests."""
+
+from __future__ import annotations
+
+import pytest
+
+from ..config import MachineConfig
+
+
[email protected]
+def make_file(tmp_path):
+    """Return a factory creating a file of `size` bytes, yielding its path."""
+
+    def _make(name: str, size: int = 16) -> str:
+        p = tmp_path / name
+        p.write_bytes(b"\0" * size)
+        return str(p)
+
+    return _make
+
+
[email protected]
+def make_machine():
+    """Return a factory building a MachineConfig for tests."""
+
+    def _make(
+        *,
+        name="m",
+        binaries=None,
+        mmu="sv48",
+        xen_bootargs="",
+    ) -> MachineConfig:
+        return MachineConfig(
+            name=name,
+            pcpu=4,
+            binaries=binaries,
+            mmu_type=mmu,
+            xen_bootargs=xen_bootargs,
+        )
+
+    return _make
diff --git a/automation/scripts/qtb/riscv/unit/test_config.py b/automation/scripts/qtb/riscv/unit/test_config.py
new file mode 100644
index 0000000000..790328a13f
--- /dev/null
+++ b/automation/scripts/qtb/riscv/unit/test_config.py
@@ -0,0 +1,128 @@
+# SPDX-License-Identifier: GPL-2.0-only
+"""Unit tests for the YAML machine-catalog parser."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+import yaml
+
+from ..config import (
+    XEN_BOOTARGS_DEFAULT,
+    XEN_MMU_TYPE_DEFAULT,
+    MachineConfig,
+    _parse_binaries,
+)
+
+
[email protected]
+def binaries(make_file):
+    """Raw binaries dict pointing at three existing files."""
+    return {
+        "qemu": make_file("qemu-system-riscv64"),
+        "firmware": make_file("fw.bin"),
+        "xen": make_file("xen"),
+    }
+
+
+# ---- _parse_binaries ----
+
+
+def test_parse_binaries_resolves_existing_paths(binaries):
+    cfg = _parse_binaries(binaries)
+    assert cfg.qemu == Path(binaries["qemu"])
+    assert cfg.firmware == Path(binaries["firmware"])
+    assert cfg.xen == Path(binaries["xen"])
+
+
+def test_parse_binaries_missing_key_raises(binaries):
+    del binaries["xen"]
+    with pytest.raises(ValueError, match="missing keys"):
+        _parse_binaries(binaries)
+
+
+def test_parse_binaries_missing_file_raises(binaries, tmp_path):
+    binaries["xen"] = str(tmp_path / "absent")
+    with pytest.raises(FileNotFoundError):
+        _parse_binaries(binaries)
+
+
+# ---- MachineConfig.from_config ----
+
+
+def _write_yaml(tmp_path, binaries, name="machine-a", **machine_overrides):
+    machine = {
+        "pcpu": 1,
+        "xen_bootargs": "com1=poll sched=null",
+    }
+    machine.update(machine_overrides)
+    doc = {"binaries": binaries, "machines": {name: machine}}
+    path = tmp_path / "config.yaml"
+    path.write_text(yaml.safe_dump(doc))
+    return str(path)
+
+
+def test_from_config_builds_machineconfig(tmp_path, binaries):
+    path = _write_yaml(tmp_path, binaries)
+
+    mc = MachineConfig.from_config(path, "machine-a")
+
+    assert mc.name == "machine-a"
+    assert mc.pcpu == 1
+    assert mc.xen_bootargs == "com1=poll sched=null"
+    assert mc.binaries.qemu == Path(binaries["qemu"])
+
+
+def test_from_config_applies_optional_defaults(tmp_path, binaries):
+    # A machine with only the required keys falls back to the module defaults.
+    path = _write_yaml(
+        tmp_path,
+        binaries,
+        name="bare",
+        mmu_type=None,
+        xen_bootargs=None,
+    )
+    # Drop the keys set to None so the parser sees them as absent.
+    doc = yaml.safe_load(Path(path).read_text())
+    for k in ("mmu_type", "xen_bootargs"):
+        doc["machines"]["bare"].pop(k, None)
+    Path(path).write_text(yaml.safe_dump(doc))
+
+    mc = MachineConfig.from_config(path, "bare")
+
+    assert mc.mmu_type == XEN_MMU_TYPE_DEFAULT
+    assert mc.xen_bootargs == XEN_BOOTARGS_DEFAULT
+
+
+def test_from_config_missing_machine_key_raises(tmp_path, binaries):
+    path = _write_yaml(tmp_path, binaries, name="bare")
+    doc = yaml.safe_load(Path(path).read_text())
+    del doc["machines"]["bare"]["pcpu"]
+    Path(path).write_text(yaml.safe_dump(doc))
+
+    with pytest.raises(ValueError, match="machine config missing keys"):
+        MachineConfig.from_config(path, "bare")
+
+
+def test_from_config_missing_top_level_key_raises(tmp_path, binaries):
+    path = _write_yaml(tmp_path, binaries)
+    doc = yaml.safe_load(Path(path).read_text())
+    del doc["binaries"]
+    Path(path).write_text(yaml.safe_dump(doc))
+
+    with pytest.raises(ValueError, match="missing keys: \\['binaries'\\]"):
+        MachineConfig.from_config(path, "machine-a")
+
+
+def test_from_config_unknown_machine_raises(tmp_path, binaries):
+    path = _write_yaml(tmp_path, binaries)
+    with pytest.raises(ValueError, match="unknown machine 'nope'"):
+        MachineConfig.from_config(path, "nope")
+
+
+def test_from_config_missing_binary_raises(tmp_path, binaries):
+    binaries["qemu"] = str(tmp_path / "gone")
+    path = _write_yaml(tmp_path, binaries)
+    with pytest.raises(FileNotFoundError):
+        MachineConfig.from_config(path, "machine-a")
diff --git a/automation/scripts/qtb/riscv/unit/test_console_test.py b/automation/scripts/qtb/riscv/unit/test_console_test.py
new file mode 100644
index 0000000000..cf15ed3b1b
--- /dev/null
+++ b/automation/scripts/qtb/riscv/unit/test_console_test.py
@@ -0,0 +1,217 @@
+# SPDX-License-Identifier: GPL-2.0-only
+"""Unit tests for the console-test test type (console_test.py)."""
+
+from __future__ import annotations
+
+from itertools import chain, repeat
+from unittest import mock
+
+import pexpect
+import pytest
+
+from ..console_test.console_test import ConsoleTest
+from ..config import MachineConfig
+
+
+# ---- helpers ----
+
+
+def _parse_test_data(machine, expect, name="dummy"):
+    """Validate `expect` against `machine`, without reading a catalog file."""
+    test_data = {"machine": "box", "expect": expect}
+    with mock.patch.object(MachineConfig, "from_config", return_value=machine):
+        return ConsoleTest._parse_test_data("config.yaml", test_data, name)
+
+
+def _console(fail_times: int = 0, matches: int = 1) -> mock.Mock:
+    """Stand in for a pexpect spawn: `fail_times` timeouts, then `matches` hits.
+
+    Every wait past `matches` times out, so a test that waits more often than it
+    should fails instead of silently passing.
+    """
+    cons = mock.Mock()
+    cons.expect_exact.side_effect = chain(
+        [pexpect.TIMEOUT("nope")] * fail_times,
+        [None] * matches,
+        repeat(pexpect.TIMEOUT("nope")),
+    )
+    return cons
+
+
+def _asked(cons: mock.Mock) -> list[str]:
+    """The strings waited for on `cons`, one entry per attempt (matched or not)."""
+    return [call.args[0] for call in cons.expect_exact.call_args_list]
+
+
+def _test(expect, machine, **opts):
+    """Build a ConsoleTest bound to `machine`, skipping the YAML read."""
+    raw = {
+        "machine_catalog": "config.yaml",
+        "tests": {"dummy": {"machine": "box", "expect": expect, **opts}},
+    }
+    with mock.patch.object(MachineConfig, "from_config", return_value=machine):
+        return ConsoleTest(raw, "dummy")
+
+
+# ---- _parse_test_data ----
+
+
+def test_parse_test_data_accepts_a_list_of_strings(make_machine):
+    machine = make_machine()
+    name, data, got = _parse_test_data(machine, {0: ["Hello", "All set up"]})
+    assert (name, got) == ("dummy", machine)
+    assert data["expect"] == {0: ["Hello", "All set up"]}
+
+
+def test_parse_test_data_bare_string_raises(make_machine):
+    # A bare string is refused, not wrapped: the YAML must spell out the list.
+    machine = make_machine()
+    with pytest.raises(ValueError, match="expects a list of string"):
+        _parse_test_data(machine, {0: "All set up"})
+
+
[email protected](
+    "expect",
+    [
+        {-1: ["All set up"]},  # console index below Xen's
+        {1: ["All set up"]},  # console index above Xen's
+        {0: ["All set up"], 1: ["More"]},  # Xen's + another unknown
+        ["All set up"],  # no console index
+        None,
+        "All set up",  # not a map at all
+    ],
+)
+def test_parse_test_data_not_the_xen_console_map_raises(expect, make_machine):
+    machine = make_machine()
+    with pytest.raises(ValueError, match="must map console index 0"):
+        _parse_test_data(machine, expect)
+
+
+def test_parse_test_data_empty_list_raises(make_machine):
+    machine = make_machine()
+    with pytest.raises(ValueError, match="Xen has no expected string"):
+        _parse_test_data(machine, {0: []})
+
+
+def test_parse_test_data_empty_string_in_list_raises(make_machine):
+    machine = make_machine()
+    with pytest.raises(ValueError, match="Xen expects non-empty strings"):
+        _parse_test_data(machine, {0: ["All set up", ""]})
+
+
+def test_parse_test_data_missing_expect_raises():
+    with pytest.raises(ValueError, match="missing keys"):
+        ConsoleTest._parse_test_data("config.yaml", {"machine": "box"}, "dummy")
+
+
+# ---- _parse_test_cfg ----
+
+
+def test_parse_test_cfg_missing_machine_catalog_raises():
+    with pytest.raises(ValueError, match="missing keys"):
+        ConsoleTest._parse_test_cfg({"tests": {}}, "dummy")
+
+
+def test_parse_test_cfg_unknown_test_raises():
+    raw = {"machine_catalog": "config.yaml", "tests": {"a": {}}}
+    with pytest.raises(ValueError, match="unknown test 'dummy'"):
+        ConsoleTest._parse_test_cfg(raw, "dummy")
+
+
+# ---- __init__ ----
+
+
+def test_timeout_and_attempts_are_read(make_machine):
+    test = _test({0: ["All set up"]}, make_machine(), timeout=7, attempts=2)
+    assert (test.timeout, test.attempts) == (7, 2)
+
+
+def test_timeout_below_one_raises(make_machine):
+    with pytest.raises(ValueError, match="timeout < 1"):
+        _test({0: ["All set up"]}, make_machine(), timeout=0)
+
+
+def test_attempts_below_one_raises(make_machine):
+    with pytest.raises(ValueError, match="attempts < 1"):
+        _test({0: ["All set up"]}, make_machine(), attempts=0)
+
+
+# ---- run ----
+
+
+def test_run_expects_each_string_in_order_on_con0(make_machine):
+    test = _test({0: ["first", "then"]}, make_machine())
+    vm = mock.Mock(console=_console(matches=2))
+
+    test.run(vm)
+
+    assert _asked(vm.console) == ["first", "then"]
+
+
+def test_run_xen_console_not_wired_raises(make_machine):
+    test = _test({0: ["All set up"]}, make_machine())
+    vm = mock.Mock(console=None)
+
+    with pytest.raises(RuntimeError, match="not launched"):
+        test.run(vm)
+
+
+# ---- _expect_string ----
+
+
+def test_expect_string_retries_after_a_timeout(make_machine):
+    test = _test({0: ["All set up"]}, make_machine(), attempts=3)
+    cons = _console(fail_times=2)
+
+    test._expect_string(cons, "All set up")
+
+    assert _asked(cons) == ["All set up"] * 3
+
+
+def test_expect_string_raises_once_attempts_are_spent(make_machine):
+    test = _test({0: ["All set up"]}, make_machine(), attempts=2)
+    cons = _console(matches=0)
+
+    with pytest.raises(pexpect.TIMEOUT):
+        test._expect_string(cons, "All set up")
+
+    assert _asked(cons) == ["All set up"] * 2
+
+
+# ---- config IO ----
+
+
+def test_list_tests_reads_the_type_yaml():
+    names = ConsoleTest.list_tests("console_test/console-test.yaml")
+    assert "dom0less-1smp-0domu-1vcpu-aplic-imsic-null" in names
+
+
+def test_list_tests_without_a_tests_key_returns_empty():
+    with mock.patch.object(ConsoleTest, "_load_yaml", return_value={}):
+        assert ConsoleTest.list_tests("console-test.yaml") == []
+
+
+def test_from_config_builds_instance(make_machine):
+    machine = make_machine()
+
+    raw = {
+        "machine_catalog": "config.yaml",
+        "tests": {"dummy": {"machine": "box", "expect": {0: ["All set up"]}}},
+    }
+    # Mock the YAML read and the catalog lookup: only the build logic is under test.
+    with (
+        mock.patch.object(ConsoleTest, "_load_yaml", return_value=raw),
+        mock.patch.object(MachineConfig, "from_config", return_value=machine),
+    ):
+        test = ConsoleTest.from_config("console-test.yaml", "dummy")
+
+    assert test.name == "dummy"
+    assert test.type_id == "console-test"
+    assert test.machine is machine
+    assert test.expect == {0: ["All set up"]}
+
+
+def test_from_config_unknown_test_raises():
+    # Also pins from_config's argument order (config file, then test name).
+    with pytest.raises(ValueError, match="unknown test 'nope'"):
+        ConsoleTest.from_config("console_test/console-test.yaml", "nope")
diff --git a/automation/scripts/qtb/riscv/unit/test_dt.py b/automation/scripts/qtb/riscv/unit/test_dt.py
new file mode 100644
index 0000000000..2201a8651f
--- /dev/null
+++ b/automation/scripts/qtb/riscv/unit/test_dt.py
@@ -0,0 +1,99 @@
+# SPDX-License-Identifier: GPL-2.0-only
+"""Unit tests for the device-tree compile path (dt.py)."""
+
+from __future__ import annotations
+
+import shutil
+from unittest import mock
+
+import pytest
+
+from .. import dt
+
+_HAS_DTC = shutil.which("dtc") is not None
+_MINIMAL_DTS = "/dts-v1/;\n/ { };\n"
+
+
+# ---- _compile_dts (dtc wrapper) ----
+
+
+def test_compile_dts_invokes_dtc(tmp_path):
+    src = tmp_path / "in.dts"
+    src.write_text(_MINIMAL_DTS)
+    dtb = tmp_path / "in.dtb"
+
+    with mock.patch.object(dt.subprocess, "run") as run:
+        dt._compile_dts(src, dtb)
+
+    run.assert_called_once()
+    argv = run.call_args.args[0]
+    assert argv == ["dtc", "-I", "dts", "-O", "dtb", "-o", str(dtb), str(src)]
+    assert run.call_args.kwargs["check"] is True
+    assert run.call_args.kwargs["capture_output"] is True
+
+
+def test_compile_dts_missing_dtc_raises_runtimeerror(tmp_path):
+    src, dtb = tmp_path / "a.dts", tmp_path / "a.dtb"
+    src.write_text(_MINIMAL_DTS)
+
+    with mock.patch.object(dt.subprocess, "run", side_effect=FileNotFoundError):
+        with pytest.raises(RuntimeError, match="dtc not found"):
+            dt._compile_dts(src, dtb)
+
+
+def test_compile_dts_dtc_failure_raises_runtimeerror(tmp_path):
+    src, dtb = tmp_path / "a.dts", tmp_path / "a.dtb"
+    src.write_text(_MINIMAL_DTS)
+    err = dt.subprocess.CalledProcessError(1, "dtc", output="out", stderr="syntax error")
+
+    with mock.patch.object(dt.subprocess, "run", side_effect=err):
+        with pytest.raises(RuntimeError, match="syntax error"):
+            dt._compile_dts(src, dtb)
+
+
+# ---- compile_to_dtb path handling ----
+
+
+def test_compile_to_dtb_missing_source_raises_filenotfound(tmp_path):
+    with pytest.raises(FileNotFoundError, match="not found"):
+        dt.compile_to_dtb(tmp_path / "nope.dts", tmp_path)
+
+
+def test_compile_to_dtb_missing_out_dir_raises_filenotfound(tmp_path):
+    src = tmp_path / "a.dts"
+    src.write_text(_MINIMAL_DTS)
+
+    with pytest.raises(FileNotFoundError, match="doesn't exist"):
+        dt.compile_to_dtb(src, tmp_path / "absent")
+
+
+def test_compile_to_dtb_source_goes_to_out_dir_with_dtb_suffix(tmp_path):
+    src = tmp_path / "host-1smp.dts"
+    src.write_text(_MINIMAL_DTS)
+    out = tmp_path / "binaries"
+    out.mkdir()
+
+    with mock.patch.object(dt, "_compile_dts") as compile_mock:
+        result = dt.compile_to_dtb(src, out)
+
+    assert result == out / "host-1smp.dtb"
+    compile_mock.assert_called_once_with(src, out / "host-1smp.dtb")
+
+
+# ---- write_dts ----
+
+
+def test_write_dts_writes_source_under_out_dir(tmp_path):
+    src = dt.write_dts(_MINIMAL_DTS, name="unit", out_dir=tmp_path)
+
+    assert src == tmp_path / "unit.dts"
+    assert src.read_text() == _MINIMAL_DTS
+
+
[email protected](not _HAS_DTC, reason="dtc not installed")
+def test_write_then_compile_produces_dtb(tmp_path):
+    src = dt.write_dts(_MINIMAL_DTS, name="unit", out_dir=tmp_path)
+    dtb = dt.compile_to_dtb(src, tmp_path)
+
+    assert dtb.is_file()
+    assert dtb.suffix == ".dtb"
diff --git a/automation/scripts/qtb/riscv/unit/test_machine.py b/automation/scripts/qtb/riscv/unit/test_machine.py
new file mode 100644
index 0000000000..0a90a5545b
--- /dev/null
+++ b/automation/scripts/qtb/riscv/unit/test_machine.py
@@ -0,0 +1,58 @@
+# SPDX-License-Identifier: GPL-2.0-only
+"""Unit tests for RiscvTestMachine QEMU argument assembly."""
+
+from __future__ import annotations
+
+from unittest import mock
+
+from ..machine import RiscvTestMachine
+from ..config import MACHINE_MEMORY, BinariesConfig
+
+
+def _binaries(tmp_path):
+    paths = {}
+    for name in ("qemu", "firmware", "xen"):
+        p = tmp_path / name
+        p.write_bytes(b"")
+        paths[name] = str(p)
+    return BinariesConfig(**paths)
+
+
+def _machine(mc) -> RiscvTestMachine:
+    """Build the machine with QtbMachine.__init__ stubbed out (it spawns QEMU)."""
+    with mock.patch("qemu.qtb.QtbMachine.__init__", return_value=None):
+        return RiscvTestMachine(mc, timeout=30)
+
+
+def test_init_forwards_cpus_to_qtbmachine(tmp_path, make_machine):
+    mc = make_machine(name="machine", binaries=_binaries(tmp_path))
+
+    with mock.patch("qemu.qtb.QtbMachine.__init__", return_value=None) as base_init:
+        vm = RiscvTestMachine(mc, timeout=30, log_dir="/logs")
+
+    assert vm.machine_conf is mc
+    assert base_init.call_args.kwargs == {
+        "memory": MACHINE_MEMORY,
+        "cpus": mc.pcpu,
+        "mirror_console": False,
+        "timeout": 30,
+        "log_dir": "/logs",
+    }
+
+
+def test_resolve_binary_is_the_configured_qemu(tmp_path, make_machine):
+    mc = make_machine(name="machine", binaries=_binaries(tmp_path))
+
+    assert _machine(mc)._resolve_binary() == str(mc.binaries.qemu)
+
+
+def test_machine_args_wires_firmware_kernel_and_dtb(tmp_path, make_machine):
+    mc = make_machine(name="machine", binaries=_binaries(tmp_path))
+
+    args = list(_machine(mc)._machine_args(memory=2048, cpus=4))
+
+    assert args[args.index("-bios") + 1] == str(mc.binaries.firmware)
+    assert args[args.index("-kernel") + 1] == str(mc.binaries.xen)
+    assert args[args.index("-dtb") + 1] == str(mc.dt.dtb)
+    assert args[args.index("-m") + 1] == "2048"
+    assert args[args.index("-smp") + 1] == "4"
diff --git a/automation/scripts/qtb/riscv/unit/test_temp_dir.py b/automation/scripts/qtb/riscv/unit/test_temp_dir.py
new file mode 100644
index 0000000000..d595d824d7
--- /dev/null
+++ b/automation/scripts/qtb/riscv/unit/test_temp_dir.py
@@ -0,0 +1,42 @@
+# SPDX-License-Identifier: GPL-2.0-only
+"""Unit tests for the temp_dir scratch-directory singleton."""
+
+from __future__ import annotations
+
+import pytest
+
+from .. import paths
+
+
[email protected](autouse=True)
+def _reset_singleton():
+    # Force each test to begin with fresh temp dir
+    yield
+    paths.cleanup_temp_dir()
+
+
+def test_temp_dir_exists_and_prefixed():
+    d = paths.temp_dir()
+    assert d.is_dir()
+    assert d.name.startswith("qtb-")
+
+
+def test_temp_dir_is_singleton():
+    assert paths.temp_dir() == paths.temp_dir()
+
+
+def test_temp_dir_handle_kept_alive():
+    h = paths._temp_dir_handle()
+    assert h is paths._temp_dir_handle()
+    assert h.name == str(paths.temp_dir())
+
+
+def test_cleanup_temp_dir_removes_and_resets():
+    d = paths.temp_dir()
+    assert d.is_dir()
+    paths.cleanup_temp_dir()
+    assert not d.exists()
+    # Cache reset: next call builds a fresh, existing dir, not the gone one.
+    fresh = paths.temp_dir()
+    assert fresh.is_dir()
+    assert fresh != d
diff --git a/automation/scripts/qtb/riscv/unit/test_xen_dt.py b/automation/scripts/qtb/riscv/unit/test_xen_dt.py
new file mode 100644
index 0000000000..37c4055b31
--- /dev/null
+++ b/automation/scripts/qtb/riscv/unit/test_xen_dt.py
@@ -0,0 +1,46 @@
+# SPDX-License-Identifier: GPL-2.0-only
+"""Unit tests for xen_dt device-tree generation."""
+
+from __future__ import annotations
+
+from unittest import mock
+
+from .. import xen_dt
+
+
+# ---- _render_xen_dts ----
+
+
+def test_render_injects_bootargs(make_machine):
+    machine = make_machine(xen_bootargs="com1=poll sched=null")
+    out = xen_dt._render_xen_dts(machine)
+
+    assert 'xen,xen-bootargs = "com1=poll sched=null";' in out
+
+
+def test_render_injects_xen_mmu_type(make_machine):
+    machine = make_machine(mmu="sv39")
+    out = xen_dt._render_xen_dts(machine)
+
+    assert 'mmu-type = "riscv,sv39";' in out
+
+
+# ---- build_xen_device_tree ----
+
+
+def test_build_xen_device_tree_compiles(tmp_path, make_machine):
+    machine = make_machine(name="unit-test")
+    dts = tmp_path / "unit-test.dts"
+    dtb = tmp_path / "unit-test.dtb"
+    with (
+        mock.patch.object(xen_dt, "temp_dir", return_value=tmp_path),
+        mock.patch.object(xen_dt, "write_dts", return_value=dts) as write_dts,
+        mock.patch.object(xen_dt, "compile_to_dtb", return_value=dtb) as compile_to_dtb,
+    ):
+        result = xen_dt.build_xen_device_tree(machine)
+
+    write_dts.assert_called_once()
+    assert write_dts.call_args.args[1] == machine.name
+    compile_to_dtb.assert_called_once_with(dts, tmp_path)
+    assert result.dts == dts
+    assert result.dtb == dtb


-- 
Baptiste Le Duc | Vates Hypervisor & Kernel Engineer

XCP-ng & Xen Orchestra - Vates solutions

web: https://vates.tech
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.