SIENTIAPDE-2202 Add gateway end-to-end test suite (e2e/) End-to-end suite at the repo root, following the Laborious/Orchestrator pattern: numbered scenarios per topic in e2e/scenarios.md (1:1 with test_scenario_* functions), scenario inputs as JSON, Postgres DDL mirrored from production, `integration` marker, excluded from the default pytest run. Every dependency runs for real: MongoDB, Redis, PostgreSQL, MinIO and NATS via testcontainers, OPC UA servers in process (asyncua). Pods are built from the environment like ingestor/app.py and driven one loop iteration at a time. 69 scenarios across 7 topics: slot leasing and failover, subscription reads, NATS writes and write_status (incl. the SIENTIAPDE-2202 timestamp/quality matrix, checked on the server and on the wire), gRPC ReadTag, active reads, client certificates/security, pod lifecycle. Replaces tests/functional (legacy slot v1 tests, fully commented out) and tests/functional/test_write_e2e.py (now scenarios 3.1.x). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
3.6 KiB
Python
94 lines
3.6 KiB
Python
"""E2E scenarios — 7. Pod lifecycle: shutdown, idle connections, startup failures. See ``e2e/scenarios.md``."""
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from e2e.helpers import (
|
|
Pod,
|
|
insert_prediction,
|
|
load_scenario_input,
|
|
readings,
|
|
stays_false,
|
|
wait_until,
|
|
write_status,
|
|
)
|
|
from e2e.opc_server import free_port
|
|
|
|
pytestmark = [pytest.mark.integration, pytest.mark.asyncio]
|
|
|
|
TIMESTAMP = '2026-10-13T12:00:00-03:00'
|
|
|
|
|
|
async def test_scenario_7_1_1_shutdown_closes_sessions_and_stops_consuming(
|
|
opc_servers, pods, put_slot, mongo_db, postgres_engine, publish_command
|
|
):
|
|
server = await opc_servers('plant-a', {'Temperature': 20.0, 'Setpoint': 0.0})
|
|
put_slot(1, load_scenario_input('slot_single_server.json', {'plant-a': server}))
|
|
insert_prediction(postgres_engine, 2, TIMESTAMP, 'pending')
|
|
pod = await pods('pod-a')
|
|
await pod.tick()
|
|
await wait_until(lambda: readings(mongo_db, 'raw_plant_a'))
|
|
assert server.session_count() == 1
|
|
|
|
await pod.stop()
|
|
|
|
await wait_until(lambda: server.session_count() == 0)
|
|
await publish_command(load_scenario_input('write_command.json'))
|
|
assert await stays_false(lambda: write_status(postgres_engine, 2, TIMESTAMP) != 'pending')
|
|
await server.set('Temperature', 50.0)
|
|
assert await stays_false(lambda: readings(mongo_db, 'raw_plant_a', value=50.0))
|
|
|
|
|
|
async def test_scenario_7_1_2_idle_write_connection_is_closed_and_reopened(
|
|
opc_servers, pods, put_slot, mongo_db, postgres_engine, publish_command
|
|
):
|
|
reader = await opc_servers('plant-a', {'Temperature': 20.0})
|
|
writer = await opc_servers('plant-b', {'Setpoint': 0.0})
|
|
put_slot(1, load_scenario_input('slot_single_server.json', {'plant-a': reader}))
|
|
connector = load_scenario_input('connector_plant_a.json', {'plant-a': writer})
|
|
mongo_db['connectors'].insert_one({**connector, 'connector_id': '2', 'name': 'plant-b'})
|
|
insert_prediction(postgres_engine, 2, TIMESTAMP, 'pending')
|
|
pod = await pods('pod-a', IDLE_CONN_MAX_S='1')
|
|
await pod.tick()
|
|
|
|
await publish_command(load_scenario_input('write_command.json', server_id='2'))
|
|
await wait_until(lambda: write_status(postgres_engine, 2, TIMESTAMP) == 'ok')
|
|
assert writer.session_count() == 1
|
|
|
|
await asyncio.sleep(1.5)
|
|
await pod.tick() # idle_sweep
|
|
|
|
await wait_until(lambda: writer.session_count() == 0)
|
|
assert reader.session_count() == 1 # subscribed: pinned, never idle
|
|
|
|
insert_prediction(postgres_engine, 3, TIMESTAMP, 'pending')
|
|
await publish_command(load_scenario_input('write_command.json', server_id='2', model_id='3'))
|
|
await wait_until(lambda: write_status(postgres_engine, 3, TIMESTAMP) == 'ok')
|
|
assert writer.session_count() == 1
|
|
|
|
|
|
async def test_scenario_7_2_1_redis_unreachable_fails_the_startup(monkeypatch):
|
|
monkeypatch.setenv('REDIS_PORT', str(free_port()))
|
|
|
|
with pytest.raises(Exception, match='(?i)connect'):
|
|
await Pod('pod-a').start()
|
|
|
|
|
|
async def test_scenario_7_2_2_restarted_pod_takes_its_slot_back(
|
|
opc_servers, pods, put_slot, mongo_db, redis_client
|
|
):
|
|
server = await opc_servers('plant-a', {'Temperature': 20.0})
|
|
put_slot(1, load_scenario_input('slot_single_server.json', {'plant-a': server}))
|
|
pod = await pods('pod-a', LEASE_TTL='2', HEARTBEAT_TTL='2')
|
|
await pod.tick()
|
|
await pod.stop()
|
|
|
|
await wait_until(lambda: redis_client.get('lease:opc_tags:1') is None, timeout=5)
|
|
restarted = await pods('pod-a', LEASE_TTL='2', HEARTBEAT_TTL='2')
|
|
await restarted.tick()
|
|
|
|
assert restarted.slots == {'1'}
|
|
await server.set('Temperature', 31.0)
|
|
await wait_until(lambda: readings(mongo_db, 'raw_plant_a', value=31.0))
|