Skip to content

Commit b129d4c

Browse files
Docs: Improve XML documentation and logging (#584)
1 parent 04eae22 commit b129d4c

4 files changed

Lines changed: 140 additions & 17 deletions

File tree

docs/how-to/test_to_doc_links.rst

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,24 @@ Reference Docs in Tests
2020
This guide explains how to annotate test cases so that
2121
docs-as-code automatically creates traceability links between tests and requirements.
2222

23+
Details of implementation
24+
-------------------------
25+
2326
The mechanism is language-agnostic:
24-
Bazel produces JUnit-style ``test.xml`` files under ``bazel-testlogs/``.
25-
The ``score_source_code_linker`` extension parses those files, extracts test metadata
27+
The ``score_source_code_linker`` extension parses ``test.xml`` files, extracts test metadata
2628
(name, file, line, result, and verification properties),
2729
and creates backlinks on the referenced requirements.
2830

31+
The extension will look for ``test.xml`` files in both ``bazel-testlogs/``
32+
as well as a folder named ``tests-report/``. This folder can be created manually if the tests
33+
require some pre-run step or are matrix tests or similar.
34+
35+
2936
Required Properties
3037
-------------------
3138

3239
Every linked test must declare the following properties
33-
(see :need:`gd_guidl__verification_specification`):
40+
(see :need:`gd_guidl__verification_specification` for detailed values):
3441

3542
``PartiallyVerifies`` *and/or* ``FullyVerifies``
3643
Comma-separated list of requirement IDs that the test covers.
@@ -43,8 +50,44 @@ Every linked test must declare the following properties
4350

4451
``Description``
4552
A human-readable explanation of the test objective and expected outcome.
46-
47-
If any mandatory property is missing the test will be skipped during link creation.
53+
*In Python tests, a docstring takes the place of the description attribute.*
54+
55+
What should a test.xml look like?
56+
---------------------------------
57+
58+
Each testcase has file as well as it's line number as additional attributes.
59+
Then as described above, each testcase also has properties inside the XML.
60+
61+
.. code-block:: xml
62+
63+
<?xml version="1.0" encoding="utf-8"?>
64+
<testsuites name="pytest tests">
65+
<testsuite errors="0" failures="0" hostname="LG-0005" name="pytest" skipped="0" tests="118" time="6.617" timestamp="2026-06-08T17:56:07.635773+00:00">
66+
<testcase classname="src.extensions.score_source_code_linker.tests.test_testlink" file="src/extensions/score_source_code_linker/tests/test_testlink.py" line="40" name="test_testlink_serialization_roundtrip" time="0.000">
67+
<properties>
68+
<property name="PartiallyVerifies" value="tool_req__docs_test_link_testcase"></property>
69+
<property name="TestType" value="requirements-based"></property>
70+
<property name="DerivationTechnique" value="requirements-analysis"></property>
71+
</properties>
72+
</testcase>
73+
<testcase classname="src.extensions.score_source_code_linker.tests.test_testlink" file="src/extensions/score_source_code_linker/tests/test_testlink.py" line="85" name="test_clean_text_removes_ansi_and_html_unescapes" time="0.000">
74+
<properties>
75+
<property name="PartiallyVerifies" value="tool_req__docs_test_link_testcase"></property>
76+
<property name="TestType" value="requirements-based"></property>
77+
<property name="DerivationTechnique" value="requirements-analysis"></property>
78+
</properties>
79+
</testcase>
80+
</testsuite>
81+
</testsuites>
82+
83+
If you are not working in Python and using the provided pytest plugin, please ensure that the xml that is written in the end
84+
looks like this, otherwise the extension will not be able to parse the xml correctly.
85+
86+
What happens when properties are missing
87+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
88+
89+
When properties or attributes are missing for some or all tests inside the test.xml, testcases are still generated.
90+
However, testcases can only be linked to the requirements if either Partially- or FullyVerifies is filled.
4891

4992
Language-Specific Annotations
5093
-----------------------------
@@ -58,15 +101,16 @@ C++ (gTest)
58101
See :need:`gd_req__verification_link_tests_cpp`.
59102

60103
Rust
61-
Use the ``#[record_property]`` attribute macro from the ``test_properties`` crate.
62-
See :need:`gd_req__verification_link_tests_rust`.
104+
Currently there is no provided official way to do this in Rust.
105+
We are working with the rust community to figure this out.
63106

64107
Python (pytest)
65108
Use the ``@add_test_properties`` decorator; the docstring serves as ``Description``.
66109
See :need:`gd_req__verification_link_tests_python`.
67110

68111
See :need:`gd_temp__verification_specification` for code templates.
69112

113+
70114
Running Tests and Building Docs
71115
-------------------------------
72116

@@ -84,9 +128,59 @@ Running Tests and Building Docs
84128
85129
The resulting documentation shows backlinks on each requirement that is referenced by at least one test.
86130

131+
132+
How do I use the generated testcases?
133+
-------------------------------------
134+
135+
Testcases can be used in different ways inside and outside your documentation.
136+
You can create lists or pie diagrams to showcase all tests as well as statistics on them.
137+
For example you can show all tests like so
138+
139+
.. code-block:: rst
140+
141+
.. needpie:: Test Results
142+
:labels: passed, failed, skipped
143+
:colors: green, red, orange
144+
145+
type == 'testcase' and result == 'passed'
146+
type == 'testcase' and result == 'failed'
147+
type == 'testcase' and result == 'skipped'
148+
149+
150+
.. needpie:: Test Results
151+
:labels: passed, failed, skipped
152+
:colors: green, red, orange
153+
154+
type == 'testcase' and result == 'passed'
155+
type == 'testcase' and result == 'failed'
156+
type == 'testcase' and result == 'skipped'
157+
158+
Or show the different types of properties in case you want
159+
160+
.. code-block:: rst
161+
162+
.. needpie:: Test Types Used In Testcases
163+
:labels: fault-injection, interface-test, requirements-based, resource-usage
164+
:legend:
165+
166+
type == 'testcase' and test_type == 'fault-injection'
167+
type == 'testcase' and test_type == 'interface-test'
168+
type == 'testcase' and test_type == 'requirements-based'
169+
type == 'testcase' and test_type == 'resource-usage'
170+
171+
172+
.. needpie:: Test Types Used In Testcases
173+
:labels: fault-injection, interface-test, requirements-based, resource-usage
174+
:legend:
175+
176+
type == 'testcase' and test_type == 'fault-injection'
177+
type == 'testcase' and test_type == 'interface-test'
178+
type == 'testcase' and test_type == 'requirements-based'
179+
type == 'testcase' and test_type == 'resource-usage'
180+
181+
87182
Limitations
88183
-----------
89184

90185
- Tests must be executed by Bazel before building docs so ``test.xml`` files exist.
91186
- Not compatible with Esbonio / live preview (no ``bazel-testlogs/`` available).
92-
- All mandatory properties must be present; partial metadata causes the test to be silently skipped.

src/extensions/score_source_code_linker/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,14 @@ def setup_test_code_linker(app: Sphinx, env: BuildEnvironment):
212212
)
213213
# sanity check if extension is enabled
214214
bazel_testlogs = ws_root / "bazel-testlogs"
215-
if not bazel_testlogs.exists():
215+
test_folder = ws_root / "tests-report"
216+
if not (bazel_testlogs.exists() or test_folder.exists()):
216217
LOGGER.info(f"{'=' * 80}", type="score_source_code_linker")
217218
LOGGER.info(
218219
f"{'=' * 32}SCORE XML PARSER{'=' * 32}", type="score_source_code_linker"
219220
)
220221
LOGGER.info(
221-
"'bazel-testlogs' was not found. If test data should be parsed,"
222+
"'bazel-testlogs' and 'tests-report' both were not found. If test data should be parsed,"
222223
+ "please run tests before building the documentation",
223224
type="score_source_code_linker",
224225
)

src/extensions/score_source_code_linker/testlink.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,11 @@ def __post_init__(self):
197197
def check_verifies_fields(self) -> bool:
198198
if self.PartiallyVerifies is None and self.FullyVerifies is None:
199199
# This might be a warning in the future, but for now we want be lenient.
200-
LOGGER.info(
200+
# Changing to debug to keep stdout spam to minimum under default settings
201+
LOGGER.debug(
201202
f"TestCase: {self.name} Error. Either 'PartiallyVerifies' or "
202-
"'FullyVerifies' must be provided."
203-
"This test case will be skipped and not linked.",
203+
"'FullyVerifies' must be provided to create a link to needs"
204+
"Testcase will still be created, but not linked.",
204205
type="score_source_code_linker",
205206
)
206207
return False

src/extensions/score_source_code_linker/xml_parser.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,17 +265,32 @@ def find_xml_files(search_path: Path) -> list[Path]:
265265
"""
266266

267267
test_file_name = "test.xml"
268-
return [x for x in search_path.rglob(test_file_name)]
268+
test_files = [x for x in search_path.rglob(test_file_name)]
269+
logger.info(f"Found {len(test_files)} test files in total. Parsing them now")
270+
if not test_files:
271+
logger.info(
272+
"Did not find any test.xml files. "
273+
"If you expected xml files to be found, please ensure that you have ran your tests "
274+
"and put all testfiles either in tests-reports or bazel-testlogs."
275+
)
276+
return test_files
269277

270278

271279
def find_test_folder(base_path: Path | None = None) -> Path | None:
272280
ws_root = base_path if base_path is not None else find_ws_root()
273281
assert ws_root is not None
274282
if os.path.isdir(ws_root / "tests-report"):
283+
logger.info(
284+
"Found `tests-report` folder. Searching for test.xml files in there"
285+
)
275286
return ws_root / "tests-report"
276287
if os.path.isdir(ws_root / "bazel-testlogs"):
288+
logger.info(
289+
"tests-report folder not found. Defaulting to `bazel-testlogs`. Searching for test.xml files in there"
290+
)
277291
return ws_root / "bazel-testlogs"
278-
logger.info("could not find tests-report or bazel-testlogs to parse testcases")
292+
# This should only happen if bazel was never started in the first place?
293+
logger.info("Could not find tests-report or bazel-testlogs to parse testcases")
279294
return None
280295

281296

@@ -291,6 +306,9 @@ def run_xml_parser(app: Sphinx, env: BuildEnvironment):
291306
xml_file_paths = find_xml_files(testlogs_dir)
292307
test_case_needs = build_test_needs_from_files(app, env, xml_file_paths)
293308
# Saving the test case needs for cache
309+
logger.info(
310+
f"Saving {len(test_case_needs)} test case needs to the cache `score_testcaseneeds_cache.json` in _build/."
311+
)
294312
store_data_of_test_case_json(
295313
app.outdir / "score_testcaseneeds_cache.json", test_case_needs
296314
)
@@ -299,6 +317,9 @@ def run_xml_parser(app: Sphinx, env: BuildEnvironment):
299317
)
300318
# This is not ideal, due to duplication, but I can't think of a better solution
301319
# right now
320+
logger.info(
321+
f"Saving {len(output)} parsed testcases to the cache `score_xml_parser_cache.json` in _build/."
322+
)
302323
store_test_xml_parsed_json(app.outdir / "score_xml_parser_cache.json", output)
303324

304325

@@ -315,10 +336,15 @@ def build_test_needs_from_files(
315336
tcns: list[DataOfTestCase] = []
316337
for file in xml_paths:
317338
# Last value can be ignored. The 'is_valid' function already prints infos
318-
test_cases, tests_missing_all_props, _ = read_test_xml_file(file)
339+
test_cases, tests_missing_all_props, tests_missing_some_props = (
340+
read_test_xml_file(file)
341+
)
319342
non_prop_tests = ", ".join(n for n in tests_missing_all_props)
320343
if non_prop_tests:
321344
logger.info(f"Tests missing all properties: {non_prop_tests}")
345+
missing_prop_tests = ", ".join(n for n in tests_missing_some_props)
346+
if missing_prop_tests:
347+
logger.info(f"Tests missing some properties: {missing_prop_tests}")
322348
tcns.extend(test_cases)
323349
for tc in test_cases:
324350
construct_and_add_need(app, tc)
@@ -343,7 +369,8 @@ def construct_and_add_need(app: Sphinx, tn: DataOfTestCase):
343369
name = tn.name
344370
external_url = ""
345371
if tn.repo_name is None or tn.hash is None or tn.url is None:
346-
logger.info(
372+
# I would change this to debug for now, as it seems too spammy in 'info'
373+
logger.debug(
347374
"Creating testcase need with fallback URL due to incomplete repo metadata: "
348375
f"name={name}, file={file}, repo_name={tn.repo_name}, "
349376
f"hash={tn.hash}, url={tn.url}",

0 commit comments

Comments
 (0)