44import importlib
55import importlib .metadata
66import re
7- import textwrap
87
98import 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+
3852def _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