700756c16f
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>
89 lines
2.6 KiB
Python
89 lines
2.6 KiB
Python
"""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,
|
|
)
|