Add Tier 1 unit tests for pure-logic helpers in main.py - #248
Conversation
3e08ec2 to
162ea72
Compare
| # are used so tests do not depend on a real `settings.toml` being present. | ||
| import os | ||
|
|
||
| os.environ.setdefault("NETBOX_MANAGER_URL", "http://localhost:8000") |
There was a problem hiding this comment.
These tests are non-hermetic w.r.t. role settings. Because dynaconf reads the NETBOX_MANAGER_ prefix, a runner with NETBOX_MANAGER_NODE_ROLES/NETBOX_MANAGER_SWITCH_ROLES exported (a legitimate deployment env) makes get_node_roles()/get_switch_roles() return the deployment list, and the is _DEFAULT_* / default-role assertions fail — reproduced as 5 failed, 58 passed. Please isolate the settings here: add os.environ.pop("NETBOX_MANAGER_NODE_ROLES", None) / ..._SWITCH_ROLES alongside the existing setdefaults, or force the state per-test with monkeypatch.delattr(main.settings, "NODE_ROLES", raising=False) in the _absent cases (test_helpers.py:25,48) and the default-role loopback cases (test_loopback_gate.py:54,58,63).
| class TestFilterPortchannelTasksByDevice: | ||
| """``_filter_portchannel_tasks_by_device`` keeps a pair if either matches.""" | ||
|
|
||
| def test_keeps_task_when_either_device_matches(self): |
There was a problem hiding this comment.
This test feeds a two-device cable task, but _generate_portchannel_tasks() (netbox_manager/main.py:1870, emit sites 2044/2058/2073/2088) only ever produces single-device device_interface tasks (LAG creations + member assignments), and _filter_portchannel_tasks_by_device is called solely on that list (main.py:2535). The "keeps a pair if either matches" path is therefore unreachable for real inputs — the test pins a fictional shape. Two things to fix: (1) test the real single-device device_interface shapes through this filter; (2) note that _filter_portchannel_tasks_by_device (main.py:2452-2461) is byte-identical to _filter_tasks_by_device (main.py:2430-2439) — fold them into one function in a follow-up, after which this test class collapses into TestFilterTasksByDevice. Also please confirm the intended semantics: is pair-preservation actually required? If so, the current duplicate doesn't provide it and that gap is hidden by this test.
| device = make_device(role_slug="control") | ||
| assert main.should_have_loopback_interface(device) is True | ||
|
|
||
| def test_node_role_does_not_require_hwsku(self, make_device): |
There was a problem hiding this comment.
This is an exact duplicate of test_node_role_always_gets_loopback above — same role_slug="control", same empty custom_fields, same is True assertion — so it adds no coverage. To make it earn its name (node-role eligibility is hwsku-independent), give it a non-empty custom_fields (e.g. {"sonic_parameters": {"hwsku": "AS7326"}}) and assert it's still True. Otherwise drop it.
Tier 1 of the #232 unit-test rollout (#247) needs tiny attribute-bag stand-ins for pynetbox devices and interfaces. Add `make_device` and `make_interface` fixtures to the shared conftest so the pure-logic helper tests -- and later tiers -- build these shapes consistently instead of re-deriving them per module. The factories expose only the attributes the helpers read (role slug/name, device_type model, custom_fields, interface type value/label), keeping them free of any live NetBox or full pynetbox mock. AI-assisted: Claude Code Signed-off-by: Christian Berendt <berendt@osism.tech>
Cover Tier 1 groups 1 and 2 of #232 in test_helpers.py: - get_node_roles / get_switch_roles: settings override vs. the in-module default fallback (absent, None, empty-list). - deep_merge: recursive merge, dict2 precedence, list replacement, type conflicts, deepcopy isolation of inputs and output. - get_leading_number, get_resource_name, get_device_role_slug, find_device_names_in_structure, is_virtual_interface, _split_tasks_by_type. The role/interface cases use the shared make_device / make_interface factories; the settings cases stub main.settings via monkeypatch (raising=False, since the keys are absent on the Dynaconf object). Implements part of #247. AI-assisted: Claude Code Signed-off-by: Christian Berendt <berendt@osism.tech>
Cover Tier 1 group 3 of #232 in test_task_filters.py: - should_skip_task_by_filter: `-`/`_` normalisation of both key and filter; skip on mismatch, keep on match. - extract_device_names_from_task: direct device field, device-creation task name, nested names, and the combined case where a direct device field is collected twice (no deduplication). - should_skip_task_by_device_filter: skip on empty names or no match; keep on any substring match. Implements part of #247. AI-assisted: Claude Code Signed-off-by: Christian Berendt <berendt@osism.tech>
Cover Tier 1 group 4 of #232 in test_autoconf_helpers.py: - get_autoconf_prefix: hundred-range x99 mapping (0, 200, 250, 1000). - _extract_device_names_from_autoconf_task: nested references, device-definition tasks ({"device": {"name": ...}}), top-level string references, and the no-device case. - _filter_tasks_by_device / _filter_tasks_by_type_by_device / _filter_portchannel_tasks_by_device: intersection with the device filter set, empty-filter and no-match results, per-bucket filtering, and keeping a pair when either device matches. Implements part of #247. AI-assisted: Claude Code Signed-off-by: Christian Berendt <berendt@osism.tech>
Cover Tier 1 group 5 of #232 in test_loopback_gate.py: - has_sonic_hwsku_parameter: false when custom_fields is missing or falsy, when sonic_parameters is absent or not a dict, or when hwsku is missing/empty; true only for a dict with a truthy hwsku. - should_have_loopback_interface: node roles always qualify; switch roles and switch device types qualify only with sonic_parameters.hwsku; everything else does not. Includes settings-driven cases where NODE_ROLES / SWITCH_ROLES overrides change the outcome. Implements part of #247. AI-assisted: Claude Code Signed-off-by: Christian Berendt <berendt@osism.tech>
A deployment environment may export NETBOX_MANAGER_NODE_ROLES / NETBOX_MANAGER_SWITCH_ROLES, which dynaconf loads as settings.NODE_ROLES / SWITCH_ROLES. That made get_node_roles()/get_switch_roles() return the deployment list and broke the default-role and `is _DEFAULT_*` assertions (5 failed, the rest passed). Pop both vars before netbox_manager.main is imported so the in-module defaults are the hermetic baseline. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Christian Berendt <berendt@osism.tech>
_filter_portchannel_tasks_by_device was byte-identical to _filter_tasks_by_device, and _generate_portchannel_tasks only emits single-device device_interface tasks (LAG creations + member assignments) -- there is no two-device "pair" to preserve, so the "keeps a pair if either matches" path was unreachable. Drop the duplicate, route the call site through _filter_tasks_by_device, and replace the fictional two-device cable test with the real LAG shapes. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Christian Berendt <berendt@osism.tech>
162ea72 to
2dc9bd4
Compare
test_node_role_does_not_require_hwsku duplicated test_node_role_always_gets_loopback exactly (same control role, empty custom_fields). Give it a SONiC hwsku so the pair brackets both hwsku states (absent and present) and actually pins node-role eligibility as hwsku-independent. Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Christian Berendt <berendt@osism.tech>
2dc9bd4 to
82d8c6d
Compare
Summary
Closes #247 — Tier 1 of the #232 unit-test rollout: unit tests for the
pure-logic helpers in
netbox_manager/main.py. These functions aredeterministic transformations of plain Python data or tiny attribute-bag
objects, so the tests need no live NetBox,
ansible_runner, filesystem, orfull
pynetboxmocks — at most atypes.SimpleNamespace.Builds on the #233/#234 foundation (pytest harness,
tests/tree,test-requirements.txt, sharedconftest.py, Zuulnetbox-manager-unit-testsjob).
Change set (commit order)
make_deviceand
make_interfacefixtures producingSimpleNamespaceattribute bags,documented for reuse by later tiers.
test_helpers.py, groups 1+2) —get_node_roles,get_switch_roles,deep_merge,get_leading_number,get_resource_name,get_device_role_slug,find_device_names_in_structure,is_virtual_interface,_split_tasks_by_type.test_task_filters.py, group 3) —should_skip_task_by_filter,extract_device_names_from_task,should_skip_task_by_device_filter.test_autoconf_helpers.py, group 4) —get_autoconf_prefix,_extract_device_names_from_autoconf_task,_filter_tasks_by_device,_filter_tasks_by_type_by_device,_filter_portchannel_tasks_by_device.test_loopback_gate.py, group 5) —has_sonic_hwsku_parameter,should_have_loopback_interface(includingsettings-driven role-list overrides).
92 new tests;
pytest tests/unitnow reports 117 passed.Local verification
pytest tests/unit— 117 passedflake8 tests/ netbox_manager/— cleanblack --check tests/ netbox_manager/— cleanmypy netbox_manager/— unchanged from baseline (pre-existingimport-untypednotes for untyped third-party libraries; the new test filesadd no errors of their own)
yamllint— unchanged (no YAML touched)Notes for reviewers
monkeypatch.setattr(main.settings, "NODE_ROLES", ..., raising=False): thekeys are not real attributes on the Dynaconf object, so
raising=Falseisrequired, and monkeypatch's teardown restores the absent → default-fallback
state.
make_device/make_interfacefactories are deliberately minimal —they expose only the attributes these helpers read. Later Meta: Unit test coverage for netbox_manager/ #232 tiers should
extend them in place rather than re-deriving device/interface shapes.
Part of #232.