[docs] 1# @Test suite for the source order of marked content on one row, TEST_ANA_ORDER_1, test, [IMPL_LNK_1, IMPL_ONE_1]
2"""Marked content on one row is listed in source order, on every run (#2150).
3
4Tree-sitter hands the comments of a file over in an order that differs between two
5parses of the same source, so two entries on one row used to come out in a different
6order from run to run. Each construction is analysed ``RUNS`` times in one process: every
7run must give the same order, and that order must be the source order -- by row, then
8by column.
9"""
10
11from pathlib import Path
12from typing import Any
13
14import pytest
15
16from sphinx_codelinks.analyse.analyse import SourceAnalyse
17from sphinx_codelinks.config import (
18 NeedIdRefsConfig,
19 OneLineCommentStyle,
20 SourceAnalyseConfig,
21)
22from sphinx_codelinks.source_discover.config import CommentType, SourceDiscoverConfig
23from sphinx_codelinks.source_discover.source_discover import SourceDiscover
24
25RUNS = 20
26ROWS = 15
27
28#: the ``[[``...``]]`` style of the corpus case ``positions-need_and_ref_on_one_row``
29BRACKETS = {
30 "start_sequence": "[[",
31 "end_sequence": "]]",
32 "needs_fields": [
33 {"name": "id"},
34 {"name": "title"},
35 {"name": "type", "default": "impl"},
36 {"name": "links", "type": "list[str]", "default": []},
37 ],
38}
39
40
41def _rows(template: str) -> str:
42 return "".join(template.format(i=i) + "\n" for i in range(ROWS))
43
44
45#: name -> (source, configuration keywords); every row holds two comments
46CONSTRUCTIONS: dict[str, tuple[str, dict[str, Any]]] = {
47 "two_need_refs": (
48 _rows("/* @need-ids: REQ_A{i} */ /* @need-ids: REQ_B{i} */"),
49 {"get_need_id_refs": True},
50 ),
51 "two_needs": (
52 _rows("/* @A{i}, IMPL_A{i}, impl */ /* @B{i}, IMPL_B{i}, impl */"),
53 {"get_oneline_needs": True},
54 ),
55 "need_then_ref": (
56 _rows("/* [[IMPL_X{i}, X]] */ /* @need-ids: REQ_{i} */"),
57 {
58 "get_oneline_needs": True,
59 "get_need_id_refs": True,
60 "oneline_comment_style": OneLineCommentStyle(**BRACKETS),
61 },
62 ),
63 "ref_then_need": (
64 _rows("/* @need-ids: REQ_{i} */ /* [[IMPL_X{i}, X]] */"),
65 {
66 "get_oneline_needs": True,
67 "get_need_id_refs": True,
68 "oneline_comment_style": OneLineCommentStyle(**BRACKETS),
69 },
70 ),
71}
72
73
74def _analyse(root: Path, source: str, **config: Any) -> SourceAnalyse:
75 path = root / "case.cpp"
76 path.write_bytes(source.encode("utf-8"))
77 analyse = SourceAnalyse(
78 SourceAnalyseConfig(
79 src_files=[path],
80 src_dir=root,
81 comment_type=CommentType.cpp,
82 **config,
83 )
84 )
85 analyse.git_remote_url = None
86 analyse.git_commit_rev = None
87 analyse.run(log_summary=False)
88 return analyse
89
90
91def _starts(entries: list[Any]) -> list[tuple[int, int]]:
92 return [
93 (entry.source_map["start"]["row"], entry.source_map["start"]["column"])
94 for entry in entries
95 ]
96
97
98@pytest.mark.parametrize("name", CONSTRUCTIONS)
99def test_one_row_is_listed_in_source_order_every_run(tmp_path: Path, name: str) -> None:
100 """``all_marked_content`` -- what ``marked_content.json`` holds -- is in source
101 order on every run; so is ``oneline_needs``, the order ``src-trace`` creates the
102 needs in."""
103 source, config = CONSTRUCTIONS[name]
104 runs = [_analyse(tmp_path, source, **config) for _ in range(RUNS)]
105
106 orders = [_starts(run.all_marked_content) for run in runs]
107 assert len(orders[0]) == 2 * ROWS
108 assert orders[0] == sorted(orders[0])
109 assert all(order == orders[0] for order in orders)
110
111 needs = [_starts(run.oneline_needs) for run in runs]
112 assert needs[0] == sorted(needs[0])
113 assert all(order == needs[0] for order in needs)
114
115
116def test_a_reference_and_a_need_starting_at_one_position_list_the_reference_first(
117 tmp_path: Path,
118) -> None:
119 """Two entries can start at one position only with overlapping markers, which the
120 configuration check does not refuse today: here a need-id marker that ends with the
121 one-line start sequence. Such a tie is listed references first, then one-line needs
122 (then multi-line needs) -- the rule ubCode is to mirror (useblocks/ubcode#3935).
123 This case goes when overlapping markers become a configuration error.
124
125 Two references tying (one marker a suffix of another) need no case: they are of one
126 kind, and keep the marker order of the configuration through the stable sort.
127 """
128 analyse = _analyse(
129 tmp_path,
130 "// x ref[[IMPL_T, Tie]]\nvoid f() {}\n",
131 get_oneline_needs=True,
132 get_need_id_refs=True,
133 oneline_comment_style=OneLineCommentStyle(**BRACKETS),
134 need_id_refs_config=NeedIdRefsConfig(markers=["ref[["]),
135 )
136
137 assert [
138 (entry.type.value, entry.source_map["start"])
139 for entry in analyse.all_marked_content
140 ] == [
141 ("need-id-refs", {"row": 0, "column": 10}),
142 ("need", {"row": 0, "column": 10}),
143 ]
144
145
146def test_entries_are_ordered_by_where_they_start_not_where_they_end(
147 tmp_path: Path,
148) -> None:
149 """A one-line need whose title holds a need reference, under the default
150 configuration: both spans run to the end of the line, the need's from further left,
151 so the need is listed first. This pins START order against a sort on the end
152 position with a configuration that overlaps no markers, so it outlives the tie case
153 above. Whether a reference inside a need's title should be extracted at all is a
154 separate question (the overlapping-markers follow-up).
155 """
156 analyse = _analyse(
157 tmp_path,
158 "// @A title @need-ids: REQ_1, IMPL_A, impl\nvoid f() {}\n",
159 get_oneline_needs=True,
160 get_need_id_refs=True,
161 )
162
163 assert [
164 (entry.type.value, entry.source_map["start"])
165 for entry in analyse.all_marked_content
166 ] == [
167 ("need", {"row": 0, "column": 4}),
168 ("need-id-refs", {"row": 0, "column": 23}),
169 ]
170
171
172def test_one_line_needs_of_two_files_on_one_row_keep_the_file_order(
173 tmp_path: Path,
174) -> None:
175 """``oneline_needs`` -- the order ``src-trace`` creates and renders needs in -- is
176 by row, then file (in discovery order), then column: on one row the first file's
177 need comes first even where the other file's sits further left, and a later row of
178 the first file comes after both. ``all_marked_content`` is by file first."""
179 a = tmp_path / "a.cpp"
180 b = tmp_path / "b.cpp"
181 a.write_bytes(b"int x; // @A0, IMPL_A0, impl\n// @A1, IMPL_A1, impl\n")
182 b.write_bytes(b"// @B0, IMPL_B0, impl\n")
183 analyse = SourceAnalyse(
184 SourceAnalyseConfig(
185 src_files=[a, b],
186 src_dir=tmp_path,
187 comment_type=CommentType.cpp,
188 get_oneline_needs=True,
189 )
190 )
191 analyse.git_remote_url = None
192 analyse.git_commit_rev = None
193 analyse.run(log_summary=False)
194
195 assert [need.need["id"] for need in analyse.oneline_needs] == [
196 "IMPL_A0",
197 "IMPL_B0",
198 "IMPL_A1",
199 ]
200 assert [entry.need["id"] for entry in analyse.all_marked_content] == [
201 "IMPL_A0",
202 "IMPL_A1",
203 "IMPL_B0",
204 ]
205
206
207def test_the_file_order_on_one_row_is_the_discovery_order(tmp_path: Path) -> None:
208 """The file part of ``oneline_needs``' key is the one discovery sorts by, not the
209 ``Path``: a ``Path`` compares by parts and would put ``a/b.cpp`` before
210 ``a-c.cpp``, which discovery lists first."""
211 nested = tmp_path / "a" / "b.cpp"
212 nested.parent.mkdir()
213 nested.write_bytes(b"// @Nested, IMPL_NESTED, impl\n")
214 flat = tmp_path / "a-c.cpp"
215 flat.write_bytes(b"// @Flat, IMPL_FLAT, impl\n")
216 src_files = SourceDiscover(
217 SourceDiscoverConfig(src_dir=tmp_path, comment_type="cpp", gitignore=False)
218 ).source_paths
219 assert src_files == [flat.resolve(), nested.resolve()]
220 analyse = SourceAnalyse(
221 SourceAnalyseConfig(
222 src_files=src_files,
223 src_dir=tmp_path,
224 comment_type=CommentType.cpp,
225 get_oneline_needs=True,
226 )
227 )
228 analyse.git_remote_url = None
229 analyse.git_commit_rev = None
230 analyse.run(log_summary=False)
231
232 assert [need.need["id"] for need in analyse.oneline_needs] == [
233 "IMPL_FLAT",
234 "IMPL_NESTED",
235 ]