mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 16:00:51 +08:00
Correctness (A): normalize port-map names with the template rules (snapshot v2, v1 replays unchanged); protect mappings/templates that are referenced; stop dropping edited mapping text; accept pulled-but-not-shutdown ports as migrated; validate baseline ownership on project create; one canonical key form; fix built-in template tolerances and upgrade old packaged values in place. Cross-vendor identity (B): single identity pipeline for every path, per-side and per-vendor interface rules, value maps, filters after normalization, mapping pre-flight for compare and cutover projects, SQL eligibility fixes. Data quality (V): no_data / suspect_parse / not_collected sheet states, command slice coverage, directional compare modes, value-sorted duplicate pairing, before-side raw names in results and CSV. Robustness (H): bounded auto-run history, background evaluate/finish with polling, manual route tables out of the acceptance gate, baseline row cache, scoped duplicate-key failures, merged HF tasks with a per-NE session cap, same-device cutover, explicit scope_by_port_map, read-only GETs, resolvable red tickets. Frontend: pre-flight report, mapping table view, template editor support for the new rules, data-state banners. Docs: canonical field contract. Not included: Huawei status parsers (no real samples), SQL-path route compare. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
120 lines
4.9 KiB
Python
120 lines
4.9 KiB
Python
"""Unit tests for iface normalize + subinterface port-map inheritance."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from netx_api.biz_state.compare_engine import apply_port_map, compare_rows
|
|
from netx_api.biz_state.iface_normalize import (
|
|
default_zte_iface_normalize_rules,
|
|
normalize_iface_name,
|
|
normalize_iface_rules,
|
|
normalize_port_map,
|
|
resolve_mapped_iface,
|
|
)
|
|
|
|
|
|
class IfaceNormalizeTests(unittest.TestCase):
|
|
def test_prefix_rules_longest_first(self) -> None:
|
|
rules = default_zte_iface_normalize_rules()
|
|
self.assertEqual(normalize_iface_name("GE1.100", rules), "gei1.100")
|
|
self.assertEqual(normalize_iface_name("SG11", rules), "smartgroup11")
|
|
self.assertEqual(normalize_iface_name("XGE-0/1/0/1", rules), "xgei-0/1/0/1")
|
|
self.assertEqual(normalize_iface_name("XXVGE-0/2/0/10", rules), "xxvgei-0/2/0/10")
|
|
self.assertEqual(normalize_iface_name("CGE0/4/1/3", rules), "cgei0/4/1/3")
|
|
|
|
def test_does_not_mangle_canonical(self) -> None:
|
|
rules = default_zte_iface_normalize_rules()
|
|
self.assertEqual(normalize_iface_name("gei-0/0/0/1", rules), "gei-0/0/0/1")
|
|
self.assertEqual(normalize_iface_name("smartgroup11", rules), "smartgroup11")
|
|
self.assertEqual(normalize_iface_name("xgei-0/3/1/20.219", rules), "xgei-0/3/1/20.219")
|
|
|
|
def test_parse_text_rules(self) -> None:
|
|
rules = normalize_iface_rules("GE,gei\n# comment\nSG\tsmartgroup\n")
|
|
self.assertEqual(rules[0]["from"], "GE")
|
|
self.assertEqual(rules[1]["from"], "SG")
|
|
|
|
|
|
class PortMapSubifTests(unittest.TestCase):
|
|
def test_exact_wins_over_parent(self) -> None:
|
|
pmap = {
|
|
"gei-0/0/0/1": "xgei-0/1/0/1",
|
|
"gei-0/0/0/1.100": "xgei-0/1/0/9.100",
|
|
}
|
|
self.assertEqual(resolve_mapped_iface("gei-0/0/0/1.100", pmap), "xgei-0/1/0/9.100")
|
|
self.assertEqual(resolve_mapped_iface("gei-0/0/0/1.200", pmap), "xgei-0/1/0/1.200")
|
|
|
|
def test_parent_inherits_suffix(self) -> None:
|
|
pmap = {"gei-0/0/0/1": "xgei-0/1/0/1"}
|
|
self.assertEqual(resolve_mapped_iface("gei-0/0/0/1", pmap), "xgei-0/1/0/1")
|
|
self.assertEqual(resolve_mapped_iface("gei-0/0/0/1.100", pmap), "xgei-0/1/0/1.100")
|
|
self.assertEqual(resolve_mapped_iface("gei-0/0/0/2.100", pmap), "gei-0/0/0/2.100")
|
|
|
|
def test_qinq_multi_level_parent(self) -> None:
|
|
"""QinQ a.b.c: map parent a.b or a, keep remaining suffix."""
|
|
pmap = {"gei-0/1/0/1": "xgei-0/2/0/1"}
|
|
self.assertEqual(
|
|
resolve_mapped_iface("gei-0/1/0/1.100.200", pmap),
|
|
"xgei-0/2/0/1.100.200",
|
|
)
|
|
pmap2 = {"gei-0/1/0/1.100": "xgei-0/2/0/1.100"}
|
|
self.assertEqual(
|
|
resolve_mapped_iface("gei-0/1/0/1.100.200", pmap2),
|
|
"xgei-0/2/0/1.100.200",
|
|
)
|
|
|
|
def test_apply_port_map_row(self) -> None:
|
|
row = apply_port_map(
|
|
{"interface": "gei-0/0/0/1.55", "admin": "up"},
|
|
iface_fields=["interface"],
|
|
port_map={"gei-0/0/0/1": "xgei-0/1/0/1"},
|
|
)
|
|
self.assertEqual(row["interface"], "xgei-0/1/0/1.55")
|
|
|
|
|
|
class NormalizePortMapTests(unittest.TestCase):
|
|
def test_keys_and_values_use_template_rules(self) -> None:
|
|
out, conflicts = normalize_port_map(
|
|
{"GE0/1/0/1": "GE0/2/0/1"},
|
|
normalize_iface_rules([{"from": "GE", "to": "gei"}]),
|
|
)
|
|
self.assertEqual(out, {"gei0/1/0/1": "gei0/2/0/1"})
|
|
self.assertEqual(conflicts, [])
|
|
|
|
def test_conflicting_keys_are_reported_first_wins(self) -> None:
|
|
out, conflicts = normalize_port_map(
|
|
{"GE0/1": "gei0/2", "gei0/1": "gei0/3"},
|
|
normalize_iface_rules([{"from": "GE", "to": "gei"}]),
|
|
)
|
|
self.assertEqual(out, {"gei0/1": "gei0/2"})
|
|
self.assertEqual(len(conflicts), 1)
|
|
self.assertEqual(conflicts[0]["dropped_to"], "gei0/3")
|
|
|
|
def test_same_target_duplicate_is_not_a_conflict(self) -> None:
|
|
_, conflicts = normalize_port_map(
|
|
{"GE0/1": "gei0/2", "gei0/1": "gei0/2"},
|
|
normalize_iface_rules([{"from": "GE", "to": "gei"}]),
|
|
)
|
|
self.assertEqual(conflicts, [])
|
|
|
|
|
|
class ComparePipelineTests(unittest.TestCase):
|
|
def test_normalize_then_map_matches(self) -> None:
|
|
before = [{"interface": "GE-0/0/0/1.100", "admin": "up"}]
|
|
after = [{"interface": "xgei-0/1/0/1.100", "admin": "up"}]
|
|
out = compare_rows(
|
|
before_rows=before,
|
|
after_rows=after,
|
|
key_fields=["interface"],
|
|
iface_fields=["interface"],
|
|
compare_fields=["admin"],
|
|
port_map={"gei-0/0/0/1": "xgei-0/1/0/1"},
|
|
iface_normalize_rules=default_zte_iface_normalize_rules(),
|
|
)
|
|
self.assertEqual(out["summary"]["unchanged"], 1)
|
|
self.assertEqual(out["summary"]["added"], 0)
|
|
self.assertEqual(out["summary"]["removed"], 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|