from __future__ import annotations import unittest from app.simulation.components.experimental.library import LIBRARY from app.simulation.components.amesim.library import LIBRARY as AMESIM_LIBRARY from app.simulation.core.base import AlgebraicComponent from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec from app.simulation.core.metadata import ParameterDefinition, ParameterOption from app.simulation.core.medium import IdealGasMedium from app.simulation.core.ports import PortDefinition from app.simulation.registry import ( COMPONENT_LIBRARY_REGISTRY, COMPONENT_MODEL_REGISTRY, discover_component_registries, validate_component_model_class, ) class ComponentRegistryTests(unittest.TestCase): def test_manifest_is_the_only_public_model_source(self) -> None: libraries, models = discover_component_registries() self.assertEqual(libraries, COMPONENT_LIBRARY_REGISTRY) self.assertEqual(models, COMPONENT_MODEL_REGISTRY) self.assertEqual( tuple(models), ( "cylinder", "tank", "pipe", "orifice", "tee", "amesim_ideal_air_medium", "amesim_helium_medium", "amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_lstp00a", "amesim_lmechn1", "amesim_pnrp17", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2", ), ) self.assertNotIn( "app.simulation.components.experimental.flow.pipe:Pipe", LIBRARY.models, ) def test_amesim_library_is_enabled_as_separate_public_boundary(self) -> None: libraries, models = discover_component_registries() self.assertIn("amesim", libraries) self.assertEqual(libraries["amesim"], AMESIM_LIBRARY) self.assertEqual( models["amesim_ideal_air_medium"].library.id, "amesim", ) self.assertEqual(models["amesim_helium_medium"].library.id, "amesim") self.assertEqual(models["amesim_pnpl01"].library.id, "amesim") self.assertEqual(models["amesim_step0"].library.id, "amesim") self.assertEqual(models["amesim_ud00"].library.id, "amesim") self.assertEqual(models["amesim_f000"].library.id, "amesim") self.assertEqual(models["amesim_forc"].library.id, "amesim") self.assertEqual(models["amesim_mecmas21"].library.id, "amesim") self.assertEqual(models["amesim_lstp00a"].library.id, "amesim") self.assertEqual(models["amesim_lmechn1"].library.id, "amesim") self.assertEqual(models["amesim_pnrp17"].library.id, "amesim") self.assertEqual(models["amesim_pnch023"].library.id, "amesim") self.assertEqual(models["amesim_pnch012"].library.id, "amesim") self.assertEqual(models["amesim_pnor001"].library.id, "amesim") self.assertEqual(models["amesim_pnvo001_fixed"].library.id, "amesim") self.assertEqual(models["amesim_pnvo001"].library.id, "amesim") self.assertEqual(models["amesim_pnl00r"].library.id, "amesim") self.assertEqual(models["amesim_pnl0001"].library.id, "amesim") self.assertEqual(models["amesim_pnl0002"].library.id, "amesim") self.assertEqual(models["amesim_pnl0003"].library.id, "amesim") self.assertEqual(models["amesim_pn3node2"].library.id, "amesim") self.assertEqual(models["amesim_p4node2"].library.id, "amesim") def test_every_public_model_owns_its_catalog_contract(self) -> None: for model_type, spec in COMPONENT_MODEL_REGISTRY.items(): with self.subTest(model_type=model_type): declarations = spec.component_class.__dict__ self.assertIn("MODEL_VERSION", declarations) self.assertIn("DISPLAY", declarations) self.assertIn("create", declarations) self.assertEqual(spec.display.library_id, spec.library.id) self.assertIn( spec.display.category_id, spec.library.category_by_id, ) def test_display_ports_must_match_physical_ports(self) -> None: class BrokenDisplayComponent(AlgebraicComponent): MODEL_TYPE = "broken_display" MODEL_VERSION = "1.0.0" PORTS = ( PortDefinition.pneumatic( "port_a", nominal_role="bidirectional", ), ) PARAMETERS = () RESULT_VARIABLES = () DISPLAY = ComponentDisplaySpec( label="错误显示模型", library_id="experimental", category_id="flow", symbol="generic", ports=(PortDisplaySpec("port_b", "right"),), ) @classmethod def create( cls, *, name: str, medium: IdealGasMedium, parameters, ): return cls(name) with self.assertRaisesRegex( ValueError, "display ports must exactly match", ): validate_component_model_class( BrokenDisplayComponent, library=LIBRARY, ) def test_property_model_parameter_options_are_strictly_validated(self) -> None: def validate_parameter(parameter: ParameterDefinition) -> None: class PropertyModelComponent(AlgebraicComponent): MODEL_TYPE = "property_model_component" MODEL_VERSION = "1.0.0" PORTS = () PARAMETERS = (parameter,) RESULT_VARIABLES = () DISPLAY = ComponentDisplaySpec( label="物性模型测试组件", library_id="experimental", category_id="flow", symbol="generic", ports=(), ) @classmethod def create(cls, *, name, medium, parameters): del medium, parameters return cls(name) validate_component_model_class( PropertyModelComponent, library=LIBRARY, ) def property_parameter( *, default: float = 0.0, minimum: float | None = None, maximum: float | None = None, options: tuple[ParameterOption, ...] = (), description: object = "", ) -> ParameterDefinition: return ParameterDefinition( "property_model", default, label="物性计算模型", quantity="dimensionless", unit="", minimum=minimum, maximum=maximum, editor="amesimGasPropertyModel", options=options, description=description, # type: ignore[arg-type] ) with self.assertRaisesRegex(ValueError, "description must be a string"): validate_parameter(property_parameter(description=None)) with self.assertRaisesRegex(ValueError, "description must not be blank"): validate_parameter(property_parameter(description=" ")) with self.assertRaisesRegex(ValueError, "must declare options"): validate_parameter(property_parameter()) with self.assertRaisesRegex(ValueError, "duplicate option values"): validate_parameter( property_parameter( options=( ParameterOption(0, "理想气体"), ParameterOption(0, "重复模型"), ) ) ) with self.assertRaisesRegex(ValueError, "finite numbers"): validate_parameter( property_parameter( options=(ParameterOption(float("nan"), "非法模型"),) ) ) with self.assertRaisesRegex(ValueError, "default value must be one of 0"): validate_parameter( property_parameter( default=1.0, options=(ParameterOption(0, "理想气体"),), ) ) with self.assertRaisesRegex(ValueError, "option 1 must be at most 0"): validate_parameter( property_parameter( minimum=0.0, maximum=0.0, options=(ParameterOption(1, "越界模型"),), ) ) with self.assertRaisesRegex(ValueError, "option values must be integers"): validate_parameter( property_parameter( default=0.5, options=(ParameterOption(0.5, "非整数模型"),), ) ) if __name__ == "__main__": unittest.main()