104 lines
3.9 KiB
Python
Executable File
104 lines
3.9 KiB
Python
Executable File
"""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]
|