Skip to content

Commit 9acac52

Browse files
committed
fix(cli,bench): stabilize metrics mode and baseline path handling
- neutralize repo quality gates in benchmark runs - resolve pyproject baseline paths from the analysis root - treat --api-surface as a metrics-mode request - refresh CLI regressions and targeted coverage tests - document relative baseline path resolution
1 parent 49b3d7c commit 9acac52

10 files changed

Lines changed: 172 additions & 9 deletions

benchmarks/run_benchmark.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,30 @@
2525
from codeclone.baseline import current_python_tag
2626

2727
BENCHMARK_SCHEMA_VERSION = "1.0"
28+
BENCHMARK_NEUTRAL_ARGS: tuple[str, ...] = (
29+
"--no-fail-on-new",
30+
"--no-fail-on-new-metrics",
31+
"--no-fail-cycles",
32+
"--no-fail-dead-code",
33+
"--no-fail-on-typing-regression",
34+
"--no-fail-on-docstring-regression",
35+
"--no-fail-on-api-break",
36+
"--no-fail-on-untested-hotspots",
37+
"--fail-threshold",
38+
"-1",
39+
"--fail-complexity",
40+
"-1",
41+
"--fail-coupling",
42+
"-1",
43+
"--fail-cohesion",
44+
"-1",
45+
"--fail-health",
46+
"-1",
47+
"--min-typing-coverage",
48+
"-1",
49+
"--min-docstring-coverage",
50+
"-1",
51+
)
2852

2953

3054
@dataclass(frozen=True)
@@ -139,6 +163,7 @@ def _run_cli_once(
139163
"-m",
140164
"codeclone.cli",
141165
str(target),
166+
*BENCHMARK_NEUTRAL_ARGS,
142167
"--json",
143168
str(report_path),
144169
"--cache-path",

codeclone/_cli_runtime.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def _metrics_flags_requested(args: _RuntimeArgs) -> bool:
101101
or args.fail_on_untested_hotspots
102102
or args.min_typing_coverage >= 0
103103
or args.min_docstring_coverage >= 0
104+
or args.api_surface
104105
or args.update_metrics_baseline
105106
or bool(getattr(args, "coverage_xml", None))
106107
)

codeclone/cli.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,17 @@ def _main_impl() -> None:
11501150
analysis_started_at_utc = _current_report_timestamp_utc()
11511151
ap = build_parser(__version__)
11521152

1153+
def _resolve_runtime_path_arg(
1154+
*,
1155+
root_path: Path,
1156+
raw_path: str,
1157+
from_cli: bool,
1158+
) -> Path:
1159+
candidate_path = Path(raw_path).expanduser()
1160+
if from_cli or candidate_path.is_absolute():
1161+
return candidate_path.resolve()
1162+
return (root_path / candidate_path).resolve()
1163+
11531164
def _prepare_run_inputs() -> tuple[
11541165
Namespace,
11551166
Path,
@@ -1174,6 +1185,9 @@ def _prepare_run_inputs() -> tuple[
11741185
or arg.startswith(("--cache-dir=", "--cache-path="))
11751186
for arg in sys.argv
11761187
)
1188+
baseline_path_from_args = any(
1189+
arg == "--baseline" or arg.startswith("--baseline=") for arg in sys.argv
1190+
)
11771191
metrics_path_from_args = any(
11781192
arg == "--metrics-baseline" or arg.startswith("--metrics-baseline=")
11791193
for arg in sys.argv
@@ -1235,7 +1249,11 @@ def _prepare_run_inputs() -> tuple[
12351249

12361250
baseline_arg_path = Path(args.baseline).expanduser()
12371251
try:
1238-
baseline_path = baseline_arg_path.resolve()
1252+
baseline_path = _resolve_runtime_path_arg(
1253+
root_path=root_path,
1254+
raw_path=args.baseline,
1255+
from_cli=baseline_path_from_args,
1256+
)
12391257
baseline_exists = baseline_path.exists()
12401258
except OSError as exc:
12411259
console.print(
@@ -1254,7 +1272,13 @@ def _prepare_run_inputs() -> tuple[
12541272
args.metrics_baseline if metrics_path_overridden else args.baseline
12551273
).expanduser()
12561274
try:
1257-
metrics_baseline_path = metrics_baseline_arg_path.resolve()
1275+
metrics_baseline_path = _resolve_runtime_path_arg(
1276+
root_path=root_path,
1277+
raw_path=(
1278+
args.metrics_baseline if metrics_path_overridden else args.baseline
1279+
),
1280+
from_cli=metrics_path_from_args,
1281+
)
12581282
if metrics_baseline_path == baseline_path:
12591283
probe = _probe_metrics_baseline_section(metrics_baseline_path)
12601284
metrics_baseline_exists = probe.has_metrics_section

docs/book/04-config-and-defaults.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ Current-run coverage join config:
166166

167167
Metrics baseline path selection contract:
168168

169+
- Relative `baseline` / `metrics_baseline` paths coming from defaults or
170+
`pyproject.toml` resolve from the analysis root.
169171
- If `--metrics-baseline` is explicitly set, that path is used.
170172
- If `metrics_baseline` in `pyproject.toml` differs from parser default, that
171173
configured path is used even without explicit CLI flag.

docs/book/09-cli.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ Refs:
108108
`api_surface` data.
109109
- `--coverage` is a current-run external Cobertura input. It does not update or
110110
compare against `codeclone.baseline.json`.
111+
- Relative clone-baseline and metrics-baseline paths from defaults or
112+
`pyproject.toml` resolve from the analysis root. Explicit CLI paths are used
113+
as provided.
111114
- Invalid Cobertura XML is warning-only in normal runs: CLI prints
112115
`Coverage join ignored`, keeps exit `0`, and shows `Coverage` as unavailable
113116
in the normal `Metrics` block. It becomes a contract error only when

tests/test_benchmark.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pytest
1010

1111
from benchmarks.run_benchmark import (
12+
BENCHMARK_NEUTRAL_ARGS,
1213
RunMeasurement,
1314
Scenario,
1415
_validate_inventory_sample,
@@ -43,6 +44,17 @@ def test_benchmark_inventory_validation_accepts_valid_cold_and_warm_samples() ->
4344
)
4445

4546

47+
def test_benchmark_neutral_args_disable_repo_quality_gates() -> None:
48+
assert "--no-fail-on-new" in BENCHMARK_NEUTRAL_ARGS
49+
assert "--no-fail-on-new-metrics" in BENCHMARK_NEUTRAL_ARGS
50+
assert "--no-fail-cycles" in BENCHMARK_NEUTRAL_ARGS
51+
assert "--no-fail-dead-code" in BENCHMARK_NEUTRAL_ARGS
52+
assert "--fail-health" in BENCHMARK_NEUTRAL_ARGS
53+
assert "--min-typing-coverage" in BENCHMARK_NEUTRAL_ARGS
54+
assert "--min-docstring-coverage" in BENCHMARK_NEUTRAL_ARGS
55+
assert "--skip-metrics" not in BENCHMARK_NEUTRAL_ARGS
56+
57+
4658
@pytest.mark.parametrize(
4759
("scenario", "measurement", "message"),
4860
(

tests/test_cli_config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ def test_validate_config_value_accepts_expected_types(
169169
("min_loc", True, "expected int"),
170170
("baseline", 1, "expected str"),
171171
("golden_fixture_paths", "tests/fixtures/golden_*", "expected list\\[str\\]"),
172+
(
173+
"golden_fixture_paths",
174+
["tests/fixtures/golden_*", 1],
175+
"expected list\\[str\\]",
176+
),
172177
("golden_fixture_paths", ["pkg/*"], "must target tests/"),
173178
],
174179
)

tests/test_cli_inprocess.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3367,9 +3367,9 @@ def test_cli_summary_format_stable(
33673367
out = capsys.readouterr().out
33683368
assert "Summary" in out
33693369
assert out.count("Summary") == 1
3370-
assert "Metrics" in out
3371-
assert "Adoption" in out
3372-
assert "Overloaded" in out
3370+
assert "Metrics" not in out
3371+
assert "Adoption" not in out
3372+
assert "Overloaded" not in out
33733373
assert "callables" in out
33743374
assert "Files parsed" not in out
33753375
assert "Input" not in out
@@ -3383,6 +3383,39 @@ def test_cli_summary_format_stable(
33833383
assert _summary_metric(out, "New vs baseline") >= 0
33843384

33853385

3386+
def test_cli_summary_with_metrics_baseline_shows_metrics_section(
3387+
tmp_path: Path,
3388+
monkeypatch: pytest.MonkeyPatch,
3389+
capsys: pytest.CaptureFixture[str],
3390+
) -> None:
3391+
src = tmp_path / "a.py"
3392+
metrics_baseline_path = tmp_path / "metrics-baseline.json"
3393+
src.write_text("def f(value: int) -> int:\n return value\n", "utf-8")
3394+
_patch_parallel(monkeypatch)
3395+
_run_main(
3396+
monkeypatch,
3397+
[
3398+
str(tmp_path),
3399+
"--no-progress",
3400+
"--metrics-baseline",
3401+
str(metrics_baseline_path),
3402+
"--update-metrics-baseline",
3403+
],
3404+
)
3405+
_ = capsys.readouterr()
3406+
_run_main(
3407+
monkeypatch,
3408+
[
3409+
str(tmp_path),
3410+
"--no-progress",
3411+
"--metrics-baseline",
3412+
str(metrics_baseline_path),
3413+
],
3414+
)
3415+
out = capsys.readouterr().out
3416+
assert_contains_all(out, "Metrics", "Adoption", "Overloaded")
3417+
3418+
33863419
def test_cli_summary_with_api_surface_shows_public_api_line(
33873420
tmp_path: Path,
33883421
monkeypatch: pytest.MonkeyPatch,
@@ -3436,10 +3469,7 @@ def test_cli_ci_summary_includes_adoption_and_public_api_lines(
34363469
],
34373470
)
34383471
out = capsys.readouterr().out
3439-
assert "Adoption" in out
3440-
assert "Public API" in out
3441-
assert "symbols=" in out
3442-
assert "docstrings=" in out
3472+
assert_contains_all(out, "Adoption", "Public API", "symbols=", "docstrings=")
34433473

34443474

34453475
def test_cli_pyproject_golden_fixture_paths_exclude_fixture_clone_groups(
@@ -3450,6 +3480,7 @@ def test_cli_pyproject_golden_fixture_paths_exclude_fixture_clone_groups(
34503480
fixtures_dir.mkdir(parents=True)
34513481
_write_duplicate_function_module(fixtures_dir, "a.py")
34523482
_write_duplicate_function_module(fixtures_dir, "b.py")
3483+
_write_current_python_baseline(tmp_path / "codeclone.baseline.json")
34533484
report_path = tmp_path / "report.json"
34543485
(tmp_path / "pyproject.toml").write_text(
34553486
"""

tests/test_cli_unit.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,6 +1379,34 @@ def test_configure_metrics_mode_does_not_force_api_surface_for_baseline_update()
13791379
assert args.api_surface is False
13801380

13811381

1382+
def test_configure_metrics_mode_forces_api_surface_for_api_break_gate() -> None:
1383+
args = Namespace(
1384+
skip_metrics=False,
1385+
fail_complexity=-1,
1386+
fail_coupling=-1,
1387+
fail_cohesion=-1,
1388+
fail_cycles=False,
1389+
fail_dead_code=False,
1390+
fail_health=-1,
1391+
fail_on_new_metrics=False,
1392+
fail_on_typing_regression=False,
1393+
fail_on_docstring_regression=False,
1394+
fail_on_api_break=True,
1395+
fail_on_untested_hotspots=False,
1396+
min_typing_coverage=-1,
1397+
min_docstring_coverage=-1,
1398+
update_metrics_baseline=False,
1399+
skip_dead_code=False,
1400+
skip_dependencies=False,
1401+
api_surface=False,
1402+
coverage_xml=None,
1403+
)
1404+
1405+
cli._configure_metrics_mode(args=args, metrics_baseline_exists=True)
1406+
1407+
assert args.api_surface is True
1408+
1409+
13821410
def test_probe_metrics_baseline_section_for_non_object_payload(tmp_path: Path) -> None:
13831411
path = tmp_path / "baseline.json"
13841412
path.write_text("[]", "utf-8")

tests/test_html_report_helpers.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,3 +546,35 @@ def test_meta_snippet_and_assembly_helpers_cover_empty_optional_paths(
546546
report_document={},
547547
)
548548
assert '[data-theme="light"] .codebox span' not in html_without_light_rules
549+
550+
551+
def test_render_meta_panel_covers_status_tones_and_runtime_mismatch() -> None:
552+
meta_html = render_meta_panel(
553+
cast(
554+
Any,
555+
SimpleNamespace(
556+
meta={
557+
"python_tag": "cp313",
558+
"baseline_python_tag": "cp312",
559+
"cache_status": "stale",
560+
"metrics_baseline_loaded": True,
561+
"metrics_baseline_payload_sha256_verified": True,
562+
},
563+
baseline_meta={"status": "FAILED"},
564+
cache_meta={},
565+
metrics_baseline_meta={},
566+
runtime_meta={},
567+
integrity_map={},
568+
report_schema_version="2.8",
569+
report_generated_at="2026-04-15T12:00:00Z",
570+
),
571+
)
572+
)
573+
assert "meta-status--err" in meta_html
574+
assert ">FAILED<" in meta_html
575+
assert "meta-status--neutral" in meta_html
576+
assert ">stale<" in meta_html
577+
assert "prov-match--mismatch" in meta_html
578+
assert "differs from runtime (cp313)" in meta_html
579+
assert '<span class="prov-badge-val">verified</span>' in meta_html
580+
assert '<span class="prov-badge-lbl">Metrics baseline</span>' in meta_html

0 commit comments

Comments
 (0)