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