"""core.sras_format: the current writer against the spec, and the parser against the pre-refactor golden fixtures. The goldens are legacy v6 files produced by the original sc3_aui_app implementation — one background for the whole file. Nothing writes that layout any more, so they lock the parser (field-for-field, including the frontier walk) rather than the writer. The writer is locked instead against bytes this test lays out from scan_format.md itself. """ import json import struct from dataclasses import asdict from pathlib import Path import numpy as np import pytest from core.sras_format import ( BG_LEN_FMT, GEOM_FMT, HDR_FMT, MAGIC, VERSION, VERSION_SAW_CHECK, SrasFile, create_scan_file, ) from golden_util import ( BACKGROUND, CHANNELS, PREAMBLES, SAMPLE_RATE, SPF, angle_background, synthetic_frame, tiny_plan, write_v7, ) GOLDEN = Path(__file__).parent / "golden" @pytest.fixture(scope="module") def expected(): with open(GOLDEN / "sras_expected.json") as f: return json.load(f) def spec_bytes(plan): """The v7 layout spelled out from scan_format.md, writer not involved.""" buf = bytearray() buf += struct.pack(HDR_FMT, MAGIC, VERSION, plan.n_angles, plan.x_start_nominal, plan.y_start_nominal, plan.x_delta_nominal, plan.y_delta_nominal, plan.row_spacing, plan.velocity_mm_s, plan.laser_freq_hz, SPF, SAMPLE_RATE, 1, len(CHANNELS)) buf += struct.pack(f">{plan.n_angles}f", *plan.angles) for pa in plan.per_angle: buf += struct.pack(GEOM_FMT, pa.x_start, pa.x_delta, pa.n_frames, pa.n_rows) for pa in plan.per_angle: buf += struct.pack(f">{pa.n_rows}f", *pa.y_positions) for pre in PREAMBLES: buf += struct.pack(">H", len(pre)) + pre.encode("utf-8") for ai, pa in enumerate(plan.per_angle): bg = angle_background(ai) buf += struct.pack(BG_LEN_FMT, len(bg)) + bg for ri in range(pa.n_rows): for ci in range(len(CHANNELS)): for fi in range(pa.n_frames): buf += synthetic_frame(ai, ri, ci, fi) return bytes(buf) def test_writer_matches_the_spec_byte_for_byte(tmp_path): out = tmp_path / "v7.sras" plan = write_v7(out) assert out.read_bytes() == spec_bytes(plan) def test_writer_refuses_the_legacy_versions(tmp_path): for version in (6, 10): with pytest.raises(ValueError, match=f"version {version}"): create_scan_file(tmp_path / "bad.sras", tiny_plan(), SPF, SAMPLE_RATE, PREAMBLES, version=version) assert not (tmp_path / "bad.sras").exists() def test_header_matches_golden(expected): sras = SrasFile(GOLDEN / "complete.sras") h = expected["header"] assert asdict(sras.header) == { "n_angles": h["n_angles"], "x_start_nominal": h["x_start_nominal"], "y_start_nominal": h["y_start_nominal"], "x_delta_nominal": h["x_delta_nominal"], "y_delta_nominal": h["y_delta_nominal"], "row_spacing": h["row_spacing"], "velocity": h["velocity"], "laser_freq": h["laser_freq"], "samples_per_frame": h["samples_per_frame"], "sample_rate": h["sample_rate"], "bytes_per_sample": h["bytes_per_sample"], "n_channels": h["n_channels"], } assert sras.data_start_offset == h["data_start_offset"] assert [pa.angle_deg for pa in sras.per_angle] == h["angles"] for pa, exp in zip(sras.per_angle, h["per_angle"], strict=True): assert pa.angle_deg == exp["angle"] assert pa.x_start == exp["x_start"] assert pa.x_delta == exp["x_delta"] assert pa.n_frames == exp["n_frames"] assert pa.n_rows == exp["n_rows"] assert pa.y_positions == exp["y_positions"] def test_frontier_all_truncation_variants(expected): """Every field the goldens recorded, plus the one added since. The expectations predate AngleStatus.bg_offset, so they are compared key by key; a v6 angle has no background block of its own, which is exactly what bg_offset == data_offset says. Two of the recorded data_offsets were corrected when the walk moved into the parser: an angle past the frontier used to report the frontier's own offset, because the old walk stopped advancing its cursor there, which handed a resumed scan the same write position for every missing angle. They are now the declared position each angle will be written at. """ for name, exp_statuses in expected["statuses"].items(): statuses = SrasFile(GOLDEN / name).angle_status() assert len(statuses) == len(exp_statuses), f"mismatch for {name}" for status, exp in zip(statuses, exp_statuses, strict=True): got = asdict(status) assert {k: got[k] for k in exp} == exp, f"mismatch for {name}" assert status.bg_offset == status.data_offset assert status.bg_bytes == 0 def test_legacy_preambles_and_shared_background(): """A v6 file's one background stands in for every angle's.""" sras = SrasFile(GOLDEN / "complete.sras") assert sras.preambles == PREAMBLES assert sras.is_legacy_layout assert sras.backgrounds == [BACKGROUND] * sras.header.n_angles assert np.array_equal(sras.background_array(1), np.frombuffer(BACKGROUND, dtype=np.int8)) # ── Per-angle backgrounds (v7/v11) ─────────────────────────────────────────── def test_each_angle_keeps_its_own_background(tmp_path): out = tmp_path / "v7.sras" plan = write_v7(out) sras = SrasFile(out) assert not sras.is_legacy_layout assert sras.backgrounds == [angle_background(ai) for ai in range(plan.n_angles)] assert [s.status for s in sras.angle_status()] == ["OK"] * plan.n_angles # Every angle's rows start just past its own background block … for st in sras.angle_status(): assert st.bg_bytes == 4 + SPF assert st.data_offset == st.bg_offset + st.bg_bytes # … and the data itself still reads back frame for frame. assert sras.load_row(1, 2, 0).tobytes() == b"".join( synthetic_frame(1, 2, 0, fi) for fi in range(plan.per_angle[1].n_frames)) sras.close() def test_saw_check_version_also_carries_per_angle_backgrounds(tmp_path): from core.saw_check import middle_row_plan out = tmp_path / "check.sras" plan = write_v7(out, plan=middle_row_plan(tiny_plan()), version=VERSION_SAW_CHECK) sras = SrasFile(out) assert sras.is_saw_check and not sras.is_legacy_layout assert sras.backgrounds == [angle_background(ai) for ai in range(plan.n_angles)] sras.close() def test_angle_missing_its_background_is_the_frontier(tmp_path): """A file cut inside a background block stops at that angle. Nothing of that angle is on disk yet — not even the reference its rows would be read against — so it is MISSING rather than TRUNCATED, and its predicted offsets are where a resumed scan would write. """ out = tmp_path / "v7.sras" write_v7(out) whole = out.read_bytes() bg1 = SrasFile(out).angle_status()[1].bg_offset for cut, expected_status in ((bg1, "MISSING"), (bg1 + 4 + SPF // 2, "MISSING")): out.write_bytes(whole[:cut]) statuses = SrasFile(out).angle_status() assert [s.status for s in statuses] == ["OK", expected_status] assert statuses[1].n_rows_available == 0 assert statuses[1].bg_offset == bg1 # The absent block is predicted at a full record's worth of bytes, # which is what the writer will produce when the scan resumes. assert statuses[1].data_offset == bg1 + 4 + SPF def test_rows_after_a_background_still_truncate_by_row(tmp_path): out = tmp_path / "v7.sras" plan = write_v7(out) whole = out.read_bytes() st1 = SrasFile(out).angle_status()[1] out.write_bytes(whole[:st1.data_offset + 2 * st1.row_bytes]) statuses = SrasFile(out).angle_status() assert [s.status for s in statuses] == ["OK", "TRUNCATED"] assert statuses[1].n_rows_available == 2 assert SrasFile(out).load_angle(1, n_rows=2).shape[0] == 2 assert plan.per_angle[1].n_rows == 3 def test_load_angle_memmap_equals_eager(): with SrasFile(GOLDEN / "complete.sras") as sras: raw = (GOLDEN / "complete.sras").read_bytes() for ai, pa in enumerate(sras.per_angle): view = sras.load_angle(ai) h = sras.header assert view.shape == (pa.n_rows, h.n_channels, pa.n_frames, h.samples_per_frame) start = sras.angle_data_offset(ai) eager = np.frombuffer( raw, dtype=np.int8, offset=start, count=view.size ).reshape(view.shape) assert np.array_equal(view, eager) assert not view.flags.writeable def test_load_row_matches_synthetic_pattern(): with SrasFile(GOLDEN / "complete.sras") as sras: for ai in range(2): for ri in range(3): for ci in range(3): row = sras.load_row(ai, ri, ci) expected_bytes = b"".join( synthetic_frame(ai, ri, ci, fi) for fi in range(sras.per_angle[ai].n_frames) ) assert row.tobytes() == expected_bytes def test_truncated_load_angle_partial_rows(): sras = SrasFile(GOLDEN / "trunc_rowboundary_a1.sras") st = sras.angle_status()[1] assert st.status == "TRUNCATED" and st.n_rows_available == 2 view = sras.load_angle(1, n_rows=st.n_rows_available) assert view.shape[0] == 2 sras.close() def test_bad_magic_and_version_rejected(tmp_path): bad = tmp_path / "bad.sras" bad.write_bytes(b"XXXX" + bytes(60)) with pytest.raises(ValueError, match="bad magic"): SrasFile(bad) data = bytearray((GOLDEN / "complete.sras").read_bytes()) data[4] = 5 # version byte v5 = tmp_path / "v5.sras" v5.write_bytes(bytes(data)) with pytest.raises(ValueError, match="version 5"): SrasFile(v5)