From 2fe5323dd2eecc6afb680de176c3714462000c36 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Nov 2025 15:45:44 -0500 Subject: [PATCH] Add topology tests for cable profiles --- netbox/dcim/cable_profiles.py | 11 +- netbox/dcim/tests/test_cablepaths2.py | 389 +++++++++++++++++++++++++- 2 files changed, 383 insertions(+), 17 deletions(-) diff --git a/netbox/dcim/cable_profiles.py b/netbox/dcim/cable_profiles.py index 8433d29b9..b6fea1cc4 100644 --- a/netbox/dcim/cable_profiles.py +++ b/netbox/dcim/cable_profiles.py @@ -53,7 +53,12 @@ class BaseCableProfile: # Pop the position stack if necessary if (local_end == 'A' and self.pop_stack_a_side) or (local_end == 'B' and self.pop_stack_b_side): - position = position_stack.pop()[0] + try: + position = position_stack.pop()[0] + except IndexError: + # TODO: Should this raise an error? + # Bottomed out of stack + pass qs = CableTermination.objects.filter( cable=terminations[0].cable, @@ -78,14 +83,14 @@ class AToManyCableProfile(BaseCableProfile): a_max_connections = 1 b_max_connections = None symmetrical = False - pop_stack_a_side = False + pop_stack_b_side = False class BToManyCableProfile(BaseCableProfile): a_max_connections = None b_max_connections = 1 symmetrical = False - pop_stack_b_side = False + pop_stack_a_side = False class Shuffle2x2MPOCableProfile(BaseCableProfile): diff --git a/netbox/dcim/tests/test_cablepaths2.py b/netbox/dcim/tests/test_cablepaths2.py index eb8994349..cb26bca6b 100644 --- a/netbox/dcim/tests/test_cablepaths2.py +++ b/netbox/dcim/tests/test_cablepaths2.py @@ -1,3 +1,4 @@ +from circuits.models import CircuitTermination from dcim.choices import CableProfileChoices from dcim.models import * from dcim.svg import CableTraceSVG @@ -10,6 +11,7 @@ class CablePathTests(CablePathTestCase): Tests are numbered as follows: 1XX: Test direct connections using each profile + 2XX: Topology tests replicated from the legacy test case and adapted to use profiles """ def test_101_cable_profile_straight_single(self): @@ -154,20 +156,20 @@ class CablePathTests(CablePathTestCase): is_complete=True, is_active=True ) - # B-to-A paths are incomplete because A side has null position + # B-to-A paths all lead to Interface 1 path2 = self.assertPathExists( - (interfaces[1], cable1, []), - is_complete=False, + (interfaces[1], cable1, interfaces[0]), + is_complete=True, is_active=True ) path3 = self.assertPathExists( - (interfaces[2], cable1, []), - is_complete=False, + (interfaces[2], cable1, interfaces[0]), + is_complete=True, is_active=True ) path4 = self.assertPathExists( - (interfaces[3], cable1, []), - is_complete=False, + (interfaces[3], cable1, interfaces[0]), + is_complete=True, is_active=True ) self.assertEqual(CablePath.objects.count(), 4) @@ -216,20 +218,20 @@ class CablePathTests(CablePathTestCase): cable1.clean() cable1.save() - # A-to-B paths are incomplete because A side has null position + # A-to-B paths all lead to Interface 4 path1 = self.assertPathExists( - (interfaces[0], cable1, []), - is_complete=False, + (interfaces[0], cable1, interfaces[3]), + is_complete=True, is_active=True ) path2 = self.assertPathExists( - (interfaces[1], cable1, []), - is_complete=False, + (interfaces[1], cable1, interfaces[3]), + is_complete=True, is_active=True ) path3 = self.assertPathExists( - (interfaces[2], cable1, []), - is_complete=False, + (interfaces[2], cable1, interfaces[3]), + is_complete=True, is_active=True ) # B-to-A path leads to all interfaces @@ -369,3 +371,362 @@ class CablePathTests(CablePathTestCase): # Check that all CablePaths have been deleted self.assertEqual(CablePath.objects.count(), 0) + + def test_202_single_path_via_pass_through_with_breakouts(self): + """ + [IF1] --C1-- [FP1] [RP1] --C2-- [IF3] + [IF2] [IF4] + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + Interface.objects.create(device=self.device, name='Interface 3'), + Interface.objects.create(device=self.device, name='Interface 4'), + ] + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1) + frontport1 = FrontPort.objects.create( + device=self.device, + name='Front Port 1', + rear_port=rearport1, + rear_port_position=1 + ) + + # Create cables + cable1 = Cable( + profile=CableProfileChoices.B_TO_MANY, + a_terminations=[interfaces[0], interfaces[1]], + b_terminations=[frontport1], + ) + cable1.save() + cable2 = Cable( + profile=CableProfileChoices.A_TO_MANY, + a_terminations=[rearport1], + b_terminations=[interfaces[2], interfaces[3]] + ) + cable2.save() + + paths = [ + self.assertPathExists( + (interfaces[0], cable1, frontport1, rearport1, cable2, interfaces[2]), + is_complete=True, + is_active=True + ), + self.assertPathExists( + (interfaces[1], cable1, frontport1, rearport1, cable2, interfaces[3]), + is_complete=True, + is_active=True + ), + self.assertPathExists( + (interfaces[2], cable2, rearport1, frontport1, cable1, interfaces[0]), + is_complete=True, + is_active=True + ), + self.assertPathExists( + (interfaces[3], cable2, rearport1, frontport1, cable1, interfaces[1]), + is_complete=True, + is_active=True + ), + ] + self.assertEqual(CablePath.objects.count(), 4) + for interface in interfaces: + interface.refresh_from_db() + self.assertPathIsSet(interfaces[0], paths[0]) + self.assertPathIsSet(interfaces[1], paths[1]) + self.assertPathIsSet(interfaces[2], paths[2]) + self.assertPathIsSet(interfaces[3], paths[3]) + + # Test SVG generation + CableTraceSVG(interfaces[0]).render() + + def test_204_multiple_paths_via_pass_through_with_breakouts(self): + """ + [IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [IF4] + [IF2] [IF5] + [IF3] --C2-- [FP1:2] [FP2:2] --C5-- [IF6] + [IF4] [IF7] + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + Interface.objects.create(device=self.device, name='Interface 3'), + Interface.objects.create(device=self.device, name='Interface 4'), + Interface.objects.create(device=self.device, name='Interface 5'), + Interface.objects.create(device=self.device, name='Interface 6'), + Interface.objects.create(device=self.device, name='Interface 7'), + Interface.objects.create(device=self.device, name='Interface 8'), + ] + rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4) + rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4) + frontport1_1 = FrontPort.objects.create( + device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1 + ) + frontport1_2 = FrontPort.objects.create( + device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2 + ) + frontport2_1 = FrontPort.objects.create( + device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1 + ) + frontport2_2 = FrontPort.objects.create( + device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2 + ) + + # Create cables + cable1 = Cable( + profile=CableProfileChoices.B_TO_MANY, + a_terminations=[interfaces[0], interfaces[1]], + b_terminations=[frontport1_1] + ) + cable1.save() + cable2 = Cable( + profile=CableProfileChoices.B_TO_MANY, + a_terminations=[interfaces[2], interfaces[3]], + b_terminations=[frontport1_2] + ) + cable2.save() + cable3 = Cable( + profile=CableProfileChoices.STRAIGHT_SINGLE, + a_terminations=[rearport1], + b_terminations=[rearport2] + ) + cable3.save() + cable4 = Cable( + profile=CableProfileChoices.A_TO_MANY, + a_terminations=[frontport2_1], + b_terminations=[interfaces[4], interfaces[5]] + ) + cable4.save() + cable5 = Cable( + profile=CableProfileChoices.A_TO_MANY, + a_terminations=[frontport2_2], + b_terminations=[interfaces[6], interfaces[7]] + ) + cable5.save() + + paths = [ + self.assertPathExists( + ( + interfaces[0], cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1, cable4, + interfaces[4], + ), + is_complete=True, + is_active=True, + ), + self.assertPathExists( + ( + interfaces[1], cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1, cable4, + interfaces[5], + ), + is_complete=True, + is_active=True, + ), + self.assertPathExists( + ( + interfaces[2], cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2, cable5, + interfaces[6], + ), + is_complete=True, + is_active=True, + ), + self.assertPathExists( + ( + interfaces[3], cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2, cable5, + interfaces[7], + ), + is_complete=True, + is_active=True, + ), + self.assertPathExists( + ( + interfaces[4], cable4, frontport2_1, rearport2, cable3, rearport1, frontport1_1, cable1, + interfaces[0], + ), + is_complete=True, + is_active=True, + ), + self.assertPathExists( + ( + interfaces[5], cable4, frontport2_1, rearport2, cable3, rearport1, frontport1_1, cable1, + interfaces[1], + ), + is_complete=True, + is_active=True, + ), + self.assertPathExists( + ( + interfaces[6], cable5, frontport2_2, rearport2, cable3, rearport1, frontport1_2, cable2, + interfaces[2], + ), + is_complete=True, + is_active=True, + ), + self.assertPathExists( + ( + interfaces[7], cable5, frontport2_2, rearport2, cable3, rearport1, frontport1_2, cable2, + interfaces[3], + ), + is_complete=True, + is_active=True, + ), + ] + self.assertEqual(CablePath.objects.count(), 8) + + for interface in interfaces: + interface.refresh_from_db() + self.assertPathIsSet(interfaces[0], paths[0]) + self.assertPathIsSet(interfaces[1], paths[1]) + self.assertPathIsSet(interfaces[2], paths[2]) + self.assertPathIsSet(interfaces[3], paths[3]) + self.assertPathIsSet(interfaces[4], paths[4]) + self.assertPathIsSet(interfaces[5], paths[5]) + self.assertPathIsSet(interfaces[6], paths[6]) + self.assertPathIsSet(interfaces[7], paths[7]) + + # Test SVG generation + CableTraceSVG(interfaces[0]).render() + + def test_212_interface_to_interface_via_circuit_with_breakouts(self): + """ + [IF1] --C1-- [CT1] [CT2] --C2-- [IF3] + [IF2] [IF4] + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + Interface.objects.create(device=self.device, name='Interface 3'), + Interface.objects.create(device=self.device, name='Interface 4'), + ] + circuittermination1 = CircuitTermination.objects.create( + circuit=self.circuit, + termination=self.site, + term_side='A' + ) + circuittermination2 = CircuitTermination.objects.create( + circuit=self.circuit, + termination=self.site, + term_side='Z' + ) + + # Create cables + cable1 = Cable( + profile=CableProfileChoices.B_TO_MANY, + a_terminations=[interfaces[0], interfaces[1]], + b_terminations=[circuittermination1] + ) + cable1.save() + cable2 = Cable( + profile=CableProfileChoices.A_TO_MANY, + a_terminations=[circuittermination2], + b_terminations=[interfaces[2], interfaces[3]] + ) + cable2.save() + + # Check for two complete paths in either direction + paths = [ + self.assertPathExists( + (interfaces[0], cable1, circuittermination1, circuittermination2, cable2, interfaces[2]), + is_complete=True, + is_active=True, + ), + self.assertPathExists( + (interfaces[1], cable1, circuittermination1, circuittermination2, cable2, interfaces[3]), + is_complete=True, + is_active=True, + ), + self.assertPathExists( + (interfaces[2], cable2, circuittermination2, circuittermination1, cable1, interfaces[0]), + is_complete=True, + is_active=True, + ), + self.assertPathExists( + (interfaces[3], cable2, circuittermination2, circuittermination1, cable1, interfaces[1]), + is_complete=True, + is_active=True, + ), + ] + self.assertEqual(CablePath.objects.count(), 4) + + for interface in interfaces: + interface.refresh_from_db() + self.assertPathIsSet(interfaces[0], paths[0]) + self.assertPathIsSet(interfaces[1], paths[1]) + self.assertPathIsSet(interfaces[2], paths[2]) + self.assertPathIsSet(interfaces[3], paths[3]) + + # Test SVG generation + CableTraceSVG(interfaces[0]).render() + + def test_217_interface_to_interface_via_rear_ports(self): + """ + [IF1] --C1-- [FP1] [RP1] --C2-- [RP3] [FP3] --C3-- [IF2] + [FP2] [RP2] [RP4] [FP4] + """ + interfaces = [ + Interface.objects.create(device=self.device, name='Interface 1'), + Interface.objects.create(device=self.device, name='Interface 2'), + ] + rear_ports = [ + RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1), + RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1), + RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1), + RearPort.objects.create(device=self.device, name='Rear Port 4', positions=1), + ] + front_ports = [ + FrontPort.objects.create( + device=self.device, name='Front Port 1', rear_port=rear_ports[0], rear_port_position=1 + ), + FrontPort.objects.create( + device=self.device, name='Front Port 2', rear_port=rear_ports[1], rear_port_position=1 + ), + FrontPort.objects.create( + device=self.device, name='Front Port 3', rear_port=rear_ports[2], rear_port_position=1 + ), + FrontPort.objects.create( + device=self.device, name='Front Port 4', rear_port=rear_ports[3], rear_port_position=1 + ), + ] + + # Create cables + cable1 = Cable( + profile=CableProfileChoices.A_TO_MANY, + a_terminations=[interfaces[0]], + b_terminations=[front_ports[0], front_ports[1]] + ) + cable1.save() + cable2 = Cable( + a_terminations=[rear_ports[0], rear_ports[1]], + b_terminations=[rear_ports[2], rear_ports[3]] + ) + cable2.save() + cable3 = Cable( + profile=CableProfileChoices.B_TO_MANY, + a_terminations=[interfaces[1]], + b_terminations=[front_ports[2], front_ports[3]] + ) + cable3.save() + + # Check for one complete path in either direction + paths = [ + self.assertPathExists( + ( + interfaces[0], cable1, (front_ports[0], front_ports[1]), (rear_ports[0], rear_ports[1]), cable2, + (rear_ports[2], rear_ports[3]), (front_ports[2], front_ports[3]), cable3, interfaces[1] + ), + is_complete=True + ), + self.assertPathExists( + ( + interfaces[1], cable3, (front_ports[2], front_ports[3]), (rear_ports[2], rear_ports[3]), cable2, + (rear_ports[0], rear_ports[1]), (front_ports[0], front_ports[1]), cable1, interfaces[0] + ), + is_complete=True + ), + ] + self.assertEqual(CablePath.objects.count(), 2) + + for interface in interfaces: + interface.refresh_from_db() + self.assertPathIsSet(interfaces[0], paths[0]) + self.assertPathIsSet(interfaces[1], paths[1]) + + # Test SVG generation + CableTraceSVG(interfaces[0]).render()