1515Testcase needs are external needs. Their ``external_url`` is therefore also
1616the URL used by the ``testlink`` metadata rendered on requirements. The same
1717URL 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
2228from __future__ import annotations
2733from docutils import nodes
2834from 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.
3038RESULT_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 {
87102
88103
89104def _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
102122def _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