[docs] 1# @Test suite for source analysis configuration validation, TEST_CONF_1, test, [IMPL_OLP_1, IMPL_MLN_1]
2import pytest
3
4from sphinx_codelinks.config import (
5 MultilineNeedsConfig,
6 NeedIdRefsConfig,
7 OneLineCommentStyle,
8 SourceAnalyseConfig,
9 convert_analyse_config,
10 generate_project_configs,
11)
12
13from .conftest import TEST_DIR
14
15
16@pytest.mark.parametrize(
17 ("analyse_config", "result"),
18 [
19 (
20 SourceAnalyseConfig(
21 src_files=[
22 TEST_DIR / "data" / "dcdc" / "charge" / "demo_1.cpp",
23 ],
24 src_dir=TEST_DIR / "data" / "dcdc",
25 comment_type=123,
26 ),
27 [
28 "Schema validation error in field 'comment_type': 123 is not of type 'string'",
29 ],
30 ),
31 (
32 SourceAnalyseConfig(
33 src_files=None,
34 src_dir=TEST_DIR / "data" / "dcdc",
35 comment_type=123,
36 ),
37 [
38 "Schema validation error in field 'comment_type': 123 is not of type 'string'",
39 "Schema validation error in field 'src_files': None is not of type 'array'",
40 ],
41 ),
42 ],
43)
44def test_config_schema_validator_negative(analyse_config, result):
45 errors = analyse_config.check_schema()
46 assert sorted(errors) == sorted(result)
47
48
49@pytest.mark.parametrize(
50 "oneline_config, result",
51 [
52 (
53 OneLineCommentStyle(
54 start_sequence="[[",
55 end_sequence="]]",
56 field_split_char=",",
57 needs_fields=[
58 {"name": "title"},
59 {"name": "id"},
60 {"name": "type", "default": "impl"},
61 {"name": "links", "type": "list[]", "default": []}, # wrong type
62 ],
63 ),
64 [
65 "Schema validation error in need_fields 'links': 'list[]' is not one of ['str', 'list[str]']"
66 ],
67 ),
68 (
69 OneLineCommentStyle(
70 start_sequence="[[",
71 end_sequence="]]",
72 field_split_char=",",
73 needs_fields=[
74 {"name": "title"},
75 {"name": "id"},
76 {"name": "type", "default": 123}, # int is invalid
77 {"name": "links", "type": "list[str]", "default": []},
78 ],
79 ),
80 [
81 "Schema validation error in need_fields 'type': 123 is not of type 'string'"
82 ],
83 ),
84 (
85 OneLineCommentStyle(
86 start_sequence="[[",
87 end_sequence="]]",
88 field_split_char=",",
89 needs_fields=[
90 {"name": "title", "qwe": "qwe"}, # invalid qwe filed
91 {"name": "id"},
92 {"name": "type", "default": "impl"},
93 {"name": "links", "type": "list[str]", "default": []},
94 ],
95 ),
96 [
97 "Schema validation error in need_fields 'title': Additional properties are not allowed ('qwe' was unexpected)"
98 ],
99 ),
100 (
101 OneLineCommentStyle(
102 start_sequence="[[",
103 end_sequence="]]",
104 field_split_char=",",
105 needs_fields=[
106 {"name": "title"},
107 {"name": "id"},
108 {
109 "name": "type",
110 "type: ": "list[str]",
111 "default": "impl",
112 }, # wring combination of type and default
113 {"name": "links", "type": "list[str]", "default": []},
114 ],
115 ),
116 [
117 "Schema validation error in need_fields 'type': Additional properties are not allowed ('type: ' was unexpected)"
118 ],
119 ),
120 (
121 OneLineCommentStyle(
122 start_sequence="[[",
123 end_sequence="]]",
124 field_split_char=",",
125 needs_fields=[
126 {"name": "id"} # "title" and "type" are not given
127 ],
128 ),
129 ["Missing required fields: ['title', 'type']"],
130 ),
131 (
132 OneLineCommentStyle(
133 start_sequence="[[",
134 end_sequence="]]",
135 field_split_char=",",
136 needs_fields=[
137 {"name": "id"},
138 {"name": "id"}, # duplicate
139 ],
140 ),
141 [
142 "Missing required fields: ['title', 'type']",
143 "Field 'id' is defined multiple times.",
144 ],
145 ),
146 (
147 OneLineCommentStyle(
148 start_sequence=1234, # wrong type
149 end_sequence=5678,
150 field_split_char=2222,
151 needs_fields=[
152 {"name": "id"},
153 ],
154 ),
155 [
156 "Schema validation error in field 'field_split_char': 2222 is not of type 'string'",
157 "Schema validation error in field 'end_sequence': 5678 is not of type 'string'",
158 "Schema validation error in field 'start_sequence': 1234 is not of type 'string'",
159 "Missing required fields: ['title', 'type']",
160 ],
161 ),
162 (
163 OneLineCommentStyle(
164 start_sequence="@need",
165 end_sequence="\n",
166 field_split_char=",",
167 needs_fields=[
168 {"name": "id"},
169 {"name": "implements", "type": "list[str]", "default": []},
170 {"name": "type", "default": "impl"},
171 {"name": "title"}, # required after optional
172 ],
173 ),
174 [
175 "Field 'title' without a default follows field 'implements' which has a default. "
176 "Fields without defaults must be defined before fields with defaults.",
177 ],
178 ),
179 ],
180)
181def test_oneline_schema_validator_negative(oneline_config, result):
182 errors = oneline_config.check_fields_configuration()
183 assert sorted(errors) == sorted(result)
184
185
186@pytest.mark.parametrize(
187 "oneline_config",
188 [
189 OneLineCommentStyle(
190 start_sequence="[[",
191 end_sequence="]]",
192 field_split_char=",",
193 needs_fields=[
194 {"name": "title"},
195 {"name": "id"},
196 {"name": "type", "default": "impl"},
197 {"name": "links", "type": "list[str]", "default": []},
198 ],
199 ),
200 OneLineCommentStyle(
201 start_sequence="[[",
202 end_sequence="]]",
203 field_split_char=",",
204 needs_fields=[
205 {"name": "title"}, # minimum need_fields config
206 {"name": "type"},
207 ],
208 ),
209 OneLineCommentStyle(
210 needs_fields=[ # minimum config
211 {"name": "title"},
212 {"name": "type"},
213 ],
214 ),
215 ],
216)
217def test_oneline_schema_validator_positive(oneline_config):
218 assert len(oneline_config.check_fields_configuration()) == 0
219
220
221def test_multiline_needs_config_defaults_are_valid() -> None:
222 config = MultilineNeedsConfig()
223
224 assert (config.start_sequence, config.end_sequence) == ("@need", "@endneed")
225 assert config.default_markup == "rst"
226 assert config.markups == {"rst": ".rst", "md": ".md"}
227 assert config.check_fields_configuration() == []
228
229
230@pytest.mark.parametrize(
231 ("kwargs", "error"),
232 [
233 pytest.param(
234 {"default_markup": "myst"},
235 "default_markup 'myst' is not a key of markups ('md', 'rst').",
236 id="default_markup_not_in_markups",
237 ),
238 pytest.param(
239 {"start_sequence": "@block", "end_sequence": "@block"},
240 "start_sequence and end_sequence cannot be the same.",
241 id="same_sequences",
242 ),
243 pytest.param(
244 {"start_sequence": ""},
245 "Schema validation error in field 'start_sequence': ",
246 id="empty_start",
247 ),
248 pytest.param(
249 {"markups": {"rst": 1}},
250 "Schema validation error in field 'markups': 1 is not of type 'string'",
251 id="markup_suffix_not_a_string",
252 ),
253 ],
254)
255def test_multiline_needs_config_negative(kwargs: dict, error: str) -> None:
256 errors = MultilineNeedsConfig(**kwargs).check_fields_configuration()
257
258 assert any(message.startswith(error) for message in errors), errors
259
260
261def test_multiline_needs_config_errors_reach_the_analyse_config() -> None:
262 """Checked when the feature is on, under their own heading."""
263 config = SourceAnalyseConfig(
264 get_multiline_needs=True,
265 multiline_needs_config=MultilineNeedsConfig(default_markup="txt"),
266 )
267
268 errors = config.check_fields_configuration()
269
270 assert "MultilineNeeds configuration errors:" in errors
271 assert "default_markup 'txt' is not a key of markups ('md', 'rst')." in errors
272 assert (
273 SourceAnalyseConfig(
274 multiline_needs_config=MultilineNeedsConfig(default_markup="txt")
275 ).check_fields_configuration()
276 == []
277 ), "not checked while the feature is off"
278
279
280def test_multiline_markers_are_checked_only_when_the_feature_is_on() -> None:
281 """A reference marker ``@need`` (valid before multi-line needs existed) is a clash
282 only for a project that switches multi-line needs on."""
283 refs = NeedIdRefsConfig(markers=["@need"])
284
285 assert (
286 SourceAnalyseConfig(need_id_refs_config=refs).check_markers_mutually_exclusive()
287 == []
288 )
289 assert SourceAnalyseConfig(
290 get_multiline_needs=True, need_id_refs_config=refs
291 ).check_markers_mutually_exclusive() == ["Marker @need is defined multiple times"]
292
293
294def test_multiline_markers_join_the_mutual_exclusion_check() -> None:
295 """Equality only: the default one-line start ``@`` is a prefix of ``@need`` and that
296 is not a clash; a multi-line marker equal to another marker is."""
297 assert SourceAnalyseConfig().check_markers_mutually_exclusive() == []
298 clash = SourceAnalyseConfig(
299 get_multiline_needs=True,
300 multiline_needs_config=MultilineNeedsConfig(start_sequence="@need-ids:"),
301 )
302 assert clash.check_markers_mutually_exclusive() == [
303 "Marker @need-ids: is defined multiple times"
304 ]
305
306
307def test_convert_analyse_config_reads_the_multiline_needs_table() -> None:
308 config = convert_analyse_config(
309 {
310 "get_multiline_needs": True,
311 "multiline_needs": {
312 "start_sequence": "@spec",
313 "end_sequence": "@endspec",
314 "default_markup": "myst",
315 "markups": {"myst": ".md"},
316 },
317 }
318 )
319
320 assert config.get_multiline_needs is True
321 assert config.multiline_needs_config == MultilineNeedsConfig(
322 start_sequence="@spec",
323 end_sequence="@endspec",
324 default_markup="myst",
325 markups={"myst": ".md"},
326 )
327
328
329def test_multiline_needs_are_not_forced_on() -> None:
330 """Unlike ``get_oneline_needs`` (#1889), nothing switches the extraction on."""
331 projects: dict = {"p": {"source_discover": {"src_dir": "."}, "analyse": {}}}
332
333 generate_project_configs(projects)
334
335 assert projects["p"]["analyse_config"].get_multiline_needs is False
336
337
338def test_convert_multiline_needs_config_rejects_an_unknown_key() -> None:
339 with pytest.raises(TypeError, match="Invalid multiline_needs configuration"):
340 convert_analyse_config({"multiline_needs": {"open": "@need"}})
341
342
343@pytest.mark.parametrize(
344 ("section", "named"),
345 [
346 ({"get_rst": True}, "'get_rst' is no longer supported"),
347 ({"marked_rst": {}}, "'[analyse.marked_rst]' is no longer supported"),
348 ],
349)
350def test_convert_analyse_config_names_the_replacement_of_a_removed_key(
351 section: dict, named: str
352) -> None:
353 """A TypeError, as every other bad key of the section raises, but naming the cure."""
354 with pytest.raises(TypeError) as excinfo:
355 convert_analyse_config(section)
356
357 assert named in str(excinfo.value)
358 assert "multi-line needs" in str(excinfo.value)
359
360
361def test_an_unknown_analyse_key_still_fails_in_the_constructor() -> None:
362 """The unknown-key policy for every other key is unchanged."""
363 with pytest.raises(TypeError, match="unexpected keyword argument 'foo'"):
364 convert_analyse_config({"foo": 1})