173 lines
5.6 KiB
Python
173 lines
5.6 KiB
Python
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()
|