blob: e62b02b5ed9969ed58ad0e905b46e867fc5d7f0f [file]
############################################################################
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################
import os
import textwrap
import threading
import time
from unittest.mock import MagicMock, patch
from xml.etree import ElementTree
import pytest
from ntfc.multi import (
ManifestConfig,
MultiOptions,
MultiSessionRunner,
SessionConfig,
SessionResult,
)
def _write_yaml(tmp: str, name: str, content: str) -> str:
"""Write a YAML file in tmp dir and return its path."""
path = os.path.join(tmp, name)
with open(path, "w") as f:
f.write(textwrap.dedent(content))
return path
def _write_config_yaml(tmp: str, name: str = "config.yaml") -> str:
"""Write a minimal NTFC config YAML and return its path."""
return _write_yaml(
tmp,
name,
"""\
config:
cwd: './external'
build_dir: './build'
product:
name: "test-product"
cores:
core0:
name: 'main'
device: 'sim'
""",
)
def _write_manifest(
tmp: str,
confpath: str,
testpath: str,
extra: str = "",
) -> str:
"""Write a valid manifest YAML referencing given paths."""
return _write_yaml(
tmp,
"manifest.yaml",
f"""\
options:
fail_fast: false
parallel: false
sessions:
- name: "session-a"
confpath: "{confpath}"
testpath: "{testpath}"
{extra}
""",
)
def _make_runner(
manifest: ManifestConfig,
rebuild: bool = False,
) -> MultiSessionRunner:
"""Create a MultiSessionRunner with common defaults."""
return MultiSessionRunner(
manifest, rebuild=rebuild, verbose=False, debug=False
)
def _make_junit_xml(path: str, suite_name: str, tests: int) -> None:
"""Create a minimal JUnit XML file."""
root = ElementTree.Element("testsuites")
suite = ElementTree.SubElement(root, "testsuite")
suite.set("name", suite_name)
suite.set("tests", str(tests))
suite.set("failures", "0")
suite.set("errors", "0")
suite.set("skipped", "0")
suite.set("time", "1.0")
for i in range(tests):
tc = ElementTree.SubElement(suite, "testcase")
tc.set("name", f"test_{i}")
tc.set("classname", f"{suite_name}::TestClass")
tc.set("time", "0.5")
tree = ElementTree.ElementTree(root)
tree.write(path)
def _write_report_xml(result_dir: str, xml: str) -> None:
"""Write a report.xml file in result_dir."""
os.makedirs(result_dir, exist_ok=True)
with open(os.path.join(result_dir, "report.xml"), "w") as f:
f.write(xml)
def test_session_config_defaults():
sc = SessionConfig(name="x", confpath="a", testpath="b")
assert sc.resources == []
assert sc.exitonfail is None
assert sc.loops is None
assert sc.timeout is None
assert sc.timeout_session is None
assert sc.modules is None
def test_multi_options_defaults():
mo = MultiOptions()
assert mo.fail_fast is False
assert mo.parallel is False
def test_session_result_fields():
sr = SessionResult(name="s", exit_code=0, result_dir="/tmp")
assert sr.name == "s"
assert sr.exit_code == 0
assert sr.result_dir == "/tmp"
def test_load_valid(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
manifest_path = _write_manifest(tmp, confpath, tmp)
mc = ManifestConfig.load(manifest_path)
assert mc.options.fail_fast is False
assert mc.options.parallel is False
assert len(mc.sessions) == 1
assert mc.sessions[0].name == "session-a"
assert mc.sessions[0].confpath == confpath
assert mc.sessions[0].testpath == tmp
def test_load_not_mapping(tmp_path):
path = _write_yaml(str(tmp_path), "bad.yaml", "- list\n- item\n")
with pytest.raises(ValueError, match="must be a YAML mapping"):
ManifestConfig.load(path)
def test_load_bad_options(tmp_path):
path = _write_yaml(
str(tmp_path),
"bad.yaml",
"""\
options: "string"
sessions:
- name: x
confpath: y
testpath: z
""",
)
with pytest.raises(ValueError, match="'options' must be a mapping"):
ManifestConfig.load(path)
def test_load_empty_sessions(tmp_path):
path = _write_yaml(str(tmp_path), "bad.yaml", "sessions: []\n")
with pytest.raises(ValueError, match="non-empty list"):
ManifestConfig.load(path)
def test_load_sessions_not_list(tmp_path):
path = _write_yaml(str(tmp_path), "bad.yaml", 'sessions: "string"\n')
with pytest.raises(ValueError, match="non-empty list"):
ManifestConfig.load(path)
def test_load_session_not_mapping(tmp_path):
path = _write_yaml(
str(tmp_path),
"bad.yaml",
"""\
sessions:
- "just a string"
""",
)
with pytest.raises(ValueError, match="session 0 must be a mapping"):
ManifestConfig.load(path)
def test_load_session_missing_name(tmp_path):
path = _write_yaml(
str(tmp_path),
"bad.yaml",
"""\
sessions:
- confpath: x
testpath: y
""",
)
with pytest.raises(ValueError, match="'name' is required"):
ManifestConfig.load(path)
def test_load_duplicate_name(tmp_path):
path = _write_yaml(
str(tmp_path),
"bad.yaml",
"""\
sessions:
- name: dup
confpath: x
testpath: y
- name: dup
confpath: a
testpath: b
""",
)
with pytest.raises(ValueError, match="duplicate session name"):
ManifestConfig.load(path)
def test_load_session_missing_confpath(tmp_path):
path = _write_yaml(
str(tmp_path),
"bad.yaml",
"""\
sessions:
- name: x
testpath: y
""",
)
with pytest.raises(ValueError, match="'confpath' is required"):
ManifestConfig.load(path)
def test_load_session_missing_testpath(tmp_path):
path = _write_yaml(
str(tmp_path),
"bad.yaml",
"""\
sessions:
- name: x
confpath: y
""",
)
with pytest.raises(ValueError, match="'testpath' is required"):
ManifestConfig.load(path)
def test_load_session_bad_resources(tmp_path):
path = _write_yaml(
str(tmp_path),
"bad.yaml",
"""\
sessions:
- name: x
confpath: y
testpath: z
resources: "string"
""",
)
with pytest.raises(ValueError, match="'resources' must be a list"):
ManifestConfig.load(path)
def test_load_with_resources_and_overrides(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
path = _write_yaml(
tmp,
"manifest.yaml",
f"""\
sessions:
- name: s1
confpath: "{confpath}"
testpath: "{tmp}"
resources: [vcan0, tap0]
exitonfail: true
loops: 3
timeout: 300
timeout_session: 7200
modules: "nsh"
""",
)
mc = ManifestConfig.load(path)
s = mc.sessions[0]
assert s.resources == ["vcan0", "tap0"]
assert s.exitonfail is True
assert s.loops == 3
assert s.timeout == 300
assert s.timeout_session == 7200
assert s.modules == "nsh"
def test_load_options_defaults(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
path = _write_yaml(
tmp,
"manifest.yaml",
f"""\
sessions:
- name: s1
confpath: "{confpath}"
testpath: "{tmp}"
""",
)
mc = ManifestConfig.load(path)
assert mc.options.fail_fast is False
assert mc.options.parallel is False
def test_resolve_session_timeout_overrides(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[
SessionConfig(
name="s1",
confpath=confpath,
testpath=tmp,
loops=5,
timeout=999,
timeout_session=8888,
)
],
)
conf = mc.resolve_session_config(mc.sessions[0])
assert conf["config"]["loops"] == 5
assert conf["config"]["timeout"] == 999
assert conf["config"]["timeout_session"] == 8888
def test_resolve_no_overrides(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
conf = mc.resolve_session_config(mc.sessions[0])
assert "build_env" not in conf.get("config", {})
def test_run_all_pass(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
testdir = os.path.join(tmp, "tests")
os.makedirs(testdir)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[
SessionConfig(name="s1", confpath=confpath, testpath=testdir),
SessionConfig(name="s2", confpath=confpath, testpath=testdir),
],
)
runner = _make_runner(mc)
mock_log_mgr = MagicMock()
mock_log_mgr.new_session_dir.return_value = tmp
with (
patch.object(runner, "_phase_build") as mock_build,
patch.object(runner, "_phase_test") as mock_test,
patch.object(runner, "_phase_report"),
patch("ntfc.multi.LogManager", return_value=mock_log_mgr),
):
mock_build.return_value = {"s1": {}, "s2": {}}
mock_test.return_value = [
SessionResult("s1", 0, os.path.join(tmp, "s1")),
SessionResult("s2", 0, os.path.join(tmp, "s2")),
]
assert runner.run() == 0
def test_run_session_fails(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
runner = _make_runner(mc)
mock_log_mgr = MagicMock()
mock_log_mgr.new_session_dir.return_value = tmp
with (
patch.object(runner, "_phase_build") as mock_build,
patch.object(runner, "_phase_test") as mock_test,
patch.object(runner, "_phase_report"),
patch("ntfc.multi.LogManager", return_value=mock_log_mgr),
):
mock_build.return_value = {"s1": {}}
mock_test.return_value = [
SessionResult("s1", 1, os.path.join(tmp, "s1")),
]
assert runner.run() == 1
def test_run_build_fails(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
runner = _make_runner(mc)
with (
patch.object(runner, "_phase_build") as mock_build,
patch.object(runner, "_phase_test") as mock_test,
):
mock_build.return_value = None
assert runner.run() == 1
mock_test.assert_not_called()
def test_phase_build_dedup(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[
SessionConfig(name="s1", confpath=confpath, testpath=tmp),
SessionConfig(name="s2", confpath=confpath, testpath=tmp),
],
)
runner = _make_runner(mc)
mock_builder = MagicMock()
mock_builder.need_build.return_value = False
with patch("ntfc.multi.NuttXBuilder", return_value=mock_builder):
result = runner._phase_build()
assert result is not None
assert "s1" in result
assert "s2" in result
assert result["s1"] is result["s2"]
def test_phase_build_exception(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
runner = _make_runner(mc)
mock_builder = MagicMock()
mock_builder.need_build.return_value = True
mock_builder.build_all.side_effect = RuntimeError("fail")
with patch("ntfc.multi.NuttXBuilder", return_value=mock_builder):
result = runner._phase_build()
assert result is None
def test_phase_build_success_with_builder(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
runner = _make_runner(mc)
new_conf = {"config": {"loops": 1}, "product": {"built": True}}
mock_builder = MagicMock()
mock_builder.need_build.return_value = True
mock_builder.new_conf.return_value = new_conf
with patch("ntfc.multi.NuttXBuilder", return_value=mock_builder):
result = runner._phase_build()
assert result is not None
assert result["s1"]["product"]["built"] is True
def test_sequential_fail_fast(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(fail_fast=True),
sessions=[
SessionConfig(name="s1", confpath=confpath, testpath=tmp),
SessionConfig(name="s2", confpath=confpath, testpath=tmp),
],
)
runner = _make_runner(mc)
call_count = 0
def mock_run_session(
session: SessionConfig,
conf: dict, # type: ignore[type-arg]
fail_event: object = None,
) -> SessionResult:
nonlocal call_count
call_count += 1
return SessionResult(name=session.name, exit_code=1, result_dir=tmp)
runner._run_session = mock_run_session # type: ignore[assignment]
results = runner._run_sequential({"s1": {}, "s2": {}})
assert len(results) == 1
assert results[0].name == "s1"
assert call_count == 1
def test_sequential_no_fail_fast(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(fail_fast=False),
sessions=[
SessionConfig(name="s1", confpath=confpath, testpath=tmp),
SessionConfig(name="s2", confpath=confpath, testpath=tmp),
],
)
runner = _make_runner(mc)
def mock_run_session(
session: SessionConfig,
conf: dict, # type: ignore[type-arg]
fail_event: object = None,
) -> SessionResult:
return SessionResult(name=session.name, exit_code=1, result_dir=tmp)
runner._run_session = mock_run_session # type: ignore[assignment]
results = runner._run_sequential({"s1": {}, "s2": {}})
assert len(results) == 2
def test_parallel_no_resources(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(parallel=True),
sessions=[
SessionConfig(name="s1", confpath=confpath, testpath=tmp),
SessionConfig(name="s2", confpath=confpath, testpath=tmp),
],
)
runner = MultiSessionRunner(mc, rebuild=False)
executed: list[str] = []
lock = threading.Lock()
def mock_run_session(
session: SessionConfig,
conf: dict, # type: ignore[type-arg]
fail_event: object = None,
) -> SessionResult:
with lock:
executed.append(session.name)
return SessionResult(name=session.name, exit_code=0, result_dir=tmp)
runner._run_session = mock_run_session # type: ignore[assignment]
results = runner._run_parallel({"s1": {}, "s2": {}})
assert len(results) == 2
assert {r.name for r in results} == {"s1", "s2"}
def test_parallel_resource_serialization(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(parallel=True),
sessions=[
SessionConfig(
name="s1",
confpath=confpath,
testpath=tmp,
resources=["vcan0"],
),
SessionConfig(
name="s2",
confpath=confpath,
testpath=tmp,
resources=["vcan0"],
),
],
)
runner = MultiSessionRunner(mc, rebuild=False)
concurrent_count = 0
max_concurrent = 0
count_lock = threading.Lock()
def mock_run_session(
session: SessionConfig,
conf: dict, # type: ignore[type-arg]
fail_event: object = None,
) -> SessionResult:
nonlocal concurrent_count, max_concurrent
with count_lock:
concurrent_count += 1
max_concurrent = max(max_concurrent, concurrent_count)
time.sleep(0.05)
with count_lock:
concurrent_count -= 1
return SessionResult(name=session.name, exit_code=0, result_dir=tmp)
runner._run_session = mock_run_session # type: ignore[assignment]
results = runner._run_parallel({"s1": {}, "s2": {}})
assert len(results) == 2
assert max_concurrent == 1
def test_parallel_fail_fast(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(fail_fast=True, parallel=True),
sessions=[
SessionConfig(
name="s1",
confpath=confpath,
testpath=tmp,
resources=["res"],
),
SessionConfig(
name="s2",
confpath=confpath,
testpath=tmp,
resources=["res"],
),
],
)
runner = MultiSessionRunner(mc, rebuild=False)
def mock_run_session(
session: SessionConfig,
conf: dict, # type: ignore[type-arg]
fail_event: object = None,
) -> SessionResult:
code = 1 if session.name == "s1" else -1
return SessionResult(name=session.name, exit_code=code, result_dir=tmp)
runner._run_session = mock_run_session # type: ignore[assignment]
results = runner._run_parallel({"s1": {}, "s2": {}})
failed = [r for r in results if r.exit_code != 0]
assert len(failed) >= 1
def test_parallel_results_sorted(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(parallel=True),
sessions=[
SessionConfig(name="alpha", confpath=confpath, testpath=tmp),
SessionConfig(name="beta", confpath=confpath, testpath=tmp),
SessionConfig(name="gamma", confpath=confpath, testpath=tmp),
],
)
runner = MultiSessionRunner(mc, rebuild=False)
def mock_run_session(
session: SessionConfig,
conf: dict, # type: ignore[type-arg]
fail_event: object = None,
) -> SessionResult:
return SessionResult(name=session.name, exit_code=0, result_dir=tmp)
runner._run_session = mock_run_session # type: ignore[assignment]
results = runner._run_parallel({"alpha": {}, "beta": {}, "gamma": {}})
assert [r.name for r in results] == ["alpha", "beta", "gamma"]
def test_run_session_abort_on_fail_event(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(fail_fast=True),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
runner = MultiSessionRunner(mc, rebuild=False)
fail_event = threading.Event()
fail_event.set()
result = runner._run_session(mc.sessions[0], {}, fail_event)
assert result.exit_code == -1
def test_run_session_calls_mypytest(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[
SessionConfig(
name="s1",
confpath=confpath,
testpath=tmp,
modules="nsh,shell",
)
],
)
runner = MultiSessionRunner(mc, rebuild=False)
runner._session_dir = tmp
mock_pt = MagicMock()
mock_pt.runner.return_value = 0
mock_pt.result_dir = "/tmp/fake_result"
with patch("ntfc.multi.MyPytest", return_value=mock_pt):
result = runner._run_session(mc.sessions[0], {})
assert result.exit_code == 0
assert result.result_dir == "/tmp/fake_result"
mock_pt.runner.assert_called_once()
# Verify result_dir was passed to runner
call_args = mock_pt.runner.call_args
result_dict = call_args[0][1]
assert result_dict["result_dir"] == os.path.join(tmp, "s1")
def test_run_session_sets_fail_event(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(fail_fast=True),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
runner = MultiSessionRunner(mc, rebuild=False)
runner._session_dir = tmp
fail_event = threading.Event()
mock_pt = MagicMock()
mock_pt.runner.return_value = 1
mock_pt.result_dir = ""
with patch("ntfc.multi.MyPytest", return_value=mock_pt):
runner._run_session(mc.sessions[0], {}, fail_event)
assert fail_event.is_set()
def test_phase_test_sequential(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(parallel=False),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
runner = MultiSessionRunner(mc, rebuild=False)
called = []
def mock_sequential(
built_configs: dict, # type: ignore[type-arg]
) -> list: # type: ignore[type-arg]
called.append("sequential")
return [SessionResult("s1", 0, tmp)]
runner._run_sequential = mock_sequential # type: ignore[assignment]
results = runner._phase_test({"s1": {}})
assert called == ["sequential"]
assert len(results) == 1
def test_phase_test_parallel(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(parallel=True),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
runner = MultiSessionRunner(mc, rebuild=False)
called = []
def mock_parallel(
built_configs: dict, # type: ignore[type-arg]
) -> list: # type: ignore[type-arg]
called.append("parallel")
return [SessionResult("s1", 0, tmp)]
runner._run_parallel = mock_parallel # type: ignore[assignment]
results = runner._phase_test({"s1": {}})
assert called == ["parallel"]
assert len(results) == 1
def test_phase_report(tmp_path):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
runner = MultiSessionRunner(mc, rebuild=False)
runner._session_dir = tmp
results = [SessionResult("s1", 0, tmp)]
with (
patch("ntfc.multi.Reporter") as mock_rep_cls,
patch.object(
MultiSessionRunner, "_merge_session_reports"
) as mock_merge,
):
mock_reporter = MagicMock()
mock_rep_cls.return_value = mock_reporter
runner._phase_report(results)
mock_merge.assert_called_once_with(tmp, results, mock_reporter)
def test_merge_two_sessions(tmp_path):
tmp = str(tmp_path)
master = os.path.join(tmp, "master")
os.makedirs(master)
s1_dir = os.path.join(master, "s1")
os.makedirs(s1_dir)
_make_junit_xml(os.path.join(s1_dir, "report.xml"), "module_a", 2)
s2_dir = os.path.join(master, "s2")
os.makedirs(s2_dir)
_make_junit_xml(os.path.join(s2_dir, "report.xml"), "module_b", 3)
results = [
SessionResult("s1", 0, s1_dir),
SessionResult("s2", 0, s2_dir),
]
reporter = MagicMock()
MultiSessionRunner._merge_session_reports(master, results, reporter)
merged_path = os.path.join(master, "report.xml")
assert os.path.exists(merged_path)
tree = ElementTree.parse(merged_path)
root = tree.getroot()
suites = root.findall("testsuite")
assert len(suites) == 2
names = {s.get("name") for s in suites}
assert "s1::module_a" in names
assert "s2::module_b" in names
for tc in root.findall(".//testcase"):
assert "::" in tc.get("classname", "")
reporter.generate_result_summary.assert_called_once_with(master)
def test_merge_missing_xml(tmp_path):
tmp = str(tmp_path)
master = os.path.join(tmp, "master")
os.makedirs(master)
results = [
SessionResult("s1", 0, os.path.join(master, "s1")),
]
reporter = MagicMock()
MultiSessionRunner._merge_session_reports(master, results, reporter)
assert os.path.exists(os.path.join(master, "report.xml"))
def test_merge_invalid_xml(tmp_path):
tmp = str(tmp_path)
master = os.path.join(tmp, "master")
s1_dir = os.path.join(master, "s1")
os.makedirs(s1_dir)
with open(os.path.join(s1_dir, "report.xml"), "w") as f:
f.write("not xml at all <><><>")
results = [SessionResult("s1", 0, s1_dir)]
reporter = MagicMock()
MultiSessionRunner._merge_session_reports(master, results, reporter)
assert os.path.exists(os.path.join(master, "report.xml"))
def test_merge_with_failure_elements(tmp_path):
tmp = str(tmp_path)
master = os.path.join(tmp, "master")
s1_dir = os.path.join(master, "s1")
os.makedirs(s1_dir)
root = ElementTree.Element("testsuites")
suite = ElementTree.SubElement(root, "testsuite")
suite.set("name", "mod")
suite.set("tests", "1")
suite.set("failures", "1")
suite.set("errors", "0")
suite.set("skipped", "0")
suite.set("time", "1.0")
tc = ElementTree.SubElement(suite, "testcase")
tc.set("name", "test_fail")
tc.set("classname", "mod::Test")
tc.set("time", "0.5")
fail = ElementTree.SubElement(tc, "failure")
fail.set("message", "assertion error")
fail.text = "traceback here"
tree = ElementTree.ElementTree(root)
tree.write(os.path.join(s1_dir, "report.xml"))
results = [SessionResult("s1", 1, s1_dir)]
reporter = MagicMock()
MultiSessionRunner._merge_session_reports(master, results, reporter)
merged = ElementTree.parse(os.path.join(master, "report.xml"))
failures = merged.findall(".//failure")
assert len(failures) == 1
assert failures[0].get("message") == "assertion error"
assert failures[0].text == "traceback here"
def test_merge_child_without_text(tmp_path):
tmp = str(tmp_path)
master = os.path.join(tmp, "master")
s1_dir = os.path.join(master, "s1")
os.makedirs(s1_dir)
root = ElementTree.Element("testsuites")
suite = ElementTree.SubElement(root, "testsuite")
suite.set("name", "mod")
suite.set("tests", "1")
suite.set("failures", "0")
suite.set("errors", "0")
suite.set("skipped", "1")
suite.set("time", "0.0")
tc = ElementTree.SubElement(suite, "testcase")
tc.set("name", "test_skip")
tc.set("classname", "mod::Test")
tc.set("time", "0.0")
skip = ElementTree.SubElement(tc, "skipped")
skip.set("message", "not applicable")
tree = ElementTree.ElementTree(root)
tree.write(os.path.join(s1_dir, "report.xml"))
results = [SessionResult("s1", 0, s1_dir)]
reporter = MagicMock()
MultiSessionRunner._merge_session_reports(master, results, reporter)
merged = ElementTree.parse(os.path.join(master, "report.xml"))
skipped = merged.findall(".//skipped")
assert len(skipped) == 1
assert skipped[0].get("message") == "not applicable"
assert skipped[0].text is None
def test_build_key_same_config():
mc = ManifestConfig(options=MultiOptions(), sessions=[])
runner = MultiSessionRunner(mc, rebuild=False)
conf = {
"config": {"build_env": {"CC": "gcc"}},
"product": {"cores": {"core0": {"defconfig": "path/a"}}},
}
assert runner._build_key(conf) == runner._build_key(conf)
def test_build_key_different_defconfig():
mc = ManifestConfig(options=MultiOptions(), sessions=[])
runner = MultiSessionRunner(mc, rebuild=False)
conf1 = {
"config": {},
"product": {"cores": {"core0": {"defconfig": "path/a"}}},
}
conf2 = {
"config": {},
"product": {"cores": {"core0": {"defconfig": "path/b"}}},
}
assert runner._build_key(conf1) != runner._build_key(conf2)
def test_build_key_no_defconfig():
mc = ManifestConfig(options=MultiOptions(), sessions=[])
runner = MultiSessionRunner(mc, rebuild=False)
conf = {"config": {}, "product": {"cores": {"core0": {}}}}
key = runner._build_key(conf)
assert isinstance(key, frozenset)
def test_build_key_kv_affects_key():
mc = ManifestConfig(options=MultiOptions(), sessions=[])
runner = MultiSessionRunner(mc, rebuild=False)
conf1 = {
"config": {"kv": {"K": "1"}},
"product": {"cores": {"core0": {"defconfig": "p"}}},
}
conf2 = {
"config": {"kv": {"K": "2"}},
"product": {"cores": {"core0": {"defconfig": "p"}}},
}
assert runner._build_key(conf1) != runner._build_key(conf2)
def test_print_summary_all_states(tmp_path, capsys):
tmp = str(tmp_path)
confpath = _write_config_yaml(tmp)
s1_dir = os.path.join(tmp, "s1")
s2_dir = os.path.join(tmp, "s2")
_write_report_xml(
s1_dir,
'<testsuites><testsuite tests="5" failures="0"'
' skipped="0" errors="0" time="10.5"/></testsuites>',
)
_write_report_xml(
s2_dir,
'<testsuites><testsuite tests="3" failures="2"'
' skipped="1" errors="0" time="5.0"/></testsuites>',
)
mc = ManifestConfig(
options=MultiOptions(),
sessions=[SessionConfig(name="s1", confpath=confpath, testpath=tmp)],
)
runner = MultiSessionRunner(mc, rebuild=False)
results = [
SessionResult("s1", 0, s1_dir),
SessionResult("s2", 1, s2_dir),
SessionResult("s3", -1, ""),
]
runner._print_summary(results)
out = capsys.readouterr().out
assert "PASS" in out
assert "FAIL" in out
assert "SKIP" in out
assert "sessions:3 passed:1 failed:1 skipped:1" in out
assert "Total" in out
def test_parse_session_counts_no_xml():
assert MultiSessionRunner._parse_session_counts("/nonexistent") == (
0,
0,
0,
0,
0.0,
)
def test_parse_session_counts_from_report_subdir(tmp_path):
tmp = str(tmp_path)
report_dir = os.path.join(tmp, "report")
os.makedirs(report_dir)
with open(os.path.join(report_dir, "001_shell.xml"), "w") as f:
f.write(
'<testsuites><testsuite tests="4" failures="1"'
' skipped="0" errors="0" time="2.0"/></testsuites>'
)
with open(os.path.join(report_dir, "002_can.xml"), "w") as f:
f.write(
'<testsuites><testsuite tests="3" failures="0"'
' skipped="1" errors="0" time="1.5"/></testsuites>'
)
p, f, s, e, t = MultiSessionRunner._parse_session_counts(tmp)
assert p == 5
assert f == 1
assert s == 1
assert e == 0
assert t == 3.5
def test_parse_session_counts_bad_xml(tmp_path):
tmp = str(tmp_path)
_write_report_xml(tmp, "<<<not xml>>>")
assert MultiSessionRunner._parse_session_counts(tmp) == (0, 0, 0, 0, 0.0)
def test_parse_session_counts_bad_xml_in_report_dir(tmp_path):
tmp = str(tmp_path)
report_dir = os.path.join(tmp, "report")
os.makedirs(report_dir)
with open(os.path.join(report_dir, "001_bad.xml"), "w") as f:
f.write("<<<not xml>>>")
assert MultiSessionRunner._parse_session_counts(tmp) == (0, 0, 0, 0, 0.0)