mirror of
https://github.com/hansjone/netx.git
synced 2026-10-12 07:40:45 +08:00
170 lines
8.3 KiB
Python
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)
|