feat: AR-ElecArrangement initial commit — Python FastAPI + uvicorn (LAN desktop app, packaged as .exe via PyInstaller)
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Sprint 0 — sanity: un Project recién creado se guarda y se carga sin pérdida.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from arelec.core.project import Project, ProjectMetadata, SCHEMA_VERSION
|
||||
|
||||
|
||||
def test_project_default_metadata() -> None:
|
||||
p = Project()
|
||||
assert p.metadata.name == "Nuevo proyecto"
|
||||
assert p.metadata.schema_version == SCHEMA_VERSION
|
||||
assert p.metadata.created_at != ""
|
||||
|
||||
|
||||
def test_save_and_load_roundtrip(tmp_path: Path) -> None:
|
||||
path = tmp_path / "demo.area"
|
||||
original = Project(metadata=ProjectMetadata(
|
||||
name="Yate Demo",
|
||||
author="ARD",
|
||||
company="AR ShipDesign",
|
||||
notes="Sprint 0 test",
|
||||
))
|
||||
original.save(path)
|
||||
assert path.exists()
|
||||
assert path.stat().st_size > 0
|
||||
|
||||
loaded = Project.load(path)
|
||||
assert loaded.metadata.name == "Yate Demo"
|
||||
assert loaded.metadata.author == "ARD"
|
||||
assert loaded.metadata.company == "AR ShipDesign"
|
||||
assert loaded.metadata.notes == "Sprint 0 test"
|
||||
assert loaded.metadata.schema_version == SCHEMA_VERSION
|
||||
|
||||
|
||||
def test_save_touches_modified_at(tmp_path: Path) -> None:
|
||||
p = Project()
|
||||
original_modified = p.metadata.modified_at
|
||||
p.save(tmp_path / "t.area")
|
||||
# Save siempre actualiza modified_at (aunque sea el mismo segundo, queda con
|
||||
# nueva timestamp ISO — comparamos que fue tocado).
|
||||
assert p.metadata.modified_at >= original_modified
|
||||
|
||||
|
||||
def test_load_rejects_future_schema(tmp_path: Path) -> None:
|
||||
"""Un .area creado por una versión más nueva debe rechazarse limpio."""
|
||||
path = tmp_path / "future.area"
|
||||
p = Project(metadata=ProjectMetadata(schema_version=SCHEMA_VERSION + 99))
|
||||
p.save(path)
|
||||
with pytest.raises(ValueError, match="schema_version"):
|
||||
Project.load(path)
|
||||
|
||||
|
||||
def test_load_rejects_missing_project_json(tmp_path: Path) -> None:
|
||||
import zipfile
|
||||
|
||||
path = tmp_path / "empty.area"
|
||||
with zipfile.ZipFile(path, mode="w") as zf:
|
||||
zf.writestr("readme.txt", "no project.json")
|
||||
with pytest.raises(KeyError):
|
||||
Project.load(path)
|
||||
|
||||
|
||||
def test_load_rejects_non_zip(tmp_path: Path) -> None:
|
||||
import zipfile
|
||||
|
||||
path = tmp_path / "garbage.area"
|
||||
path.write_bytes(b"this is not a zip file")
|
||||
with pytest.raises(zipfile.BadZipFile):
|
||||
Project.load(path)
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Sprint 0 — sanity: conversiones SI ↔ imperial round-trip y casos límite.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from arelec.core.units import (
|
||||
AWG_TO_MM2,
|
||||
awg_to_mm2,
|
||||
ft_to_m,
|
||||
kg_to_lb,
|
||||
kw_to_hp,
|
||||
lb_to_kg,
|
||||
m_to_ft,
|
||||
mm2_to_awg,
|
||||
hp_to_kw,
|
||||
)
|
||||
|
||||
|
||||
def test_length_roundtrip() -> None:
|
||||
assert m_to_ft(1.0) == pytest.approx(3.28084, rel=1e-4)
|
||||
assert ft_to_m(m_to_ft(42.0)) == pytest.approx(42.0)
|
||||
|
||||
|
||||
def test_mass_roundtrip() -> None:
|
||||
assert kg_to_lb(1.0) == pytest.approx(2.20462, rel=1e-4)
|
||||
assert lb_to_kg(kg_to_lb(100.0)) == pytest.approx(100.0)
|
||||
|
||||
|
||||
def test_power_roundtrip() -> None:
|
||||
assert hp_to_kw(1.0) == pytest.approx(0.7355)
|
||||
assert kw_to_hp(hp_to_kw(150.0)) == pytest.approx(150.0)
|
||||
|
||||
|
||||
def test_awg_lookup_known_values() -> None:
|
||||
# ABYC E-11 tabla VI — valores publicados estándar
|
||||
assert awg_to_mm2("10") == 5.26
|
||||
assert awg_to_mm2("4/0") == 107.0
|
||||
assert awg_to_mm2("14") == 2.08
|
||||
|
||||
|
||||
def test_mm2_to_awg_picks_next_larger() -> None:
|
||||
# 5.0 mm² no es exactamente AWG 10 (5.26), pero AWG 10 cubre el cable.
|
||||
# Criterio conservador: elegir AWG cuya área es ≥ pedida.
|
||||
assert mm2_to_awg(5.0) == "10"
|
||||
assert mm2_to_awg(5.26) == "10"
|
||||
assert mm2_to_awg(2.0) == "14" # 2.08
|
||||
assert mm2_to_awg(100.0) == "4/0" # 107
|
||||
|
||||
|
||||
def test_mm2_to_awg_rejects_invalid() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
mm2_to_awg(0)
|
||||
with pytest.raises(ValueError):
|
||||
mm2_to_awg(-1.0)
|
||||
with pytest.raises(ValueError):
|
||||
mm2_to_awg(500.0) # más grande que 4/0
|
||||
|
||||
|
||||
def test_awg_table_monotonic() -> None:
|
||||
"""La tabla AWG debe ir de menor a mayor área (asumido por mm2_to_awg)."""
|
||||
areas = list(AWG_TO_MM2.values())
|
||||
assert areas == sorted(areas)
|
||||
Reference in New Issue
Block a user