Files
kucherenko__jscpd/rust/scripts/regression_check.sh
Andrey Kucherenko 425f57b142 perf(rust/core): wave 1 — eliminate hash O(N×k) loop and FxHashMap rehashing
Wave 1 of the perf-rust-optimization heist. Algorithmic-only wins, zero
public-API breakage, identical detection results (290-pair golden gate).

Changes:
- hash.rs: `roll()` now takes a precomputed `window_power: u64` instead of
  recomputing `base_pow(window_size - 1)` on every call. Removes an
  O(window_size) loop from the inner sliding-window hot path. `base_pow`
  is now `pub` so callers can hoist the power once per format group.
- detect.rs: hoist `window_power = base_pow(min_tokens - 1)` once before
  the outer file loop in `detect_in_group()`. Pass it into every `roll()`
  call.
- detect.rs: switch the per-call `by_format` map from `std::HashMap` to
  `FxHashMap` for consistency with the rest of the detection path.
- store.rs: add `Store::reserve(n)` as a default-noop trait method
  (performance hint only — correctness must not depend on it). Implement
  on `MemoryStore` as `self.inner.reserve(n)`. Trait remains `dyn`-safe
  (verified by new test that calls `reserve` through `&mut dyn Store`).
- detect.rs: call `store.reserve(total_windows)` once before the detection
  loop, where `total_windows` is the sum of `hashes.len() - min_tokens`
  across all prepared files. Eliminates `FxHashMap` rehashing mid-detect.

Tooling:
- Add `rust/scripts/regression_check.sh` — golden-baseline gate run after
  each Wave change. Normalizes file paths to `../fixtures/...` form so the
  gate is independent of absolute vs relative path emission.
- Add `rust/tests/golden.json` — frozen 290-pair clone set captured from
  the pre-Heist baseline.

Verification:
- `cargo test -p cpd-core` — 22 pass
- `scripts/regression_check.sh` — 290 pairs match golden, exit 0
2026-06-04 15:42:23 +02:00

62 lines
2.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# regression_check.sh — verify detection output matches golden baseline
# Run after each Wave 2 and Wave 3 sub-change.
# Exit 0 = no regression. Exit 1 = regression or tool missing.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RUST_DIR="$(dirname "$SCRIPT_DIR")"
GOLDEN="$RUST_DIR/tests/golden.json"
FIXTURES_DIR="$RUST_DIR/../fixtures"
BINARY="$RUST_DIR/target/release/cpd"
REPORT="$RUST_DIR/report/jscpd-report.json"
# Check prerequisites
if ! command -v jq &>/dev/null; then
echo "ERROR: jq is required but not found. Install with: brew install jq"
exit 1
fi
if [[ ! -f "$BINARY" ]]; then
echo "ERROR: release binary not found at $BINARY"
echo " Run: cargo build --release --bin cpd"
exit 1
fi
if [[ ! -f "$GOLDEN" ]]; then
echo "ERROR: golden baseline not found at $GOLDEN"
echo " Capture it before Wave 2: see Contract FR-013"
exit 1
fi
# Run detection and produce report
"$BINARY" "$FIXTURES_DIR" -r json >/dev/null 2>&1
# Extract clone pairs (format + sorted file pair) from current run.
# Normalize source paths to "../fixtures/..." form so the gate is independent
# of whether the binary emits absolute or relative paths.
CURRENT="$(jq '[.duplicates[] | {files: ([.fragment_a.source_id, .fragment_b.source_id] | map(sub(".*/fixtures/"; "../fixtures/")) | sort), format: .format}] | sort_by(.format + .files[0] + .files[1])' "$REPORT")"
GOLDEN_COUNT="$(jq length "$GOLDEN")"
CURRENT_COUNT="$(echo "$CURRENT" | jq length)"
if [[ "$GOLDEN_COUNT" != "$CURRENT_COUNT" ]]; then
echo "REGRESSION: clone count changed: golden=$GOLDEN_COUNT current=$CURRENT_COUNT"
exit 1
fi
# Compare file-pair sets (order-insensitive, format-grouped)
MATCH="$(jq -n \
--argjson golden "$(cat "$GOLDEN")" \
--argjson current "$CURRENT" \
'($golden | sort_by(.format + .files[0] + .files[1])) == ($current | sort_by(.format + .files[0] + .files[1]))')"
if [[ "$MATCH" != "true" ]]; then
echo "REGRESSION: clone file-pair sets differ from golden baseline"
echo " Run: diff <(cat '$GOLDEN') <(echo '$CURRENT' | jq .)"
exit 1
fi
echo "OK: $CURRENT_COUNT clone pairs match golden baseline"