netx/tests/test_biz_state_compare_snapshot.py

170 lines
8.3 KiB
Python

"""Historical comparisons must use the settings captured when they were queued."""
from __future__ import annotations
import csv
import io
import json
import unittest
import zipfile
from copy import deepcopy
from unittest.mock import patch
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from netx_api.biz_state.compare_service import (
_create_running_run, _execute_compare_into_run, cancel_compare_run,
export_run_zip, get_run, list_run_diffs, template_metrics,
)
from netx_api.db import Base
from netx_api.models import (
BizCompareDiff, BizCompareJob, BizCompareTemplate, BizPortMapping,
BizPortMappingRow, BizStateMetricRow,
)
class CompareSnapshotTests(unittest.TestCase):
def setUp(self):
self.engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(self.engine)
self.factory = sessionmaker(bind=self.engine, expire_on_commit=False)
self.db = self.factory()
# Progress uses a separate session in production; keep every test write local.
self.session_patch = patch("netx_api.db.SessionLocal", self.factory)
self.session_patch.start()
self.sheet = {
"sheet_id": "sheet", "metric_id": "test", "title": "Original sheet",
"key_fields": ["id", "interface"], "iface_fields": ["interface"],
"compare_fields": ["state"], "display_fields": ["id", "interface", "state"],
"row_filters": [], "ignore_port_changes": False,
}
self.tpl = BizCompareTemplate(
id="tpl", name="Original template", metrics_json=[deepcopy(self.sheet)],
iface_normalize_json=[{"from": "GE", "to": "gei"}],
)
self.job = BizCompareJob(id="job", name="Original job", template_id="tpl",
mapping_id="map", store_unchanged="sample")
self.mapping = BizPortMapping(id="map", name="Original mapping")
self.pair = BizPortMappingRow(mapping_id="map", before_if="gei-1", after_if="gei-2")
self.db.add_all([self.tpl, self.job, self.mapping, self.pair])
for side, iface, note in (("before", "GE-1.100", "old"), ("after", "GE-2.100", "new")):
self.db.add(BizStateMetricRow(id=side, batch_id=side, metric_id="test", seq=0,
data_json={"id": "1", "interface": iface,
"state": "up", "note": note}))
self.db.commit()
self.run = _create_running_run(
self.db, job=self.job, tpl=self.tpl, before_batch_id="before",
after_batch_id="after", sheets_cfg=template_metrics(self.tpl),
)
def tearDown(self):
self.session_patch.stop()
self.db.close()
self.engine.dispose()
def edit_current_settings(self):
changed = {**self.sheet, "title": "Edited sheet", "compare_fields": ["note"],
"row_filters": [{"field": "state", "op": "eq", "value": "down"}]}
self.tpl.name = "Edited template"
self.tpl.metrics_json = [changed]
self.tpl.iface_normalize_json = [{"from": "GE", "to": "other"}]
self.pair.after_if = "gei-99"
self.job.name = "Edited job"
self.job.store_unchanged = "never"
self.job.enabled_sheet_ids = ["different-sheet"]
self.mapping.name = "Edited mapping"
self.db.commit()
def execute(self):
return _execute_compare_into_run(self.db, self.run.id)
def test_queued_run_uses_original_sheet_rules_mapping_and_storage(self):
original = deepcopy(self.run.summary_json["config_snapshot"])
self.edit_current_settings()
detail = self.execute()
self.assertEqual(detail["status"], "success")
self.assertEqual(detail["summary"]["unchanged"], 1)
self.assertEqual(detail["summary"]["unchanged_listed"], 1)
self.assertEqual(detail["summary"]["store_unchanged"], "sample")
self.assertEqual(detail["sheets"][0]["compare_fields"], ["state"])
self.assertEqual(self.run.summary_json["config_snapshot"], original)
def test_compact_hydration_restores_original_normalized_values(self):
self.execute()
self.edit_current_settings()
diff = list_run_diffs(self.db, self.run.id, metric_id="sheet", kind="unchanged")["items"][0]
self.assertEqual(diff["before"]["interface"], "GE-1.100")
self.assertEqual(diff["mapped_before"]["interface"], "gei-2.100")
self.assertEqual(diff["after"]["interface"], "gei-2.100")
def test_live_search_does_not_inherit_new_filters_or_transforms(self):
with patch("netx_api.biz_state.compare_service._UNCHANGED_SAMPLE_MAX", 0):
self.execute()
self.edit_current_settings()
result = list_run_diffs(self.db, self.run.id, metric_id="sheet", kind="unchanged", kw="old")
self.assertEqual(result["source"], "live")
self.assertEqual(result["total"], 1)
self.assertEqual(result["items"][0]["mapped_before"]["interface"], "gei-2.100")
def test_snapshot_can_be_read_after_mapping_and_template_are_deleted(self):
self.execute()
self.db.query(BizPortMappingRow).delete()
self.db.delete(self.mapping)
self.db.delete(self.tpl)
self.db.commit()
detail = get_run(self.db, self.run.id)
self.assertEqual(detail["template_name"], "Original template")
self.assertEqual(detail["mapping_name"], "Original mapping")
diff = list_run_diffs(self.db, self.run.id, metric_id="sheet", kind="unchanged")["items"][0]
self.assertEqual(diff["mapped_before"]["interface"], "gei-2.100")
def test_queued_run_can_execute_after_its_template_and_mapping_are_deleted(self):
self.db.query(BizPortMappingRow).delete()
self.db.delete(self.mapping)
self.db.delete(self.tpl)
self.db.commit()
detail = self.execute()
self.assertEqual(detail["status"], "success")
self.assertEqual(detail["summary"]["unchanged"], 1)
def test_detail_returns_original_names_and_independent_template_copy(self):
self.edit_current_settings()
detail = get_run(self.db, self.run.id)
self.assertEqual(detail["job_name"], "Original job")
self.assertEqual(detail["config_snapshot_version"], 1)
self.assertEqual(detail["template"]["iface_normalize_rules"], [{"from": "GE", "to": "gei"}])
self.assertNotIn("config_snapshot", detail["summary"])
detail["template"]["iface_normalize_rules"].clear()
self.assertTrue(self.run.summary_json["config_snapshot"]["template"]["iface_normalize_rules"])
def test_export_contains_original_config_and_hydrated_csv(self):
self.execute()
self.edit_current_settings()
with zipfile.ZipFile(io.BytesIO(export_run_zip(self.db, self.run.id))) as archive:
config = json.loads(archive.read("config_snapshot.json"))
self.assertEqual(config["port_map"], {"gei-1": "gei-2"})
self.assertEqual(config["template"]["name"], "Original template")
rows = list(csv.DictReader(io.StringIO(archive.read("tables/sheet.csv").decode("utf-8-sig"))))
self.assertEqual(rows[0]["interface"], "gei-2.100")
def test_failed_and_cancelled_runs_keep_snapshot(self):
original = deepcopy(self.run.summary_json["config_snapshot"])
with patch("netx_api.biz_state.compare_service._run_sheet", side_effect=RuntimeError("test-failure")):
with self.assertRaisesRegex(RuntimeError, "test-failure"):
self.execute()
self.assertEqual(self.run.summary_json["config_snapshot"], original)
self.run.status = "running"
self.db.commit()
cancel_compare_run(self.db, self.run.id)
self.assertEqual(self.run.summary_json["config_snapshot"], original)
def test_legacy_runs_without_snapshot_remain_readable(self):
self.execute()
self.run.summary_json = {k: v for k, v in self.run.summary_json.items() if k != "config_snapshot"}
self.db.commit()
detail = get_run(self.db, self.run.id)
self.assertEqual(detail["status"], "success")
self.assertIsNone(detail["config_snapshot_version"])
self.assertEqual(list_run_diffs(self.db, self.run.id, metric_id="sheet", kind="unchanged")["total"], 1)
self.assertEqual(self.db.query(BizCompareDiff).count(), 1)