Skip to content

Commit 47291fc

Browse files
committed
fix(parsers): CM-A is_test_file anchored to path components, not substring (BUG-NEW 2,22)
Local-only finder-fixes-54. 3 parametrized tests across 5 langs. is_test_file used unanchored 'pattern in path_lower' (zig worst: bare test/spec tokens), so real source whose name CONTAINS a token (latest/contest/attestation/inspector) was silently dropped from extraction (skip_tests=True is the pipeline default). Anchored to exact path DIRECTORY components + basename conventions across c/php/python/ruby/zig repository_scanner. JS/Go already anchored -> untouched (judge-confirmed). Decoys not excluded; real test files still excluded (recall preserved). Judge: AGREE / SHIP-AS-IS. Local-only; not pushed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit d8c5f3fb0387c969a7e7c899a8d59e598c46a477)
1 parent ad17f3f commit 47291fc

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Cross-language regression: repository_scanner test-file classification must be anchored.
2+
3+
Bug (path_substring_exclusion family, bundle entries [2] multi-lang + [22] zig):
4+
`is_test_file` (c/php/python/ruby) and `_is_test_file`/`_is_test_directory` (zig)
5+
classified a file/dir as a TEST using an UNANCHORED substring match
6+
(`for pattern in test_patterns: if pattern in path_lower`). Because `test_` is a
7+
substring of `latest_`/`greatest_`/`contest_`, and zig's bare `test`/`spec` tokens
8+
are substrings of `latest`/`contest`/`attestation`/`inspector`, real source files
9+
whose name merely CONTAINS a test token were silently classified as tests and
10+
DROPPED from extraction (default `skip_tests=True`).
11+
12+
Fix shape (one mechanism across all 5 langs): anchor the match to whole PATH
13+
COMPONENTS (a directory part == test/tests/spec/specs) OR basename conventions
14+
(`test_*`, `*_test.<ext>`, `*_spec.<ext>`, `*Test.<ext>`, `conftest.py`, etc.).
15+
16+
This test drives each scanner's classification predicate directly:
17+
- DECOY case: a real source whose name CONTAINS a token as substring -> NOT a test.
18+
- POSITIVE case: a genuine test file/dir -> still IS a test (don't over-narrow).
19+
20+
JS (`isTestFile` regex) and Go (`HasSuffix "_test.go"`) are already anchored and are
21+
intentionally NOT exercised here.
22+
"""
23+
24+
import sys
25+
from pathlib import Path
26+
27+
import pytest
28+
29+
_CORE_ROOT = Path(__file__).resolve().parents[2]
30+
sys.path.insert(0, str(_CORE_ROOT))
31+
32+
33+
def _make_scanner(lang, repo_path="/tmp/repo"):
34+
"""Instantiate each language's RepositoryScanner with skip_tests on."""
35+
if lang == "c":
36+
from parsers.c.repository_scanner import RepositoryScanner
37+
return RepositoryScanner(repo_path, {"skip_tests": True})
38+
if lang == "php":
39+
from parsers.php.repository_scanner import RepositoryScanner
40+
return RepositoryScanner(repo_path, {"skip_tests": True})
41+
if lang == "python":
42+
from parsers.python.repository_scanner import RepositoryScanner
43+
return RepositoryScanner(repo_path, {"skip_tests": True})
44+
if lang == "ruby":
45+
from parsers.ruby.repository_scanner import RepositoryScanner
46+
return RepositoryScanner(repo_path, {"skip_tests": True})
47+
if lang == "zig":
48+
from parsers.zig.repository_scanner import RepositoryScanner
49+
return RepositoryScanner(repo_path, skip_tests=True)
50+
raise ValueError(lang)
51+
52+
53+
def _is_test(lang, scanner, relative_path):
54+
"""Call the per-language test-file classification predicate."""
55+
if lang == "zig":
56+
return scanner._is_test_file(relative_path)
57+
return scanner.is_test_file(relative_path)
58+
59+
60+
# (lang, decoy_real_source_that_must_NOT_be_a_test)
61+
DECOYS = [
62+
("c", "latest_dir/main.c"),
63+
("c", "contest/sol.c"),
64+
("c", "src/protest.c"),
65+
("php", "src/protest_api.php"),
66+
("php", "app/latest_controller.php"),
67+
("python", "pkg/latest.py"),
68+
("python", "pkg/greatest_helper.py"),
69+
("ruby", "lib/latest_x.rb"),
70+
("ruby", "lib/contest.rb"),
71+
("zig", "src/latest.zig"),
72+
("zig", "src/contest.zig"),
73+
("zig", "src/attestation.zig"),
74+
("zig", "inspector/foo.zig"),
75+
]
76+
77+
# (lang, genuine_test_file_that_MUST_still_be_classified_as_a_test)
78+
POSITIVES = [
79+
("c", "tests/test_foo.c"),
80+
("c", "src/foo_test.c"),
81+
("php", "tests/FooTest.php"),
82+
("php", "src/test_helper.php"),
83+
("python", "tests/test_foo.py"),
84+
("python", "pkg/conftest.py"),
85+
("ruby", "spec/foo_spec.rb"),
86+
("ruby", "test/test_foo.rb"),
87+
("zig", "test/foo.zig"),
88+
("zig", "src/foo_test.zig"),
89+
]
90+
91+
92+
@pytest.mark.parametrize("lang,relative_path", DECOYS)
93+
def test_decoy_real_source_not_classified_as_test(lang, relative_path):
94+
scanner = _make_scanner(lang)
95+
assert not _is_test(lang, scanner, relative_path), (
96+
f"{lang}: real source {relative_path!r} wrongly classified as a test "
97+
f"(unanchored substring match)"
98+
)
99+
100+
101+
@pytest.mark.parametrize("lang,relative_path", POSITIVES)
102+
def test_positive_genuine_test_still_classified(lang, relative_path):
103+
scanner = _make_scanner(lang)
104+
assert _is_test(lang, scanner, relative_path), (
105+
f"{lang}: genuine test {relative_path!r} must still be classified as a test"
106+
)
107+
108+
109+
def test_zig_test_directory_decoy_not_excluded():
110+
"""zig dir-level: `inspector`/`latest` dirs must NOT be treated as test dirs."""
111+
scanner = _make_scanner("zig")
112+
assert not scanner._is_test_directory("inspector")
113+
assert not scanner._is_test_directory("latest_dir")
114+
# positive: a real `test` dir IS a test dir
115+
assert scanner._is_test_directory("test")
116+
assert scanner._is_test_directory("tests")
117+
assert scanner._is_test_directory("spec")

0 commit comments

Comments
 (0)