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