from unittest.mock import AsyncMock, MagicMock, call import pytest import ingestor.metrics as metrics from ingestor.managers.server_reader import ServerReader SERVER_CONFIG = { 'name': 'kepserver', 'server_id': 1, # the slot may carry it as int; the pool keys by str 'url': 'opc.tcp://kepserver:49320', 'server_uri': 'urn:sientia:opc-ingestor', 'use_security': True, 'subscription_period_ms': 15000.0, } def make_reader(): pool = MagicMock(get=AsyncMock(), evict=AsyncMock()) reader = ServerReader( name='kepserver', server_config=SERVER_CONFIG, pool=pool, sink=AsyncMock(), logger=MagicMock(), notification_handler=MagicMock(), metrics_controller=AsyncMock(), metadata={}, ) reader.engine = MagicMock( subscriptions={}, pod_id='pod', create_subscription=AsyncMock(), subscribe=AsyncMock(), unsubscribe=AsyncMock(), check_cycles=AsyncMock(), check_opc_listenning=AsyncMock(return_value=False), emit_metric=AsyncMock(), ) return reader, pool def test_server_cfg_keeps_only_connection_fields(): reader, _ = make_reader() assert reader.server_id == '1' assert reader.server_cfg == { 'url': 'opc.tcp://kepserver:49320', 'server_uri': 'urn:sientia:opc-ingestor', 'use_security': True, 'subscription_period_ms': 15000.0, } @pytest.mark.asyncio async def test_connect_opens_pooled_connection(): reader, pool = make_reader() await reader.connect() # use_security=True: the pool fetches the certificate from MinIO (CertProvider) pool.get.assert_awaited_once_with('1', reader.server_cfg) @pytest.mark.asyncio async def test_connect_propagates_failure(): reader, pool = make_reader() pool.get.side_effect = ConnectionError('unreachable') with pytest.raises(ConnectionError): await reader.connect() @pytest.mark.asyncio async def test_subscription_calls_delegate_to_engine_per_slot(): reader, _ = make_reader() nodes = {'ns=2;i=2': {'frequency': '1000', 'names': [{'collection': 'raw_a', 'tag_name': 'a'}]}} await reader.create_subscription('1') await reader.subscribe('1', nodes, 10) await reader.unsubscribe('1') reader.engine.create_subscription.assert_awaited_once_with('1', reader.server_cfg, '1') reader.engine.subscribe.assert_awaited_once_with('1', nodes, 10) reader.engine.unsubscribe.assert_awaited_once_with('1') @pytest.mark.asyncio async def test_supervision_delegates_to_engine(): reader, _ = make_reader() reader.engine.check_opc_listenning.return_value = True await reader.check_cycles() lost = await reader.check_opc_listenning() reader.engine.check_cycles.assert_awaited_once() reader.engine.check_opc_listenning.assert_awaited_once_with('1') assert lost is True def test_subscriptions_are_the_engine_ones(): reader, _ = make_reader() reader.engine.subscriptions['1'] = MagicMock() assert reader.subscriptions is reader.engine.subscriptions @pytest.mark.asyncio async def test_shutdown_unsubscribes_all_slots_evicts_and_zeroes_metrics(): reader, pool = make_reader() reader.engine.subscriptions.update({'1': MagicMock(), '2': MagicMock()}) await reader.shutdown() reader.engine.unsubscribe.assert_has_awaits([call('1'), call('2')]) # Dropping the pooled connection makes the next initialization reconnect. pool.evict.assert_awaited_once_with('1') reader.engine.emit_metric.assert_any_await( metric_object=metrics.OPC_CONNECTION_STATUS, method='set', value=0, tags={'pod_id': 'pod', 'server_name': '1', 'server_url': 'opc.tcp://kepserver:49320'}, ) reader.engine.emit_metric.assert_any_await( metric_object=metrics.OPC_TAGS_SUBSCRIBED, method='set', value=0, tags={'pod_id': 'pod', 'server_name': '1'}, ) @pytest.mark.asyncio async def test_shutdown_without_evict_keeps_the_shared_connection(): """Slot moved to another pod: drop the subscriptions, keep the pooled connection for NATS writes/gRPC (review #7).""" reader, pool = make_reader() reader.engine.subscriptions.update({'1': MagicMock()}) await reader.shutdown(evict=False) reader.engine.unsubscribe.assert_awaited_once_with('1') pool.evict.assert_not_awaited() for c in reader.engine.emit_metric.await_args_list: assert c.kwargs['metric_object'] is not metrics.OPC_CONNECTION_STATUS