netx/tests/test_collect_lanes.py
oliver f279431c70 Run heavy CLI dumps on a dedicated long-timeout SSH lane.
Split interface detail/route-class collects from the light lane so they no longer block other checks, and scope Collect Now busy state per task.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 10:56:17 +08:00

162 lines
5 KiB
Python

"""Light/heavy collect lane partition and dual-lane scheduling."""
from __future__ import annotations
import time
import unittest
from unittest.mock import patch
from netx_api.biz_state.collect_runner import (
partition_work,
work_item_lane,
)
from netx_api.biz_state.profiles import get_profile, reload_profiles
class CollectLaneProfileTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
reload_profiles()
def test_interface_detail_is_heavy(self) -> None:
p = get_profile("zte.interface_detail")
self.assertIsNotNone(p)
assert p is not None
self.assertEqual(p.collect_lane, "heavy")
def test_route_profiles_are_heavy(self) -> None:
for pid in (
"zte.ip_route_vrf",
"zte.ip_route",
"zte.ipv6_route_vrf",
"zte.ipv6_route",
):
p = get_profile(pid)
self.assertIsNotNone(p, pid)
assert p is not None
self.assertEqual(p.collect_lane, "heavy", pid)
def test_arp_is_light(self) -> None:
p = get_profile("zte.arp")
self.assertIsNotNone(p)
assert p is not None
self.assertEqual(p.collect_lane, "light")
class PartitionWorkTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
reload_profiles()
def test_custom_raw_always_light(self) -> None:
self.assertEqual(work_item_lane("", "custom"), "light")
self.assertEqual(work_item_lane("zte.interface_detail", "custom"), "light")
def test_partition_mixed(self) -> None:
work = [
("show arp", {}, "zte.arp", "i1", "normal"),
(
"show interface | include ifindex",
{},
"zte.interface_detail",
"i2",
"normal",
),
("show ip forwarding route vrf a", {}, "zte.ip_route_vrf", "i3", "normal"),
("show version", {}, "", "i4", "custom"),
]
light, heavy = partition_work(work)
self.assertEqual(len(light), 2)
self.assertEqual({x[2] for x in light}, {"zte.arp", ""})
self.assertEqual(len(heavy), 2)
self.assertEqual(
{x[2] for x in heavy},
{"zte.interface_detail", "zte.ip_route_vrf"},
)
def test_partition_light_only(self) -> None:
work = [("show arp", {}, "zte.arp", "i1", "normal")]
light, heavy = partition_work(work)
self.assertEqual(len(light), 1)
self.assertEqual(heavy, [])
def test_partition_heavy_only(self) -> None:
work = [
(
"show interface | include ifindex",
{},
"zte.interface_detail",
"i1",
"normal",
)
]
light, heavy = partition_work(work)
self.assertEqual(light, [])
self.assertEqual(len(heavy), 1)
class DualLaneParallelTests(unittest.TestCase):
def test_light_not_blocked_by_heavy_sleep(self) -> None:
"""When both lanes run, light completes without waiting for heavy."""
from netx_api.biz_state import collect_runner as cr
order: list[str] = []
def fake_lane(*, work, label, **_kwargs):
if "heavy" in label:
time.sleep(0.35)
order.append("heavy_done")
return (0, len(work), False, True)
order.append("light_done")
return (0, len(work), False, True)
light_work = [("show arp", {}, "zte.arp", "i1", "normal")]
heavy_work = [
(
"show interface | include ifindex",
{},
"zte.interface_detail",
"i2",
"normal",
)
]
with patch.object(cr, "_run_collect_lane", side_effect=fake_lane):
t0 = time.perf_counter()
heavy_fut = cr._heavy_cli_pool().submit(
lambda: cr._run_collect_lane(
work=heavy_work,
batch_id="b",
creds={},
vendor_eff="zte",
device_type_eff="",
vendor_key="zte",
per_cmd=1,
cap=10,
label="biz_state_heavy",
)
)
light_res = cr._run_collect_lane(
work=light_work,
batch_id="b",
creds={},
vendor_eff="zte",
device_type_eff="",
vendor_key="zte",
per_cmd=1,
cap=10,
label="biz_state_light",
)
light_elapsed = time.perf_counter() - t0
heavy_res = heavy_fut.result()
total_elapsed = time.perf_counter() - t0
self.assertEqual(light_res[1], 1)
self.assertEqual(heavy_res[1], 1)
self.assertEqual(order[0], "light_done")
self.assertLess(light_elapsed, 0.25)
self.assertGreaterEqual(total_elapsed, 0.3)
if __name__ == "__main__":
unittest.main()