[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"