"""Burst sizing and row-splitting, exercised without any instrument.""" import pytest from core.scope_burst import ( frame_means_block, normalize_row, rows_per_burst, split_row_counts, ) SPF = 8 # ── rows_per_burst ─────────────────────────────────────────────────────────── def test_rows_per_burst_rounds_down(): # 9.7 rows' worth of capacity is 9 rows: a partial row is unusable. assert rows_per_burst(97, 10, SPF, rows_remaining=100) == 9 assert rows_per_burst(100, 10, SPF, rows_remaining=100) == 10 def test_rows_per_burst_clamped_by_rows_remaining(): assert rows_per_burst(1000, 10, SPF, rows_remaining=3) == 3 def test_rows_per_burst_clamped_by_memory_budget(): # Budget holds 4 rows of 10 frames × 8 samples; the scope would hold 100. assert rows_per_burst(1000, 10, SPF, rows_remaining=100, memory_budget=4 * 10 * SPF) == 4 def test_rows_per_burst_headroom_reserves_slack_per_row(): assert rows_per_burst(100, 10, SPF, rows_remaining=100, headroom=0) == 10 assert rows_per_burst(100, 10, SPF, rows_remaining=100, headroom=2) == 8 def test_rows_per_burst_never_returns_zero(): """A row too big for any budget still goes, or the scan cannot progress.""" assert rows_per_burst(5, 10, SPF, rows_remaining=100) == 1 assert rows_per_burst(1000, 10, SPF, rows_remaining=100, memory_budget=1) == 1 def test_rows_per_burst_rejects_degenerate_geometry(): with pytest.raises(ValueError): rows_per_burst(100, 0, SPF, rows_remaining=1) # ── split_row_counts ───────────────────────────────────────────────────────── def test_split_row_counts_differences_the_cumulative_counter(): assert split_row_counts([4, 8, 12]) == [4, 4, 4] assert split_row_counts([4, 7, 12]) == [4, 3, 5] assert split_row_counts([]) == [] def test_split_row_counts_rejects_a_counter_that_went_backwards(): # Only happens if the acquisition restarted mid-burst, which would # misattribute every later row. with pytest.raises(RuntimeError, match="backwards"): split_row_counts([8, 4]) # ── normalize_row ──────────────────────────────────────────────────────────── def test_normalize_row_passes_an_exact_row_through(): buf = bytes(range(4 * SPF)) assert bytes(normalize_row(buf, 0, 4, 4, SPF)) == buf def test_normalize_row_pads_a_short_row(): buf = bytes(range(3 * SPF)) out = bytes(normalize_row(buf, 0, 3, 4, SPF)) assert len(out) == 4 * SPF assert out[:3 * SPF] == buf assert out[3 * SPF:] == bytes(SPF) def test_normalize_row_truncates_a_long_row(): buf = bytes(range(6 * SPF)) out = bytes(normalize_row(buf, 0, 6, 4, SPF)) assert out == buf[:4 * SPF] def test_normalize_row_reads_at_an_offset(): buf = bytes(range(8 * SPF)) out = bytes(normalize_row(buf, 2 * SPF, 4, 4, SPF)) assert out == buf[2 * SPF:6 * SPF] def test_normalize_row_pads_a_buffer_that_ends_early(): """Defensive: a truncated transfer must not shorten the row on disk.""" out = bytes(normalize_row(bytes(2 * SPF), 0, 4, 4, SPF)) assert len(out) == 4 * SPF # ── frame_means_block ──────────────────────────────────────────────────────── def test_frame_means_block_is_per_frame(): buf = bytes([1] * SPF + [3] * SPF) assert frame_means_block(buf, 0, 2, SPF) == [1.0, 3.0] def test_frame_means_block_reads_signed_samples_at_an_offset(): buf = bytes([0] * SPF) + bytes([0xFF] * SPF) # 0xFF == -1 as int8 assert frame_means_block(buf, SPF, 1, SPF) == [-1.0]