feat(scada): add project monitoring replay
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""Real project SQL, with all schema, observations, events and tasks rolled back."""
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import psycopg
|
||||
from psycopg.rows import dict_row
|
||||
import pytest
|
||||
|
||||
from app.auth.project_dependencies import ProjectContext
|
||||
from app.domain.schemas.scada_replay import ScadaReplayCreate, ScadaReplayCommand
|
||||
from app.infra.db.metadb.database import SessionLocal, engine
|
||||
from app.infra.db.metadb.repositories.metadata_repository import MetadataRepository
|
||||
from app.infra.db.postgresql.scada_replays import ScadaReplayRepository
|
||||
from app.infra.db.postgresql.scada import ScadaInfoRepository
|
||||
from app.services.scada_replay import ScadaReplayService
|
||||
|
||||
pytestmark = pytest.mark.skipif(os.getenv("SCADA_REPLAY_STORAGE_TEST") != "1", reason="explicit rollback-only SQL test required")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend(): return "asyncio"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_real_manual_steps_clean_raw_and_persist_events_roll_back(monkeypatch):
|
||||
actual_project = UUID("871d7538-589c-47de-a6d5-3185ccb9f0e1")
|
||||
async with SessionLocal() as session:
|
||||
metadata = MetadataRepository(session)
|
||||
business = await metadata.get_project_db_routing(actual_project, "biz_data")
|
||||
timeseries = await metadata.get_project_db_routing(actual_project, "iot_data")
|
||||
await engine.dispose()
|
||||
pg = await psycopg.AsyncConnection.connect(business.dsn, row_factory=dict_row, connect_timeout=5)
|
||||
ts = await psycopg.AsyncConnection.connect(timeseries.dsn, row_factory=dict_row, connect_timeout=5)
|
||||
try:
|
||||
async with pg.transaction(force_rollback=True), ts.transaction(force_rollback=True):
|
||||
actual = await (await pg.execute("SELECT current_database() AS db")).fetchone()
|
||||
assert actual["db"] == "tjwater2_v2"
|
||||
migration = (Path(__file__).parents[2] / "resources/sql/031_scada_replays.sql").read_text()
|
||||
migration = migration.replace("BEGIN;\n", "", 1).removesuffix("COMMIT;\n")
|
||||
await pg.execute(migration)
|
||||
device = "SCADA_TEST_" + uuid4().hex[:12]
|
||||
start = datetime(2026, 10, 8, tzinfo=timezone.utc)
|
||||
for minute, value in ((0, 10), (15, 20), (30, 20), (45, 10), (60, 10)):
|
||||
await ts.execute("INSERT INTO scada.measurements(time,device_id,monitored_value) VALUES(%s,%s,%s)",
|
||||
(start + timedelta(minutes=minute), device, value))
|
||||
monkeypatch.setattr(ScadaInfoRepository, "get_scadas", AsyncMock(return_value=[{
|
||||
"device_id": device, "device_type": "pressure", "measurement_unit": "m",
|
||||
"transmission_frequency": "0:15:00", "transmission_mode": "non_realtime"
|
||||
}]))
|
||||
context = ProjectContext(
|
||||
project_id=uuid4(),
|
||||
project_code="tjwater2_v2",
|
||||
user_id=uuid4(),
|
||||
project_role="member",
|
||||
)
|
||||
repo = ScadaReplayRepository(pg, context.project_id)
|
||||
wall = [start]
|
||||
service = ScadaReplayService(repo, context, ts, clock=lambda: wall[0])
|
||||
config = ScadaReplayCreate(request_id=uuid4(), start_time=start, end_time=start + timedelta(minutes=75),
|
||||
device_ids=[device],
|
||||
rules=[{"rule_id": "pressure", "device_ids": [device], "kind": "bounds", "upper": 15, "unit": "m"}])
|
||||
result = await service.create(config, monitoring_interval_seconds=900)
|
||||
while result.status != "completed":
|
||||
await service.command(result.replay_id, ScadaReplayCommand(
|
||||
request_id=uuid4(), expected_revision=result.revision, action="step"
|
||||
))
|
||||
result = await service.process_pending_step(result.replay_id)
|
||||
assert result.status == "completed"
|
||||
assert result.processed_count == 5
|
||||
assert result.anomaly_count == 1
|
||||
events = await repo.events(result.replay_id)
|
||||
assert {event["change"] for event in events} == {"raised", "resolved"}
|
||||
assert all(event["task_id"] is None for event in events)
|
||||
raw = await (await ts.execute("SELECT monitored_value,cleaned_value FROM scada.measurements WHERE device_id=%s ORDER BY time", (device,))).fetchall()
|
||||
assert [row["monitored_value"] for row in raw] == [10, 20, 20, 10, 10]
|
||||
assert [row["cleaned_value"] for row in raw] == [10, 20, 20, 10, 10]
|
||||
finally:
|
||||
await pg.close(); await ts.close()
|
||||
Reference in New Issue
Block a user