"""Tests del motor de reglas y del auto-asignador (Sprint 2).""" from __future__ import annotations from vmssailor.core.enums import SystemId, VesselSubtype, VesselType from vmssailor.studio.designer.rule_engine import RuleContext, RuleEngine def test_rule_engine_loads_default_library(): eng = RuleEngine() # La biblioteca seed tiene yacht_motor_planeo.yaml ctx = RuleContext( vessel_type=VesselType.YACHT_MOTOR, vessel_subtype=VesselSubtype.PLANING, length_overall_m=23.5, beam_max_m=5.65, draft_m=1.85, systems_enabled=[SystemId.MAIN_ENGINE, SystemId.GENSET], ) applicable = eng.applicable_rules(ctx) assert "yacht_motor_planeo" in applicable def test_rule_engine_proposes_two_main_engines(): eng = RuleEngine() ctx = RuleContext( vessel_type=VesselType.YACHT_MOTOR, vessel_subtype=VesselSubtype.PLANING, length_overall_m=23.5, beam_max_m=5.65, draft_m=1.85, systems_enabled=[SystemId.MAIN_ENGINE], ) proposals = eng.propose(ctx) me = [p for p in proposals if p.system_id == SystemId.MAIN_ENGINE] assert len(me) == 2 prefixes = {p.tag_prefix for p in me} assert prefixes == {"ME_PORT", "ME_STBD"} def test_rule_engine_picks_volvo_for_smaller_vessel(): eng = RuleEngine() ctx = RuleContext( vessel_type=VesselType.YACHT_MOTOR, vessel_subtype=VesselSubtype.PLANING, length_overall_m=20.0, # rango Volvo 18-26 beam_max_m=5.0, draft_m=1.5, systems_enabled=[SystemId.MAIN_ENGINE], ) proposals = eng.propose(ctx) me = [p for p in proposals if p.system_id == SystemId.MAIN_ENGINE] # 20 m está en el rango Volvo (18-26) y NO en MTU (22-32) → primero match es Volvo assert all(p.model_ref == "volvo_d13_900hp" for p in me) def test_rule_engine_picks_mtu_for_larger(): eng = RuleEngine() ctx = RuleContext( vessel_type=VesselType.YACHT_MOTOR, vessel_subtype=VesselSubtype.PLANING, length_overall_m=27.0, # fuera del rango Volvo, en el de MTU beam_max_m=6.0, draft_m=2.0, systems_enabled=[SystemId.MAIN_ENGINE], ) proposals = eng.propose(ctx) me = [p for p in proposals if p.system_id == SystemId.MAIN_ENGINE] assert all(p.model_ref == "mtu_12v_2000_m96" for p in me) def test_rule_engine_does_not_propose_disabled_systems(): eng = RuleEngine() ctx = RuleContext( vessel_type=VesselType.YACHT_MOTOR, vessel_subtype=VesselSubtype.PLANING, length_overall_m=23.5, beam_max_m=5.65, draft_m=1.85, systems_enabled=[SystemId.GENSET], # main_engine no habilitado ) proposals = eng.propose(ctx) assert all(p.system_id != SystemId.MAIN_ENGINE for p in proposals) def test_rule_engine_filters_by_loa_range(): eng = RuleEngine() ctx = RuleContext( vessel_type=VesselType.YACHT_MOTOR, vessel_subtype=VesselSubtype.PLANING, length_overall_m=50.0, # fuera del rango 18-32 de yacht_motor_planeo beam_max_m=9.0, draft_m=2.5, systems_enabled=[SystemId.MAIN_ENGINE], ) assert "yacht_motor_planeo" not in eng.applicable_rules(ctx) def test_port_auto_assigner_builds_topology(): from vmssailor.core.coords import ShipCoord from vmssailor.core.equipment import Equipment from vmssailor.library import load_library from vmssailor.studio.designer.port_auto_assigner import auto_assign lib = load_library() model_lookup = {m.id: m for m in lib.equipment_models} me_port = Equipment( id="eq_me_port", model_ref="mtu_12v_2000_m96", tag_prefix="ME_PORT", display_name="Motor babor", location=ShipCoord(x_pp=6.0, y_cl=-0.9, z_bl=1.2), system_id=SystemId.MAIN_ENGINE, ) me_stbd = Equipment( id="eq_me_stbd", model_ref="mtu_12v_2000_m96", tag_prefix="ME_STBD", display_name="Motor estribor", location=ShipCoord(x_pp=6.0, y_cl=0.9, z_bl=1.2), system_id=SystemId.MAIN_ENGINE, ) report = auto_assign([me_port, me_stbd], model_lookup) assert report.bus is not None assert len(report.cards) >= 2 # una por motor + posiblemente aux assert len(report.tags) > 10 # cada motor trae ~12 sensores # Verifica que los bindings caen en canales válidos for t in report.tags: if t.physical_binding: assert t.physical_binding.channel_number >= 1 assert t.physical_binding.card_id in {c.id for c in report.cards} def test_port_auto_assigner_creates_topology_compatible_with_project(): """El topology generado debe pasar validación de Pydantic.""" from vmssailor.core.coords import ShipCoord from vmssailor.core.equipment import Equipment from vmssailor.core.project import Project from vmssailor.core.vessel import Deck, Vessel from vmssailor.library import load_library from vmssailor.studio.designer.port_auto_assigner import auto_assign lib = load_library() model_lookup = {m.id: m for m in lib.equipment_models} eq = Equipment( id="eq_test", model_ref="volvo_d13_900hp", tag_prefix="ME", display_name="Motor", location=ShipCoord(x_pp=6, y_cl=0, z_bl=1.2), system_id=SystemId.MAIN_ENGINE, ) report = auto_assign([eq], model_lookup) vessel = Vessel( id="v", name="V", type=VesselType.YACHT_MOTOR, subtype=VesselSubtype.PLANING, length_overall_m=24, beam_max_m=5.5, draft_m=1.8, decks=[Deck(id="lower", name="Lower", z_bl_bottom=0.5, z_bl_top=2.5)], ) project = Project( id="test", name="test", vessel=vessel, systems_enabled=[SystemId.MAIN_ENGINE], equipment=[eq], tags=report.tags, topology=report.topology(), ) assert project.stats()["cards"] >= 1 assert project.stats()["tags"] > 5 def test_equipment_editor_smoke(qtbot, sample_project): from vmssailor.studio.editors.equipment_editor import EquipmentEditor ed = EquipmentEditor() qtbot.addWidget(ed) ed.set_project(sample_project) assert ed._table.rowCount() == len(sample_project.equipment) def test_wizard_step_05_initializes(qtbot): from vmssailor.studio.wizard.step_05_equipment import Step05Equipment step = Step05Equipment() qtbot.addWidget(step) # Sin un QWizard padre los fields no funcionan, pero el widget debe construir assert step._table.columnCount() == 7