Convert test scripts to pytest; extend the equivalence harness

- pyproject.toml replaces sras_viewer_requirements.txt (same pins) and
  adds a dev extra with pytest.
- tools/test_refactor.py, test_alignment.py, test_gui.py become
  tests/test_compute.py, tests/test_alignment.py, tests/test_gui.py with
  assertions preserved verbatim. test_gui.py stays one ordered
  integration sequence over a shared module-scoped window.
- check_equivalence.py: drop the dead pre-refactor monolith shim (and the
  _compute_angle_alignment alias it consumed), extend the pad sweep to
  (1, 2, 4, 8, 40), add legacy-v4 and big-endian int16 legs (new bps=2
  option in make_test_sras) so the padded FFT path and the >i2 memmap
  path are in the baseline before the FFT rewrite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Ales
2026-08-06 09:45:35 -05:00
parent dc513e3fd0
commit 007089dd48
12 changed files with 1065 additions and 1104 deletions
+24 -27
View File
@@ -1,13 +1,9 @@
#!/usr/bin/env python3
"""Golden-output equivalence harness for the sras-viewer refactor.
"""Golden-output equivalence harness for compute-path refactors.
Computes a battery of DC / FFT / alignment outputs and prints a stable hash
for each. Run it on the pre-refactor commit to capture a baseline, then again
after the refactor and diff the two reports — every line must match.
Imports work against both the pre-refactor monolith (`sras_viewer`) and the
post-refactor split (`sras_format` + `sras_compute`), so the *same* script
produces both sides of the comparison.
for each. Run it before a refactor to capture a baseline, then again after
and diff the two reports — every line must match.
Hashes canonicalise to native little-endian float64 before hashing, so a
deliberate dtype/byte-order change that preserves values does not show up as
@@ -29,23 +25,11 @@ import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
# --- Import shim: split modules if present, else the monolith --------------
try:
from sras_format import SrasFile, CH1_IDX, CH3_IDX, CH4_IDX, adc_to_mv
import sras_compute as C
_LAYOUT = "split"
except ImportError:
import sras_viewer as _V
from sras_viewer import SrasFile, CH1_IDX, CH3_IDX, CH4_IDX, adc_to_mv
C = _V
_LAYOUT = "monolith"
compute_dc_image = C.compute_dc_image
compute_rf_image = C.compute_rf_image
compute_alignment = C._compute_angle_alignment
apply_alignment = C.apply_alignment
import tools.make_test_sras as gen # noqa: E402
from sras_format import SrasFile, CH3_IDX, CH4_IDX, adc_to_mv # noqa: E402
from sras_compute import ( # noqa: E402
apply_alignment, compute_angle_alignment, compute_dc_image, compute_rf_image,
)
import tools.make_test_sras as gen # noqa: E402
def h(arr) -> str:
@@ -109,7 +93,7 @@ def check_file(path: Path, lines: list[str], tag: str,
for bg in (False, True):
if bg and s.background is None:
continue
for pad in (1, 2):
for pad in (1, 2, 4, 8, 40):
n_fft = s.samples_per_frame * pad if pad > 1 else None
for ti, thr in enumerate(thresholds):
img = compute_rf_image(s, a, dc_threshold_mv=thr,
@@ -135,7 +119,7 @@ def check_alignment(path: Path, lines: list[str], tag: str):
sras.ch_ymult_mv[CH4_IDX], sras.ch_yoff_adc[CH4_IDX],
sras.ch_yzero_mv[CH4_IDX])
thr = float(np.median(dc4))
res = compute_alignment(sras, 0, thr)
res = compute_angle_alignment(sras, 0, thr)
report(lines, f"[{tag}] align canvas_shape", str(res.canvas_shape))
report(lines, f"[{tag}] align canvas_origin",
f"{res.canvas_origin_mm[0]:.9g},{res.canvas_origin_mm[1]:.9g}")
@@ -165,7 +149,7 @@ def main():
help="directory for generated synthetic files")
args = p.parse_args()
lines = [f"# layout: {_LAYOUT}", f"# numpy: {np.__version__}"]
lines = [f"# numpy: {np.__version__}"]
scratch = Path(args.scratch)
synth = scratch / "equiv_synth.sras"
@@ -179,6 +163,19 @@ def main():
gen.write(synth_odd, n_angles=2, seed=7, samples_per_frame=37)
check_file(synth_odd, lines, "odd", angles=[0, 1], n_rows=None)
# A legacy v4 file exercises the uniform-geometry legacy layout through
# the same DC/FFT battery.
synth_v4 = scratch / "equiv_synth_v4.sras"
gen.write_legacy(synth_v4, version=4, n_angles=2, n_rows=6,
n_frames=14, samples_per_frame=48, seed=5)
check_file(synth_v4, lines, "v4", angles=[0, 1], n_rows=None)
# A big-endian int16 v6 file (real acquisitions are >i2; the other
# synthetics are int8).
synth_i16 = scratch / "equiv_synth_i16.sras"
gen.write(synth_i16, n_angles=2, seed=9, samples_per_frame=64, bps=2)
check_file(synth_i16, lines, "int16", angles=[0, 1], n_rows=None)
if args.real:
real = Path(args.real)
if real.exists():