支持后端解析参数表达式并保留工程JSON
This commit is contained in:
1 parent
48da6be21c
commit
a87d462e94
7 files changed
+640
-3
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user