feat: file-level package mapping; add 7 new tests
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
- _file_to_package: infer package from file path (kotlin/java/python/src) - _top_packages: most common packages per cluster - Packages included in meta dict and JSON output - New tests: test_file_to_package, test_top_packages, test_extract_imports, test_extract_imports_empty, test_extract_imports_no_imports, test_extract_arg_info_python, test_summarize_arg_info
This commit is contained in:
parent
b6c18c39f2
commit
c059d0b7a4
2 changed files with 152 additions and 2 deletions
|
|
@ -58,6 +58,48 @@ def _match_glob(filepath, pattern):
|
|||
return spec.match_file(filepath)
|
||||
|
||||
|
||||
def _file_to_package(fp, ext):
|
||||
"""Infer the package/directory from a file path.
|
||||
|
||||
For Kotlin/Java: derives from `src/main/kotlin/` or `src/test/kotlin/` tree.
|
||||
For other: shows the relative directory path.
|
||||
"""
|
||||
p = PurePath(fp)
|
||||
try:
|
||||
idx = p.parts.index("kotlin")
|
||||
return "/".join(p.parts[idx + 1:-1])
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
idx = p.parts.index("java")
|
||||
return "/".join(p.parts[idx + 1:-1])
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
idx = p.parts.index("python")
|
||||
return "/".join(p.parts[idx + 1:-1])
|
||||
except ValueError:
|
||||
pass
|
||||
# Fallback: show parent dirs relative to first language-agnostic src
|
||||
parts = p.parts
|
||||
for keyword in ("src", "lib", "app"):
|
||||
try:
|
||||
idx = parts.index(keyword)
|
||||
return "/".join(parts[idx:-1])
|
||||
except ValueError:
|
||||
continue
|
||||
return str(p.parent)
|
||||
|
||||
|
||||
def _top_packages(file_paths, ext, top_n=3):
|
||||
"""Return the most common packages among a set of files."""
|
||||
pkg_counts = Counter()
|
||||
for fp in file_paths:
|
||||
pkg = _file_to_package(fp, ext)
|
||||
pkg_counts[pkg] += 1
|
||||
return [pkg for pkg, _ in pkg_counts.most_common(top_n)]
|
||||
|
||||
|
||||
def _build_arg_patterns(file_paths):
|
||||
"""Extract merged argument patterns across a set of files."""
|
||||
merged = {}
|
||||
|
|
@ -245,7 +287,8 @@ def analyze_clusters(file_paths, extension, min_coverage=0.2, prefer=None, kmax=
|
|||
arg_patterns = _build_arg_patterns(cluster_fps)
|
||||
symbol_seqs = [[text for _, text, _ in seq] for seq in cluster_seqs]
|
||||
result = infer_ensemble(symbol_seqs, kmax=kmax, N=N, prefer=prefer)
|
||||
meta = {"files": cluster_fps, "imports": imports, "arg_patterns": arg_patterns}
|
||||
packages = _top_packages(cluster_fps, extension)
|
||||
meta = {"files": cluster_fps, "imports": imports, "arg_patterns": arg_patterns, "packages": packages}
|
||||
results.append((label, result, len(cluster_seqs), meta))
|
||||
|
||||
return results
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""Tests for tag-preprocessor orchestrator (analyze.py)."""
|
||||
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePath
|
||||
import tempfile
|
||||
import sys
|
||||
|
||||
|
|
@ -8,7 +8,9 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|||
|
||||
from bex.tag_preprocessor.analyze import (
|
||||
scan_directory, frequency_filter, infer, analyze_directory, _match_glob,
|
||||
_file_to_package, _top_packages, _extract_imports,
|
||||
)
|
||||
from bex.tag_preprocessor.code import _summarize_arg_info, _classify_arg_node
|
||||
|
||||
|
||||
def test_scan_directory_empty():
|
||||
|
|
@ -161,6 +163,104 @@ def test_infer_low_coverage_filters_noise():
|
|||
print(" PASS test_infer_low_coverage_filters_noise")
|
||||
|
||||
|
||||
def test_file_to_package():
|
||||
assert _file_to_package(
|
||||
"/repo/src/main/kotlin/com/example/app/Foo.kt", ".kt"
|
||||
) == "com/example/app"
|
||||
assert _file_to_package(
|
||||
"/repo/src/test/java/com/example/FooTest.java", ".java"
|
||||
) == "com/example"
|
||||
assert _file_to_package(
|
||||
"/repo/src/main/python/mypackage/module.py", ".py"
|
||||
) == "mypackage"
|
||||
assert _file_to_package("/repo/lib/foo.py", ".py") == "lib"
|
||||
print(" PASS test_file_to_package")
|
||||
|
||||
|
||||
def test_top_packages():
|
||||
fps = {
|
||||
"/repo/src/main/kotlin/com/example/a/Foo.kt",
|
||||
"/repo/src/main/kotlin/com/example/a/Bar.kt",
|
||||
"/repo/src/main/kotlin/com/example/b/Baz.kt",
|
||||
}
|
||||
pkgs = _top_packages(fps, ".kt")
|
||||
assert pkgs[0] == "com/example/a"
|
||||
assert pkgs[1] == "com/example/b"
|
||||
print(" PASS test_top_packages")
|
||||
|
||||
|
||||
def test_extract_imports():
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
d = Path(td)
|
||||
(d / "a.kt").write_text(
|
||||
"package com.example\nimport io.mockk.every\nimport io.mockk.verify\n\nclass Foo"
|
||||
)
|
||||
(d / "b.kt").write_text(
|
||||
"package com.example\nimport org.junit.Test\n\nclass Bar"
|
||||
)
|
||||
result = _extract_imports({str(d / "a.kt"), str(d / "b.kt")})
|
||||
assert "import io.mockk.every" in result
|
||||
assert "import io.mockk.verify" in result
|
||||
assert "import org.junit.Test" in result
|
||||
assert len(result) == 3
|
||||
print(" PASS test_extract_imports")
|
||||
|
||||
|
||||
def test_extract_imports_empty():
|
||||
assert _extract_imports(set()) == []
|
||||
print(" PASS test_extract_imports_empty")
|
||||
|
||||
|
||||
def test_extract_imports_no_imports():
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
d = Path(td)
|
||||
(d / "a.py").write_text("x = 1\ny = 2")
|
||||
result = _extract_imports({str(d / "a.py")})
|
||||
assert result == []
|
||||
print(" PASS test_extract_imports_no_imports")
|
||||
|
||||
|
||||
def test_extract_arg_info_python():
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
d = Path(td)
|
||||
(d / "a.py").write_text(
|
||||
"def test_foo():\n"
|
||||
" result = compute(42)\n"
|
||||
" assert result == 0\n"
|
||||
" run(mock, times=1)\n"
|
||||
)
|
||||
from bex.tag_preprocessor.code import extract_arg_info
|
||||
fp = str(d / "a.py")
|
||||
code = Path(fp).read_text()
|
||||
info = extract_arg_info(fp, code)
|
||||
# compute(42) -> 1 arg: literal
|
||||
assert "compute" in info
|
||||
obs = info["compute"]
|
||||
assert any(n == 1 and ts == ("lit",) for n, ts in obs)
|
||||
# run(mock, times=1) -> 2 args: var + kwarg
|
||||
assert "run" in info
|
||||
obs2 = info["run"]
|
||||
assert any(n == 2 for n, ts in obs2)
|
||||
assert any(ts == ("var", "kwarg") for n, ts in obs2)
|
||||
print(" PASS test_extract_arg_info_python")
|
||||
|
||||
|
||||
def test_summarize_arg_info():
|
||||
info = {
|
||||
"foo": [(2, ("var", "lit")), (2, ("var", "lit")), (3, ("var", "lit", "lit"))],
|
||||
"bar": [(0, ()), (1, ("lambda",))],
|
||||
}
|
||||
summary = _summarize_arg_info(info)
|
||||
assert summary["foo"]["occurrences"] == 3
|
||||
assert summary["foo"]["arg_count"]["min"] == 2
|
||||
assert summary["foo"]["arg_count"]["max"] == 3
|
||||
assert summary["foo"]["arg_count"]["common"] == 2
|
||||
assert len(summary["foo"]["patterns"]) == 2
|
||||
assert summary["bar"]["occurrences"] == 2
|
||||
assert summary["bar"]["arg_count"]["min"] == 0
|
||||
print(" PASS test_summarize_arg_info")
|
||||
|
||||
|
||||
def run_all():
|
||||
tests = [
|
||||
test_scan_directory_empty,
|
||||
|
|
@ -175,6 +275,13 @@ def run_all():
|
|||
test_frequency_filter_edge_empty_sequences,
|
||||
test_infer_returns_ensemble_dict,
|
||||
test_infer_low_coverage_filters_noise,
|
||||
test_file_to_package,
|
||||
test_top_packages,
|
||||
test_extract_imports,
|
||||
test_extract_imports_empty,
|
||||
test_extract_imports_no_imports,
|
||||
test_extract_arg_info_python,
|
||||
test_summarize_arg_info,
|
||||
]
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue