支持后端解析参数表达式并保留工程JSON

This commit is contained in:
ljz committed 2026-09-03 18:18:47 +08:00
1 parent 48da6be21c
commit a87d462e94
7 files changed
+640 -3

No files matched your search

+172
View File
@@ -0,0 +1,172 @@
from __future__ import annotations
import unittest
from xml.etree import ElementTree as ET
from app.main import (
ReactFlowNodePayload,
ReactFlowProjectPayload,
build_reactflow_system_xml,
reactflow_project_storage_data,
validate_reactflow_component_contract,
)
from app.parameter_expression import (
ParameterExpressionError,
evaluate_parameter_expression,
)
from app.simulation.registry import get_component_model_spec
from tests.test_amesim_pnvo001_signal_xml import amesim_pnvo001_signal_project
AREA_EXPRESSION = "3.14*10**2/4"
AREA_IN_SQUARE_METRES = 7.85e-5
def pnvo001_project_with_area(
area0: object,
*,
selected_unit: str = "mm2",
) -> ReactFlowProjectPayload:
project = amesim_pnvo001_signal_project().model_copy(deep=True)
valve = next(node for node in project.nodes if node.id == "valve_1")
valve.data.parameters["area0"] = area0
valve.data.parameterUnits["area0"] = selected_unit
return project
def pnvo001_node(project: ReactFlowProjectPayload) -> ReactFlowNodePayload:
return next(node for node in project.nodes if node.id == "valve_1")
class ParameterExpressionParserTests(unittest.TestCase):
def test_supported_arithmetic_constants_and_functions(self) -> None:
cases = {
"=3.14*10^2/4": 78.5,
"3.14*10**2/4": 78.5,
"(2 + 3) * 4": 20.0,
"2^3^2": 512.0,
"-2^2": -4.0,
"2.5E-3": 0.0025,
"sqrt(16) + abs(-2)": 6.0,
"sin(pi/2) + ln(e)": 2.0,
"max(1, 5, 3) + pow(2, 3)": 13.0,
}
for expression, expected in cases.items():
with self.subTest(expression=expression):
self.assertAlmostEqual(
evaluate_parameter_expression(expression),
expected,
places=12,
)
def test_invalid_or_unsafe_expressions_are_rejected(self) -> None:
cases = (
"",
"=",
"1 / 0",
"sqrt(-1)",
"pow(-1, 0.5)",
"unknown + 1",
"window.alert(1)",
"__import__('os')",
"1 + * 2",
"1e309",
"min()",
"max(" + ",".join("1" for _ in range(17)) + ")",
"(" * 34 + "1" + ")" * 34,
"1" * 513,
)
for expression in cases:
with self.subTest(expression=expression[:40]):
with self.assertRaises(ParameterExpressionError):
evaluate_parameter_expression(expression)
class ParameterExpressionExecutionTests(unittest.TestCase):
def test_pnvo001_area_expression_uses_selected_mm2_unit(self) -> None:
project = pnvo001_project_with_area(AREA_EXPRESSION)
valve = pnvo001_node(project)
spec = get_component_model_spec(valve.data.modelType)
parameters = validate_reactflow_component_contract(valve, spec)
self.assertAlmostEqual(
parameters["area0"],
AREA_IN_SQUARE_METRES,
places=15,
)
def test_plain_numeric_si_value_is_not_converted_again(self) -> None:
for stored_value in (AREA_IN_SQUARE_METRES, "7.85e-5"):
with self.subTest(stored_value=stored_value):
project = pnvo001_project_with_area(stored_value)
valve = pnvo001_node(project)
spec = get_component_model_spec(valve.data.modelType)
parameters = validate_reactflow_component_contract(valve, spec)
self.assertEqual(parameters["area0"], AREA_IN_SQUARE_METRES)
def test_storage_preserves_the_original_expression(self) -> None:
project = pnvo001_project_with_area(AREA_EXPRESSION)
stored = reactflow_project_storage_data(project)
stored_valve = next(
node for node in stored["nodes"] if node["id"] == "valve_1"
)
self.assertEqual(
stored_valve["data"]["parameters"]["area0"],
AREA_EXPRESSION,
)
self.assertEqual(
pnvo001_node(project).data.parameters["area0"],
AREA_EXPRESSION,
)
def test_xml_contains_resolved_si_value_without_mutating_project(self) -> None:
project = pnvo001_project_with_area(AREA_EXPRESSION)
xml_bytes = build_reactflow_system_xml(project)
root = ET.fromstring(xml_bytes)
area_parameter = root.find(
"./Components/Component[@id='valve_1']/Parameter[@name='area0']"
)
self.assertIsNotNone(area_parameter)
assert area_parameter is not None
self.assertAlmostEqual(
float(area_parameter.attrib["value"]),
AREA_IN_SQUARE_METRES,
places=15,
)
self.assertNotIn(AREA_EXPRESSION, xml_bytes.decode("utf-8"))
self.assertEqual(
pnvo001_node(project).data.parameters["area0"],
AREA_EXPRESSION,
)
def test_invalid_expression_has_stable_execution_error_code(self) -> None:
project = pnvo001_project_with_area("sqrt(-1)")
with self.assertRaisesRegex(
ValueError,
"PARAMETER_EXPRESSION_INVALID.*area0.*valve_1",
):
build_reactflow_system_xml(project)
def test_discrete_parameter_expression_is_rejected(self) -> None:
project = pnvo001_project_with_area(AREA_IN_SQUARE_METRES)
pnvo001_node(project).data.parameters["flowset"] = "1 + 0"
with self.assertRaisesRegex(
ValueError,
"PARAMETER_EXPRESSION_FORBIDDEN.*flowset.*valve_1",
):
build_reactflow_system_xml(project)
if __name__ == "__main__":
unittest.main()