Refactor radar system

This commit is contained in:
Ayzen
2026-10-05 16:46:28 +03:00
parent 515049d2ed
commit 8f097dfa2e
179 changed files with 8212 additions and 6676 deletions
+144
View File
@@ -13,9 +13,12 @@ Pins the agreed contract:
from __future__ import annotations
import struct
import threading
import time
import unittest
from contextlib import suppress
from pathlib import Path
from unittest import mock
import numpy as np
@@ -119,6 +122,28 @@ class TraceCollectionRoundTripTest(unittest.TestCase):
class ResultCollectionRoundTripTest(unittest.TestCase):
def test_empty_tables_and_images_retain_their_declared_dimensions(self) -> None:
for shape in ((0, 3), (2, 0), (0, 0)):
for kind in (3, 4):
with self.subTest(shape=shape, kind=kind):
payload = ResultPayload('empty', kind, table=np.empty(shape, dtype=np.float32),
image=np.empty(shape, dtype=np.float32),
image_x_axis=np.zeros(shape[1], dtype=np.float32),
image_y_axis=np.zeros(shape[0], dtype=np.float32))
if kind == 4 and shape == (2, 0):
with self.assertRaises(ValueError):
serialize_result_collection(ResultCollection(1, 2, collection_payloads=[payload]))
continue
result = decode_result_collection(serialize_result_collection(
ResultCollection(1, 2, collection_payloads=[payload])))
decoded = result.collection_payloads[0]
self.assertEqual((decoded.image if kind == 3 else decoded.table).shape, shape)
def test_multidimensional_trace_cannot_be_serialized_as_a_flat_trace(self) -> None:
payload = ResultPayload('bad', 1, frequency_hz=np.ones((1, 2)), trace=np.ones((1, 2)))
with self.assertRaises(ValueError):
serialize_result_collection(ResultCollection(1, 2, collection_payloads=[payload]))
def test_all_payload_kinds_round_trip(self) -> None:
image = np.arange(6, dtype=np.float32).reshape((2, 3))
table = np.array([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0]], dtype=np.float32)
@@ -240,6 +265,26 @@ class RingRoundTripTest(_RingTestCase):
class RingOverflowTest(_RingTestCase):
def test_reader_cannot_rewind_cursor_after_producer_laps_during_commit(self) -> None:
from python_app.orchestration.shm import atomics
name = self._ring_name()
writer = self._writer(name, capacity=4, slot_size=64)
reader = self._reader(name)
writer.push(b"f0")
advance = atomics.advance
def lap_before_reader_commit(buffer, offset, desired):
if buffer is reader._mmap and desired == 1:
for i in range(1, 6):
writer.push(f"f{i}".encode())
advance(buffer, offset, desired)
with mock.patch.object(atomics, "advance", side_effect=lap_before_reader_commit):
self.assertEqual(reader.pop_payload(), b"f0")
self.assertEqual(reader._read_u64(32), 2)
self.assertEqual([reader.pop_payload() for _ in range(4)], [b"f2", b"f3", b"f4", b"f5"])
def test_latest_wins_drops_oldest(self) -> None:
name = self._ring_name()
writer = self._writer(name, capacity=4, slot_size=64)
@@ -254,6 +299,26 @@ class RingOverflowTest(_RingTestCase):
class PeekLatestTest(_RingTestCase):
def test_peek_rejects_a_slot_while_it_is_being_overwritten(self) -> None:
name = self._ring_name()
writer = self._writer(name, capacity=1, slot_size=8)
reader = self._reader(name)
writer.push(b"aaaaaaaa")
observations = []
write_size = writer._write_u32
def overwrite_in_progress(offset: int, value: int) -> None:
write_size(offset, value)
if offset == 64:
writer._mmap[80:84] = b"bbbb"
observations.append(reader.peek_latest_payload())
with mock.patch.object(writer, "_write_u32", side_effect=overwrite_in_progress):
writer.push(b"bbbbbbbb")
self.assertEqual(observations, [None])
self.assertEqual(reader.peek_latest_payload(), b"bbbbbbbb")
def test_peek_is_latest_and_non_consuming(self) -> None:
name = self._ring_name()
writer = self._writer(name, capacity=8, slot_size=64)
@@ -269,6 +334,85 @@ class PeekLatestTest(_RingTestCase):
class RingOpenTest(_RingTestCase):
def test_invalid_geometry_and_names_fail_before_opening_files(self) -> None:
for capacity, slot in ((True, 8), (1.5, 8), (1, 9), (2 ** 32, 8)):
with self.subTest(capacity=capacity, slot=slot), self.assertRaises(ValueError):
ShmRingWriter(self._ring_name(), capacity, slot)
for name in ("/", "/../tmp/ring", "/nested/ring", "/bad\x00ring"):
with self.subTest(name=name):
with self.assertRaises(ValueError):
ShmRingWriter(name, 1, 8)
with self.assertRaises(ValueError):
ShmRingReader(name, open_timeout_s=0)
def test_failed_mapping_closes_opened_file(self) -> None:
from python_app.orchestration.shm import ring_writer
name = self._ring_name()
opened = []
fdopen = ring_writer.os.fdopen
def track_fd(fd, *args, **kwargs):
handle = fdopen(fd, *args, **kwargs)
opened.append(handle)
return handle
self.addCleanup(lambda: (Path('/dev/shm') / name[1:]).unlink(missing_ok=True))
with mock.patch.object(ring_writer.os, 'fdopen', side_effect=track_fd), \
mock.patch.object(ring_writer.mmap, 'mmap', side_effect=OSError('mapping failed')), \
self.assertRaises(OSError):
ShmRingWriter(name, 1, 8)
self.assertTrue(opened[0].closed)
def test_owner_replaces_wrong_size_without_truncating_existing_mappings(self) -> None:
name = self._ring_name()
writer = self._writer(name, capacity=8, slot_size=64)
reader = self._reader(name)
writer.push(b"old frame")
path = Path("/dev/shm") / name[1:]
old_inode = path.stat().st_ino
replacement = ShmRingWriter(name, capacity=1, slot_size_bytes=8)
self.addCleanup(self._safe_close, replacement)
self.assertNotEqual(path.stat().st_ino, old_inode)
self.assertEqual(reader.pop_payload(), b"old frame")
self.assertTrue(replacement.push(b"new"))
self.assertEqual(self._reader(name).pop_payload(), b"new")
def test_waits_for_created_but_unallocated_ring(self) -> None:
name = self._ring_name()
path = Path("/dev/shm") / name[1:]
path.touch()
self.addCleanup(lambda: path.unlink(missing_ok=True))
writers = []
def initialize() -> None:
time.sleep(0.02)
writers.append(ShmRingWriter(name, capacity=1, slot_size_bytes=8))
creator = threading.Thread(target=initialize)
creator.start()
def cleanup_creator() -> None:
creator.join()
for writer in writers:
writer.close()
self.addCleanup(cleanup_creator)
reader = self._reader(name)
creator.join()
self.assertEqual(reader.capacity, 1)
self.assertEqual(reader.slot_size_bytes, 8)
def test_header_geometry_outside_mapping_is_rejected(self) -> None:
name = self._ring_name()
writer = self._writer(name, capacity=1, slot_size=8)
writer._write_u32(12, 100)
with self.assertRaisesRegex(RuntimeError, "geometry mismatch"):
ShmRingReader(name)
def test_missing_ring_fails_fast(self) -> None:
with self.assertRaises(FileNotFoundError):
ShmRingReader("/radar_test_definitely_missing", open_timeout_s=0.1, open_poll_s=0.02)