""" E2E tests for OPC export using an in-process asyncua server and real OpcRepository. Covers scenarios 3.1.2, 3.2.2, 3.2.4, and 3.2.5 from e2e/scenarios.md. Mock-based OPC tests remain in test_predictions_batch_format_export.py. """ import asyncio import pytest from temporalio.testing import WorkflowEnvironment from temporalio.worker import Worker from e2e.helpers import assert_prediction, insert_sample_data, make_workflow_id, start_and_await_workflow from e2e.opc_test_server import UNKNOWN_NODE_ID, OpcE2ETestServer, build_opc_output_config from e2e.test_predictions_batch_format_export import get_base_input_data from laborious.activities.activities import Activities from laborious.activities.opc import OPC_RECONNECT_IN_PROGRESS_COMMENT from laborious.utils.repository.opc_repository import OpcRepository from laborious.workflows.predictions_batch import PredictionsBatch async def _slow_reconnect_under_lock(repo: OpcRepository, hold_seconds: float = 0.75) -> None: """ Hold the connection lock briefly so concurrent writes see reconnect_in_progress. Args: repo (OpcRepository): Connected repository. hold_seconds (float): Time to keep the lock before reconnecting. """ async with repo._connection_lock: await asyncio.sleep(hold_seconds) await repo._reconnect_locked() @pytest.mark.asyncio @pytest.mark.integration @pytest.mark.opc async def test_scenario_3_1_2_export_with_opc_only_real_server( temporal_test_env: WorkflowEnvironment, temporal_worker_real_opc: Worker, test_activities_real_opc: Activities, opc_e2e_server: OpcE2ETestServer, postgres_engine, ): """ Scenario 3.1.2 (real OPC): connect, write prediction and confidence, verify server values. """ client = temporal_test_env.client model_id = 412 insert_sample_data(postgres_engine, model_id, [23.5, 78.2]) input_data = get_base_input_data(model_id) input_data['opc_output_config'] = build_opc_output_config(opc_e2e_server.node_ids) input_data['pi_web_api_output_config'] = None await start_and_await_workflow( client, PredictionsBatch.run, input_data, make_workflow_id('test-opc-real-happy'), ) test_activities_real_opc.pi_web_api_client.write_value.assert_not_called() assert await opc_e2e_server.read_prediction() == pytest.approx(0.5) assert await opc_e2e_server.read_confidence() == pytest.approx(0.0) assert_prediction(postgres_engine, model_id) @pytest.mark.asyncio @pytest.mark.integration @pytest.mark.opc async def test_scenario_3_2_2_opc_write_error_real_server( temporal_test_env: WorkflowEnvironment, temporal_worker_real_opc: Worker, test_activities_real_opc: Activities, opc_e2e_server: OpcE2ETestServer, postgres_engine, ): """ Scenario 3.2.2 (real OPC): unknown NodeId yields generic write failure (confidence 12). """ client = temporal_test_env.client model_id = 422 insert_sample_data(postgres_engine, model_id, [23.5, 78.2]) node_ids = opc_e2e_server.node_ids input_data = get_base_input_data(model_id) input_data['opc_output_config'] = build_opc_output_config( node_ids, prediction_tag=UNKNOWN_NODE_ID, confidence_tag=UNKNOWN_NODE_ID, ) input_data['pi_web_api_output_config'] = None await start_and_await_workflow( client, PredictionsBatch.run, input_data, make_workflow_id('test-opc-real-bad-node'), ) assert_prediction( postgres_engine, model_id, prediction_confidence=12, comments='Some data could not be written to OPC servers', ) @pytest.mark.asyncio @pytest.mark.integration @pytest.mark.opc async def test_scenario_3_2_4_opc_session_bad_real_server( temporal_test_env: WorkflowEnvironment, temporal_worker_real_opc: Worker, test_activities_real_opc: Activities, opc_e2e_server: OpcE2ETestServer, postgres_engine, ): """ Scenario 3.2.4 (real OPC): server PreWrite fault injects BadSessionIdInvalid (confidence 14). """ client = temporal_test_env.client model_id = 424 insert_sample_data(postgres_engine, model_id, [23.5, 78.2]) opc_e2e_server.set_session_bad_on_write(True) try: input_data = get_base_input_data(model_id) input_data['opc_output_config'] = build_opc_output_config( opc_e2e_server.node_ids, prediction_only=True, ) input_data['pi_web_api_output_config'] = None await start_and_await_workflow( client, PredictionsBatch.run, input_data, make_workflow_id('test-opc-real-session-bad'), ) finally: opc_e2e_server.set_session_bad_on_write(False) assert_prediction( postgres_engine, model_id, prediction_confidence=14, comments_contains='OPC UA session/channel error: BadSessionIdInvalid', ) @pytest.mark.asyncio @pytest.mark.integration @pytest.mark.opc async def test_scenario_3_2_5_opc_write_blocked_during_reconnect_real_server( temporal_test_env: WorkflowEnvironment, temporal_worker_real_opc: Worker, test_activities_real_opc: Activities, opc_e2e_server: OpcE2ETestServer, postgres_engine, ): """ Scenario 3.2.5 (real OPC): writes rejected while reconnect holds the connection lock. """ client = temporal_test_env.client model_id = 425 insert_sample_data(postgres_engine, model_id, [23.5, 78.2]) repo = test_activities_real_opc.opc_repository['1'] repo._session_ready.clear() reconnect_task = asyncio.create_task(_slow_reconnect_under_lock(repo)) input_data = get_base_input_data(model_id) input_data['opc_output_config'] = build_opc_output_config(opc_e2e_server.node_ids) input_data['pi_web_api_output_config'] = None try: await start_and_await_workflow( client, PredictionsBatch.run, input_data, make_workflow_id('test-opc-real-reconnect-block'), ) finally: await reconnect_task assert_prediction( postgres_engine, model_id, prediction_confidence=14, comments_contains=OPC_RECONNECT_IN_PROGRESS_COMMENT, )