完善建模交互、组件图标与系统协议

This commit is contained in:
ljz committed 2026-08-15 17:40:18 +08:00
1 parent 456c29b3b6
commit 6572defaa4
66 files changed
+10067 -4163

No files matched your search

+70 -31
View File
@@ -2,16 +2,16 @@ from __future__ import annotations
import asyncio
import unittest
from xml.etree import ElementTree as ET
from fastapi import HTTPException, Request
from app.main import (
ReactFlowEdgePayload,
ReactFlowNodePayload,
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_system_xml_model,
compile_reactflow_network,
compile_system_xml_network,
parse_system_xml,
)
from app.system_xml import validate_system_xml_document
@@ -32,6 +32,38 @@ def issue_codes(xml: bytes | str) -> set[str]:
}
def with_parameter_value(
xml: bytes,
component_id: str,
parameter_name: str,
value: float,
) -> bytes:
root = ET.fromstring(xml)
parameter = root.find(
"./Components/Component[@id='{}']/Parameter[@name='{}']".format(
component_id,
parameter_name,
)
)
assert parameter is not None
parameter.set("value", str(value))
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
def without_parameter(
xml: bytes,
component_id: str,
parameter_name: str,
) -> bytes:
root = ET.fromstring(xml)
component = root.find(f"./Components/Component[@id='{component_id}']")
assert component is not None
parameter = component.find(f"./Parameter[@name='{parameter_name}']")
assert parameter is not None
component.remove(parameter)
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
def xml_request(body: bytes) -> Request:
delivered = False
@@ -54,17 +86,17 @@ def xml_request(body: bytes) -> Request:
class SystemXmlParserTests(unittest.TestCase):
def test_valid_v2_xml_round_trips_and_compiles(self) -> None:
def test_valid_v3_xml_parses_and_compiles_directly(self) -> None:
report = validate_system_xml_document(valid_xml())
self.assertTrue(report.valid)
self.assertEqual(report.issues, ())
assert report.document is not None
project = ReactFlowProjectPayload(**report.document.as_project_data())
network = compile_reactflow_network(project)
network = compile_system_xml_network(report.document)
self.assertEqual(project.name, "protocol-test")
self.assertEqual(project.simulation.method, "BDF")
self.assertEqual(report.document.name, "protocol-test")
self.assertEqual(report.document.simulation.method, "BDF")
self.assertEqual(report.document.simulation.sample_step, 0.2)
self.assertEqual(set(network.components), {"cylinder_1", "tank_1"})
self.assertEqual(len(network.connections), 1)
self.assertEqual(network.connections[0].id, "edge-1")
@@ -76,8 +108,24 @@ class SystemXmlParserTests(unittest.TestCase):
self.assertEqual(response["name"], "protocol-test")
self.assertEqual(len(response["components"]), 2)
self.assertEqual(len(response["connections"]), 1)
self.assertEqual(response["simulation"]["sample_step"], 0.2)
self.assertTrue(response["validation"]["valid"])
def test_parse_api_returns_execution_model_not_editor_project(self) -> None:
response = asyncio.run(parse_system_xml(xml_request(valid_xml())))
self.assertTrue(response["success"])
self.assertNotIn("project", response)
model = response["model"]
self.assertEqual(model["schemaVersion"], "3")
self.assertEqual(model["unitSystem"], "SI")
self.assertEqual(model["name"], "protocol-test")
self.assertEqual(model["simulation"]["sample_step"], 0.2)
self.assertEqual(model["components"][0]["model_type"], "cylinder")
self.assertEqual(model["components"][0]["model_version"], "1.0.0")
self.assertNotIn("nodes", model)
self.assertNotIn("edges", model)
def test_parse_api_returns_structured_422_diagnostics(self) -> None:
with self.assertRaises(HTTPException) as caught:
asyncio.run(parse_system_xml(xml_request(b"<broken>")))
@@ -111,7 +159,7 @@ class SystemXmlParserTests(unittest.TestCase):
self.assertEqual(report.issues[0].code, "XML_DTD_NOT_ALLOWED")
def test_wrong_schema_version_reports_xsd_error(self) -> None:
xml = valid_xml().replace(b'schemaVersion="2"', b'schemaVersion="1"')
xml = valid_xml().replace(b'schemaVersion="3"', b'schemaVersion="2"')
report = validate_system_xml_document(xml)
self.assertFalse(report.valid)
@@ -121,11 +169,7 @@ class SystemXmlParserTests(unittest.TestCase):
self.assertIn("XSD_VALIDATION_ERROR", issue_codes(xml))
def test_parameter_range_is_checked_semantically(self) -> None:
xml = valid_xml().replace(
b'<Parameter name="volume" value="0.01"',
b'<Parameter name="volume" value="-1"',
1,
)
xml = with_parameter_value(valid_xml(), "cylinder_1", "volume", -1)
report = validate_system_xml_document(xml)
@@ -138,25 +182,21 @@ class SystemXmlParserTests(unittest.TestCase):
self.assertIn("cylinder_1.volume", issue.message)
def test_zero_initial_pressure_is_rejected_before_model_creation(self) -> None:
xml = valid_xml().replace(
b'<Parameter name="p0" value="35000000"',
b'<Parameter name="p0" value="0"',
1,
)
xml = with_parameter_value(valid_xml(), "cylinder_1", "p0", 0)
report = validate_system_xml_document(xml)
self.assertFalse(report.valid)
self.assertIn("PARAMETER_VALUE_INVALID", issue_codes(xml))
def test_registered_port_contract_is_checked_semantically(self) -> None:
def test_endpoint_port_is_checked_against_registered_contract(self) -> None:
xml = valid_xml().replace(
b'nominalRole="outlet"',
b'nominalRole="bidirectional"',
b'component="cylinder_1" port="port_b"',
b'component="cylinder_1" port="missing_port"',
1,
)
self.assertIn("PORT_NOMINAL_ROLE_MISMATCH", issue_codes(xml))
self.assertIn("ENDPOINT_PORT_UNKNOWN", issue_codes(xml))
def test_unknown_connection_endpoint_is_reported(self) -> None:
xml = valid_xml().replace(
@@ -178,6 +218,7 @@ class SystemXmlParserTests(unittest.TestCase):
"label": "receiver-tank-2",
"componentType": "tank",
"modelType": "tank",
"modelVersion": "1.0.0",
"ports": [physical_port("port_a", "inlet", "left")],
"parameters": {"volume": 0.1, "p0": 100000, "T0": 300},
},
@@ -193,9 +234,11 @@ class SystemXmlParserTests(unittest.TestCase):
})
)
xml = build_reactflow_system_xml(project)
self.assertIn("PHYSICAL_PORT_ALREADY_CONNECTED", issue_codes(xml))
with self.assertRaisesRegex(
ValueError,
"PHYSICAL_PORT_ALREADY_CONNECTED",
):
build_reactflow_system_xml(project)
def test_unconnected_registered_port_is_a_warning(self) -> None:
project = physical_connection_project()
@@ -211,11 +254,7 @@ class SystemXmlParserTests(unittest.TestCase):
)
def test_missing_required_parameter_is_reported(self) -> None:
xml = valid_xml().replace(
b' <Parameter name="T0" value="300" />\n',
b"",
1,
)
xml = without_parameter(valid_xml(), "cylinder_1", "T0")
self.assertIn("PARAMETER_REQUIRED_MISSING", issue_codes(xml))
@@ -225,7 +264,7 @@ class SystemXmlParserTests(unittest.TestCase):
self.assertIn("SIMULATION_METHOD_UNSUPPORTED", issue_codes(xml))
def test_non_positive_simulation_steps_are_rejected_by_schema(self) -> None:
xml = valid_xml().replace(b'step="0.2"', b'step="0"')
xml = valid_xml().replace(b'sampleStep="0.2"', b'sampleStep="0"')
xml = xml.replace(b'maxStep="0.01"', b'maxStep="-1"')
report = validate_system_xml_document(xml)