Skip to content

Commit 4adec22

Browse files
committed
cuda_bindings: improve dict/__doc__ parity test (Doxygen :: strip, deprecated skips)
Add _strip_doxygen_double_colon_prefixes to remove Doxygen :: before CUDA identifiers in explanation dict text (not C++ Foo::Bar scope), and use it in _explanation_dict_text_for_cleaned_doc_compare. Add small unit tests. Refactor test_explanations_dict_matches_cleaned_enum_docstrings to parametrize per enum member so pytest can report per-case skips and failures. Skip comparison when __doc__ is missing, or when strip().endswith('[Deprecated]') (stub-only or suffix deprecation notes). Drop unused textwrap import. Made-with: Cursor
1 parent f4094c5 commit 4adec22

1 file changed

Lines changed: 74 additions & 39 deletions

File tree

cuda_bindings/tests/test_enum_explanations.py

Lines changed: 74 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import importlib
55
import importlib.metadata
66
import re
7-
import textwrap
87

98
import pytest
109

@@ -35,15 +34,30 @@ def _explanation_text_from_dict_value(value):
3534
return value
3635

3736

37+
def _strip_doxygen_double_colon_prefixes(s: str) -> str:
38+
"""Remove Doxygen-style ``::`` before CUDA identifiers in header-comment text.
39+
40+
Matches ``::`` only when it *starts* a reference (not C++ scope between two names):
41+
use a negative lookbehind so ``Foo::Bar`` keeps the inner ``::``.
42+
43+
Applied repeatedly so ``::a ::b`` becomes ``a b``.
44+
"""
45+
prev = None
46+
while prev != s:
47+
prev = s
48+
s = re.sub(r"(?<![A-Za-z0-9_])::+([A-Za-z_][A-Za-z0-9_]*)", r"\1", s)
49+
return s
50+
51+
3852
def _explanation_dict_text_for_cleaned_doc_compare(value) -> str:
3953
"""Normalize hand-maintained dict text to compare with ``clean_enum_member_docstring`` output.
4054
41-
Dicts follow CUDA header comments (``::cuInit()``-style refs); cleaned enum ``__doc__``
42-
uses plain names after Sphinx role stripping. Strip a leading ``::`` before ``name(`` and
43-
collapse whitespace so both sides use the same conventions as ``clean_enum_member_docstring``.
55+
Dicts use Doxygen ``::Symbol`` for APIs, types, and constants; cleaned enum ``__doc__``
56+
uses plain names after Sphinx role stripping. Strip those ``::`` prefixes on the fly,
57+
then collapse whitespace like ``clean_enum_member_docstring``.
4458
"""
4559
s = _explanation_text_from_dict_value(value)
46-
s = re.sub(r"::([a-zA-Z_][a-zA-Z0-9_]*\()", r"\1", s)
60+
s = _strip_doxygen_double_colon_prefixes(s)
4761
s = re.sub(r"\s+", " ", s).strip()
4862
return s
4963

@@ -105,25 +119,55 @@ def test_clean_enum_member_docstring_none_input():
105119
assert clean_enum_member_docstring(None) is None
106120

107121

122+
@pytest.mark.parametrize(
123+
("raw", "expected"),
124+
[
125+
pytest.param("see ::CUDA_SUCCESS", "see CUDA_SUCCESS", id="type_ref"),
126+
pytest.param("Foo::Bar unchanged", "Foo::Bar unchanged", id="cpp_scope_preserved"),
127+
pytest.param("::cuInit() and ::CUstream", "cuInit() and CUstream", id="multiple_prefixes"),
128+
],
129+
)
130+
def test_strip_doxygen_double_colon_prefixes(raw, expected):
131+
assert _strip_doxygen_double_colon_prefixes(raw) == expected
132+
133+
134+
def _enum_docstring_parity_cases():
135+
for module_name, dict_name, enum_type in _EXPLANATION_MODULES:
136+
for error in enum_type:
137+
yield pytest.param(
138+
module_name,
139+
dict_name,
140+
error,
141+
id=f"{enum_type.__name__}.{error.name}",
142+
)
143+
144+
108145
@pytest.mark.xfail(
109146
reason=(
110-
"Even after clean_enum_member_docstring and dict-side ::/whitespace alignment, "
111-
"some members still differ (e.g. [Deprecated] stub vs full paragraph in dict; "
112-
"wording drift). Remove xfail when dicts and generated docstrings share one source."
147+
"Some members still differ after clean_enum_member_docstring and dict-side "
148+
"::/whitespace alignment (wording drift, etc.). [Deprecated] stubs are skipped. "
149+
"Remove xfail when dicts and generated docstrings share one source."
113150
),
114151
strict=False,
115152
)
116-
@pytest.mark.parametrize("module_name,dict_name,enum_type", _EXPLANATION_MODULES)
117-
def test_explanations_dict_matches_cleaned_enum_docstrings(module_name, dict_name, enum_type):
153+
@pytest.mark.parametrize(
154+
"module_name,dict_name,error",
155+
list(_enum_docstring_parity_cases()),
156+
)
157+
def test_explanations_dict_matches_cleaned_enum_docstrings(module_name, dict_name, error):
118158
"""Hand-maintained explanation dict entries should match cleaned enum ``__doc__`` text.
119159
120160
cuda-bindings 13.2+ attaches per-member documentation on driver ``CUresult`` and
121161
runtime ``cudaError_t``. This compares ``clean_enum_member_docstring(member.__doc__)``
122162
to dict text normalized with ``_explanation_dict_text_for_cleaned_doc_compare`` (same
123163
whitespace rules; strip Doxygen ``::`` before ``name(`` to align with Sphinx output).
124164
125-
Marked xfail while mismatches remain; run ``pytest --runxfail`` on this test for the
126-
full mismatch report (normalized dict vs cleaned ``__doc__``).
165+
Members whose ``__doc__`` is the ``[Deprecated]`` stub alone, or ends with
166+
``[Deprecated]`` after stripping whitespace, are skipped (dicts may keep longer
167+
text; we do not compare those).
168+
169+
Marked xfail while any non-skipped member still mismatches; many cases already match
170+
(reported as xpassed when this mark is present).
127171
"""
128172
if _get_binding_version() < _MIN_BINDING_VERSION_FOR_DOCSTRING_COMPARE:
129173
pytest.skip(
@@ -134,33 +178,24 @@ def test_explanations_dict_matches_cleaned_enum_docstrings(module_name, dict_nam
134178
mod = importlib.import_module(f"cuda.bindings._utils.{module_name}")
135179
expl_dict = getattr(mod, dict_name)
136180

137-
mismatches = []
138-
for error in enum_type:
139-
code = int(error)
140-
assert code in expl_dict
141-
expected = _explanation_dict_text_for_cleaned_doc_compare(expl_dict[code])
142-
raw_doc = error.__doc__
143-
if raw_doc is None:
144-
continue
145-
actual = clean_enum_member_docstring(raw_doc)
146-
if expected != actual:
147-
mismatches.append((error, expected, actual))
148-
149-
if not mismatches:
150-
return
151-
152-
lines = [
153-
f"{len(mismatches)} enum member(s) where normalized dict text != clean_enum_member_docstring(__doc__):",
154-
]
155-
for error, expected, actual in mismatches[:15]:
156-
lines.append(f" {error!r}")
157-
lines.append(" dict (normalized for compare):")
158-
lines.extend(" | " + ln for ln in textwrap.wrap(repr(expected), width=100) or [""])
159-
lines.append(" cleaned __doc__:")
160-
lines.extend(" | " + ln for ln in textwrap.wrap(repr(actual), width=100) or [""])
161-
if len(mismatches) > 15:
162-
lines.append(f" ... and {len(mismatches) - 15} more")
163-
pytest.fail("\n".join(lines))
181+
code = int(error)
182+
assert code in expl_dict
183+
184+
raw_doc = error.__doc__
185+
if raw_doc is not None and raw_doc.strip().endswith("[Deprecated]"):
186+
pytest.skip(f"SKIPPED: {error.name} is deprecated (__doc__ is or ends with [Deprecated])")
187+
188+
if raw_doc is None:
189+
pytest.skip(f"SKIPPED: {error.name} has no __doc__")
190+
191+
expected = _explanation_dict_text_for_cleaned_doc_compare(expl_dict[code])
192+
actual = clean_enum_member_docstring(raw_doc)
193+
if expected != actual:
194+
pytest.fail(
195+
f"normalized dict != cleaned __doc__ for {error!r}:\n"
196+
f" dict (normalized for compare): {expected!r}\n"
197+
f" cleaned __doc__: {actual!r}"
198+
)
164199

165200

166201
@pytest.mark.parametrize("module_name,dict_name,enum_type", _EXPLANATION_MODULES)

0 commit comments

Comments
 (0)