SIENTIAPDE-1811

Update .gitignore, requirements, and enhance OPC UA error handling

- Added new entries to .gitignore for openspec and cursor directories.
- Updated sientia-dataops-library dependency version in requirements-light.txt from 1.10.4 to 1.12.0.
- Enhanced OPC UA communication by refining reconnect logic and error handling in opc_repository.py, including the introduction of a reconnect flag and improved session management.
- Updated tests to cover new reconnect scenarios and ensure robust error handling for protocol states.
This commit is contained in:
vitor-aignosi
2026-05-19 11:45:08 -03:00
parent 7c1dae8ef6
commit cd1be2430a
6 changed files with 266 additions and 61 deletions

View File

@@ -257,6 +257,7 @@ async def test_disconnect(opc_repository, mock_client):
opc_repository._disconnection_fallback.assert_called_once()
assert opc_repository.client is None
assert opc_repository._allow_reconnect is False
@pytest.mark.asyncio
@@ -331,14 +332,19 @@ async def test_write_data_validate_connection_do_nothing(opc_repository):
@pytest.mark.asyncio
async def test_write_data_validate_connection_failed(opc_repository):
opc_repository.validate_connection = AsyncMock(return_value=(False, {}))
opc_repository.client = AsyncMock()
opc_repository.client = MagicMock()
opc_repository.client.uaclient.protocol = MagicMock(state='closed')
opc_repository._start_reconnect = AsyncMock()
result = await opc_repository.write_data('ns=2;s=TestNode', 42.0, 'float', metadata['metadata'])
is_success, error_data = await opc_repository.write_data(
'ns=2;s=TestNode', 42.0, 'float', metadata['metadata']
)
opc_repository.validate_connection.assert_called_once()
opc_repository.client.get_node.assert_not_called()
assert result == (False, {})
opc_repository._start_reconnect.assert_called_once()
assert opc_repository._start_reconnect.call_args.args[0] == 'ProtocolClosed'
assert is_success is False
assert error_data['opc_error_kind'] == 'connection_lost'
assert error_data['opc_status'] == 'ProtocolClosed'
@pytest.mark.asyncio
@@ -439,7 +445,7 @@ def test_is_reconnectable_opcua_bad():
async def test_write_data_bad_session_id_invalid_schedules_reconnect(opc_repository, mock_client):
opc_repository.validate_connection = AsyncMock(return_value=(True, {}))
opc_repository.client = mock_client
opc_repository._start_reconnect_on_bad = AsyncMock()
opc_repository._start_reconnect = AsyncMock()
mock_node = AsyncMock()
mock_client.get_node = MagicMock(return_value=mock_node)
mock_node.write_value.side_effect = BadSessionIdInvalid()
@@ -449,7 +455,7 @@ async def test_write_data_bad_session_id_invalid_schedules_reconnect(opc_reposit
)
mock_node.write_value.assert_called_once()
opc_repository._start_reconnect_on_bad.assert_called_once()
opc_repository._start_reconnect.assert_called_once()
assert is_success is False
assert error_data['opc_error_kind'] == 'session_bad'
assert error_data['opc_status'] == 'BadSessionIdInvalid'
@@ -476,15 +482,85 @@ async def test_write_data_reconnect_in_progress_immediate(opc_repository):
@pytest.mark.asyncio
async def test_start_reconnect_on_bad_skips_within_interval(opc_repository):
async def test_start_reconnect_skips_within_interval(opc_repository):
opc_repository.last_reconnection_time = datetime.now()
opc_repository.reconnection_interval = 3600
await opc_repository._start_reconnect_on_bad('BadSessionIdInvalid', 'tok')
await opc_repository._start_reconnect('BadSessionIdInvalid', 'tok')
assert opc_repository._reconnect_task is None
@pytest.mark.asyncio
async def test_write_data_protocol_closed_schedules_reconnect(opc_repository):
opc_repository.client = MagicMock()
opc_repository.client.uaclient.protocol = MagicMock(state='closed')
opc_repository._start_reconnect = AsyncMock()
is_success, error_data = await opc_repository.write_data(
'ns=2;s=TestNode', 42.0, 'float', metadata['metadata']
)
opc_repository._start_reconnect.assert_called_once()
assert opc_repository._start_reconnect.call_args.args[0] == 'ProtocolClosed'
assert is_success is False
assert error_data['opc_error_kind'] == 'connection_lost'
assert error_data['opc_status'] == 'ProtocolClosed'
@pytest.mark.asyncio
async def test_write_data_protocol_closed_skips_reconnect_within_interval(opc_repository):
opc_repository.client = MagicMock()
opc_repository.client.uaclient.protocol = MagicMock(state='closed')
opc_repository.last_reconnection_time = datetime.now()
opc_repository.reconnection_interval = 3600
is_success, error_data = await opc_repository.write_data(
'ns=2;s=TestNode', 42.0, 'float', metadata['metadata']
)
assert opc_repository._reconnect_task is None
assert is_success is False
assert error_data['opc_error_kind'] == 'connection_lost'
@pytest.mark.asyncio
async def test_write_data_after_failed_reconnect_schedules_again(opc_repository):
opc_repository._session_ready.clear()
opc_repository.reconnection_interval = 0
opc_repository.last_reconnection_time = None
opc_repository._reconnect_locked = AsyncMock(
return_value=(False, {'message': 'connect failed'})
)
await opc_repository.write_data('ns=2;s=TestNode', 42.0, 'float', metadata['metadata'])
await asyncio.sleep(0.1)
assert opc_repository._reconnect_locked.call_count == 1
assert not opc_repository._reconnect_task_in_progress()
await opc_repository.write_data('ns=2;s=TestNode', 42.0, 'float', metadata['metadata'])
await asyncio.sleep(0.1)
assert opc_repository._reconnect_locked.call_count == 2
@pytest.mark.asyncio
async def test_write_data_after_disconnect_does_not_schedule_reconnect(opc_repository, mock_client):
opc_repository.client = mock_client
proto = MagicMock()
proto.state = 'closed'
mock_client.uaclient = MagicMock(protocol=proto)
opc_repository._disconnection_fallback = AsyncMock(return_value=[])
await opc_repository.disconnect()
is_success, error_data = await opc_repository.write_data(
'ns=2;s=TestNode', 42.0, 'float', metadata['metadata']
)
assert opc_repository._reconnect_task is None
assert is_success is False
assert error_data['opc_error_kind'] == 'connection_lost'
@pytest.mark.asyncio
async def test_parallel_bad_writes_single_reconnect_task(opc_repository, mock_client):
opc_repository.validate_connection = AsyncMock(return_value=(True, {}))