From d9f3fc48753f02fb8b215f4d573771d7814c1dfb Mon Sep 17 00:00:00 2001 From: Tobias Kaestner Date: Fri, 2 Oct 2026 21:16:29 +0200 Subject: [PATCH] fix: testmodule: find the scenario file under each name twister reads The scenario table read only /testcase.yaml. Newer Zephyr trees name the file tests.yaml, so every test module warned that it could not read testcase.yaml, and the table was missing. Take the first of testcase.yaml, tests.yaml and sample.yaml that exists, in twister's order (twisterlib/testplan.py). Older trees keep working. Each name is an input of the document. When no file exists, the warning names the files it tried. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tobias Kaestner --- .../howto/render-test-specifications.rst | 5 ++-- doc/manual/reference/directives-and-roles.rst | 3 +- .../_extensions/_tests/test_rst_builders.py | 27 ++++++++++++++++++ sphinx/_extensions/_tests/test_test_module.py | 5 ++-- sphinx/_extensions/rst_builders.py | 28 ++++++++++++++++++- sphinx/_extensions/test_module.py | 12 ++++++-- 6 files changed, 71 insertions(+), 9 deletions(-) diff --git a/doc/manual/howto/render-test-specifications.rst b/doc/manual/howto/render-test-specifications.rst index d926fa7..3bff7d5 100644 --- a/doc/manual/howto/render-test-specifications.rst +++ b/doc/manual/howto/render-test-specifications.rst @@ -84,8 +84,9 @@ vocabulary rather than ``test_case``/``verifies``/etc., is a matching pair of :module: checks/widget/probe The argument is the Doxygen **module** group's name, never a suite; ``:module:`` -is a project-relative path used only to find that module's ``testcase.yaml`` -for the scenario table. +is a project-relative path used only to find that module's scenario file +for the scenario table. That file is the first of ``testcase.yaml``, +``tests.yaml`` and ``sample.yaml`` that exists, in twister's order. 6. Add the report half (optional) --------------------------------------- diff --git a/doc/manual/reference/directives-and-roles.rst b/doc/manual/reference/directives-and-roles.rst index 0c3666b..9336518 100644 --- a/doc/manual/reference/directives-and-roles.rst +++ b/doc/manual/reference/directives-and-roles.rst @@ -100,7 +100,8 @@ The chain from annotated ztest C source to a rendered, traceable test report ``testmodule``'s argument is a Doxygen ``@defgroup`` name (the *module* group, never a suite or a path); ``:module:`` is a project-relative path used only to -locate that module's ``testcase.yaml`` for the rendered scenario table. Every +locate that module's scenario file for the rendered scenario table: the first +of ``testcase.yaml``, ``tests.yaml`` and ``sample.yaml`` that exists. Every ``ZTEST``/``ZTEST_SUITE``/... in the named group and its inner suite/procedure groups becomes one need each — nothing is written by hand per test case. diff --git a/sphinx/_extensions/_tests/test_rst_builders.py b/sphinx/_extensions/_tests/test_rst_builders.py index b6a23bb..f112253 100644 --- a/sphinx/_extensions/_tests/test_rst_builders.py +++ b/sphinx/_extensions/_tests/test_rst_builders.py @@ -196,6 +196,33 @@ def test_build_scenario_table_missing_yaml(): assert lines == [] +def test_find_scenario_yaml_takes_tests_yaml(tmp_path): + # Newer Zephyr trees name the file tests.yaml. + (tmp_path / "tests.yaml").write_text("tests: {}\n") + assert rb.find_scenario_yaml(tmp_path) == tmp_path / "tests.yaml" + + +def test_find_scenario_yaml_takes_sample_yaml(tmp_path): + (tmp_path / "sample.yaml").write_text("tests: {}\n") + assert rb.find_scenario_yaml(tmp_path) == tmp_path / "sample.yaml" + + +def test_find_scenario_yaml_follows_twister_order(tmp_path): + # Twister reads testcase.yaml first, so it wins over tests.yaml. + for name in ("testcase.yaml", "tests.yaml", "sample.yaml"): + (tmp_path / name).write_text("tests: {}\n") + assert rb.find_scenario_yaml(tmp_path) == tmp_path / "testcase.yaml" + + +def test_find_scenario_yaml_warns_with_the_names_it_tried(tmp_path, caplog): + with caplog.at_level("WARNING", logger=rb.logger.name): + assert rb.find_scenario_yaml(tmp_path) is None + message = caplog.text + assert str(tmp_path) in message + for name in ("testcase.yaml", "tests.yaml", "sample.yaml"): + assert name in message + + def test_build_procedure_need_rst_links_prose_but_keeps_title_plain(): # A need's title is not parsed as RST, so link markup there would render # verbatim; the details are parsed and link like the see-also line does. diff --git a/sphinx/_extensions/_tests/test_test_module.py b/sphinx/_extensions/_tests/test_test_module.py index 0dfba95..be2b532 100644 --- a/sphinx/_extensions/_tests/test_test_module.py +++ b/sphinx/_extensions/_tests/test_test_module.py @@ -406,7 +406,7 @@ def test_testmodule_directive_suite_heading(app): @pytest.mark.sphinx("html", srcdir=str(_ROOTS / "test-testmodule")) -def test_testmodule_directive_notes_its_xml_and_testcase_yaml_as_inputs(app): +def test_testmodule_directive_notes_its_xml_and_scenario_files_as_inputs(app): # Without these, an incremental build after the test sources change (a # retagged status, a renamed test) keeps the old test cases. app.build() @@ -414,7 +414,8 @@ def test_testmodule_directive_notes_its_xml_and_testcase_yaml_as_inputs(app): xml_dir = Path(app.config.testmodule_xml_dir) assert str(xml_dir / "index.xml") in inputs assert any(p.endswith(".xml") and "group__" in p for p in inputs) - assert any(p.endswith("testcase.yaml") for p in inputs) + for name in ("testcase.yaml", "tests.yaml", "sample.yaml"): + assert any(p.endswith(name) for p in inputs) @pytest.mark.sphinx("html", srcdir=str(_ROOTS / "test-testmodule")) diff --git a/sphinx/_extensions/rst_builders.py b/sphinx/_extensions/rst_builders.py index 581e15a..44319ff 100644 --- a/sphinx/_extensions/rst_builders.py +++ b/sphinx/_extensions/rst_builders.py @@ -23,6 +23,8 @@ "build_result_rst", "build_symbol_need_rst", "build_scenario_table", + "SCENARIO_YAML_NAMES", + "find_scenario_yaml", ] logger = logging.getLogger(__name__) @@ -392,8 +394,32 @@ def build_symbol_need_rst(info, need_names=None, depends_field=False): return "\n".join(lines) +# The names that twister reads for the scenarios of a test directory, in +# twister's order (scripts/pylib/twister/twisterlib/testplan.py). Newer Zephyr +# trees name the file tests.yaml, older ones testcase.yaml. +SCENARIO_YAML_NAMES = ("testcase.yaml", "tests.yaml", "sample.yaml") + + +def find_scenario_yaml(module_dir): + """Return the first scenario file in ``module_dir`` that exists, or None. + + The names come from ``SCENARIO_YAML_NAMES``. When no file exists, log a + warning that names each file it tried. + """ + module_dir = Path(module_dir) + for name in SCENARIO_YAML_NAMES: + path = module_dir / name + if path.is_file(): + return path + logger.warning( + f"testmodule: no scenario file in {module_dir} " + f"(tried {', '.join(SCENARIO_YAML_NAMES)})" + ) + return None + + def build_scenario_table(testcase_yaml_path): - """Return RST lines for a list-table of scenarios from testcase.yaml.""" + """Return RST lines for a list-table of scenarios from a scenario file.""" try: with open(testcase_yaml_path) as f: data = yaml.safe_load(f) diff --git a/sphinx/_extensions/test_module.py b/sphinx/_extensions/test_module.py index 06b8b66..06e18ee 100644 --- a/sphinx/_extensions/test_module.py +++ b/sphinx/_extensions/test_module.py @@ -20,11 +20,13 @@ from needs_fields import depends_field, field_type from rst_builders import ( RESULT_FIELD_ROLES, + SCENARIO_YAML_NAMES, _need_name, build_need_rst, build_procedure_need_rst, build_result_rst, build_scenario_table, + find_scenario_yaml, ) from sphinx.util import logging from twister_reader import ( @@ -629,9 +631,13 @@ def run(self): suite_refids, proc_refids = _classify_inner_groups(module_cdef, xml_dir) need_names = _need_names_from_config(app) - testcase_yaml = Path(module_root) / module_path / "testcase.yaml" - _note_input(env, testcase_yaml) - scenario_lines = build_scenario_table(testcase_yaml) + # Each name twister accepts is an input, so a scenario file that is + # added or renamed later also re-reads the document. + module_dir = Path(module_root) / module_path + for name in SCENARIO_YAML_NAMES: + _note_input(env, module_dir / name) + scenario_yaml = find_scenario_yaml(module_dir) + scenario_lines = build_scenario_table(scenario_yaml) if scenario_yaml else [] all_rst = list(scenario_lines) for suite_refid in suite_refids: all_rst += _build_suite_rst(