sprint-0: foundations -- data model, seed library, tests, demo
Initial commit. Delivers what the brief calls 'Sprint 0 - Foundations' (see docs/AR_Autopilot_brief.md section 12): - Complete repository structure (arautopilot package + firmware, display, installer, tools placeholders + docs). - Core data model (Pydantic v2): modes, alarms, actuator config, PID config + gain scheduling, vessel config, knob state machine, project config with YAML/JSON serialisation. - Seed library: 2 actuator profiles (hydraulic & electric DC reversible) and 2 default tunings (yacht motor planeo 30 m and 40 m). Conservative literature values, NOT the integrator's production tuning IP. - Firmware skeleton: only src/hal/pinout.h with the 21 I/O contract for the AR-NMEA-IO v1.0 board. No drivers, no main loop. - Studio stubs (real PySide6 app starts in Sprint 4). - pytest suite (80 tests, all green): modes, alarms, actuator, PID (incl. gain interpolation and the +/-50% adaptive bound from brief section 6), vessel, knob state, project config, library loader, end-to-end roundtrip. - examples/sprint0_demo.py - the acceptance demo from the brief. Acceptance criteria met: - pytest green (80/80) - demo creates, saves (YAML + JSON), reloads, and verifies a full ProjectConfig using the seed library - repository ready for tag `sprint-0-approved` See CHANGELOG.md for the detailed scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Tests for ``arautopilot.core.alarms``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from arautopilot.core.alarms import (
|
||||
Alarm,
|
||||
AlarmSeverity,
|
||||
AlarmType,
|
||||
default_message,
|
||||
default_severity,
|
||||
triggers_auto_disengage,
|
||||
)
|
||||
|
||||
|
||||
def test_catalogue_covers_every_alarm_type() -> None:
|
||||
"""The internal catalogue must define metadata for every enum member."""
|
||||
for at in AlarmType:
|
||||
# Calling any of the helpers must succeed for every type.
|
||||
sev = default_severity(at)
|
||||
msg = default_message(at)
|
||||
flag = triggers_auto_disengage(at)
|
||||
assert isinstance(sev, AlarmSeverity)
|
||||
assert isinstance(msg, str) and msg
|
||||
assert isinstance(flag, bool)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"alarm_type",
|
||||
[
|
||||
AlarmType.OFF_COURSE_SEVERE,
|
||||
AlarmType.RUDDER_NOT_RESPONDING,
|
||||
AlarmType.HEADING_SENSOR_LOST,
|
||||
AlarmType.WATCHDOG_TRIPPED,
|
||||
AlarmType.VMS_CRITICAL,
|
||||
AlarmType.ACTUATOR_OVERCURRENT,
|
||||
AlarmType.VOLTAGE_LOW,
|
||||
],
|
||||
)
|
||||
def test_safety_critical_alarms_trigger_auto_disengage(alarm_type: AlarmType) -> None:
|
||||
"""All EMERGENCY alarms (and HIGH overcurrent/voltage) must auto-disengage."""
|
||||
assert triggers_auto_disengage(alarm_type) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"alarm_type",
|
||||
[AlarmType.OFF_COURSE, AlarmType.LIMIT_SWITCH_REACHED],
|
||||
)
|
||||
def test_warning_alarms_do_not_disengage(alarm_type: AlarmType) -> None:
|
||||
assert triggers_auto_disengage(alarm_type) is False
|
||||
|
||||
|
||||
def test_alarm_from_type_uses_catalogue_defaults() -> None:
|
||||
a = Alarm.from_type(AlarmType.OFF_COURSE_SEVERE)
|
||||
assert a.type is AlarmType.OFF_COURSE_SEVERE
|
||||
assert a.severity is AlarmSeverity.EMERGENCY
|
||||
assert a.auto_disengage_triggered is True
|
||||
assert a.message # non-empty
|
||||
assert a.source == "firmware"
|
||||
assert a.acknowledged is False
|
||||
|
||||
|
||||
def test_alarm_is_frozen() -> None:
|
||||
a = Alarm.from_type(AlarmType.OFF_COURSE)
|
||||
with pytest.raises((TypeError, ValueError)):
|
||||
a.acknowledged = True # type: ignore[misc]
|
||||
|
||||
|
||||
def test_alarm_rejects_unknown_field() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
Alarm(
|
||||
type=AlarmType.OFF_COURSE,
|
||||
severity=AlarmSeverity.LOW,
|
||||
message="hi",
|
||||
foo="bar", # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
|
||||
def test_alarm_message_length_limits() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
Alarm(type=AlarmType.OFF_COURSE, severity=AlarmSeverity.LOW, message="")
|
||||
with pytest.raises(ValueError):
|
||||
Alarm(
|
||||
type=AlarmType.OFF_COURSE,
|
||||
severity=AlarmSeverity.LOW,
|
||||
message="x" * 241,
|
||||
)
|
||||
Reference in New Issue
Block a user