feat: file-level package mapping; add 7 new tests
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:
tobjend 2026-07-03 21:59:26 +02:00
parent b6c18c39f2
commit c059d0b7a4
2 changed files with 152 additions and 2 deletions

View file

@ -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

View file

@ -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