Add initial cable path tests for profiles
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run

This commit is contained in:
Jeremy Stretch
2025-11-14 10:28:43 -05:00
parent 90c38f8520
commit 43067c39c8
4 changed files with 464 additions and 84 deletions

View File

@@ -32,7 +32,7 @@ class BaseCableProfile:
'Maximum B side connections for profile {profile}: {max}' 'Maximum B side connections for profile {profile}: {max}'
).format( ).format(
profile=cable.get_profile_display(), profile=cable.get_profile_display(),
max=self.a_max_connections, max=self.b_max_connections,
) )
}) })
if self.symmetrical and len(cable.a_terminations) != len(cable.b_terminations): if self.symmetrical and len(cable.a_terminations) != len(cable.b_terminations):

View File

@@ -1,100 +1,21 @@
from django.test import TestCase
from circuits.models import * from circuits.models import *
from dcim.choices import LinkStatusChoices from dcim.choices import LinkStatusChoices
from dcim.models import * from dcim.models import *
from dcim.svg import CableTraceSVG from dcim.svg import CableTraceSVG
from dcim.utils import object_to_path_node from dcim.tests.utils import CablePathTestCase
from utilities.exceptions import AbortRequest from utilities.exceptions import AbortRequest
class CablePathTestCase(TestCase): class LegacyCablePathTests(CablePathTestCase):
""" """
Test NetBox's ability to trace and retrace CablePaths in response to data model changes. Tests are numbered Test NetBox's ability to trace and retrace CablePaths in response to data model changes, without cable profiles.
as follows:
Tests are numbered as follows:
1XX: Test direct connections between different endpoint types 1XX: Test direct connections between different endpoint types
2XX: Test different cable topologies 2XX: Test different cable topologies
3XX: Test responses to changes in existing objects 3XX: Test responses to changes in existing objects
4XX: Test to exclude specific cable topologies 4XX: Test to exclude specific cable topologies
""" """
@classmethod
def setUpTestData(cls):
# Create a single device that will hold all components
cls.site = Site.objects.create(name='Site', slug='site')
manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Test Device')
role = DeviceRole.objects.create(name='Device Role', slug='device-role')
cls.device = Device.objects.create(site=cls.site, device_type=device_type, role=role, name='Test Device')
cls.powerpanel = PowerPanel.objects.create(site=cls.site, name='Power Panel')
provider = Provider.objects.create(name='Provider', slug='provider')
circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type')
cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1')
def _get_cablepath(self, nodes, **kwargs):
"""
Return a given cable path
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
:return: The matching CablePath (if any)
"""
path = []
for step in nodes:
if type(step) in (list, tuple):
path.append([object_to_path_node(node) for node in step])
else:
path.append([object_to_path_node(step)])
return CablePath.objects.filter(path=path, **kwargs).first()
def assertPathExists(self, nodes, **kwargs):
"""
Assert that a CablePath from origin to destination with a specific intermediate path exists. Returns the
first matching CablePath, if found.
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
"""
cablepath = self._get_cablepath(nodes, **kwargs)
self.assertIsNotNone(cablepath, msg='CablePath not found')
return cablepath
def assertPathDoesNotExist(self, nodes, **kwargs):
"""
Assert that a specific CablePath does *not* exist.
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
"""
cablepath = self._get_cablepath(nodes, **kwargs)
self.assertIsNone(cablepath, msg='Unexpected CablePath found')
def assertPathIsSet(self, origin, cablepath, msg=None):
"""
Assert that a specific CablePath instance is set as the path on the origin.
:param origin: The originating path endpoint
:param cablepath: The CablePath instance originating from this endpoint
:param msg: Custom failure message (optional)
"""
if msg is None:
msg = f"Path #{cablepath.pk} not set on originating endpoint {origin}"
self.assertEqual(origin._path_id, cablepath.pk, msg=msg)
def assertPathIsNotSet(self, origin, msg=None):
"""
Assert that a specific CablePath instance is set as the path on the origin.
:param origin: The originating path endpoint
:param msg: Custom failure message (optional)
"""
if msg is None:
msg = f"Path #{origin._path_id} set as origin on {origin}; should be None!"
self.assertIsNone(origin._path_id, msg=msg)
def test_101_interface_to_interface(self): def test_101_interface_to_interface(self):
""" """
[IF1] --C1-- [IF2] [IF1] --C1-- [IF2]

View File

@@ -0,0 +1,371 @@
from dcim.choices import CableProfileChoices
from dcim.models import *
from dcim.svg import CableTraceSVG
from dcim.tests.utils import CablePathTestCase
class CablePathTests(CablePathTestCase):
"""
Test the creation of CablePaths for Cables with different profiles applied.
Tests are numbered as follows:
1XX: Test direct connections using each profile
"""
def test_101_cable_profile_straight_single(self):
"""
[IF1] --C1-- [IF2]
Cable profile: Straight single
"""
interfaces = [
Interface.objects.create(device=self.device, name='Interface 1'),
Interface.objects.create(device=self.device, name='Interface 2'),
]
# Create cable 1
cable1 = Cable(
profile=CableProfileChoices.STRAIGHT_SINGLE,
a_terminations=[interfaces[0]],
b_terminations=[interfaces[1]],
)
cable1.clean()
cable1.save()
path1 = self.assertPathExists(
(interfaces[0], cable1, interfaces[1]),
is_complete=True,
is_active=True
)
path2 = self.assertPathExists(
(interfaces[1], cable1, interfaces[0]),
is_complete=True,
is_active=True
)
self.assertEqual(CablePath.objects.count(), 2)
interfaces[0].refresh_from_db()
interfaces[1].refresh_from_db()
self.assertPathIsSet(interfaces[0], path1)
self.assertPathIsSet(interfaces[1], path2)
self.assertEqual(interfaces[0].cable_position, 1)
self.assertEqual(interfaces[1].cable_position, 1)
# Test SVG generation
CableTraceSVG(interfaces[0]).render()
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_102_cable_profile_straight_multi(self):
"""
[IF1] --C1-- [IF3]
[IF2] [IF4]
Cable profile: Straight multi
"""
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'),
]
# Create cable 1
cable1 = Cable(
profile=CableProfileChoices.STRAIGHT_MULTI,
a_terminations=[interfaces[0], interfaces[1]],
b_terminations=[interfaces[2], interfaces[3]],
)
cable1.clean()
cable1.save()
path1 = self.assertPathExists(
(interfaces[0], cable1, interfaces[2]),
is_complete=True,
is_active=True
)
path2 = self.assertPathExists(
(interfaces[1], cable1, interfaces[3]),
is_complete=True,
is_active=True
)
path3 = self.assertPathExists(
(interfaces[2], cable1, interfaces[0]),
is_complete=True,
is_active=True
)
path4 = self.assertPathExists(
(interfaces[3], 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], path1)
self.assertPathIsSet(interfaces[1], path2)
self.assertPathIsSet(interfaces[2], path3)
self.assertPathIsSet(interfaces[3], path4)
self.assertEqual(interfaces[0].cable_position, 1)
self.assertEqual(interfaces[1].cable_position, 2)
self.assertEqual(interfaces[2].cable_position, 1)
self.assertEqual(interfaces[3].cable_position, 2)
# Test SVG generation
CableTraceSVG(interfaces[0]).render()
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_103_cable_profile_a_to_many(self):
"""
[IF1] --C1-- [IF2]
[IF3]
[IF4]
Cable profile: A to many
"""
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'),
]
# Create cable 1
cable1 = Cable(
profile=CableProfileChoices.A_TO_MANY,
a_terminations=[interfaces[0]],
b_terminations=[interfaces[1], interfaces[2], interfaces[3]],
)
cable1.clean()
cable1.save()
# A-to-B path leads to all interfaces
path1 = self.assertPathExists(
(interfaces[0], cable1, [interfaces[1], interfaces[2], interfaces[3]]),
is_complete=True,
is_active=True
)
# B-to-A paths are incomplete because A side has null position
path2 = self.assertPathExists(
(interfaces[1], cable1, []),
is_complete=False,
is_active=True
)
path3 = self.assertPathExists(
(interfaces[2], cable1, []),
is_complete=False,
is_active=True
)
path4 = self.assertPathExists(
(interfaces[3], cable1, []),
is_complete=False,
is_active=True
)
self.assertEqual(CablePath.objects.count(), 4)
for interface in interfaces:
interface.refresh_from_db()
self.assertPathIsSet(interfaces[0], path1)
self.assertPathIsSet(interfaces[1], path2)
self.assertPathIsSet(interfaces[2], path3)
self.assertPathIsSet(interfaces[3], path4)
self.assertIsNone(interfaces[0].cable_position)
self.assertEqual(interfaces[1].cable_position, 1)
self.assertEqual(interfaces[2].cable_position, 2)
self.assertEqual(interfaces[3].cable_position, 3)
# Test SVG generation
CableTraceSVG(interfaces[0]).render()
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_104_cable_profile_b_to_many(self):
"""
[IF1] --C1-- [IF4]
[IF2]
[IF3]
Cable profile: B to many
"""
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'),
]
# Create cable 1
cable1 = Cable(
profile=CableProfileChoices.B_TO_MANY,
a_terminations=[interfaces[0], interfaces[1], interfaces[2]],
b_terminations=[interfaces[3]],
)
cable1.clean()
cable1.save()
# A-to-B paths are incomplete because A side has null position
path1 = self.assertPathExists(
(interfaces[0], cable1, []),
is_complete=False,
is_active=True
)
path2 = self.assertPathExists(
(interfaces[1], cable1, []),
is_complete=False,
is_active=True
)
path3 = self.assertPathExists(
(interfaces[2], cable1, []),
is_complete=False,
is_active=True
)
# B-to-A path leads to all interfaces
path4 = self.assertPathExists(
(interfaces[3], cable1, [interfaces[0], interfaces[1], interfaces[2]]),
is_complete=True,
is_active=True
)
self.assertEqual(CablePath.objects.count(), 4)
for interface in interfaces:
interface.refresh_from_db()
self.assertPathIsSet(interfaces[0], path1)
self.assertPathIsSet(interfaces[1], path2)
self.assertPathIsSet(interfaces[2], path3)
self.assertPathIsSet(interfaces[3], path4)
self.assertEqual(interfaces[0].cable_position, 1)
self.assertEqual(interfaces[1].cable_position, 2)
self.assertEqual(interfaces[2].cable_position, 3)
self.assertIsNone(interfaces[3].cable_position)
# Test SVG generation
CableTraceSVG(interfaces[0]).render()
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_105_cable_profile_2x2_mpo(self):
"""
[IF1:1] --C1-- [IF3:1]
[IF1:2] [IF3:2]
[IF1:3] [IF3:3]
[IF1:4] [IF3:4]
[IF2:1] [IF4:1]
[IF2:2] [IF4:2]
[IF2:3] [IF4:3]
[IF2:4] [IF4:4]
Cable profile: Shuffle (2x2 MPO)
"""
interfaces = [
Interface.objects.create(device=self.device, name='Interface 1:1'),
Interface.objects.create(device=self.device, name='Interface 1:2'),
Interface.objects.create(device=self.device, name='Interface 1:3'),
Interface.objects.create(device=self.device, name='Interface 1:4'),
Interface.objects.create(device=self.device, name='Interface 2:1'),
Interface.objects.create(device=self.device, name='Interface 2:2'),
Interface.objects.create(device=self.device, name='Interface 2:3'),
Interface.objects.create(device=self.device, name='Interface 2:4'),
Interface.objects.create(device=self.device, name='Interface 3:1'),
Interface.objects.create(device=self.device, name='Interface 3:2'),
Interface.objects.create(device=self.device, name='Interface 3:3'),
Interface.objects.create(device=self.device, name='Interface 3:4'),
Interface.objects.create(device=self.device, name='Interface 4:1'),
Interface.objects.create(device=self.device, name='Interface 4:2'),
Interface.objects.create(device=self.device, name='Interface 4:3'),
Interface.objects.create(device=self.device, name='Interface 4:4'),
]
# Create cable 1
cable1 = Cable(
profile=CableProfileChoices.SHUFFLE_2X2_MPO,
a_terminations=interfaces[0:8],
b_terminations=interfaces[8:16],
)
cable1.clean()
cable1.save()
paths = [
# A-to-B paths
self.assertPathExists(
(interfaces[0], cable1, interfaces[8]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[1], cable1, interfaces[9]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[2], cable1, interfaces[12]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[3], cable1, interfaces[13]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[4], cable1, interfaces[10]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[5], cable1, interfaces[11]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[6], cable1, interfaces[14]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[7], cable1, interfaces[15]), is_complete=True, is_active=True
),
# B-to-A paths
self.assertPathExists(
(interfaces[8], cable1, interfaces[0]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[9], cable1, interfaces[1]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[10], cable1, interfaces[4]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[11], cable1, interfaces[5]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[12], cable1, interfaces[2]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[13], cable1, interfaces[3]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[14], cable1, interfaces[6]), is_complete=True, is_active=True
),
self.assertPathExists(
(interfaces[15], cable1, interfaces[7]), is_complete=True, is_active=True
),
]
self.assertEqual(CablePath.objects.count(), len(paths))
for i, (interface, path) in enumerate(zip(interfaces, paths)):
interface.refresh_from_db()
self.assertPathIsSet(interface, path)
self.assertEqual(interface.cable_end, 'A' if i < 8 else 'B')
self.assertEqual(interface.cable_position, (i % 8) + 1)
# Test SVG generation
CableTraceSVG(interfaces[0]).render()
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)

View File

@@ -0,0 +1,88 @@
from django.test import TestCase
from circuits.models import *
from dcim.models import *
from dcim.utils import object_to_path_node
__all__ = (
'CablePathTestCase',
)
class CablePathTestCase(TestCase):
"""
Base class for test cases for cable paths.
"""
@classmethod
def setUpTestData(cls):
manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Test Device')
role = DeviceRole.objects.create(name='Device Role', slug='device-role')
provider = Provider.objects.create(name='Provider', slug='provider')
circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type')
# Create reusable test objects
cls.site = Site.objects.create(name='Site', slug='site')
cls.device = Device.objects.create(site=cls.site, device_type=device_type, role=role, name='Test Device')
cls.powerpanel = PowerPanel.objects.create(site=cls.site, name='Power Panel')
cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1')
def _get_cablepath(self, nodes, **kwargs):
"""
Return a given cable path
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
:return: The matching CablePath (if any)
"""
path = []
for step in nodes:
if type(step) in (list, tuple):
path.append([object_to_path_node(node) for node in step])
else:
path.append([object_to_path_node(step)])
return CablePath.objects.filter(path=path, **kwargs).first()
def assertPathExists(self, nodes, **kwargs):
"""
Assert that a CablePath from origin to destination with a specific intermediate path exists. Returns the
first matching CablePath, if found.
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
"""
cablepath = self._get_cablepath(nodes, **kwargs)
self.assertIsNotNone(cablepath, msg='CablePath not found')
return cablepath
def assertPathDoesNotExist(self, nodes, **kwargs):
"""
Assert that a specific CablePath does *not* exist.
:param nodes: Iterable of steps, with each step being either a single node or a list of nodes
"""
cablepath = self._get_cablepath(nodes, **kwargs)
self.assertIsNone(cablepath, msg='Unexpected CablePath found')
def assertPathIsSet(self, origin, cablepath, msg=None):
"""
Assert that a specific CablePath instance is set as the path on the origin.
:param origin: The originating path endpoint
:param cablepath: The CablePath instance originating from this endpoint
:param msg: Custom failure message (optional)
"""
if msg is None:
msg = f"Path #{cablepath.pk} not set on originating endpoint {origin}"
self.assertEqual(origin._path_id, cablepath.pk, msg=msg)
def assertPathIsNotSet(self, origin, msg=None):
"""
Assert that a specific CablePath instance is set as the path on the origin.
:param origin: The originating path endpoint
:param msg: Custom failure message (optional)
"""
if msg is None:
msg = f"Path #{origin._path_id} set as origin on {origin}; should be None!"
self.assertIsNone(origin._path_id, msg=msg)