Prepared for playback engine

This commit is contained in:
2026-09-11 23:45:27 +02:00
parent 19b356a94d
commit bbc5aac891
14 changed files with 955 additions and 189 deletions
+7 -9
View File
@@ -30,13 +30,6 @@ dl_core = Core(config)
dl_core.start()
dl_core.start_recording()
for i in range(2):
dl_core.wait(1)
values = dl_core.get_all_live_values()
log.debug(f"Core values: {values}")
unit_id = dl_core.bind_derive_unit(
processing_str,
[
@@ -50,6 +43,13 @@ unit_id = dl_core.bind_derive_unit(
SignalDescriptor(id=uuid4(), name="Dummy sum", type="number", timeout_ms=5000),
)
for i in range(10):
dl_core.wait(1)
values = dl_core.get_all_live_values()
log.debug(f"Core values: {values}")
dl_core.set_mode("playback")
try:
while True:
@@ -58,6 +58,4 @@ try:
log.debug(f"Core values: {values}")
except KeyboardInterrupt:
log.info("Received keyboard interrupt")
dl_core.stop_recording()
dl_core._processing_buffer.write("./", "output.dlpak")
dl_core.stop()
+61
View File
@@ -1,9 +1,14 @@
import logging
import threading
from uuid import uuid4
import pytest
from dynalab_core import Core
from dynalab_core.config import CoreConfig
from dynalab_core.errors import CoreStateMismatchError
from dynalab_core.protocols.endpoint import ConnectorEndpoint
from dynalab_core.protocols.packets.data import ValueDescriptor
from dynalab_core.protocols.packets.handshake import ConnectorHello
from test.common import find_available_port
@@ -32,3 +37,59 @@ def test_core_cannot_start_twice(caplog: pytest.LogCaptureFixture) -> None:
)
assert rejection.levelno == logging.WARNING
assert rejection.core_state == "started"
def test_core_input_worker_logs_failure(
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
) -> None:
core = Core(CoreConfig(port=find_available_port(8765)))
def fail_routing(message: ValueDescriptor) -> None:
raise RuntimeError("routing failed")
monkeypatch.setattr(core._derive_registry, "put_data", fail_routing)
with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
core.start()
try:
core._data_input_queue.put_nowait(
ValueDescriptor(signal_id=uuid4(), value=1.0, timestamp=1)
)
assert core._input_worker_stopped_event.wait(1.0)
finally:
core.stop()
failures = [
record
for record in caplog.records
if getattr(record, "event", None) == "core.input_worker_failed"
]
assert len(failures) == 1
assert failures[0].exception_type == "RuntimeError"
assert failures[0].message_type == "ValueDescriptor"
assert failures[0].exc_info is not None
def test_core_mode_is_shared_with_endpoints() -> None:
core = Core(CoreConfig(port=find_available_port(8765)))
endpoint = ConnectorEndpoint(
ConnectorHello(
connector_uuid=uuid4(),
connector_name="test",
connector_version="1.0",
signals=[],
),
threading.Event(),
threading.Event(),
core._data_input_queue,
core._connector_registry._core_mode,
)
try:
assert endpoint._core_mode is core._mode
core.set_mode("playback")
assert endpoint._core_mode.value == "playback"
finally:
core.stop()
+43 -3
View File
@@ -1,7 +1,9 @@
import logging
from queue import Empty, Queue
import time
from uuid import UUID, uuid4
import pytest
from dynalab_core.derive import DeriveRegistry
from dynalab_core.protocols.packets.data import ValueDescriptor
from dynalab_core.protocols.packets.handshake import SignalDescriptor
@@ -19,6 +21,14 @@ def add(left: ValueDescriptor, right: ValueDescriptor, output: SignalDescriptor)
)
"""
FAILING_PROCESSING_FUNCTION = """
from dynalab_core.protocols.packets.data import ValueDescriptor
from dynalab_core.protocols.packets.handshake import SignalDescriptor
def fail(value: ValueDescriptor, output: SignalDescriptor) -> ValueDescriptor:
raise RuntimeError("derive failed")
"""
def test_derive_registry_routes_with_live_values() -> None:
output_queue: Queue[ValueDescriptor] = Queue()
@@ -32,9 +42,7 @@ def test_derive_registry_routes_with_live_values() -> None:
unit = registry.register(
PROCESSING_FUNCTION, [left_signal, right_signal], output_signal
)
right_value = ValueDescriptor(
signal_id=right_signal.id, value=2.0, timestamp=1
)
right_value = ValueDescriptor(signal_id=right_signal.id, value=2.0, timestamp=1)
live_values[right_signal.id] = right_value
left_value = ValueDescriptor(signal_id=left_signal.id, value=3.0, timestamp=2)
@@ -59,6 +67,7 @@ def test_derive_registry_routes_with_live_values() -> None:
assert result.value == 5.0
assert result.timestamp == left_value.timestamp
def test_derive_registry_ignores_unknown_unit_on_unregister() -> None:
registry = DeriveRegistry(Queue(), lambda signal_id: None)
@@ -66,3 +75,34 @@ def test_derive_registry_ignores_unknown_unit_on_unregister() -> None:
registry.unregister(uuid4())
finally:
registry.stop()
def test_derive_worker_logs_failure_and_stops(
caplog: pytest.LogCaptureFixture,
) -> None:
input_signal = SignalDescriptor(id=uuid4(), name="Input", type="number")
output_signal = SignalDescriptor(id=uuid4(), name="Output", type="number")
registry = DeriveRegistry(Queue(), lambda signal_id: None)
with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
try:
unit = registry.register(
FAILING_PROCESSING_FUNCTION, [input_signal], output_signal
)
unit.put_data(
[ValueDescriptor(signal_id=input_signal.id, value=1.0, timestamp=1)]
)
assert unit._stopped_event.wait(1.0)
assert registry.get_all_signal_descriptors() == []
finally:
registry.stop()
failures = [
record
for record in caplog.records
if getattr(record, "event", None) == "derive.worker_failed"
]
assert len(failures) == 1
assert failures[0].derive_uuid == str(unit.uuid())
assert failures[0].exception_type == "RuntimeError"
assert failures[0].exc_info is not None
+29
View File
@@ -0,0 +1,29 @@
import logging
from datetime import datetime, timezone
from uuid import uuid4
import pytest
from dynalab_core.buffer import ValueBuffer
from dynalab_core.dlpak import DLPak
from dynalab_core.protocols.packets.handshake import SignalDescriptor
def test_dlpak_logs_written_archive(caplog: pytest.LogCaptureFixture, tmp_path) -> None:
signal = SignalDescriptor(id=uuid4(), name="Signal", type="number")
buffer = ValueBuffer()
buffer.append(1, signal.id, 2.0)
package = DLPak()
package.set_data(buffer)
package.set_manifest(datetime.now(timezone.utc), [signal])
with caplog.at_level(logging.INFO, logger="dynalab_core"):
package.write(tmp_path, "recording")
record = next(
record
for record in caplog.records
if getattr(record, "event", None) == "dlpak.written"
)
assert record.output_filename == "recording.dlpak"
assert record.sample_count == 1
assert record.signal_count == 1
+68 -1
View File
@@ -1,3 +1,4 @@
import asyncio
import logging
import socket
import time
@@ -7,6 +8,7 @@ import pytest
from dynalab_core import Core
from dynalab_core.config import CoreConfig
from dynalab_core.protocols.constants import PROTOCOL_VERSION
from dynalab_core.protocols.common import VersionDescriptor
from dynalab_core.protocols.json.errors import (
JsonServerStartupError,
JsonServerTimeoutError,
@@ -91,7 +93,7 @@ def test_json_server_logs_invalid_handshake(
with socket.create_connection(("127.0.0.1", port), timeout=1) as peer:
server_hello = peer.makefile("rb").readline()
assert server_hello
peer.sendall(b"not-json\n")
peer.sendall(b'{"secret":"must-not-appear-in-logs"}\n')
rejection = _wait_for_event(caplog, "connector.handshake_rejected")
finally:
@@ -101,6 +103,69 @@ def test_json_server_logs_invalid_handshake(
assert rejection.reason == "invalid_frame"
assert rejection.connection_id
assert rejection.peer_address
assert "must-not-appear-in-logs" not in caplog.text
def test_json_server_reports_unexpected_thread_startup_failure(
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
) -> None:
async def fail_start_server(*args: object, **kwargs: object) -> None:
raise RuntimeError("unexpected startup failure")
monkeypatch.setattr(asyncio, "start_server", fail_start_server)
core = Core(CoreConfig(port=find_available_port(8765)))
with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
try:
with pytest.raises(JsonServerStartupError):
core.start()
finally:
core.stop()
failures = [
record
for record in caplog.records
if getattr(record, "event", None) == "json_server.thread_failed"
]
assert len(failures) == 1
assert failures[0].startup_complete is False
assert failures[0].exception_type == "RuntimeError"
assert failures[0].exc_info is not None
def test_rejected_handshake_is_not_logged_as_connected(
caplog: pytest.LogCaptureFixture,
) -> None:
port = find_available_port(8765)
core = Core(CoreConfig(port=port))
connector_uuid = uuid4()
connector_hello = ConnectorHello(
connector_uuid=connector_uuid,
protocol_version=VersionDescriptor(type="alpha", major=999, minor=0, patch=0),
connector_name="Incompatible connector",
connector_version="0.1.0-test",
signals=[SignalDescriptor(id=uuid4(), name="Dummy signal", type="number")],
)
with caplog.at_level(logging.DEBUG, logger="dynalab_core"):
core.start()
try:
with socket.create_connection(("127.0.0.1", port), timeout=1) as peer:
peer_file = peer.makefile("rb")
assert peer_file.readline()
peer.sendall(connector_hello.model_dump_json().encode("utf-8") + b"\n")
assert b'"type":"handshake_rejected"' in peer_file.readline()
rejection = _wait_for_event(caplog, "endpoint.handshake_rejected")
finally:
core.stop()
assert rejection.reason == "protocol_version_mismatch"
assert rejection.connector_uuid == str(connector_uuid)
assert not any(
getattr(record, "event", None) == "connector.connected"
and getattr(record, "connector_uuid", None) == str(connector_uuid)
for record in caplog.records
)
def test_json_server_waits_for_connection_handlers_on_stop(
@@ -133,6 +198,8 @@ def test_json_server_waits_for_connection_handlers_on_stop(
events = [getattr(record, "event", None) for record in caplog.records]
assert "json_server.stop_timeout" not in events
assert "connector.connected" in events
assert events.index("connector.registered") < events.index("connector.connected")
assert core._json_server._stopped_event.is_set()
assert not core._json_server._handler_tasks
assert core._connector_registry.get(connector_uuid) is None