[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})