"""Tests for AppackCompiler — Sprint 4.""" from __future__ import annotations import json import zipfile from pathlib import Path import pytest from arautopilot.core.actuator_config import ActuatorConfig, ActuatorType from arautopilot.core.pid_config import PidConfig, PidGains from arautopilot.core.project_config import ProjectConfig from arautopilot.core.sensor_config import ( DualRudderSensorConfig, RudderSensorConfig, RudderSensorType, ) from arautopilot.core.vessel_config import VesselConfig, VesselType from arautopilot.studio.compiler.appack import AppackCompiler def _make_project(*, dual_sensors: bool = False) -> ProjectConfig: redundant = ( RudderSensorConfig( type=RudderSensorType.AS5048A_SPI, label="Redundant – actuator arm", spi_cs_gpio=11, ) if dual_sensors else None ) return ProjectConfig( client_name="Test Client", project_name="Test Project", vessel=VesselConfig( name="Test Vessel", type=VesselType.YACHT_MOTOR_PLANEO, length_m=30.0, max_speed_kn=18.0, actuator=ActuatorConfig( type=ActuatorType.HYDRAULIC_REVERSIBLE, max_rudder_angle_deg=35.0, ), pid=PidConfig( inner_loop_base=PidGains(kp=1.5, ki=0.1, kd=0.05), outer_loop_base=PidGains(kp=2.0, ki=0.05, kd=0.3), ), sensors=DualRudderSensorConfig( primary=RudderSensorConfig( type=RudderSensorType.AS5048A_SPI, spi_cs_gpio=10, ), redundant=redundant, ), ), ) class TestAppackCompiler: def test_compile_creates_file(self, tmp_path: Path) -> None: out = tmp_path / "test.appack" compiler = AppackCompiler(_make_project()) result = compiler.compile(out) assert result == out assert out.exists() def test_appack_is_valid_zip(self, tmp_path: Path) -> None: out = tmp_path / "test.appack" AppackCompiler(_make_project()).compile(out) assert zipfile.is_zipfile(out) def test_appack_contains_required_files(self, tmp_path: Path) -> None: out = tmp_path / "test.appack" project = _make_project() AppackCompiler(project).compile(out) prefix = str(project.project_id) with zipfile.ZipFile(out) as zf: names = set(zf.namelist()) assert f"{prefix}/manifest.json" in names assert f"{prefix}/project.yaml" in names assert f"{prefix}/firmware_config.h" in names assert f"{prefix}/install_notes.txt" in names def test_manifest_fields(self, tmp_path: Path) -> None: out = tmp_path / "test.appack" project = _make_project() AppackCompiler(project).compile(out) prefix = str(project.project_id) with zipfile.ZipFile(out) as zf: manifest = json.loads(zf.read(f"{prefix}/manifest.json")) assert manifest["project_id"] == str(project.project_id) assert manifest["client_name"] == "Test Client" assert manifest["vessel_name"] == "Test Vessel" assert "checksums" in manifest assert "project_yaml_sha256" in manifest["checksums"] assert "firmware_config_h_sha256" in manifest["checksums"] def test_firmware_header_contains_defines(self, tmp_path: Path) -> None: out = tmp_path / "test.appack" project = _make_project() AppackCompiler(project).compile(out) prefix = str(project.project_id) with zipfile.ZipFile(out) as zf: header = zf.read(f"{prefix}/firmware_config.h").decode() assert "#define AR_RUDDER_ANGLE_LIMIT_DEG" in header assert "#define AR_PID_INNER_KP" in header assert "#define AR_PID_OUTER_KP" in header assert "#define AR_SENSOR_PRIMARY_TYPE" in header assert "ACTUATOR_HYDRAULIC_REVERSIBLE" in header assert "RUDDER_SENSOR_AS5048A_SPI" in header assert "35.0f" in header def test_firmware_header_dual_sensor(self, tmp_path: Path) -> None: out = tmp_path / "test.appack" project = _make_project(dual_sensors=True) AppackCompiler(project).compile(out) prefix = str(project.project_id) with zipfile.ZipFile(out) as zf: header = zf.read(f"{prefix}/firmware_config.h").decode() notes = zf.read(f"{prefix}/install_notes.txt").decode() assert "#define AR_SENSOR_REDUNDANT 1" in header assert "GPIO CS=11" in notes def test_firmware_header_single_sensor(self, tmp_path: Path) -> None: out = tmp_path / "test.appack" project = _make_project(dual_sensors=False) AppackCompiler(project).compile(out) prefix = str(project.project_id) with zipfile.ZipFile(out) as zf: header = zf.read(f"{prefix}/firmware_config.h").decode() assert "#define AR_SENSOR_REDUNDANT 0" in header def test_project_yaml_roundtrips(self, tmp_path: Path) -> None: out = tmp_path / "test.appack" project = _make_project() AppackCompiler(project).compile(out) prefix = str(project.project_id) with zipfile.ZipFile(out) as zf: yaml_text = zf.read(f"{prefix}/project.yaml").decode() restored = ProjectConfig.from_yaml(yaml_text) assert restored.client_name == project.client_name assert restored.vessel.name == project.vessel.name def test_empty_client_name_raises(self) -> None: project = _make_project() object.__setattr__(project, "client_name", " ") with pytest.raises(ValueError, match="client_name"): AppackCompiler(project) def test_output_parent_created(self, tmp_path: Path) -> None: out = tmp_path / "deep" / "nested" / "output.appack" AppackCompiler(_make_project()).compile(out) assert out.exists()