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