[docs] 1# @Test suite for attaching need id references, TEST_NEED_ID_REFS_ATTACH_1, test, [IMPL_LNK_1]
2""":func:`attach_need_id_refs` over plain records and plain needs: no build involved,
3which is the point -- it is the seam a pre-analysed input file will feed."""
4
5import re
6from pathlib import PureWindowsPath
7from typing import Any
8
9import pytest
10
11from sphinx_codelinks.analyse.references import NeedIdRef
12from sphinx_codelinks.sphinx_extension.directives.src_trace import fill_remote_url
13from sphinx_codelinks.sphinx_extension.need_id_refs import attach_need_id_refs
14from sphinx_codelinks.sphinx_extension.string_links import REF_LINK_REGEX
15
16SHA = "a" * 40
17
18
19def _ref(need_id: str, lineno: int, **kwargs: Any) -> NeedIdRef:
20 path = kwargs.pop("path", "src/refs.cpp")
21 values: dict[str, Any] = {
22 "need_id": need_id,
23 "project": "src",
24 "marker": "@need-ids:",
25 "path": path,
26 "lineno": lineno,
27 "start_column": 13,
28 "end_column": 20,
29 "remote_url": f"https://example.com/blob/{SHA}/{path}#L{lineno}",
30 }
31 values.update(kwargs)
32 return NeedIdRef(**values)
33
34
35def _needs(*ids: str, docname: str = "index") -> dict[str, dict[str, Any]]:
36 return {need_id: {"docname": docname, "code_url": None} for need_id in ids}
37
38
39def test_one_entry_per_reference_sorted_and_deduplicated() -> None:
40 """Overlapping directives give every record twice; the list carries each once,
41 in ``(path, lineno, start_column)`` order whatever order the records came in."""
42 refs = [_ref("REQ_1", 9), _ref("REQ_1", 3), _ref("REQ_1", 3), _ref("REQ_1", 9)]
43 refs.append(_ref("REQ_1", 1, path="a/first.cpp"))
44 needs = _needs("REQ_1", "REQ_2")
45
46 result = attach_need_id_refs(refs, needs, fields={"src": "code_url"})
47
48 assert needs["REQ_1"]["code_url"] == [
49 f"https://example.com/blob/{SHA}/a/first.cpp#L1",
50 f"https://example.com/blob/{SHA}/src/refs.cpp#L3",
51 f"https://example.com/blob/{SHA}/src/refs.cpp#L9",
52 ]
53 assert needs["REQ_2"]["code_url"] is None
54 assert result.attached == {"src": 3}
55 assert result.unknown == []
56
57
58def test_unknown_ids_are_reported_not_attached() -> None:
59 needs = _needs("REQ_1")
60 result = attach_need_id_refs(
61 [_ref("REQ_1", 1), _ref("NOSUCH", 5)], needs, fields={"src": "code_url"}
62 )
63 assert [(ref.need_id, ref.path, ref.lineno) for ref in result.unknown] == [
64 ("NOSUCH", "src/refs.cpp", 5)
65 ]
66 assert result.attached == {"src": 1}
67
68
69def test_ids_are_matched_as_written() -> None:
70 """``::`` and ``.`` are part of the id: nothing is split or normalised."""
71 needs = _needs("A::B::C.d")
72 result = attach_need_id_refs(
73 [_ref("A::B::C.d", 2), _ref("A::B::C", 2)], needs, fields={"src": "code_url"}
74 )
75 assert needs["A::B::C.d"]["code_url"] == [
76 f"https://example.com/blob/{SHA}/src/refs.cpp#L2"
77 ]
78 assert [ref.need_id for ref in result.unknown] == ["A::B::C"]
79
80
81def test_two_projects_naming_one_field_share_the_list() -> None:
82 needs = _needs("REQ_1")
83 attach_need_id_refs(
84 [_ref("REQ_1", 4, project="one"), _ref("REQ_1", 2, project="two")],
85 needs,
86 fields={"one": "code_url", "two": "code_url"},
87 )
88 assert needs["REQ_1"]["code_url"] == [
89 f"https://example.com/blob/{SHA}/src/refs.cpp#L2",
90 f"https://example.com/blob/{SHA}/src/refs.cpp#L4",
91 ]
92
93
94def test_projects_without_a_field_are_ignored() -> None:
95 needs = _needs("REQ_1")
96 result = attach_need_id_refs(
97 [_ref("REQ_1", 4, project="off"), _ref("NOSUCH", 2, project="off")],
98 needs,
99 fields={"src": "code_url"},
100 )
101 assert needs["REQ_1"]["code_url"] is None
102 assert result.attached == {}
103 assert result.unknown == []
104
105
106def test_local_url_is_relative_to_the_needs_document() -> None:
107 """Without a remote URL the local one is used, relative to the document the need
108 is in -- as ``local-url`` is relative to the document of the need it is on."""
109 refs = [_ref("REQ_1", 3, remote_url=None, local_url="src/refs.cpp#L3")]
110 for docname, expected in [
111 ("index", "src/refs.cpp#L3"),
112 ("sub/deep", "../src/refs.cpp#L3"),
113 ]:
114 needs = _needs("REQ_1", docname=docname)
115 attach_need_id_refs(refs, needs, fields={"src": "code_url"})
116 assert needs["REQ_1"]["code_url"] == [expected]
117
118
119@pytest.mark.parametrize(
120 ("value", "url", "page"),
121 [
122 pytest.param(
123 f"https://example.com/blob/{SHA}/src/refs.cpp#L3",
124 f"https://example.com/blob/{SHA}/src/refs.cpp#L3",
125 None,
126 id="remote-url",
127 ),
128 pytest.param("../src/refs.cpp#L3", None, "../src/refs", id="local-value"),
129 pytest.param("foo", None, None, id="text"),
130 pytest.param("javascript:alert(1)", None, None, id="scheme-without-slashes"),
131 ],
132)
133def test_reference_field_link_kinds(
134 value: str, url: str | None, page: str | None
135) -> None:
136 """The field's string link: a ``scheme://`` URL links to itself, a local value to
137 its source page, anything else matches neither and renders as text."""
138 groups = re.search(REF_LINK_REGEX, value).groupdict() # ty: ignore[possibly-missing-attribute]
139 assert groups["codelinks_url"] == url
140 assert groups["codelinks_page"] == page
141
142
143def test_reference_remote_url_path_is_posix_for_windows_paths() -> None:
144 """The references' remote URL goes through ``fill_remote_url``, as a created
145 need's does: built from Windows paths, the URL path is still POSIX."""
146 url = fill_remote_url(
147 "https://github.com/o/r/blob/{commit}/{path}#L{line}",
148 SHA,
149 PureWindowsPath("src") / PureWindowsPath("deeper", "refs.cpp"), # ty: ignore[invalid-argument-type]
150 3,
151 )
152 assert url == f"https://github.com/o/r/blob/{SHA}/src/deeper/refs.cpp#L3"