- data_sink.py: _IDENTIFIER_RE uses \w+/re.ASCII instead of the equivalent-but-verbose [A-Za-z0-9_]+ character class (verified same match behavior). NOSONAR on update_write_status's 'async with no await inside' - deliberate, per the ponytail comment already in the method body (uniform async interface across DataSink, no async pg driver). - test_grpc_server.py: move make_request() out of the pytest.raises block so exactly one call can raise the expected exception. - sonar-project.properties: exclude ingestor/proto/ (generated grpc code) from analysis - its ReadTag/etc. names are dictated by grpcio-tools, not a style choice, and renaming them breaks the wire.
98 lines
3.1 KiB
Python
98 lines
3.1 KiB
Python
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import grpc
|
|
import pytest
|
|
|
|
from ingestor.adapters.grpc_server import GrpcServer, ReadTagServicer
|
|
from ingestor.proto.opc_gateway_pb2 import ReadTagRequest
|
|
|
|
|
|
def make_request(server_id='srv1', addr='ns=2;i=5'):
|
|
return ReadTagRequest(server_id=server_id, addr=addr, data_type='float')
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_tag_maps_full_result_onto_response():
|
|
active_read = AsyncMock()
|
|
active_read.read_now.return_value = {
|
|
'value': 1.5,
|
|
'opc_status_code': 'Good',
|
|
'source_timestamp': '2026-07-15T10:00:00-03:00',
|
|
'server_timestamp': '2026-07-15T10:00:01-03:00',
|
|
'persisted': False,
|
|
}
|
|
servicer = ReadTagServicer(active_read, MagicMock())
|
|
|
|
response = await servicer.ReadTag(make_request(), AsyncMock())
|
|
|
|
assert response.value == '1.5'
|
|
assert response.opc_status_code == 'Good'
|
|
assert response.source_timestamp == '2026-07-15T10:00:00-03:00'
|
|
assert response.server_timestamp == '2026-07-15T10:00:01-03:00'
|
|
assert response.persisted is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_tag_none_timestamps_become_empty_string():
|
|
active_read = AsyncMock()
|
|
active_read.read_now.return_value = {
|
|
'value': 1.5,
|
|
'opc_status_code': None,
|
|
'source_timestamp': None,
|
|
'server_timestamp': None,
|
|
'persisted': False,
|
|
}
|
|
servicer = ReadTagServicer(active_read, MagicMock())
|
|
|
|
response = await servicer.ReadTag(make_request(), AsyncMock())
|
|
|
|
assert response.opc_status_code == ''
|
|
assert response.source_timestamp == ''
|
|
assert response.server_timestamp == ''
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_tag_aborts_with_internal_on_engine_failure():
|
|
active_read = AsyncMock()
|
|
active_read.read_now.side_effect = RuntimeError('boom')
|
|
logger = MagicMock()
|
|
servicer = ReadTagServicer(active_read, logger)
|
|
context = AsyncMock()
|
|
context.abort.side_effect = grpc.RpcError('aborted')
|
|
|
|
request = make_request()
|
|
with pytest.raises(grpc.RpcError):
|
|
await servicer.ReadTag(request, context)
|
|
|
|
context.abort.assert_awaited_once()
|
|
args = context.abort.call_args.args
|
|
assert args[0] == grpc.StatusCode.INTERNAL
|
|
logger.error.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_tag_uses_slot_lookup_names_when_provided():
|
|
active_read = AsyncMock()
|
|
active_read.read_now.return_value = {
|
|
'value': 1,
|
|
'opc_status_code': 'Good',
|
|
'source_timestamp': None,
|
|
'server_timestamp': None,
|
|
'persisted': True,
|
|
}
|
|
names = [{'collection': 'c1', 'name': 't1'}]
|
|
slot_lookup = MagicMock(return_value=names)
|
|
servicer = ReadTagServicer(active_read, MagicMock(), slot_lookup=slot_lookup)
|
|
|
|
await servicer.ReadTag(make_request(server_id='srv1', addr='ns=2;i=5'), AsyncMock())
|
|
|
|
slot_lookup.assert_called_once_with('srv1', 'ns=2;i=5')
|
|
active_read.read_now.assert_awaited_once_with('srv1', 'ns=2;i=5', names=names)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_grpc_server_stop_before_start_does_not_raise():
|
|
server = GrpcServer(active_read=AsyncMock(), logger=MagicMock())
|
|
|
|
await server.stop() # no raise
|