Skip to content

Add Tier 1 unit tests for pure-logic helpers in main.py - #248

Merged
ideaship merged 8 commits into
mainfrom
implement/issue-247-tier1-unit-tests
Jun 16, 2026
Merged

Add Tier 1 unit tests for pure-logic helpers in main.py#248
ideaship merged 8 commits into
mainfrom
implement/issue-247-tier1-unit-tests

Conversation

@berendt

@berendt berendt commented Jun 9, 2026

Copy link
Copy Markdown
Member

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 are
deterministic transformations of plain Python data or tiny attribute-bag
objects, so the tests need no live NetBox, ansible_runner, filesystem, or
full pynetbox mocks — at most a types.SimpleNamespace.

Builds on the #233/#234 foundation (pytest harness, tests/ tree,
test-requirements.txt, shared conftest.py, Zuul netbox-manager-unit-tests
job).

Change set (commit order)

  1. Add shared device/interface factories to test conftestmake_device
    and make_interface fixtures producing SimpleNamespace attribute bags,
    documented for reuse by later tiers.
  2. settings/role and generic helpers (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.
  3. task-filter helpers (test_task_filters.py, group 3) —
    should_skip_task_by_filter, extract_device_names_from_task,
    should_skip_task_by_device_filter.
  4. autoconf helpers (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.
  5. loopback-gate helpers (test_loopback_gate.py, group 5) —
    has_sonic_hwsku_parameter, should_have_loopback_interface (including
    settings-driven role-list overrides).

92 new tests; pytest tests/unit now reports 117 passed.

Local verification

  • pytest tests/unit — 117 passed
  • flake8 tests/ netbox_manager/ — clean
  • black --check tests/ netbox_manager/ — clean
  • mypy netbox_manager/ — unchanged from baseline (pre-existing
    import-untyped notes for untyped third-party libraries; the new test files
    add no errors of their own)
  • yamllint — unchanged (no YAML touched)

Notes for reviewers

  • The settings-stub cases use
    monkeypatch.setattr(main.settings, "NODE_ROLES", ..., raising=False): the
    keys are not real attributes on the Dynaconf object, so raising=False is
    required, and monkeypatch's teardown restores the absent → default-fallback
    state.
  • The make_device / make_interface factories 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.

@berendt
berendt force-pushed the implement/issue-247-tier1-unit-tests branch from 3e08ec2 to 162ea72 Compare June 9, 2026 17:01
@berendt
berendt marked this pull request as ready for review June 9, 2026 17:38
@berendt
berendt requested a review from ideaship June 9, 2026 17:39
@ideaship ideaship moved this from Ready to In review in Human Board Jun 10, 2026

@ideaship ideaship left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No major issues.

Comment thread tests/conftest.py
# 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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

berendt added 7 commits June 10, 2026 13:24
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>
@berendt
berendt force-pushed the implement/issue-247-tier1-unit-tests branch from 162ea72 to 2dc9bd4 Compare June 10, 2026 11:25
@berendt
berendt requested a review from ideaship June 10, 2026 11:25
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>
@berendt
berendt force-pushed the implement/issue-247-tier1-unit-tests branch from 2dc9bd4 to 82d8c6d Compare June 10, 2026 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Unit tests: Tier 1 — pure-logic helpers in netbox_manager/main.py

3 participants