Files
gandalf/tests/test_cutsheet.py
T
jaredandClaude Opus 5.5 7408b1feca
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 53s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m0s
Lint / Python (flake8) (pull_request) Successful in 52s
Test / Python Tests (pytest) (push) Successful in 1m7s
Lint / JS (eslint) (pull_request) Successful in 10s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m52s
Test / Python Tests (pytest) (pull_request) Successful in 1m51s
Rack: Sliger fronts in red, white and blue, and the UniFi brush panel
From the rack photo on the wiki, the three Sligers have their coloured
perforated fronts fitted, not the faceplate-off view: Front drawings
"Sliger CX4712 4U, red/white/blue front" draw the hex-perforated panel
(about 20 rows of holes, the drive cages faint behind it), black rack ears,
the power button with Gandalf's status ring, and the USB recess.

UniFi UACC-Rack-Panel-Brush-1U is drawn from Ubiquiti's product images:
silver face, ear plates with rounded slots, a recessed chamfered frame and
two rows of bristles meeting at a seam.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 01:51:29 -05:00

395 lines
20 KiB
Python

"""Tests for cutsheet.py: validation, path tracing and plan-vs-actual checks."""
import os
import sys
import pytest
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
import cutsheet as cs # noqa: E402
PANEL = {'id': 1, 'name': 'PP1', 'kind': 'patch_panel', 'port_count': 24}
JACK = {'id': 2, 'name': 'Bedroom jack', 'kind': 'wall_jack', 'port_count': 2}
SHELF = {'id': 3, 'name': 'Shelf', 'kind': 'shelf', 'port_count': None}
ITEMS = {i['name']: i for i in (PANEL, JACK, SHELF)}
def cable(cid, a, b=None, status='installed', label=None, **kw):
a_dev, a_port, a_side = (a + ('',) * 3)[:3]
b_dev, b_port, b_side = ((b or ('', '', '')) + ('',) * 3)[:3]
c = {'id': cid, 'label': label, 'a_device': a_dev, 'a_port': a_port, 'a_side': a_side,
'b_device': b_dev, 'b_port': b_port, 'b_side': b_side, 'status': status,
'cable_type': '', 'length_m': None, 'color': '', 'notes': '', 'label_applied': False}
c.update(kw)
return c
def switch(ports):
return {'sw': {'ports': {f'Port {p["port_idx"]}': p for p in ports}}}
def port(idx, up=True, seen='', **kw):
p = {'port_idx': idx, 'up': up}
if seen:
p['lldp'] = {'system_name': seen}
p.update(kw)
return p
class TestValidation:
def test_minimal_cable(self):
c = cs.validate_cable({'a_device': 'sw', 'a_port': '3'}, ITEMS)
assert c['status'] == 'installed' and c['label'] is None and c['b_device'] == ''
def test_panel_needs_side_and_valid_port(self):
with pytest.raises(cs.ValidationError, match='front or rear'):
cs.validate_cable({'a_device': 'PP1', 'a_port': '3'}, ITEMS)
with pytest.raises(cs.ValidationError, match='ports 1 to 24'):
cs.validate_cable({'a_device': 'PP1', 'a_port': '25', 'a_side': 'front'}, ITEMS)
c = cs.validate_cable({'a_device': 'PP1', 'a_port': '3', 'a_side': 'rear'}, ITEMS)
assert c['a_side'] == 'rear'
def test_side_is_dropped_for_active_devices(self):
c = cs.validate_cable({'a_device': 'sw', 'a_port': '3', 'a_side': 'front'}, ITEMS)
assert c['a_side'] == ''
@pytest.mark.parametrize('data, message', [
({}, 'From device is required'),
({'a_device': 'sw', 'label': 'bad label!'}, 'Label may use'),
({'a_device': 'sw', 'status': 'gone'}, 'status must be'),
({'a_device': 'sw', 'cable_type': 'Cat9'}, 'Type must be'),
({'a_device': 'sw', 'length_m': 'long'}, 'Length must be a number'),
({'a_device': 'sw', 'length_m': '0'}, 'between 0 and 999'),
({'a_device': 'sw', 'b_port': '3'}, 'Choose a device before a port'),
({'a_device': 'sw', 'a_port': '1', 'b_device': 'sw', 'b_port': '1'}, 'same port'),
({'a_device': 'x' * 101}, '100 characters'),
])
def test_rejects(self, data, message):
with pytest.raises(cs.ValidationError, match=message):
cs.validate_cable(data, ITEMS)
def test_item_rules(self):
assert cs.validate_item({'name': 'S', 'kind': 'shelf', 'rack_u': '5'})['rack_height'] == 1
with pytest.raises(cs.ValidationError, match='port count'):
cs.validate_item({'name': 'P', 'kind': 'patch_panel'})
with pytest.raises(cs.ValidationError, match='rack unit to go with'):
cs.validate_item({'name': 'S', 'rack_height': '2'})
with pytest.raises(cs.ValidationError, match='kind must be'):
cs.validate_item({'name': 'S', 'kind': 'rack'})
def test_next_label_fills_gaps(self):
assert cs.next_label([]) == 'C01'
assert cs.next_label([{'label': 'C01'}, {'label': 'c03'}, {'label': 'X9'}]) == 'C02'
class TestTrace:
def test_switch_through_panel_to_jack(self):
cables = [
cable(1, ('sw', '13'), ('PP1', '5', 'front')),
cable(2, ('PP1', '5', 'rear'), ('Bedroom jack', '1', 'rear')),
cable(3, ('Bedroom jack', '1', 'front'), ('tv', 'eth0')),
]
for start in cables:
hops = cs.trace(start, cables, ITEMS)['hops']
ends = {hops[0][0], hops[-1][0]}
assert ends == {'sw', 'tv'}
assert cs.trace(start, cables, ITEMS)['cable_ids'] in ([1, 2, 3], [3, 2, 1])
def test_retired_cables_are_not_followed(self):
cables = [cable(1, ('sw', '1'), ('PP1', '1', 'front')),
cable(2, ('PP1', '1', 'rear'), ('Bedroom jack', '1', 'rear'), status='retired')]
assert cs.trace(cables[0], cables, ITEMS)['cable_ids'] == [1]
def test_unterminated_end(self):
hops = cs.trace(cable(1, ('PP1', '9', 'rear')), [cable(1, ('PP1', '9', 'rear'))], ITEMS)['hops']
assert hops[-1] is None
class TestEvaluate:
def verdict(self, cables, switches, cid=1):
return cs.evaluate(cables, ITEMS, switches)[cid]['verdict']
def test_match_through_a_panel(self):
cables = [cable(1, ('sw', '13'), ('PP1', '5', 'front')),
cable(2, ('PP1', '5', 'rear'), ('tv', 'eth0'))]
assert self.verdict(cables, switch([port(13, seen='tv')])) == 'match'
assert self.verdict(cables, switch([port(13, seen='TV ')]), cid=2) == 'match'
def test_wrong_neighbor(self):
cables = [cable(1, ('sw', '13'), ('tv', 'eth0'))]
result = cs.evaluate(cables, ITEMS, switch([port(13, seen='printer')]))[1]
assert result['verdict'] == 'wrong_neighbor' and 'printer' in result['detail']
def test_no_link_vs_idle(self):
assert self.verdict([cable(1, ('sw', '13'), ('tv', 'eth0'))], switch([port(13, up=False)])) == 'no_link'
# A run that ends at a jack with nothing plugged in is normal
jack = [cable(1, ('sw', '13'), ('Bedroom jack', '1', 'rear'))]
assert self.verdict(jack, switch([port(13, up=False)])) == 'idle'
def test_unverified_when_nothing_identifies_the_far_end(self):
assert self.verdict([cable(1, ('sw', '13'), ('tv', 'eth0'))], switch([port(13)])) == 'unverified'
jack = [cable(1, ('sw', '13'), ('Bedroom jack', '1', 'rear'))]
result = cs.evaluate(jack, ITEMS, switch([port(13, seen='camera')]))[1]
assert result['verdict'] == 'unverified' and 'camera' in result['detail']
def test_missing_port_and_unknown_devices(self):
assert self.verdict([cable(1, ('sw', '99'), ('tv', 'eth0'))], switch([port(13)])) == 'missing_port'
assert self.verdict([cable(1, ('host-a', 'eth0'), ('host-b', 'eth0'))], switch([])) == 'not_checked'
def test_statuses_skip_the_check(self):
for status in ('planned', 'dark', 'retired'):
cables = [cable(1, ('sw', '13'), ('tv', 'eth0'), status=status)]
assert self.verdict(cables, switch([port(13, seen='printer')])) == status
def test_a_planned_hop_makes_the_run_planned(self):
cables = [cable(1, ('sw', '13'), ('PP1', '5', 'front')),
cable(2, ('PP1', '5', 'rear'), ('tv', 'eth0'), status='planned')]
assert self.verdict(cables, switch([port(13, seen='printer')])) == 'planned'
def test_dac_serial_confirms_both_ends(self):
switches = {
'gw': {'ports': {'p': port(11, sfp={'kind': 'dac', 'cable_peer': {'switch': 'agg', 'port_idx': 7}})}},
'agg': {'ports': {'p': port(7, sfp={'kind': 'dac', 'cable_peer': {'switch': 'gw', 'port_idx': 11}})}},
}
result = cs.evaluate([cable(1, ('gw', '11'), ('agg', '7'))], ITEMS, switches)[1]
assert result['verdict'] == 'match' and 'DAC' in result['detail']
def test_switch_to_switch_reports_the_worse_end(self):
switches = {'a': {'ports': {'p': port(1, seen='b')}}, 'b': {'ports': {'p': port(2, seen='c')}}}
assert self.verdict([cable(1, ('a', '1'), ('b', '2'))], switches) == 'wrong_neighbor'
def test_two_installed_cables_on_one_port_conflict(self):
cables = [cable(1, ('sw', '13'), ('tv', 'eth0'), label='C01'),
cable(2, ('sw', '13'), ('printer', 'eth0'), label='C02'),
cable(3, ('sw', '13'), ('tv', 'eth0'), status='planned')]
result = cs.evaluate(cables, ITEMS, switch([port(13, seen='tv')]))
assert result[1]['verdict'] == 'conflict' and 'C02' in result[1]['detail']
assert result[3]['verdict'] == 'planned'
class TestUndocumented:
def test_lists_live_ports_without_a_cable(self):
switches = switch([port(1, seen='tv'), port(2), port(3, up=False)])
found = cs.undocumented([cable(1, ('sw', '2'), ('x', ''))], switches)
assert [(s['port_idx'], s['neighbor']) for s in found] == [(1, 'tv')]
def test_retired_cables_dont_count(self):
found = cs.undocumented([cable(1, ('sw', '1'), ('x', ''), status='retired')], switch([port(1)]))
assert len(found) == 1
def test_dac_offered_once_from_one_end(self):
switches = {
'agg': {'ports': {'p': port(7, sfp={'kind': 'dac', 'cable_peer': {'switch': 'gw', 'port_idx': 11}})}},
'gw': {'ports': {'p': port(11, sfp={'kind': 'dac', 'cable_peer': {'switch': 'agg', 'port_idx': 7}})}},
}
found = cs.undocumented([], switches)
assert len(found) == 1 and found[0]['cable_type'] == 'DAC' and found[0]['via'] == 'DAC serial'
class TestLabels:
def test_only_unapplied_labels_of_live_cables(self):
cables = [cable(1, ('sw', '1'), ('tv', ''), label='C10'), cable(2, ('sw', '2'), ('tv', ''), label='C2'),
cable(3, ('sw', '3'), ('tv', ''), label='C3', label_applied=True),
cable(4, ('sw', '4'), label='C4', status='dark'),
cable(5, ('sw', '5'), ('tv', ''), label='C5', status='planned'),
cable(6, ('sw', '6'), ('tv', ''))]
ports = [{'item_name': 'PP1', 'port': '10', 'label': 'OFFICE', 'applied': False},
{'item_name': 'PP1', 'port': '2', 'label': 'GARAGE', 'applied': False},
{'item_name': 'PP1', 'port': '3', 'label': 'DONE', 'applied': True}]
out = cs.labels_to_make(cables, ITEMS, ports)
assert [c['text'] for c in out['cables']] == ['C2', 'C4', 'C10']
assert [c['copies'] for c in out['cables']] == [2, 1, 2]
assert [p['text'] for p in out['ports']] == ['GARAGE', 'OFFICE']
assert out['unlabeled_cable_ids'] == [6]
class TestCsv:
def test_round_trip(self):
original = [cable(1, ('sw', '13'), ('PP1', '5', 'front'), label='C01', cable_type='Cat6',
length_m=1.5, color='white', label_applied=True, notes='to office, via panel')]
rows, errors = cs.parse_csv(cs.cables_to_csv(original), ITEMS)
assert errors == []
assert rows[0]['label'] == 'C01' and rows[0]['b_side'] == 'front' and rows[0]['length_m'] == 1.5
assert rows[0]['label_applied'] is True and rows[0]['notes'] == 'to office, via panel'
def test_formulas_are_neutralised_and_restored(self):
text = cs.cables_to_csv([cable(1, ('sw', '1'), label='C01', notes='=HYPERLINK("x")')])
assert "'=HYPERLINK" in text
rows, _ = cs.parse_csv(text, ITEMS)
assert rows[0]['notes'] == '=HYPERLINK("x")'
def test_any_bad_row_is_reported(self):
text = 'label,a_device,a_port,a_side\nC01,sw,1,\nC01,sw,2,\n,PP1,40,front\n'
rows, errors = cs.parse_csv(text, ITEMS)
assert any('appears twice' in e for e in errors) and any('Line 4' in e for e in errors)
def test_missing_required_column(self):
assert cs.parse_csv('label\nC1\n', ITEMS) == ([], ['Missing column: a_device'])
class TestProblems:
def test_one_problem_per_run_named_after_the_switch_cable(self):
cables = [cable(1, ('PP1', '5', 'rear'), ('tv', 'eth0')),
cable(2, ('sw', '13'), ('PP1', '5', 'front'), label='C07')]
found = cs.problems(cables, ITEMS, switch([port(13, seen='printer')]))
assert len(found) == 1
p = found[0]
assert (p['label'], p['switch'], p['port'], p['verdict']) == ('C07', 'sw', '13', 'wrong_neighbor')
assert p['key'] == 'cable 2' and 'PP1 port 5 (front)' in p['path']
def test_only_installed_problems(self):
cables = [cable(1, ('sw', '13'), ('tv', 'eth0'), status='planned'),
cable(2, ('sw', '14'), ('tv', 'eth0')),
cable(3, ('sw', '15'), ('Bedroom jack', '1', 'rear'))]
switches = switch([port(13, seen='x'), port(14, seen='tv'), port(15, up=False)])
assert cs.problems(cables, ITEMS, switches) == []
def test_each_conflicting_cable_is_reported(self):
cables = [cable(1, ('sw', '13'), ('tv', 'eth0')), cable(2, ('sw', '13'), ('pc', 'eth0'))]
found = cs.problems(cables, ITEMS, switch([port(13, seen='tv')]))
assert sorted(p['cable_id'] for p in found) == [1, 2]
class TestRackLayout:
_ids = {}
def _item(self, name, u, h=None):
# IDs in creation order: on a tie for the same top unit, the earlier record is drawn
return {'id': self._ids.setdefault(name, len(self._ids) + 1), 'name': name, 'kind': 'device',
'rack_u': u, 'rack_height': h}
def test_top_first_with_top_unit(self):
out = cs.rack_layout([self._item('a', 1), self._item('b', 10, 2), {'name': 'loose', 'rack_u': None}], 42)
assert [(i['name'], i['rack_top']) for i in out['placed']] == [('b', 11), ('a', 1)]
assert out['overlaps'] == [] and out['outside'] == []
def test_overlap_is_reported_not_drawn(self):
out = cs.rack_layout([self._item('ups', 1, 2), self._item('shelf', 2)], 42)
assert [i['name'] for i in out['placed']] == ['ups']
assert out['overlaps'] == [{'name': 'shelf', 'units': 'U2', 'with': ['ups']}]
def test_past_the_top(self):
out = cs.rack_layout([self._item('tall', 41, 3)], 42)
assert out['placed'] == [] and out['outside'] == [{'name': 'tall', 'units': 'U41-43'}]
class TestPortMarks:
def test_worst_cable_wins_and_runs_merge(self):
cables = [
dict(cable(1, ('sw', '3'), ('PP1', '1', 'front'), label='C1'), verdict='match', detail='ok', run=[1, 2]),
dict(cable(2, ('PP1', '1', 'rear'), ('jack', '1')), verdict='wrong_neighbor', detail='bad', run=[1, 2]),
dict(cable(3, ('sw', '4'), ('x',)), verdict='planned', detail='Planned', run=[3]),
dict(cable(4, ('sw', '5'), ('y',), status='retired'), verdict='retired', detail='', run=[4]),
]
marks = cs.port_marks(cables)
assert marks[('sw', '3')]['mark'] == 'ok'
assert marks[('PP1', '1')]['mark'] == 'bad' and marks[('PP1', '1')]['cable_id'] == 2
assert marks[('PP1', '1')]['run'] == [1, 2]
assert marks[('PP1', '1')]['text'] == 'cable C1: ok; cable #2: bad'
assert marks[('sw', '4')]['text'] == 'cable #3: Planned'
assert marks[('sw', '4')]['mark'] == 'doc'
assert ('sw', '5') not in marks and ('x', '') not in marks
def test_shared_check_is_said_once(self):
cables = [
dict(cable(1, ('sw', '3'), ('PP1', '1', 'front'), label='C1'), verdict='idle', detail='Idle', run=[1, 2]),
dict(cable(2, ('PP1', '1', 'rear'), ('jack', '1')), verdict='idle', detail='Idle', run=[1, 2]),
]
assert cs.port_marks(cables)[('PP1', '1')]['text'] == 'cables C1, #2: Idle'
class TestDeviceAliases:
ITEMS = {'UDM-Pro': {'name': 'UDM-Pro', 'kind': 'device', 'device': 'udm'},
'Big box': {'name': 'Big box', 'kind': 'device', 'device': 'large1'}}
def test_cable_on_an_item_is_checked_against_its_device(self):
switches = {'udm': switch([port(3, seen='large1')])['sw']}
c = cable(1, ('UDM-Pro', '3'), ('Big box', 'eth0'))
assert cs.evaluate([c], self.ITEMS, switches)[1]['verdict'] == 'match'
def test_wrong_neighbor_still_caught_through_an_alias(self):
switches = {'udm': switch([port(3, seen='printer')])['sw']}
c = cable(1, ('UDM-Pro', '3'), ('Big box', 'eth0'))
assert cs.evaluate([c], self.ITEMS, switches)[1]['verdict'] == 'wrong_neighbor'
def test_aliased_cable_counts_as_documented(self):
switches = {'udm': switch([port(3, seen='large1')])['sw']}
c = cable(1, ('UDM-Pro', '3'), ('Big box', 'eth0'))
assert cs.undocumented([c], switches, self.ITEMS) == []
assert len(cs.undocumented([c], switches)) == 1
def test_runs_for_the_device_include_aliased_cables(self):
c = cable(1, ('UDM-Pro', '3'), ('Big box', 'eth0'), label='C1')
assert [r['label'] for r in cs.runs_for('udm', [c], self.ITEMS, {})] == ['C1']
class TestRackFaces:
def _item(self, name, u, h=1, mount='front', id_=1):
return {'id': id_, 'name': name, 'kind': 'device', 'rack_u': u, 'rack_height': h, 'mount': mount}
def test_front_and_rear_share_a_unit(self):
items = [self._item('switch', 10), self._item('pdu', 10, mount='rear', id_=2)]
front = cs.rack_layout(items, 25, 'front')
rear = cs.rack_layout(items, 25, 'rear')
assert [i['name'] for i in front['placed']] == ['switch'] and front['overlaps'] == []
assert [i['name'] for i in rear['placed']] == ['pdu'] and rear['overlaps'] == []
def test_full_depth_takes_both_sides(self):
items = [self._item('server', 5, 4, mount='full'), self._item('pdu', 6, mount='rear', id_=2)]
rear = cs.rack_layout(items, 25, 'rear')
assert [(i['name'], i['through']) for i in rear['placed']] == [('server', True)]
assert rear['overlaps'] == [{'name': 'pdu', 'units': 'U6', 'with': ['server']}]
front = cs.rack_layout(items, 25, 'front')
assert [(i['name'], i['through']) for i in front['placed']] == [('server', False)]
assert front['overlaps'] == rear['overlaps'] # reported on both faces
def test_mount_is_validated_and_shelf_items_take_the_shelf_side(self):
assert cs.validate_item({'name': 'p', 'mount': 'rear', 'rack_u': 3})['mount'] == 'rear'
assert cs.validate_item({'name': 'p'})['mount'] == 'front'
with pytest.raises(cs.ValidationError):
cs.validate_item({'name': 'p', 'mount': 'side'})
shelf = {'Shelf': {'name': 'Shelf', 'kind': 'shelf'}}
assert cs.validate_item({'name': 'm', 'shelf': 'Shelf', 'mount': 'rear'}, shelf)['mount'] == 'front'
def test_validate_rack(self):
rack = cs.validate_rack({'name': ' StarTech 25U ', 'units': '25'})
assert rack == {'name': 'StarTech 25U', 'units': 25, 'notes': ''}
for bad in ({'name': '', 'units': 25}, {'name': 'x'}, {'name': 'x', 'units': 61}, {'name': 'x', 'units': 0}):
with pytest.raises(cs.ValidationError):
cs.validate_rack(bad)
class TestChassis:
def test_known_drawing_kept_unknown_refused(self):
assert cs.validate_item({'name': 'nvr', 'chassis': 'unvr-pro'})['chassis'] == 'unvr-pro'
assert cs.validate_item({'name': 'x'})['chassis'] == ''
with pytest.raises(cs.ValidationError):
cs.validate_item({'name': 'x', 'chassis': 'tardis'})
def test_every_drawing_has_a_label(self):
assert all(cs.CHASSIS.values())
assert {'sliger-cx4712', 'rosewill-rsv-l4500u', 'unvr-pro', 'uacc-patch-24',
'usw-pro-max-24-poe', 'usw-pro-max-48-poe', 'zimaboard-832', 'zimaboard-832-cx3',
'resting', 'uacc-brush-1u', 'sliger-cx4712-red', 'sliger-cx4712-white',
'sliger-cx4712-blue'} <= set(cs.CHASSIS)
class TestFacingAndSlots:
def test_facing_and_slot_validation(self):
shelf = {'S': {'name': 'S', 'kind': 'shelf'}}
assert cs.validate_item({'name': 'p', 'facing': 'rear'})['facing'] == 'rear'
assert cs.validate_item({'name': 'p'})['facing'] == 'front'
assert cs.validate_item({'name': 'z', 'shelf': 'S', 'slot': '2'}, shelf)['slot'] == 2
with pytest.raises(cs.ValidationError):
cs.validate_item({'name': 'p', 'facing': 'up'})
with pytest.raises(cs.ValidationError):
cs.validate_item({'name': 'p', 'slot': 1}) # a position needs a shelf
def test_rear_posts_facing_front_are_seen_from_the_front(self):
items = [{'id': 1, 'name': 'pdu', 'kind': 'device', 'rack_u': 24, 'rack_height': 2, 'mount': 'rear'},
{'id': 2, 'name': 'udm', 'kind': 'device', 'rack_u': 24, 'rack_height': 1}]
assert [i['name'] for i in cs.rack_layout(items, 25, 'front')['placed']] == ['udm']
behind = cs.rack_layout(items, 25, 'behind')['placed']
assert [(i['name'], i['through']) for i in behind] == [('pdu', False)]
assert [(i['name'], i['through']) for i in cs.rack_layout(items, 25, 'rear')['placed']] == [('pdu', True)]