[docs] 1# @Test suite for source code analysis and marker extraction, TEST_ANA_1, test, [IMPL_LNK_1, IMPL_ONE_1, IMPL_MLN_1]
2import json
3from pathlib import Path
4
5import pytest
6
7from sphinx_codelinks.analyse.analyse import SourceAnalyse, _count
8from sphinx_codelinks.config import SourceAnalyseConfig
9from sphinx_codelinks.source_discover.config import CommentType
10from tests.conftest import (
11 ONELINE_COMMENT_STYLE,
12 ONELINE_COMMENT_STYLE_DEFAULT,
13 TEST_DIR,
14)
15
16TEST_DATA_DIR = Path(__file__).parent.parent / "tests" / "data"
17
18
19@pytest.mark.parametrize(
20 ("src_dir", "src_paths"),
21 [
22 (
23 TEST_DATA_DIR,
24 [
25 TEST_DATA_DIR / "oneline_comment_default" / "default_oneliners.c",
26 TEST_DATA_DIR / "need_id_refs" / "dummy_1.cpp",
27 TEST_DATA_DIR / "multiline_needs" / "dummy_1.cpp",
28 ],
29 )
30 ],
31)
32def test_analyse(src_dir, src_paths, tmp_path, snapshot_marks):
33 src_analyse_config = SourceAnalyseConfig(
34 src_files=src_paths,
35 src_dir=src_dir,
36 get_need_id_refs=True,
37 get_oneline_needs=True,
38 get_multiline_needs=True,
39 )
40
41 analyse = SourceAnalyse(src_analyse_config)
42 analyse.git_remote_url = None
43 analyse.git_commit_rev = None
44 analyse.run()
45 analyse.dump_marked_content(tmp_path)
46
47 dumped_content = tmp_path / "marked_content.json"
48 with dumped_content.open("r") as f:
49 marked_content = json.load(f)
50 # normalize filepath
51 for obj in marked_content:
52 obj["filepath"] = (
53 Path(obj["filepath"]).relative_to(src_analyse_config.src_dir)
54 ).as_posix()
55 if "source" in obj:
56 # a multi-line need's path is relative to the git root when the checkout is
57 # a repository, else to src_dir: pin it against src_dir, as filepath is
58 assert obj["source"].pop("root") in ("git", "src_dir")
59 assert obj["source"]["path"].endswith(obj["filepath"])
60 obj["source"]["path"] = obj["filepath"]
61 assert marked_content == snapshot_marks
62
63
64@pytest.mark.parametrize(
65 "src_dir, src_paths , oneline_comment_style, result",
66 [
67 (
68 TEST_DIR / "data" / "dcdc",
69 [
70 TEST_DIR / "data" / "dcdc" / "charge" / "demo_1.cpp",
71 TEST_DIR / "data" / "dcdc" / "charge" / "demo_2.cpp",
72 TEST_DIR / "data" / "dcdc" / "discharge" / "demo_3.cpp",
73 TEST_DIR / "data" / "dcdc" / "supercharge.cpp",
74 ],
75 ONELINE_COMMENT_STYLE,
76 {
77 "num_src_files": 4,
78 "num_uncached_files": 4,
79 "num_cached_files": 0,
80 "num_comments": 29,
81 "num_oneline_warnings": 0,
82 },
83 ),
84 (
85 TEST_DIR / "data" / "oneline_comment_basic",
86 [
87 TEST_DIR / "data" / "oneline_comment_basic" / "basic_oneliners.c",
88 ],
89 ONELINE_COMMENT_STYLE,
90 {
91 "num_src_files": 1,
92 "num_uncached_files": 1,
93 "num_cached_files": 0,
94 "num_comments": 14,
95 "num_oneline_warnings": 0,
96 },
97 ),
98 (
99 TEST_DIR / "data" / "oneline_comment_default",
100 [
101 TEST_DIR / "data" / "oneline_comment_default" / "default_oneliners.c",
102 ],
103 ONELINE_COMMENT_STYLE_DEFAULT,
104 {
105 "num_src_files": 1,
106 "num_uncached_files": 1,
107 "num_cached_files": 0,
108 "num_comments": 5,
109 "num_oneline_warnings": 1,
110 },
111 ),
112 (
113 TEST_DIR / "data" / "rust",
114 [
115 TEST_DIR / "data" / "rust" / "demo.rs",
116 ],
117 ONELINE_COMMENT_STYLE_DEFAULT,
118 {
119 "num_src_files": 1,
120 "num_uncached_files": 1,
121 "num_cached_files": 0,
122 "num_comments": 6,
123 "num_oneline_warnings": 0,
124 },
125 ),
126 (
127 TEST_DIR / "data" / "jsonc",
128 [
129 TEST_DIR / "data" / "jsonc" / "demo.jsonc",
130 ],
131 ONELINE_COMMENT_STYLE_DEFAULT,
132 {
133 "num_src_files": 1,
134 "num_uncached_files": 1,
135 "num_cached_files": 0,
136 "num_comments": 4,
137 "num_oneline_warnings": 0,
138 "comment_type": CommentType.jsonc,
139 },
140 ),
141 ],
142)
143def test_analyse_oneline_needs(
144 tmp_path, src_dir, src_paths, oneline_comment_style, result
145):
146 src_analyse_config = SourceAnalyseConfig(
147 src_files=src_paths,
148 src_dir=src_dir,
149 get_need_id_refs=False,
150 get_oneline_needs=True,
151 get_multiline_needs=False,
152 oneline_comment_style=oneline_comment_style,
153 comment_type=result.get("comment_type", CommentType.cpp),
154 )
155 src_analyse = SourceAnalyse(src_analyse_config)
156 src_analyse.run()
157
158 assert len(src_analyse.src_files) == result["num_src_files"]
159 assert len(src_analyse.warnings) == result["num_oneline_warnings"]
160
161 cnt_comments = 0
162 for src_file in src_analyse.src_files:
163 cnt_comments += len(src_file.src_comments)
164 assert cnt_comments == result["num_comments"]
165
166
167def test_explicit_git_root_configuration(tmp_path):
168 """Test that explicit git_root configuration is used instead of auto-detection."""
169 # Create a fake git repo structure in tmp_path
170 fake_git_root = tmp_path / "fake_repo"
171 fake_git_root.mkdir()
172 (fake_git_root / ".git").mkdir()
173
174 # Create a minimal .git/config with remote URL
175 git_config = fake_git_root / ".git" / "config"
176 git_config.write_text(
177 '[remote "origin"]\n url = https://github.com/test/repo.git\n'
178 )
179
180 # Create HEAD file pointing to a branch ref
181 git_head = fake_git_root / ".git" / "HEAD"
182 git_head.write_text("ref: refs/heads/main\n")
183
184 # Create the refs/heads/main file with the commit hash
185 refs_dir = fake_git_root / ".git" / "refs" / "heads"
186 refs_dir.mkdir(parents=True)
187 (refs_dir / "main").write_text("abc123def456\n")
188
189 # Create source file in a deeply nested location
190 src_dir = tmp_path / "deeply" / "nested" / "src"
191 src_dir.mkdir(parents=True)
192 src_file = src_dir / "test.c"
193 src_file.write_text("// @Test, TEST_1\nvoid test() {}\n")
194
195 # Configure with explicit git_root
196 src_analyse_config = SourceAnalyseConfig(
197 src_files=[src_file],
198 src_dir=src_dir,
199 get_need_id_refs=False,
200 get_oneline_needs=True,
201 get_multiline_needs=False,
202 git_root=fake_git_root,
203 )
204
205 src_analyse = SourceAnalyse(src_analyse_config)
206
207 # Verify the explicit git_root was used
208 assert src_analyse.git_root == fake_git_root.resolve()
209 assert src_analyse.git_remote_url == "https://github.com/test/repo.git"
210 assert src_analyse.git_commit_rev == "abc123def456"
211
212
213def test_git_root_auto_detection_when_not_configured(tmp_path):
214 """Test that git_root is auto-detected when not explicitly configured."""
215 src_dir = TEST_DIR / "data" / "dcdc"
216 src_paths = [src_dir / "charge" / "demo_1.cpp"]
217
218 # Don't set git_root - it should auto-detect
219 src_analyse_config = SourceAnalyseConfig(
220 src_files=src_paths,
221 src_dir=src_dir,
222 get_need_id_refs=False,
223 get_oneline_needs=True,
224 get_multiline_needs=False,
225 # git_root is not set, so auto-detection should be used
226 )
227
228 src_analyse = SourceAnalyse(src_analyse_config)
229
230 # The test is running inside a git repo, so git_root should be detected
231 # We just verify it's not None (since this test runs in the sphinx-codelinks repo)
232 assert src_analyse.git_root is not None
233 assert (src_analyse.git_root / ".git").exists()
234
235
236def test_oneline_parser_warnings_are_collected(tmp_path):
237 """Test that oneline parser warnings are collected for later output."""
238 src_dir = TEST_DIR / "data" / "oneline_comment_default"
239 src_paths = [src_dir / "default_oneliners.c"]
240 src_analyse_config = SourceAnalyseConfig(
241 src_files=src_paths,
242 src_dir=src_dir,
243 get_need_id_refs=False,
244 get_oneline_needs=True,
245 get_multiline_needs=False,
246 oneline_comment_style=ONELINE_COMMENT_STYLE_DEFAULT,
247 )
248 src_analyse = SourceAnalyse(src_analyse_config)
249 src_analyse.run()
250
251 # Verify that warnings were collected
252 assert len(src_analyse.warnings) == 1
253 warning = src_analyse.warnings[0]
254 assert "too_many_fields" in warning.sub_type
255 assert warning.lineno == 17
256 # the old name reads the same list, for one release
257 assert src_analyse.oneline_warnings is src_analyse.warnings
258
259
260def test_count_pluralizes_nouns() -> None:
261 assert _count(0, "file") == "0 files"
262 assert _count(1, "file") == "1 file"
263 assert _count(2, "marker") == "2 markers"
264 assert _count(1, "multi-line need") == "1 multi-line need"