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)
|
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):
|
def _build_arg_patterns(file_paths):
|
||||||
"""Extract merged argument patterns across a set of files."""
|
"""Extract merged argument patterns across a set of files."""
|
||||||
merged = {}
|
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)
|
arg_patterns = _build_arg_patterns(cluster_fps)
|
||||||
symbol_seqs = [[text for _, text, _ in seq] for seq in cluster_seqs]
|
symbol_seqs = [[text for _, text, _ in seq] for seq in cluster_seqs]
|
||||||
result = infer_ensemble(symbol_seqs, kmax=kmax, N=N, prefer=prefer)
|
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))
|
results.append((label, result, len(cluster_seqs), meta))
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""Tests for tag-preprocessor orchestrator (analyze.py)."""
|
"""Tests for tag-preprocessor orchestrator (analyze.py)."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path, PurePath
|
||||||
import tempfile
|
import tempfile
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
@ -8,7 +8,9 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
from bex.tag_preprocessor.analyze import (
|
from bex.tag_preprocessor.analyze import (
|
||||||
scan_directory, frequency_filter, infer, analyze_directory, _match_glob,
|
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():
|
def test_scan_directory_empty():
|
||||||
|
|
@ -161,6 +163,104 @@ def test_infer_low_coverage_filters_noise():
|
||||||
print(" PASS 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():
|
def run_all():
|
||||||
tests = [
|
tests = [
|
||||||
test_scan_directory_empty,
|
test_scan_directory_empty,
|
||||||
|
|
@ -175,6 +275,13 @@ def run_all():
|
||||||
test_frequency_filter_edge_empty_sequences,
|
test_frequency_filter_edge_empty_sequences,
|
||||||
test_infer_returns_ensemble_dict,
|
test_infer_returns_ensemble_dict,
|
||||||
test_infer_low_coverage_filters_noise,
|
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
|
passed = 0
|
||||||
failed = 0
|
failed = 0
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue