Files
AR-Autopilot/arautopilot/tests/test_roundtrip.py
T
alro65 700756c16f 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>
2026-05-17 23:57:18 -04:00

81 lines
2.6 KiB
Python

"""End-to-end roundtrip test — Sprint 0 acceptance criterion.
This is the test the brief calls out by name (section 12, "Criterio de
aceptación"): build a project programmatically, persist it, reload it,
and verify the reloaded object matches the original exactly.
"""
from __future__ import annotations
from pathlib import Path
from arautopilot.core import (
ActuatorConfig,
ActuatorType,
ProjectConfig,
VesselConfig,
VesselType,
)
from arautopilot.library.loader import (
load_actuator_profile,
load_default_tuning,
)
def test_full_roundtrip_using_seed_library(tmp_path: Path) -> None:
# 1. Assemble a project using the seed library
actuator: ActuatorConfig = load_actuator_profile("hydraulic_reversible")
pid = load_default_tuning("yacht_motor_planeo_30m")
vessel = VesselConfig(
name="M/Y Sprint-0",
type=VesselType.YACHT_MOTOR_PLANEO,
length_m=30.0,
displacement_t=125.0,
max_speed_kn=28.0,
actuator=actuator,
pid=pid,
)
project = ProjectConfig(
client_name="Acceptance Test Client",
project_name="Sprint 0 Roundtrip",
notes="Demonstrates the brief's acceptance criterion.",
vessel=vessel,
)
# 2. Save to YAML and JSON
yaml_path = tmp_path / "project.yaml"
json_path = tmp_path / "project.json"
project.save_yaml(yaml_path)
project.save_json(json_path)
assert yaml_path.exists() and yaml_path.stat().st_size > 0
assert json_path.exists() and json_path.stat().st_size > 0
# 3. Reload both and verify exact equality
from_yaml = ProjectConfig.load(yaml_path)
from_json = ProjectConfig.load(json_path)
assert from_yaml == project
assert from_json == project
# Critical structural invariants survive serialisation
assert from_yaml.vessel.actuator.type is ActuatorType.HYDRAULIC_REVERSIBLE
assert from_yaml.vessel.pid.inner_loop_freq_hz > from_yaml.vessel.pid.outer_loop_freq_hz
assert len(from_yaml.vessel.pid.gain_schedule) == 3
def test_roundtrip_preserves_ids(tmp_path: Path, basic_project: ProjectConfig) -> None:
p = tmp_path / "p.yaml"
basic_project.save_yaml(p)
rebuilt = ProjectConfig.load(p)
assert rebuilt.project_id == basic_project.project_id
assert rebuilt.vessel.vessel_id == basic_project.vessel.vessel_id
def test_roundtrip_preserves_timestamps(tmp_path: Path, basic_project: ProjectConfig) -> None:
p = tmp_path / "p.json"
basic_project.save_json(p)
rebuilt = ProjectConfig.load(p)
assert rebuilt.created_at == basic_project.created_at
assert rebuilt.modified_at == basic_project.modified_at