929 lines
31 KiB
Python
929 lines
31 KiB
Python
"""Tests for the evaluation framework (scorer, reporter, runner, benchmarks)."""
|
|
|
|
import csv
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from code_review_graph.eval.reporter import (
|
|
generate_full_report,
|
|
generate_markdown_report,
|
|
generate_readme_tables,
|
|
)
|
|
|
|
try:
|
|
import yaml as _yaml # noqa: F401
|
|
|
|
from code_review_graph.eval.runner import write_csv
|
|
_HAS_YAML = True
|
|
except ImportError:
|
|
_HAS_YAML = False
|
|
write_csv = None # type: ignore[assignment]
|
|
from code_review_graph.eval.scorer import (
|
|
compute_mrr,
|
|
compute_precision_recall,
|
|
compute_token_efficiency,
|
|
)
|
|
|
|
# --- Existing scorer tests ---
|
|
|
|
|
|
def test_token_efficiency():
|
|
result = compute_token_efficiency(10000, 3000)
|
|
assert result["raw_tokens"] == 10000
|
|
assert result["graph_tokens"] == 3000
|
|
assert result["ratio"] == 0.3
|
|
assert result["reduction_percent"] == 70.0
|
|
|
|
|
|
def test_token_efficiency_zero_raw():
|
|
result = compute_token_efficiency(0, 100)
|
|
assert result["ratio"] == 0.0
|
|
assert result["reduction_percent"] == 0.0
|
|
|
|
|
|
def test_mrr_found_at_rank_2():
|
|
result = compute_mrr("b", ["a", "b", "c"])
|
|
assert result == 0.5
|
|
|
|
|
|
def test_mrr_found_at_rank_1():
|
|
result = compute_mrr("a", ["a", "b", "c"])
|
|
assert result == 1.0
|
|
|
|
|
|
def test_mrr_not_found():
|
|
result = compute_mrr("z", ["a", "b", "c"])
|
|
assert result == 0.0
|
|
|
|
|
|
def test_precision_recall():
|
|
predicted = {"a", "b", "c", "d"}
|
|
actual = {"b", "c", "e"}
|
|
result = compute_precision_recall(predicted, actual)
|
|
assert result["precision"] == 0.5
|
|
assert result["recall"] == round(2 / 3, 4)
|
|
expected_f1 = round(2 * 0.5 * (2 / 3) / (0.5 + 2 / 3), 4)
|
|
assert result["f1"] == expected_f1
|
|
|
|
|
|
def test_precision_recall_empty_sets():
|
|
result = compute_precision_recall(set(), set())
|
|
assert result["precision"] == 1.0
|
|
assert result["recall"] == 1.0
|
|
assert result["f1"] == 1.0
|
|
|
|
|
|
def test_precision_recall_no_overlap():
|
|
result = compute_precision_recall({"a"}, {"b"})
|
|
assert result["precision"] == 0.0
|
|
assert result["recall"] == 0.0
|
|
assert result["f1"] == 0.0
|
|
|
|
|
|
def test_generate_markdown_report():
|
|
results = [
|
|
{
|
|
"benchmark": "token_efficiency",
|
|
"ratio": 0.3,
|
|
"reduction_percent": 70.0,
|
|
},
|
|
{
|
|
"benchmark": "search_mrr",
|
|
"ratio": "-",
|
|
"reduction_percent": "-",
|
|
},
|
|
]
|
|
report = generate_markdown_report(results)
|
|
assert "# Evaluation Report" in report
|
|
assert "## Summary" in report
|
|
assert "token_efficiency" in report
|
|
assert "search_mrr" in report
|
|
assert "70.0" in report
|
|
assert "| Benchmark |" in report
|
|
|
|
|
|
def test_generate_markdown_report_empty():
|
|
report = generate_markdown_report([])
|
|
assert "No benchmark results" in report
|
|
|
|
|
|
# --- New tests ---
|
|
|
|
|
|
@pytest.mark.skipif(not _HAS_YAML, reason="pyyaml not installed")
|
|
def test_load_config():
|
|
"""Load a temp YAML config and verify structure."""
|
|
import yaml
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".yaml", delete=False
|
|
) as f:
|
|
yaml.dump(
|
|
{
|
|
"name": "test-repo",
|
|
"url": "https://example.com/repo.git",
|
|
"commit": "HEAD",
|
|
"language": "python",
|
|
"size_category": "small",
|
|
"test_commits": [{"sha": "abc123", "description": "test"}],
|
|
"entry_points": ["main.py::main"],
|
|
"search_queries": [
|
|
{"query": "hello", "expected": "main.py::greet"}
|
|
],
|
|
},
|
|
f,
|
|
)
|
|
tmp_path = f.name
|
|
|
|
try:
|
|
import yaml as _yaml
|
|
|
|
with open(tmp_path) as fh:
|
|
config = _yaml.safe_load(fh)
|
|
|
|
assert config["name"] == "test-repo"
|
|
assert config["language"] == "python"
|
|
assert len(config["test_commits"]) == 1
|
|
assert len(config["entry_points"]) == 1
|
|
assert len(config["search_queries"]) == 1
|
|
finally:
|
|
os.unlink(tmp_path)
|
|
|
|
|
|
@pytest.mark.skipif(not _HAS_YAML, reason="pyyaml not installed")
|
|
def test_write_csv():
|
|
"""Write results to CSV and read back."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
path = Path(tmpdir) / "results" / "test.csv"
|
|
results = [
|
|
{"repo": "foo", "tokens": 100, "ratio": 2.5},
|
|
{"repo": "bar", "tokens": 200, "ratio": 1.5},
|
|
]
|
|
write_csv(results, path)
|
|
|
|
assert path.exists()
|
|
with open(path, newline="") as f:
|
|
reader = csv.DictReader(f)
|
|
rows = list(reader)
|
|
|
|
assert len(rows) == 2
|
|
assert rows[0]["repo"] == "foo"
|
|
assert rows[1]["tokens"] == "200"
|
|
|
|
|
|
@pytest.mark.skipif(not _HAS_YAML, reason="pyyaml not installed")
|
|
def test_write_csv_empty():
|
|
"""Writing empty results should be a no-op."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
path = Path(tmpdir) / "empty.csv"
|
|
write_csv([], path)
|
|
assert not path.exists()
|
|
|
|
|
|
def test_generate_readme_tables():
|
|
"""Feed sample CSV data and verify table format."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
results_dir = Path(tmpdir)
|
|
|
|
# Write token efficiency CSV
|
|
te_path = results_dir / "test_token_efficiency_2026-01-01.csv"
|
|
with open(te_path, "w", newline="") as f:
|
|
w = csv.DictWriter(
|
|
f,
|
|
fieldnames=[
|
|
"repo", "commit", "description", "changed_files",
|
|
"naive_tokens", "standard_tokens", "graph_tokens",
|
|
"naive_to_graph_ratio", "standard_to_graph_ratio",
|
|
],
|
|
)
|
|
w.writeheader()
|
|
w.writerow({
|
|
"repo": "myrepo", "commit": "abc", "description": "test",
|
|
"changed_files": "3", "naive_tokens": "1000",
|
|
"standard_tokens": "500", "graph_tokens": "200",
|
|
"naive_to_graph_ratio": "5.0",
|
|
"standard_to_graph_ratio": "2.5",
|
|
})
|
|
|
|
tables = generate_readme_tables(results_dir)
|
|
assert "### Token Efficiency" in tables
|
|
assert "myrepo" in tables
|
|
assert "1000" in tables
|
|
|
|
|
|
def test_generate_full_report():
|
|
"""Feed sample CSV data and verify report sections."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
results_dir = Path(tmpdir)
|
|
|
|
# Write a build_performance CSV
|
|
bp_path = results_dir / "test_build_performance_2026-01-01.csv"
|
|
with open(bp_path, "w", newline="") as f:
|
|
w = csv.DictWriter(
|
|
f,
|
|
fieldnames=[
|
|
"repo", "file_count", "node_count", "edge_count",
|
|
"flow_detection_seconds", "community_detection_seconds",
|
|
"search_avg_ms", "nodes_per_second",
|
|
],
|
|
)
|
|
w.writeheader()
|
|
w.writerow({
|
|
"repo": "testrepo", "file_count": "10", "node_count": "50",
|
|
"edge_count": "30", "flow_detection_seconds": "0.1",
|
|
"community_detection_seconds": "0.2",
|
|
"search_avg_ms": "5.0", "nodes_per_second": "500",
|
|
})
|
|
|
|
report = generate_full_report(results_dir)
|
|
assert "# Evaluation Report" in report
|
|
assert "## Methodology" in report
|
|
assert "## Build Performance" in report
|
|
assert "testrepo" in report
|
|
|
|
|
|
@pytest.mark.skipif(not _HAS_YAML, reason="pyyaml not installed")
|
|
def test_runner_with_mock_repo():
|
|
"""Create a tiny git repo with 2 Python files, run benchmarks, verify output."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = Path(tmpdir) / "mock_repo"
|
|
repo_path.mkdir()
|
|
|
|
# Init git repo
|
|
subprocess.run(
|
|
["git", "init"], cwd=str(repo_path), capture_output=True
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.email", "test@test.com"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.name", "Test"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
|
|
# Create two Python files
|
|
(repo_path / "main.py").write_text(
|
|
'from helper import greet\n\ndef main():\n greet("world")\n',
|
|
encoding="utf-8",
|
|
)
|
|
(repo_path / "helper.py").write_text(
|
|
'def greet(name):\n print(f"Hello {name}")\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
subprocess.run(
|
|
["git", "add", "."], cwd=str(repo_path), capture_output=True
|
|
)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "initial"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
|
|
# Second commit: modify helper.py
|
|
(repo_path / "helper.py").write_text(
|
|
'def greet(name):\n print(f"Hi {name}!")\n',
|
|
encoding="utf-8",
|
|
)
|
|
subprocess.run(
|
|
["git", "add", "."], cwd=str(repo_path), capture_output=True
|
|
)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "update greeting"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
|
|
# Build graph
|
|
from code_review_graph.graph import GraphStore
|
|
from code_review_graph.incremental import full_build, get_db_path
|
|
|
|
db_path = get_db_path(repo_path)
|
|
store = GraphStore(db_path)
|
|
full_build(repo_path, store)
|
|
|
|
config = {
|
|
"name": "mock",
|
|
"language": "python",
|
|
"test_commits": [
|
|
{"sha": "HEAD", "description": "update greeting"},
|
|
],
|
|
"entry_points": ["main.py::main"],
|
|
"search_queries": [
|
|
{"query": "greet", "expected": "helper.py::greet"},
|
|
],
|
|
}
|
|
|
|
# Run token_efficiency
|
|
from code_review_graph.eval.benchmarks import token_efficiency
|
|
te_results = token_efficiency.run(repo_path, store, config)
|
|
assert len(te_results) >= 1
|
|
assert "naive_tokens" in te_results[0]
|
|
assert "graph_tokens" in te_results[0]
|
|
|
|
# Run impact_accuracy
|
|
from code_review_graph.eval.benchmarks import impact_accuracy
|
|
ia_results = impact_accuracy.run(repo_path, store, config)
|
|
assert len(ia_results) >= 1
|
|
assert "precision" in ia_results[0]
|
|
assert "f1" in ia_results[0]
|
|
|
|
# Run search_quality
|
|
from code_review_graph.eval.benchmarks import search_quality
|
|
sq_results = search_quality.run(repo_path, store, config)
|
|
assert len(sq_results) == 1
|
|
assert "reciprocal_rank" in sq_results[0]
|
|
|
|
# Run build_performance
|
|
from code_review_graph.eval.benchmarks import build_performance
|
|
bp_results = build_performance.run(repo_path, store, config)
|
|
assert len(bp_results) == 1
|
|
assert "node_count" in bp_results[0]
|
|
assert bp_results[0]["node_count"] > 0
|
|
|
|
store.close()
|
|
|
|
|
|
# --- Token benchmark tests ---
|
|
|
|
|
|
def test_estimate_tokens_basic():
|
|
"""estimate_tokens should return a reasonable approximation."""
|
|
from code_review_graph.eval.token_benchmark import estimate_tokens
|
|
|
|
# Simple string: "hello" => JSON '"hello"' (7 chars) => 7 // 4 = 1
|
|
assert estimate_tokens("hello") == 1
|
|
|
|
# Dict: {"a": 1} => '{"a": 1}' (8 chars) => 8 // 4 = 2
|
|
assert estimate_tokens({"a": 1}) == 2
|
|
|
|
# Longer content should scale proportionally
|
|
long_text = "x" * 400
|
|
tokens = estimate_tokens(long_text)
|
|
# JSON adds 2 quote chars: (400 + 2) // 4 = 100
|
|
assert tokens == 100
|
|
|
|
|
|
def test_estimate_tokens_nested():
|
|
"""estimate_tokens handles nested structures."""
|
|
from code_review_graph.eval.token_benchmark import estimate_tokens
|
|
|
|
nested = {"nodes": [{"name": "foo"}, {"name": "bar"}], "count": 2}
|
|
tokens = estimate_tokens(nested)
|
|
assert tokens > 0
|
|
assert isinstance(tokens, int)
|
|
|
|
|
|
def test_estimate_tokens_non_serializable():
|
|
"""estimate_tokens uses default=str for non-serializable objects."""
|
|
from pathlib import Path
|
|
|
|
from code_review_graph.eval.token_benchmark import estimate_tokens
|
|
|
|
# Path objects are not JSON-serializable but default=str handles them
|
|
tokens = estimate_tokens({"path": Path("/tmp/test")})
|
|
assert tokens > 0
|
|
|
|
|
|
def test_benchmark_review_workflow():
|
|
"""benchmark_review_workflow completes and returns expected structure."""
|
|
from code_review_graph.eval.token_benchmark import benchmark_review_workflow
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = Path(tmpdir) / "bench_repo"
|
|
repo_path.mkdir()
|
|
|
|
# Init git repo with two commits
|
|
subprocess.run(
|
|
["git", "init"], cwd=str(repo_path), capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.email", "test@test.com"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.name", "Test"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
|
|
(repo_path / "main.py").write_text(
|
|
'from helper import greet\n\ndef main():\n greet("world")\n',
|
|
encoding="utf-8",
|
|
)
|
|
(repo_path / "helper.py").write_text(
|
|
'def greet(name):\n print(f"Hello {name}")\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
subprocess.run(
|
|
["git", "add", "."], cwd=str(repo_path), capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "initial"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
|
|
# Second commit
|
|
(repo_path / "helper.py").write_text(
|
|
'def greet(name):\n print(f"Hi {name}!")\n',
|
|
encoding="utf-8",
|
|
)
|
|
subprocess.run(
|
|
["git", "add", "."], cwd=str(repo_path), capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "update greeting"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
|
|
# Build graph
|
|
from code_review_graph.graph import GraphStore
|
|
from code_review_graph.incremental import full_build, get_db_path
|
|
|
|
db_path = get_db_path(repo_path)
|
|
store = GraphStore(db_path)
|
|
full_build(repo_path, store)
|
|
store.close()
|
|
|
|
# Run the review benchmark
|
|
result = benchmark_review_workflow(
|
|
repo_root=str(repo_path), base="HEAD~1",
|
|
)
|
|
|
|
assert result["workflow"] == "review"
|
|
assert result["total_tokens"] > 0
|
|
assert result["tool_calls"] == 2
|
|
assert len(result["calls"]) == 2
|
|
assert result["calls"][0]["tool"] == "get_minimal_context"
|
|
assert result["calls"][1]["tool"] == "detect_changes_minimal"
|
|
for call in result["calls"]:
|
|
assert call["tokens"] >= 0
|
|
|
|
|
|
def test_run_all_benchmarks():
|
|
"""run_all_benchmarks returns results for all workflows."""
|
|
from code_review_graph.eval.token_benchmark import run_all_benchmarks
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = Path(tmpdir) / "all_bench_repo"
|
|
repo_path.mkdir()
|
|
|
|
subprocess.run(
|
|
["git", "init"], cwd=str(repo_path), capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.email", "test@test.com"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.name", "Test"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
|
|
(repo_path / "app.py").write_text(
|
|
'def main():\n print("hello")\n',
|
|
encoding="utf-8",
|
|
)
|
|
|
|
subprocess.run(
|
|
["git", "add", "."], cwd=str(repo_path), capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "initial"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
|
|
(repo_path / "app.py").write_text(
|
|
'def main():\n print("hi")\n',
|
|
encoding="utf-8",
|
|
)
|
|
subprocess.run(
|
|
["git", "add", "."], cwd=str(repo_path), capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "update"],
|
|
cwd=str(repo_path), capture_output=True,
|
|
)
|
|
|
|
from code_review_graph.graph import GraphStore
|
|
from code_review_graph.incremental import full_build, get_db_path
|
|
|
|
db_path = get_db_path(repo_path)
|
|
store = GraphStore(db_path)
|
|
full_build(repo_path, store)
|
|
store.close()
|
|
|
|
results = run_all_benchmarks(repo_root=str(repo_path), base="HEAD~1")
|
|
|
|
# Should have one result per workflow (5 total)
|
|
assert len(results) == 5
|
|
|
|
workflow_names = {r["workflow"] for r in results}
|
|
assert workflow_names == {
|
|
"review", "architecture", "debug", "onboard", "pre_merge",
|
|
}
|
|
|
|
# Each successful result should have total_tokens
|
|
for r in results:
|
|
if "error" not in r:
|
|
assert r["total_tokens"] >= 0
|
|
assert "calls" in r
|
|
|
|
|
|
# --- Failure-inflation regression tests + agent_baseline + co-change mode ---
|
|
|
|
|
|
def _git(repo_path, *args):
|
|
subprocess.run(["git", *args], cwd=str(repo_path), capture_output=True)
|
|
|
|
|
|
def _make_repo(tmpdir, two_file_commit=False):
|
|
"""Tiny git repo: initial commit, then a second commit touching 1 or 2 files."""
|
|
repo_path = Path(tmpdir) / "mock_repo"
|
|
repo_path.mkdir()
|
|
_git(repo_path, "init")
|
|
_git(repo_path, "config", "user.email", "test@test.com")
|
|
_git(repo_path, "config", "user.name", "Test")
|
|
|
|
(repo_path / "main.py").write_text(
|
|
'from helper import greet\n\ndef main():\n greet("world")\n',
|
|
encoding="utf-8",
|
|
)
|
|
(repo_path / "helper.py").write_text(
|
|
'def greet(name):\n print(f"Hello {name}")\n',
|
|
encoding="utf-8",
|
|
)
|
|
_git(repo_path, "add", ".")
|
|
_git(repo_path, "commit", "-m", "initial")
|
|
|
|
(repo_path / "helper.py").write_text(
|
|
'def greet(name):\n print(f"Hi {name}!")\n',
|
|
encoding="utf-8",
|
|
)
|
|
if two_file_commit:
|
|
(repo_path / "main.py").write_text(
|
|
'from helper import greet\n\ndef main():\n greet("there")\n',
|
|
encoding="utf-8",
|
|
)
|
|
_git(repo_path, "add", ".")
|
|
_git(repo_path, "commit", "-m", "update greeting")
|
|
return repo_path
|
|
|
|
|
|
def _build_store(repo_path):
|
|
from code_review_graph.graph import GraphStore
|
|
from code_review_graph.incremental import full_build, get_db_path
|
|
|
|
store = GraphStore(get_db_path(repo_path))
|
|
full_build(repo_path, store)
|
|
return store
|
|
|
|
|
|
def _mock_config(**extra):
|
|
config = {
|
|
"name": "mock",
|
|
"language": "python",
|
|
"test_commits": [{"sha": "HEAD", "description": "update greeting"}],
|
|
"entry_points": ["main.py::main"],
|
|
"search_queries": [{"query": "greet", "expected": "helper.py::greet"}],
|
|
}
|
|
config.update(extra)
|
|
return config
|
|
|
|
|
|
def test_token_efficiency_failure_marked_error_not_inflated(monkeypatch):
|
|
"""A thrown get_review_context must yield status=error, not ratio=naive/1."""
|
|
from code_review_graph.eval.benchmarks import token_efficiency
|
|
|
|
def _boom(**kwargs):
|
|
raise RuntimeError("boom")
|
|
|
|
monkeypatch.setattr("code_review_graph.tools.get_review_context", _boom)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = _make_repo(tmpdir)
|
|
store = _build_store(repo_path)
|
|
try:
|
|
results = token_efficiency.run(repo_path, store, _mock_config())
|
|
finally:
|
|
store.close()
|
|
|
|
assert len(results) >= 1
|
|
for row in results:
|
|
assert row["status"] == "error"
|
|
assert "boom" in row["error"]
|
|
# Failed measurements must not look like valid (inflated) ratios.
|
|
assert row["graph_tokens"] == ""
|
|
assert row["naive_to_graph_ratio"] == ""
|
|
assert row["standard_to_graph_ratio"] == ""
|
|
|
|
agg = token_efficiency.aggregate(results)
|
|
assert agg["ok_rows"] == 0
|
|
assert agg["error_rows"] == len(results)
|
|
assert agg["median_naive_to_graph_ratio"] is None
|
|
|
|
|
|
def test_token_efficiency_success_rows_status_ok():
|
|
from code_review_graph.eval.benchmarks import token_efficiency
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = _make_repo(tmpdir)
|
|
store = _build_store(repo_path)
|
|
try:
|
|
results = token_efficiency.run(repo_path, store, _mock_config())
|
|
finally:
|
|
store.close()
|
|
|
|
assert len(results) >= 1
|
|
for row in results:
|
|
assert row["status"] == "ok"
|
|
assert row["error"] == ""
|
|
assert isinstance(row["graph_tokens"], int)
|
|
assert isinstance(row["naive_to_graph_ratio"], float)
|
|
|
|
agg = token_efficiency.aggregate(results)
|
|
assert agg["ok_rows"] == len(results)
|
|
assert agg["error_rows"] == 0
|
|
assert isinstance(agg["median_naive_to_graph_ratio"], float)
|
|
|
|
|
|
def test_impact_accuracy_failure_marked_error_not_perfect_recall(monkeypatch):
|
|
"""A thrown analyze_changes must not silently score recall 1.0."""
|
|
from code_review_graph.eval.benchmarks import impact_accuracy
|
|
|
|
def _boom(*args, **kwargs):
|
|
raise RuntimeError("analysis exploded")
|
|
|
|
monkeypatch.setattr("code_review_graph.changes.analyze_changes", _boom)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = _make_repo(tmpdir, two_file_commit=True)
|
|
store = _build_store(repo_path)
|
|
try:
|
|
results = impact_accuracy.run(repo_path, store, _mock_config())
|
|
finally:
|
|
store.close()
|
|
|
|
assert len(results) >= 2 # both modes attempted, both failed
|
|
for row in results:
|
|
assert row["status"] == "error"
|
|
assert "analysis exploded" in row["error"]
|
|
assert row["recall"] == "" # NOT 1.0
|
|
assert row["precision"] == ""
|
|
assert row["f1"] == ""
|
|
|
|
agg = impact_accuracy.aggregate(results)
|
|
assert agg["graph_derived"]["ok_rows"] == 0
|
|
assert agg["co_change"]["ok_rows"] == 0
|
|
assert agg["graph_derived"]["mean_recall"] is None
|
|
assert agg["error_rows"] == len(results)
|
|
|
|
|
|
def test_impact_accuracy_emits_both_ground_truth_modes():
|
|
from code_review_graph.eval.benchmarks import impact_accuracy
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = _make_repo(tmpdir, two_file_commit=True)
|
|
store = _build_store(repo_path)
|
|
try:
|
|
results = impact_accuracy.run(repo_path, store, _mock_config())
|
|
finally:
|
|
store.close()
|
|
|
|
modes = {r["ground_truth_mode"] for r in results}
|
|
assert impact_accuracy.MODE_GRAPH_DERIVED in modes
|
|
assert impact_accuracy.MODE_CO_CHANGE in modes
|
|
|
|
graph_rows = [
|
|
r for r in results
|
|
if r["ground_truth_mode"] == impact_accuracy.MODE_GRAPH_DERIVED
|
|
]
|
|
co_rows = [
|
|
r for r in results
|
|
if r["ground_truth_mode"] == impact_accuracy.MODE_CO_CHANGE
|
|
]
|
|
|
|
for row in graph_rows:
|
|
assert row["status"] == "ok"
|
|
assert 0.0 <= row["recall"] <= 1.0
|
|
assert row["seed_file"] == ""
|
|
|
|
# Commit touched helper.py + main.py: seed is the sorted-first file and
|
|
# the ground truth is the *other* co-changed file — independent of the graph.
|
|
assert len(co_rows) == 1
|
|
co = co_rows[0]
|
|
assert co["status"] == "ok"
|
|
assert co["seed_file"] == "helper.py"
|
|
assert co["actual_files"] == 1
|
|
assert 0.0 <= co["precision"] <= 1.0
|
|
assert 0.0 <= co["recall"] <= 1.0
|
|
|
|
|
|
def test_impact_accuracy_co_change_skipped_for_single_file_commit():
|
|
from code_review_graph.eval.benchmarks import impact_accuracy
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = _make_repo(tmpdir, two_file_commit=False)
|
|
store = _build_store(repo_path)
|
|
try:
|
|
results = impact_accuracy.run(repo_path, store, _mock_config())
|
|
finally:
|
|
store.close()
|
|
|
|
co_rows = [
|
|
r for r in results
|
|
if r["ground_truth_mode"] == impact_accuracy.MODE_CO_CHANGE
|
|
]
|
|
assert len(co_rows) == 1
|
|
assert co_rows[0]["status"] == "skipped"
|
|
assert "co-changed" in co_rows[0]["error"]
|
|
|
|
agg = impact_accuracy.aggregate(results)
|
|
assert agg["skipped_rows"] == 1
|
|
assert agg["co_change"]["ok_rows"] == 0
|
|
|
|
|
|
# --- agent_baseline benchmark ---
|
|
|
|
|
|
def test_derive_search_terms_extracts_identifiers_and_keywords():
|
|
from code_review_graph.eval.benchmarks.agent_baseline import derive_search_terms
|
|
|
|
terms = derive_search_terms("How does Client.request send an HTTP request?")
|
|
assert "client.request" in terms
|
|
assert "how" not in terms # stopword
|
|
assert "does" not in terms # stopword
|
|
assert all(t == t.lower() for t in terms)
|
|
|
|
|
|
def test_grep_rank_orders_by_match_count_and_takes_top_k():
|
|
from code_review_graph.eval.benchmarks.agent_baseline import grep_rank
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
corpus = Path(tmpdir)
|
|
(corpus / "a.py").write_text("greet()\ngreet()\ngreet()\n", encoding="utf-8")
|
|
(corpus / "b.py").write_text("greet()\n", encoding="utf-8")
|
|
(corpus / "c.py").write_text("nothing here\n", encoding="utf-8")
|
|
(corpus / "d.txt").write_text("greet greet greet greet\n", encoding="utf-8")
|
|
sub = corpus / "node_modules"
|
|
sub.mkdir()
|
|
(sub / "e.py").write_text("greet greet greet greet greet\n", encoding="utf-8")
|
|
|
|
ranked = grep_rank(corpus, ["greet"], k=3)
|
|
# d.txt (non-source ext) and node_modules/e.py (skipped dir) excluded
|
|
assert ranked == [("a.py", 3), ("b.py", 1)]
|
|
|
|
top1 = grep_rank(corpus, ["greet"], k=1)
|
|
assert top1 == [("a.py", 3)]
|
|
|
|
assert grep_rank(corpus, [], k=3) == []
|
|
|
|
|
|
def test_grep_rank_tie_breaks_on_path():
|
|
from code_review_graph.eval.benchmarks.agent_baseline import grep_rank
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
corpus = Path(tmpdir)
|
|
(corpus / "zz.py").write_text("token token\n", encoding="utf-8")
|
|
(corpus / "aa.py").write_text("token token\n", encoding="utf-8")
|
|
ranked = grep_rank(corpus, ["token"], k=2)
|
|
assert ranked == [("aa.py", 2), ("zz.py", 2)]
|
|
|
|
|
|
def test_agent_baseline_run_with_mock_repo():
|
|
from code_review_graph.eval.benchmarks import agent_baseline
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = _make_repo(tmpdir)
|
|
store = _build_store(repo_path)
|
|
config = _mock_config(
|
|
agent_questions=["How does greet print a greeting"],
|
|
)
|
|
try:
|
|
results = agent_baseline.run(repo_path, store, config)
|
|
finally:
|
|
store.close()
|
|
|
|
assert len(results) == 1
|
|
row = results[0]
|
|
assert row["question"] == "How does greet print a greeting"
|
|
assert "greet" in row["terms"]
|
|
assert row["files_matched"] >= 1
|
|
assert "helper.py" in row["top_files"]
|
|
assert row["baseline_tokens"] > 0
|
|
assert row["status"] in ("ok", "no_graph_results")
|
|
if row["status"] == "ok":
|
|
assert isinstance(row["baseline_to_graph_ratio"], float)
|
|
|
|
|
|
def test_agent_baseline_falls_back_to_search_queries():
|
|
from code_review_graph.eval.benchmarks import agent_baseline
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = _make_repo(tmpdir)
|
|
store = _build_store(repo_path)
|
|
try:
|
|
results = agent_baseline.run(repo_path, store, _mock_config())
|
|
finally:
|
|
store.close()
|
|
|
|
assert len(results) == 1
|
|
assert results[0]["question"] == "greet"
|
|
|
|
|
|
def test_agent_baseline_search_failure_marked_error(monkeypatch):
|
|
from code_review_graph.eval.benchmarks import agent_baseline
|
|
|
|
def _boom(*args, **kwargs):
|
|
raise RuntimeError("search down")
|
|
|
|
monkeypatch.setattr("code_review_graph.search.hybrid_search", _boom)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
repo_path = _make_repo(tmpdir)
|
|
store = _build_store(repo_path)
|
|
config = _mock_config(agent_questions=["How does greet work"])
|
|
try:
|
|
results = agent_baseline.run(repo_path, store, config)
|
|
finally:
|
|
store.close()
|
|
|
|
assert len(results) == 1
|
|
assert results[0]["status"] == "error"
|
|
assert "search down" in results[0]["error"]
|
|
assert results[0]["baseline_to_graph_ratio"] == ""
|
|
|
|
agg = agent_baseline.aggregate(results)
|
|
assert agg["ok_rows"] == 0
|
|
assert agg["error_rows"] == 1
|
|
assert agg["median_baseline_to_graph_ratio"] is None
|
|
|
|
|
|
def test_agent_baseline_aggregate_excludes_non_ok_rows():
|
|
from code_review_graph.eval.benchmarks import agent_baseline
|
|
|
|
rows = [
|
|
{"status": "ok", "baseline_to_graph_ratio": 4.0},
|
|
{"status": "ok", "baseline_to_graph_ratio": 8.0},
|
|
{"status": "error", "baseline_to_graph_ratio": ""},
|
|
{"status": "no_graph_results", "baseline_to_graph_ratio": ""},
|
|
]
|
|
agg = agent_baseline.aggregate(rows)
|
|
assert agg["total_rows"] == 4
|
|
assert agg["ok_rows"] == 2
|
|
assert agg["error_rows"] == 1
|
|
assert agg["median_baseline_to_graph_ratio"] == 6.0
|
|
|
|
|
|
@pytest.mark.skipif(not _HAS_YAML, reason="pyyaml not installed")
|
|
def test_agent_baseline_registered_in_runner():
|
|
from code_review_graph.eval.runner import BENCHMARK_REGISTRY
|
|
|
|
assert "agent_baseline" in BENCHMARK_REGISTRY
|
|
|
|
|
|
def test_reporter_impact_f1_skips_error_and_co_change_rows():
|
|
"""Table B must aggregate only ok graph-derived rows."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
results_dir = Path(tmpdir)
|
|
ia_path = results_dir / "mock_impact_accuracy_2026-01-01.csv"
|
|
fieldnames = [
|
|
"repo", "commit", "ground_truth_mode", "seed_file",
|
|
"predicted_files", "actual_files", "true_positives",
|
|
"precision", "recall", "f1", "status", "error",
|
|
]
|
|
with open(ia_path, "w", newline="") as f:
|
|
w = csv.DictWriter(f, fieldnames=fieldnames)
|
|
w.writeheader()
|
|
w.writerow({
|
|
"repo": "mock", "commit": "abc",
|
|
"ground_truth_mode": "graph-derived (circular — upper bound)",
|
|
"seed_file": "", "predicted_files": "2", "actual_files": "2",
|
|
"true_positives": "1", "precision": "0.5", "recall": "0.5",
|
|
"f1": "0.5", "status": "ok", "error": "",
|
|
})
|
|
w.writerow({
|
|
"repo": "mock", "commit": "def",
|
|
"ground_truth_mode": "graph-derived (circular — upper bound)",
|
|
"seed_file": "", "predicted_files": "", "actual_files": "",
|
|
"true_positives": "", "precision": "", "recall": "",
|
|
"f1": "", "status": "error", "error": "boom",
|
|
})
|
|
w.writerow({
|
|
"repo": "mock", "commit": "abc",
|
|
"ground_truth_mode": "co-change (same commit, seed excluded)",
|
|
"seed_file": "a.py", "predicted_files": "1", "actual_files": "1",
|
|
"true_positives": "1", "precision": "1.0", "recall": "1.0",
|
|
"f1": "0.9", "status": "ok", "error": "",
|
|
})
|
|
|
|
tables = generate_readme_tables(results_dir)
|
|
|
|
# 0.5 comes only from the single ok graph-derived row; the error row and
|
|
# the co-change row (different metric) must not pollute the column.
|
|
assert "0.5" in tables
|
|
assert "0.9" not in tables
|