Skip to content

Commit eda4e2c

Browse files
docs: clarify testcase annotation rationale
1 parent 92fae9b commit eda4e2c

1 file changed

Lines changed: 47 additions & 5 deletions

File tree

src/extensions/score_source_code_linker/testcase_annotations.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@
1515
Testcase needs are external needs. Their ``external_url`` is therefore also
1616
the URL used by the ``testlink`` metadata rendered on requirements. The same
1717
URL is used for incoming ``fully_verified_by``/``partially_verified_by`` links.
18-
This hook matches both forms to the testcase need and appends a coloured,
19-
theme-compatible result annotation to the rendered reference.
18+
This hook handles both kinds of references: resolved need links can identify a
19+
testcase by ``refid``, while GitHub ``testlink`` references can be matched by
20+
their ``refuri``.
21+
22+
An ``external_url`` points to a repository, file, and source line. It is not
23+
guaranteed to identify one testcase, because multiple testcases can share a
24+
source location. ID-based references are therefore preferred; URL-only
25+
references are annotated only when exactly one testcase matches.
2026
"""
2127

2228
from __future__ import annotations
@@ -27,15 +33,24 @@
2733
from docutils import nodes
2834
from sphinx_needs.data import SphinxNeedsData
2935

36+
# Known result values get semantic classes so the stylesheet can provide
37+
# readable colours for both light and dark themes.
3038
RESULT_CLASSES = {
3139
"passed": "score-testcase-result--passed",
3240
"failed": "score-testcase-result--failed",
3341
"skipped": "score-testcase-result--skipped",
3442
"disabled": "score-testcase-result--disabled",
3543
}
44+
# Keep an unknown result visible, but do not assign it the meaning of a known
45+
# status such as ``passed`` or ``failed``.
3646
_FALLBACK_CLASS = "score-testcase-result--unknown"
47+
# The event handler is expected to be idempotent for a doctree. This marker
48+
# prevents a second invocation from appending the same status again.
3749
_ANNOTATED_ATTR = "score_source_code_linker_testcase_result_annotated"
3850

51+
# The styles are inserted into a document only when that document contains an
52+
# annotation. Keeping them in one block avoids repeating inline style rules on
53+
# every testcase link and lets the same classes handle theme changes.
3954
_TESTCASE_STATUS_CSS = """
4055
<style>
4156
.score-testcase-result {
@@ -87,7 +102,12 @@
87102

88103

89104
def _resolved_target_need_id(ref: nodes.reference) -> str | None:
90-
"""Return the need ID a resolved reference points to, if available."""
105+
"""Return the need ID encoded in a resolved reference, if available.
106+
107+
Depending on how sphinx-needs created the reference, the target is stored
108+
either as ``refid`` or as the fragment of a local ``refuri``. The caller
109+
can then perform an exact lookup before falling back to URL matching.
110+
"""
91111
refid = ref.get("refid")
92112
if refid:
93113
return refid
@@ -100,13 +120,20 @@ def _resolved_target_need_id(ref: nodes.reference) -> str | None:
100120

101121

102122
def _testcases_by_external_url(needs: Any) -> dict[str, list[dict[str, Any]]]:
103-
"""Group testcase needs by the URL used for their rendered GitHub link."""
123+
"""Group testcase needs by their rendered GitHub URL without losing duplicates.
124+
125+
The URL identifies a source location, not a testcase identity. Keeping a
126+
list is important because parameterized tests or multiple reported tests
127+
can point to the same file and line.
128+
"""
104129
testcases_by_url: dict[str, list[dict[str, Any]]] = {}
105130
for need in needs.values():
106131
if need.get("type") != "testcase":
107132
continue
108133
external_url = need.get("external_url")
109134
if external_url:
135+
# Do not overwrite an earlier testcase with the same location.
136+
# _testcase_for_reference() uses the list to detect ambiguity.
110137
testcases_by_url.setdefault(external_url, []).append(need)
111138
return testcases_by_url
112139

@@ -116,7 +143,12 @@ def _testcase_for_reference(
116143
needs: Any,
117144
testcases_by_url: dict[str, list[dict[str, Any]]],
118145
) -> dict[str, Any] | None:
119-
"""Resolve a reference either by need ID or by its external GitHub URL."""
146+
"""Resolve a reference by exact need ID or by an unambiguous external URL.
147+
148+
A need ID identifies one testcase even when its source URL is shared. A
149+
URL-only reference is used only when it has exactly one testcase candidate;
150+
choosing one of several candidates could display the wrong result.
151+
"""
120152
target_id = _resolved_target_need_id(ref)
121153
if target_id:
122154
target_need = needs.get(target_id)
@@ -138,13 +170,20 @@ def annotate_testcase_results(app, doctree, docname):
138170
The handler runs after sphinx-needs' own ``doctree-resolved`` handlers.
139171
It therefore sees both regular resolved references and the external
140172
references generated for GitHub ``testlink`` metadata.
173+
174+
References without a testcase match or without a result are left as they
175+
are. This keeps the post-processing safe when test reports are incomplete
176+
or when a source URL is shared by multiple testcases.
141177
"""
142178
needs = SphinxNeedsData(app.env).get_needs_view()
143179
testcases_by_url = _testcases_by_external_url(needs)
180+
# CSS applies to the whole document, so one style block is enough even if
181+
# the document contains many annotated references.
144182
css_added = False
145183

146184
for ref in list(doctree.findall(nodes.reference)):
147185
if ref.get(_ANNOTATED_ATTR):
186+
# A repeated event invocation must not append another badge.
148187
continue
149188

150189
testneed = _testcase_for_reference(ref, needs, testcases_by_url)
@@ -157,6 +196,8 @@ def annotate_testcase_results(app, doctree, docname):
157196
continue
158197

159198
result_text = str(result)
199+
# Preserve unexpected report values as text, but use a neutral class
200+
# instead of presenting them as one of the known statuses.
160201
result_class = RESULT_CLASSES.get(result_text, _FALLBACK_CLASS)
161202
status_html = (
162203
f'<span class="score-testcase-result {result_class}"> '
@@ -165,5 +206,6 @@ def annotate_testcase_results(app, doctree, docname):
165206
if not css_added:
166207
doctree.insert(0, nodes.raw("", _TESTCASE_STATUS_CSS, format="html"))
167208
css_added = True
209+
# Preserve the existing link label and append the result annotation.
168210
ref.append(nodes.raw("", status_html, format="html"))
169211
ref[_ANNOTATED_ATTR] = True

0 commit comments

Comments
 (0)