some fixes and improvements
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Tests for Kamil ADC config, parser, and producer wiring."""
|
||||
"""Tests for Kamil ADC config, frame parsing, TTY reader, and producer wiring."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -9,10 +9,14 @@ import pty
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import tty
|
||||
import unittest
|
||||
|
||||
from python_app.hardware_full.kamil_adc_service import KamilAdcFrameParser, KamilAdcTtyReader
|
||||
from python_app.hardware_full.kamil_adc_service import (
|
||||
KamilAdcTtyReader,
|
||||
_parse_point_frame,
|
||||
)
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.orchestration.process_supervisor import ProcessSupervisor
|
||||
|
||||
@@ -25,28 +29,41 @@ def _point_frame(step: int, real: int, imag: int, *, marker: int = 0x000A) -> by
|
||||
return struct.pack("<HHhh", marker, step, real, imag)
|
||||
|
||||
|
||||
class KamilAdcFrameParserTest(unittest.TestCase):
|
||||
def test_parse_valid_point(self) -> None:
|
||||
value = KamilAdcFrameParser.parse_point(_point_frame(1, 123, -45), expected_step=1)
|
||||
class ParsePointFrameTest(unittest.TestCase):
|
||||
def test_parses_valid_point(self) -> None:
|
||||
value = _parse_point_frame(_point_frame(1, 123, -45), expected_step=1)
|
||||
self.assertEqual(value, complex(123, -45))
|
||||
|
||||
def test_bad_marker_is_rejected(self) -> None:
|
||||
def test_rejects_bad_marker(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "marker mismatch"):
|
||||
KamilAdcFrameParser.parse_point(_point_frame(1, 10, 20, marker=0x001A), expected_step=1)
|
||||
_parse_point_frame(_point_frame(1, 10, 20, marker=0x001A), expected_step=1)
|
||||
|
||||
def test_wrong_step_is_rejected(self) -> None:
|
||||
def test_rejects_wrong_step(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "step mismatch"):
|
||||
KamilAdcFrameParser.parse_point(_point_frame(2, 10, 20), expected_step=1)
|
||||
_parse_point_frame(_point_frame(2, 10, 20), expected_step=1)
|
||||
|
||||
|
||||
class KamilAdcTtyReaderTest(unittest.TestCase):
|
||||
def test_valid_stream_reads_complex_sweep(self) -> None:
|
||||
"""End-to-end tests over a PTY exercising the background reader thread."""
|
||||
|
||||
def _open_pty_reader(self) -> tuple[int, int, KamilAdcTtyReader]:
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
reader: KamilAdcTtyReader | None = None
|
||||
tty.setraw(slave_fd)
|
||||
reader = KamilAdcTtyReader(os.ttyname(slave_fd))
|
||||
reader.open()
|
||||
return master_fd, slave_fd, reader
|
||||
|
||||
@staticmethod
|
||||
def _close(master_fd: int, slave_fd: int, reader: KamilAdcTtyReader) -> None:
|
||||
try:
|
||||
reader.close()
|
||||
finally:
|
||||
os.close(master_fd)
|
||||
os.close(slave_fd)
|
||||
|
||||
def test_publishes_first_complete_sweep(self) -> None:
|
||||
master_fd, slave_fd, reader = self._open_pty_reader()
|
||||
try:
|
||||
tty.setraw(slave_fd)
|
||||
reader = KamilAdcTtyReader(os.ttyname(slave_fd))
|
||||
reader.open()
|
||||
os.write(
|
||||
master_fd,
|
||||
_start_frame()
|
||||
@@ -54,23 +71,39 @@ class KamilAdcTtyReaderTest(unittest.TestCase):
|
||||
+ _point_frame(2, -20, 2)
|
||||
+ _start_frame(),
|
||||
)
|
||||
|
||||
values = reader.read_sweep(timeout_s=1.0)
|
||||
|
||||
self.assertEqual(values.tolist(), [complex(10, -1), complex(-20, 2)])
|
||||
self.assertEqual(reader.locked_points, 2)
|
||||
finally:
|
||||
if reader is not None:
|
||||
reader.close()
|
||||
os.close(master_fd)
|
||||
os.close(slave_fd)
|
||||
self._close(master_fd, slave_fd, reader)
|
||||
|
||||
def test_stream_reads_consecutive_variable_length_sweeps(self) -> None:
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
reader: KamilAdcTtyReader | None = None
|
||||
def test_consecutive_constant_length_sweeps(self) -> None:
|
||||
"""Each newly-completed sweep is delivered once new data arrives after a read."""
|
||||
master_fd, slave_fd, reader = self._open_pty_reader()
|
||||
try:
|
||||
os.write(
|
||||
master_fd,
|
||||
_start_frame()
|
||||
+ _point_frame(1, 10, -1)
|
||||
+ _point_frame(2, -20, 2)
|
||||
+ _start_frame(),
|
||||
)
|
||||
first = reader.read_sweep(timeout_s=1.0)
|
||||
self.assertEqual(first.tolist(), [complex(10, -1), complex(-20, 2)])
|
||||
|
||||
os.write(
|
||||
master_fd,
|
||||
_point_frame(1, 30, -3) + _point_frame(2, -40, 4) + _start_frame(),
|
||||
)
|
||||
second = reader.read_sweep(timeout_s=1.0)
|
||||
self.assertEqual(second.tolist(), [complex(30, -3), complex(-40, 4)])
|
||||
finally:
|
||||
self._close(master_fd, slave_fd, reader)
|
||||
|
||||
def test_shorter_sweep_after_lock_raises(self) -> None:
|
||||
"""A later sweep with fewer points than the locked-in count fails fast."""
|
||||
master_fd, slave_fd, reader = self._open_pty_reader()
|
||||
try:
|
||||
tty.setraw(slave_fd)
|
||||
reader = KamilAdcTtyReader(os.ttyname(slave_fd))
|
||||
reader.open()
|
||||
os.write(
|
||||
master_fd,
|
||||
_start_frame()
|
||||
@@ -80,60 +113,73 @@ class KamilAdcTtyReaderTest(unittest.TestCase):
|
||||
+ _point_frame(1, 30, -3)
|
||||
+ _start_frame(),
|
||||
)
|
||||
|
||||
first = reader.read_sweep(timeout_s=1.0)
|
||||
second = reader.read_sweep(timeout_s=1.0)
|
||||
|
||||
self.assertEqual(first.tolist(), [complex(10, -1), complex(-20, 2)])
|
||||
self.assertEqual(second.tolist(), [complex(30, -3)])
|
||||
with self.assertRaisesRegex(RuntimeError, "sweep length changed"):
|
||||
reader.read_sweep(timeout_s=1.0)
|
||||
finally:
|
||||
if reader is not None:
|
||||
reader.close()
|
||||
os.close(master_fd)
|
||||
os.close(slave_fd)
|
||||
self._close(master_fd, slave_fd, reader)
|
||||
|
||||
def test_expected_point_count_discards_mismatched_sweep(self) -> None:
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
reader: KamilAdcTtyReader | None = None
|
||||
def test_longer_sweep_after_lock_raises(self) -> None:
|
||||
"""A later sweep with more points than the locked-in count fails fast."""
|
||||
master_fd, slave_fd, reader = self._open_pty_reader()
|
||||
try:
|
||||
tty.setraw(slave_fd)
|
||||
reader = KamilAdcTtyReader(os.ttyname(slave_fd))
|
||||
reader.open()
|
||||
os.write(
|
||||
master_fd,
|
||||
_start_frame()
|
||||
+ _point_frame(1, 5, -5)
|
||||
+ _start_frame()
|
||||
+ _point_frame(1, 10, -1)
|
||||
+ _point_frame(2, -20, 2)
|
||||
+ _start_frame()
|
||||
+ _point_frame(1, 30, -3)
|
||||
+ _point_frame(2, -40, 4)
|
||||
+ _start_frame(),
|
||||
)
|
||||
|
||||
values = reader.read_sweep(timeout_s=1.0, expected_points=2)
|
||||
|
||||
self.assertEqual(values.tolist(), [complex(10, -1), complex(-20, 2)])
|
||||
first = reader.read_sweep(timeout_s=1.0)
|
||||
self.assertEqual(first.tolist(), [complex(10, -1)])
|
||||
with self.assertRaisesRegex(RuntimeError, "exceeded locked point count"):
|
||||
reader.read_sweep(timeout_s=1.0)
|
||||
finally:
|
||||
if reader is not None:
|
||||
reader.close()
|
||||
os.close(master_fd)
|
||||
os.close(slave_fd)
|
||||
self._close(master_fd, slave_fd, reader)
|
||||
|
||||
def test_stream_without_next_start_times_out_with_received_count(self) -> None:
|
||||
master_fd, slave_fd = pty.openpty()
|
||||
reader: KamilAdcTtyReader | None = None
|
||||
def test_no_completed_sweep_times_out(self) -> None:
|
||||
master_fd, slave_fd, reader = self._open_pty_reader()
|
||||
try:
|
||||
tty.setraw(slave_fd)
|
||||
reader = KamilAdcTtyReader(os.ttyname(slave_fd))
|
||||
reader.open()
|
||||
# Start marker plus a partial sweep with no follow-up boundary.
|
||||
os.write(master_fd, _start_frame() + _point_frame(1, 10, -1))
|
||||
|
||||
with self.assertRaisesRegex(TimeoutError, "sweep end: received 1 points"):
|
||||
reader.read_sweep(timeout_s=0.05)
|
||||
with self.assertRaisesRegex(TimeoutError, "Timed out waiting for Kamil ADC sweep"):
|
||||
reader.read_sweep(timeout_s=0.1)
|
||||
finally:
|
||||
if reader is not None:
|
||||
reader.close()
|
||||
os.close(master_fd)
|
||||
os.close(slave_fd)
|
||||
self._close(master_fd, slave_fd, reader)
|
||||
|
||||
def test_only_latest_sweep_is_published(self) -> None:
|
||||
"""If multiple sweeps arrive before the consumer reads, only the newest survives."""
|
||||
master_fd, slave_fd, reader = self._open_pty_reader()
|
||||
try:
|
||||
payload = (
|
||||
_start_frame()
|
||||
+ _point_frame(1, 1, 0)
|
||||
+ _point_frame(2, 2, 0)
|
||||
+ _start_frame()
|
||||
+ _point_frame(1, 3, 0)
|
||||
+ _point_frame(2, 4, 0)
|
||||
+ _start_frame()
|
||||
+ _point_frame(1, 5, 0)
|
||||
+ _point_frame(2, 6, 0)
|
||||
+ _start_frame()
|
||||
)
|
||||
os.write(master_fd, payload)
|
||||
# Wait until the reader thread has parsed all three sweeps before
|
||||
# reading from the mailbox — otherwise we'd race the producer and
|
||||
# might consume an intermediate value.
|
||||
deadline = time.monotonic() + 1.0
|
||||
while time.monotonic() < deadline and reader.published_count < 3:
|
||||
time.sleep(0.005)
|
||||
self.assertGreaterEqual(reader.published_count, 3)
|
||||
values = reader.read_sweep(timeout_s=1.0)
|
||||
# The reader thread overwrites unread sweeps; the consumer sees the
|
||||
# most recently completed one.
|
||||
self.assertEqual(values.tolist(), [complex(5, 0), complex(6, 0)])
|
||||
finally:
|
||||
self._close(master_fd, slave_fd, reader)
|
||||
|
||||
|
||||
class KamilAdcConfigTest(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user