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()