[docs] 1# @Test suite for the warning on a marker above a preprocessor conditional, TEST_ABOVE_COND_1, test, [IMPL_EXTR_1]
2"""A C or C++ marker that attachment rule 7 places on nothing -- alone on its line,
3outside a function's body, directly above a preprocessor conditional's line -- is
4warned about: one ``marker_above_conditional`` warning per marker, at its line, typed as
5the marker (``analyse.rst``, rule 7). Every other marker on nothing, and every marker the
6rule does not reach, stays silent.
7
8The analyser cases run ``SourceAnalyse`` on one file; the build cases build a copy of
9``doc_test/need_id_refs`` (see ``test_need_id_refs``), whose ``index`` traces the whole of
10project ``src``, with ``src/refs.cpp`` reduced to one reference that resolves.
11"""
12
13import re
14from pathlib import Path
15
16import pytest
17from typer.testing import CliRunner
18
19from sphinx_codelinks.analyse.analyse import (
20 ABOVE_CONDITIONAL_MSG,
21 AnalyseWarning,
22 SourceAnalyse,
23)
24from sphinx_codelinks.analyse.models import WarningSubTypeEnum
25from sphinx_codelinks.cmd import app as cli
26from sphinx_codelinks.config import PreprocessorConfig, SourceAnalyseConfig
27from sphinx_needs_testkit import assert_no_warnings, build_warnings
28
29from .test_config_only_refs import NO_DIRECTIVE
30from .test_need_id_refs import _SHOWS_WARNING_TYPES, _build, _json, _MakeApp, _project
31
32#: The message, as both implementations of the attachment rules emit it
33MSG = (
34 "directly above a preprocessor conditional, so attached to no entity: "
35 "put the marker inside the arm, above the definition"
36)
37
38SUB_TYPE = "marker_above_conditional"
39
40#: A one-arm conditional, the line right after the marker in most cases
41ARM = "#if FEATURE_B\nvoid active() {}\n#endif\n"
42
43
44def _analyse(
45 tmp_path: Path, source: str, *, preprocessor: PreprocessorConfig | None = None
46) -> SourceAnalyse:
47 """Every extractor run over ``source``, written to ``x.cpp``."""
48 path = tmp_path / "x.cpp"
49 path.write_text(source, encoding="utf-8")
50 config = SourceAnalyseConfig(
51 src_files=[path],
52 src_dir=tmp_path,
53 get_oneline_needs=True,
54 get_need_id_refs=True,
55 get_multiline_needs=True,
56 preprocessor=preprocessor,
57 )
58 analyse = SourceAnalyse(config)
59 analyse.git_remote_url = None
60 analyse.git_commit_rev = None
61 analyse.run(log_summary=False)
62 return analyse
63
64
65def _above(analyse: SourceAnalyse) -> list[tuple[int, str]]:
66 """The ``(lineno, type)`` of each ``marker_above_conditional`` warning, sorted."""
67 return sorted(
68 (warning.lineno, warning.type)
69 for warning in analyse.warnings
70 if warning.sub_type == SUB_TYPE
71 )
72
73
74def test_the_message_is_the_shared_one() -> None:
75 """The text is a contract with the other implementation of the attachment rules,
76 byte for byte, and the sub-type is a ``WarningSubTypeEnum`` member."""
77 assert ABOVE_CONDITIONAL_MSG == MSG
78 assert WarningSubTypeEnum.marker_above_conditional.value == SUB_TYPE
79
80
81def test_a_one_line_need_warns_with_every_field(tmp_path: Path) -> None:
82 """One warning, every field as the other analyser warnings write it: the file, the
83 marker's 1-based line, the marker's kind, the sub-type and the shared message."""
84 analyse = _analyse(
85 tmp_path, "namespace lib {\n// @Above, IMPL_ABOVE, impl, []\n" + ARM + "}\n"
86 )
87
88 assert analyse.warnings == [
89 AnalyseWarning(str(tmp_path / "x.cpp"), 2, MSG, "need", SUB_TYPE)
90 ]
91 assert analyse.oneline_needs[0].scope is None
92
93
94@pytest.mark.parametrize(
95 ("source", "expected"),
96 [
97 pytest.param(
98 "class Widget {\n // @need-ids: REQ_A, REQ_B\n" + ARM + "};\n",
99 [(2, "need-id-refs")],
100 id="need-id-reference",
101 ),
102 pytest.param(
103 "namespace lib {\n// @need impl: Above\n// :id: IMPL_ML\n// @endneed\n"
104 + ARM
105 + "}\n",
106 [(2, "multiline-need")],
107 id="multi-line-need-at-its-open-line",
108 ),
109 pytest.param(
110 "namespace lib {\n/*\n * @First, IMPL_A, impl, []\n"
111 " * @Second, IMPL_B, impl, []\n */\n" + ARM + "}\n",
112 [(3, "need"), (4, "need")],
113 id="two-markers-in-one-comment",
114 ),
115 pytest.param(
116 "namespace lib {\n// @First, IMPL_A, impl, []\n"
117 "// @need-ids: REQ_A\n" + ARM + "}\n",
118 [(2, "need"), (3, "need-id-refs")],
119 id="a-need-and-a-reference-in-one-comment-run",
120 ),
121 pytest.param(
122 "void first() {}\n// @At file level, IMPL_FILE, impl, []\n" + ARM,
123 [(2, "need")],
124 id="file-level",
125 ),
126 pytest.param(
127 "class Widget {\n#if FEATURE_B\n void active() {}\n"
128 " // @Above else, IMPL_ELSE, impl, []\n#else\n void fallback() {}\n"
129 "#endif\n};\n",
130 [(4, "need")],
131 id="above-else",
132 ),
133 ],
134)
135def test_each_marker_rule_7_places_on_nothing_warns(
136 tmp_path: Path, source: str, expected: list[tuple[int, str]]
137) -> None:
138 """Every marker kind warns, at its own line, typed as itself: one warning per
139 marker, so a comment holding two gives two."""
140 assert _above(_analyse(tmp_path, source)) == expected
141
142
143@pytest.mark.parametrize(
144 "source",
145 [
146 pytest.param(
147 "namespace lib {\nvoid first() {} // @Trailing, IMPL_T, impl, []\n"
148 + ARM
149 + "}\n",
150 id="trailing",
151 ),
152 pytest.param(
153 "void outer() {\n // @In the body, IMPL_B, impl, []\n"
154 "#if FEATURE_B\n fast();\n#endif\n}\n",
155 id="function-body",
156 ),
157 pytest.param(
158 "namespace lib {\n// @Blank line, IMPL_BL, impl, []\n\n" + ARM + "}\n",
159 id="blank-line-between",
160 ),
161 pytest.param(
162 "namespace lib {\n// @Above a define, IMPL_D, impl, []\n"
163 "#define FAST 1\nvoid active() {}\n}\n",
164 id="define-above",
165 ),
166 pytest.param(
167 "// @File header, IMPL_H, impl, []\n\n" + ARM, id="file-header-comment"
168 ),
169 pytest.param(
170 "void first() {}\n// @At the end, IMPL_E, impl, []\n",
171 id="file-level-with-nothing-after",
172 ),
173 pytest.param(
174 "namespace lib {\n#if FEATURE_B\n// @Inside the arm, IMPL_ARM, impl, []\n"
175 "void active() {}\n#endif\n}\n",
176 id="inside-the-arm",
177 ),
178 ],
179)
180def test_a_marker_rule_7_does_not_place_stays_silent(
181 tmp_path: Path, source: str
182) -> None:
183 """No warning where rule 7 does not decide: a trailing marker, one in a function's
184 body, one with a blank line or another directive between, and a marker on nothing
185 by another rule (a file header, nothing after it at file level)."""
186 analyse = _analyse(tmp_path, source)
187
188 assert analyse.all_marked_content
189 assert analyse.warnings == []
190
191
192def test_the_libclang_engine_never_warns(tmp_path: Path) -> None:
193 """The libclang engine attaches no marker to an entity, so rule 7 never decides
194 there: the marker is extracted, and nothing is warned."""
195 pytest.importorskip("clang.cindex")
196 analyse = _analyse(
197 tmp_path,
198 "namespace lib {\n// @Above, IMPL_ABOVE, impl, []\n" + ARM + "}\n",
199 preprocessor=PreprocessorConfig(defines=[]),
200 )
201
202 assert [need.need["id"] for need in analyse.oneline_needs] == ["IMPL_ABOVE"]
203 assert analyse.warnings == []
204
205
206def test_codelinks_analyse_prints_it(tmp_path: Path) -> None:
207 """The CLI prints it in the one shape every analyser warning takes."""
208 (tmp_path / "x.cpp").write_text(
209 "namespace lib {\n// @Above, IMPL_ABOVE, impl, []\n" + ARM + "}\n",
210 encoding="utf-8",
211 )
212 config = tmp_path / "ubproject.toml"
213 config.write_text(
214 "[codelinks.projects.p.source_discover]\n"
215 'src_dir = "./"\n'
216 'comment_type = "cpp"\n'
217 "[codelinks.projects.p.analyse]\n"
218 "get_oneline_needs = true\n",
219 encoding="utf-8",
220 )
221 outdir = tmp_path / "out"
222 outdir.mkdir()
223
224 result = CliRunner().invoke(cli, ["analyse", str(config), "--outdir", str(outdir)])
225
226 assert result.exit_code == 0, result.output
227 # rich wraps the console at its width, anywhere in a long path: compare without
228 # whitespace
229 expected = (
230 f"Analyse warning in {tmp_path.resolve() / 'x.cpp'}:2 - {SUB_TYPE}: {MSG}"
231 )
232 assert re.sub(r"\s+", "", expected) in re.sub(r"\s+", "", result.output)
233
234
235#: ``refs.cpp`` with nothing to warn about
236REFS = {"src/refs.cpp": "// @need-ids: REQ_001\nvoid implements_a() {}\n"}
237
238#: line 2 is a one-line need directly above a conditional in a namespace
239ABOVE = {"src/x.cpp": "namespace lib {\n// @Above, IMPL_ABOVE\n" + ARM + "}\n"}
240
241
242def test_the_build_warns_at_the_marker_s_line(
243 tmp_path: Path, make_app: _MakeApp
244) -> None:
245 """One warning at the file and line, the sub-type leading the message, typed
246 ``codelinks.marker_above_conditional``; the need is still created, on no entity."""
247 _project(tmp_path, files={**REFS, **ABOVE})
248 app = _build(tmp_path, make_app)
249
250 suffix = " [codelinks.marker_above_conditional]" if _SHOWS_WARNING_TYPES else ""
251 assert app.statuscode == 0
252 assert build_warnings(app) == [f"src/x.cpp:2: WARNING: {SUB_TYPE}: {MSG}{suffix}"]
253 assert "IMPL_ABOVE" in _json(app)["needs"]
254
255
256@pytest.mark.parametrize(
257 ("suppressed", "silenced"),
258 [
259 pytest.param(["codelinks.marker_above_conditional"], True, id="its-own"),
260 pytest.param(["codelinks.oneline"], False, id="oneline"),
261 pytest.param(["codelinks.multiline_need"], False, id="multiline-need"),
262 ],
263)
264def test_suppress_warnings_silences_it(
265 tmp_path: Path, make_app: _MakeApp, suppressed: list[str], silenced: bool
266) -> None:
267 """Its own ``suppress_warnings`` entry silences it, and ``codelinks.oneline`` and
268 ``codelinks.multiline_need`` do not: they are separate sub-types."""
269 _project(tmp_path, files={**REFS, **ABOVE})
270 app = _build(tmp_path, make_app, confoverrides={"suppress_warnings": suppressed})
271
272 if silenced:
273 assert_no_warnings(app)
274 else:
275 suffix = " [codelinks.marker_above_conditional]" if _SHOWS_WARNING_TYPES else ""
276 assert build_warnings(app) == [
277 f"src/x.cpp:2: WARNING: {SUB_TYPE}: {MSG}{suffix}"
278 ]
279 assert "IMPL_ABOVE" in _json(app)["needs"]
280
281
282def test_a_config_only_project_reports_none(tmp_path: Path, make_app: _MakeApp) -> None:
283 """A project no directive traces reports no analyser warning, this one included."""
284 _project(tmp_path, files={**REFS, **NO_DIRECTIVE, **ABOVE})
285 app = _build(tmp_path, make_app)
286
287 assert_no_warnings(app)