Files
SystemSimulationApp/tests/test_medium_reference_contract.py
T

457 lines
14 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
import unittest
from unittest.mock import patch
from app.simulation.components.experimental.library import LIBRARY
from app.simulation.core.base import AlgebraicComponent
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
from app.simulation.core.metadata import ParameterDefinition
from app.simulation.core.ports import PortDefinition
from app.simulation.registry import (
COMPONENT_MODEL_REGISTRY,
ComponentModelSpec,
validate_component_model_class,
)
from app.system_xml import validate_system_xml_document
GAS_REFERENCE_PARAMETER = ParameterDefinition(
"gi",
0.0,
label="气体类型索引",
minimum=0.0,
maximum=99.0,
editor="amesimGasReference",
)
class GasReferenceFixture(AlgebraicComponent):
MODEL_TYPE = "gas_reference_fixture"
MODEL_VERSION = "1.0.0"
PORTS = ()
PARAMETERS = (GAS_REFERENCE_PARAMETER,)
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="气体引用测试组件",
library_id="experimental",
category_id="flow",
symbol="generic",
ports=(),
)
def __init__(self, name: str, gi: float) -> None:
super().__init__(name)
self.set_parameter_values({"gi": gi})
@classmethod
def create(cls, *, name, medium, parameters):
del medium
return cls(name, parameters["gi"])
class GasMediumDefinitionFixture(AlgebraicComponent):
MODEL_TYPE = "gas_medium_definition_fixture"
MODEL_VERSION = "1.0.0"
PORTS = ()
PARAMETERS = (
ParameterDefinition(
"gi",
1.0,
label="介质索引",
minimum=1.0,
maximum=99.0,
),
)
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="气体介质定义测试组件",
library_id="experimental",
category_id="flow",
symbol="generic",
ports=(),
role="amesimGasMediumDefinition",
)
def __init__(self, name: str, gi: float) -> None:
super().__init__(name)
self.set_parameter_values({"gi": gi})
@classmethod
def create(cls, *, name, medium, parameters):
del medium
return cls(name, parameters["gi"])
class ConnectedGasReferenceFixture(AlgebraicComponent):
MODEL_TYPE = "connected_gas_reference_fixture"
MODEL_VERSION = "1.0.0"
PORTS = (
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
)
PARAMETERS = (GAS_REFERENCE_PARAMETER,)
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="相连气体引用测试组件",
library_id="experimental",
category_id="flow",
symbol="generic",
ports=(PortDisplaySpec("port_1", "right"),),
)
def __init__(self, name: str, gi: float) -> None:
super().__init__(name)
self.register_declared_port("port_1")
self.set_parameter_values({"gi": gi})
@classmethod
def create(cls, *, name, medium, parameters):
del medium
return cls(name, parameters["gi"])
GAS_REFERENCE_SPEC = ComponentModelSpec(
component_class=GasReferenceFixture,
library=LIBRARY,
)
GAS_MEDIUM_DEFINITION_SPEC = ComponentModelSpec(
component_class=GasMediumDefinitionFixture,
library=LIBRARY,
)
CONNECTED_GAS_REFERENCE_SPEC = ComponentModelSpec(
component_class=ConnectedGasReferenceFixture,
library=LIBRARY,
)
def system_xml(
*,
gi: float | None,
medium_gas_indices: tuple[float, ...] = (),
) -> bytes:
reference_parameter = (
f'<Parameter name="gi" value="{gi}"/>' if gi is not None else ""
)
medium_components = "".join(
f"""
<Component id="medium_{index}" type="gas_medium_definition_fixture"
modelVersion="1.0.0">
<Parameter name="gi" value="{gas_index}"/>
</Component>"""
for index, gas_index in enumerate(medium_gas_indices, start=1)
)
return f"""<?xml version="1.0" encoding="UTF-8"?>
<System name="medium-contract" schemaVersion="3" unitSystem="SI">
<Simulation tStart="0" tStop="1" sampleStep="0.1" maxStep="0.01" method="BDF"/>
<Components>
<Component id="consumer_1" type="gas_reference_fixture"
modelVersion="1.0.0">
{reference_parameter}
</Component>{medium_components}
</Components>
<Connections/>
</System>
""".encode()
def connected_system_xml(
*,
first_gi: float,
second_gi: float,
medium_gas_indices: tuple[float, ...],
) -> bytes:
medium_components = "".join(
f"""
<Component id="medium_{index}" type="gas_medium_definition_fixture"
modelVersion="1.0.0">
<Parameter name="gi" value="{gas_index}"/>
</Component>"""
for index, gas_index in enumerate(medium_gas_indices, start=1)
)
return f"""<?xml version="1.0" encoding="UTF-8"?>
<System name="connected-medium-contract" schemaVersion="3" unitSystem="SI">
<Simulation tStart="0" tStop="1" sampleStep="0.1" maxStep="0.01" method="BDF"/>
<Components>
<Component id="consumer_1" type="connected_gas_reference_fixture"
modelVersion="1.0.0">
<Parameter name="gi" value="{first_gi}"/>
</Component>
<Component id="consumer_2" type="connected_gas_reference_fixture"
modelVersion="1.0.0">
<Parameter name="gi" value="{second_gi}"/>
</Component>{medium_components}
</Components>
<Connections>
<Connection id="gas_connection">
<Endpoint component="consumer_1" port="port_1"/>
<Endpoint component="consumer_2" port="port_1"/>
</Connection>
</Connections>
</System>
""".encode()
def air_medium_system_xml(*, property_model: float | None) -> bytes:
property_parameter = (
f'<Parameter name="property_model" value="{property_model}"/>'
if property_model is not None
else ""
)
return f"""<?xml version="1.0" encoding="UTF-8"?>
<System name="air-medium-contract" schemaVersion="3" unitSystem="SI">
<Simulation tStart="0" tStop="1" sampleStep="0.1" maxStep="0.01" method="BDF"/>
<Components>
<Component id="air_1" type="amesim_ideal_air_medium"
modelVersion="0.2.0">
<Parameter name="gi" value="1"/>
{property_parameter}
</Component>
</Components>
<Connections/>
</System>
""".encode()
class MediumReferenceCatalogContractTests(unittest.TestCase):
def test_registry_emits_parameter_editor_and_component_role(self) -> None:
validate_component_model_class(GasReferenceFixture, library=LIBRARY)
validate_component_model_class(GasMediumDefinitionFixture, library=LIBRARY)
reference_payload = GAS_REFERENCE_SPEC.as_catalog_dict()
definition_payload = GAS_MEDIUM_DEFINITION_SPEC.as_catalog_dict()
self.assertEqual(
reference_payload["parameters"][0]["editor"],
"amesimGasReference",
)
self.assertEqual(
definition_payload["role"],
"amesimGasMediumDefinition",
)
def test_committed_catalog_schema_constrains_the_extension_values(self) -> None:
schema_path = (
Path(__file__).resolve().parent.parent
/ "schemas"
/ "component-catalog-v1.schema.json"
)
schema = json.loads(schema_path.read_text(encoding="utf-8"))
self.assertEqual(
schema["$defs"]["parameter"]["properties"]["editor"]["enum"],
["amesimGasReference", "amesimGasPropertyModel", "choice"],
)
self.assertEqual(
schema["$defs"]["parameter"]["properties"]["options"]["items"][
"$ref"
],
"#/$defs/parameterOption",
)
self.assertEqual(
schema["$defs"]["component"]["properties"]["role"]["enum"],
["amesimGasMediumDefinition"],
)
class MediumReferenceSystemXmlContractTests(unittest.TestCase):
def setUp(self) -> None:
self.registry_patch = patch.dict(
COMPONENT_MODEL_REGISTRY,
{
GasReferenceFixture.MODEL_TYPE: GAS_REFERENCE_SPEC,
GasMediumDefinitionFixture.MODEL_TYPE: GAS_MEDIUM_DEFINITION_SPEC,
ConnectedGasReferenceFixture.MODEL_TYPE: CONNECTED_GAS_REFERENCE_SPEC,
},
)
self.registry_patch.start()
self.addCleanup(self.registry_patch.stop)
def test_positive_reference_without_definition_is_not_reinterpreted(self) -> None:
report = validate_system_xml_document(system_xml(gi=1.0))
self.assertFalse(report.valid)
self.assertEqual(
{issue.code for issue in report.issues},
{"AMESIM_GAS_REFERENCE_UNDEFINED"},
)
assert report.document is not None
self.assertEqual(
report.document.as_model_data()["components"][0]["parameters"]["gi"],
1.0,
)
def test_missing_gi_is_not_defaulted(self) -> None:
report = validate_system_xml_document(system_xml(gi=None))
self.assertFalse(report.valid)
self.assertEqual(
{issue.code for issue in report.issues},
{"PARAMETER_REQUIRED_MISSING"},
)
def test_builtin_air_reference_zero_is_valid_canonical_data(self) -> None:
report = validate_system_xml_document(system_xml(gi=0.0))
self.assertTrue(report.valid)
self.assertEqual(report.issues, ())
assert report.document is not None
self.assertEqual(
report.document.as_model_data()["components"][0]["parameters"]["gi"],
0.0,
)
def test_positive_reference_with_definition_is_preserved(self) -> None:
report = validate_system_xml_document(
system_xml(gi=1.0, medium_gas_indices=(1.0,))
)
self.assertTrue(report.valid)
self.assertEqual(report.issues, ())
assert report.document is not None
self.assertEqual(
report.document.as_model_data()["components"][0]["parameters"]["gi"],
1.0,
)
def test_positive_reference_accepts_matching_definition(self) -> None:
report = validate_system_xml_document(
system_xml(
gi=2.0,
medium_gas_indices=(2.0,),
)
)
self.assertTrue(report.valid)
self.assertEqual(report.issues, ())
def test_missing_property_model_is_not_defaulted(self) -> None:
report = validate_system_xml_document(
air_medium_system_xml(property_model=None)
)
self.assertFalse(report.valid)
self.assertEqual(
{issue.code for issue in report.issues},
{"PARAMETER_REQUIRED_MISSING"},
)
def test_explicit_supported_property_model_round_trips_without_warning(self) -> None:
report = validate_system_xml_document(
air_medium_system_xml(property_model=0.0)
)
self.assertTrue(report.valid)
self.assertEqual(report.issues, ())
def test_unsupported_property_model_is_rejected_semantically(self) -> None:
for property_model in (1.0, 99.0):
with self.subTest(property_model=property_model):
report = validate_system_xml_document(
air_medium_system_xml(property_model=property_model)
)
self.assertFalse(report.valid)
self.assertEqual(
{issue.code for issue in report.issues},
{"PARAMETER_VALUE_INVALID"},
)
def test_medium_definition_index_must_be_integer_in_range(self) -> None:
fractional = validate_system_xml_document(
system_xml(
gi=0.0,
medium_gas_indices=(1.5,),
)
)
zero = validate_system_xml_document(
system_xml(
gi=0.0,
medium_gas_indices=(0.0,),
)
)
self.assertIn(
"AMESIM_GAS_MEDIUM_INDEX_INVALID",
{issue.code for issue in fractional.issues},
)
self.assertIn(
"AMESIM_GAS_MEDIUM_INDEX_INVALID",
{issue.code for issue in zero.issues},
)
def test_medium_definition_indices_must_be_unique(self) -> None:
report = validate_system_xml_document(
system_xml(
gi=1.0,
medium_gas_indices=(1.0, 1.0),
)
)
self.assertFalse(report.valid)
self.assertIn(
"AMESIM_GAS_MEDIUM_INDEX_DUPLICATE",
{issue.code for issue in report.issues},
)
def test_gas_reference_index_must_be_integer_in_range(self) -> None:
fractional = validate_system_xml_document(
system_xml(gi=1.5)
)
out_of_range = validate_system_xml_document(
system_xml(gi=100.0)
)
self.assertIn(
"AMESIM_GAS_REFERENCE_INDEX_INVALID",
{issue.code for issue in fractional.issues},
)
self.assertIn(
"AMESIM_GAS_REFERENCE_INDEX_INVALID",
{issue.code for issue in out_of_range.issues},
)
def test_connected_pneumatic_references_cannot_conflict(self) -> None:
report = validate_system_xml_document(
connected_system_xml(
first_gi=1.0,
second_gi=2.0,
medium_gas_indices=(1.0, 2.0),
)
)
self.assertFalse(report.valid)
self.assertEqual(
{issue.code for issue in report.issues},
{"AMESIM_GAS_REFERENCE_CONFLICT"},
)
def test_connected_pneumatic_references_may_share_one_medium(self) -> None:
report = validate_system_xml_document(
connected_system_xml(
first_gi=1.0,
second_gi=1.0,
medium_gas_indices=(1.0,),
)
)
self.assertTrue(report.valid)
self.assertEqual(report.issues, ())
def test_removed_medium_reference_version_is_rejected_by_xsd(self) -> None:
removed_marker_xml = system_xml(gi=0.0).replace(
b'<System name="medium-contract"',
b'<System name="medium-contract" mediumReferenceVersion="1"',
1,
)
report = validate_system_xml_document(removed_marker_xml)
self.assertFalse(report.valid)
self.assertIsNone(report.document)
self.assertEqual({issue.code for issue in report.issues}, {"XSD_VALIDATION_ERROR"})
if __name__ == "__main__":
unittest.main()