Add unit tests for osism/utils/rabbitmq.py#2352
Conversation
1a31079 to
3807e56
Compare
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The test module is quite long and dense; consider splitting it into smaller logical sections (e.g., separate files or pytest classes for
get_rabbitmq_node_addressesandload_rabbitmq_password) or introducing shared fixtures to reduce repetition and improve readability.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The test module is quite long and dense; consider splitting it into smaller logical sections (e.g., separate files or pytest classes for `get_rabbitmq_node_addresses` and `load_rabbitmq_password`) or introducing shared fixtures to reduce repetition and improve readability.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
ideaship
left a comment
There was a problem hiding this comment.
Per-host failures abort the whole discovery and discard partial results — is that intended?
Inside the per-host loop, json.loads(facts_data) (corrupt cache), the --host subprocess.check_output, json.loads(result) (non-JSON), and re.match(...) on a truthy non-string internal_interface are all uncaught locally. Any one of them propagates to the function-level handlers and returns None, throwing away every address already collected for earlier hosts. That's a different blast radius than the continue skip-branches right next to them, which only drop the offending host. The existing CalledProcessError/JSONDecodeError tests only hit the first (group-listing) call, so this asymmetry isn't exercised — which also means the commit message's "every error and skip branch" / 100% coverage framing is a bit optimistic here. Mostly I'd want to know: is abort-all-after-partial-success the behavior you want, or should these continue like the skips? Whatever the answer, a partial-success test (host1 resolves, host2 fails) would pin it.
Optional: two production arguments carry correctness weight but aren't asserted, because their collaborators are mocked — so a future regression flipping either would pass silently:
- the hostvars lookup deliberately uses
prefer_minified=False(the minified inventory omits hostvars, sointernal_interfacewouldn't resolve) --limit rabbitmqis the only thing scoping results to the group (get_hosts_from_inventorydoes no group filtering)
Each is a one-line call_args assertion if you think they're worth pinning. Not blocking.
In get_rabbitmq_node_addresses, a failure while resolving a single host (corrupt facts in the Redis cache, a failing ansible-inventory --host call, non-JSON hostvars or an internal_interface value that is not a string) raised out of the per-host loop and made the whole function return None, discarding the addresses already collected for other hosts. Expected data problems right next to these spots (missing facts, missing interface, missing IPv4 address) only skipped the affected host. Catch failures per host, log them and continue with the remaining hosts, matching the existing skip branches. The function still returns None if no host resolves at all. Assisted-by: Claude:claude-fable-5 Signed-off-by: Christian Berendt <berendt@osism.tech>
Cover both functions in osism/utils/rabbitmq.py: - get_rabbitmq_node_addresses: inventory and host discovery, per-host interface resolution (literal interface names, Jinja2 template traversal, interface-name normalization) and result aggregation, covering every error and skip branch. Partial-success tests pin that a failure on one host (corrupt cached facts, a failing or non-JSON hostvars lookup, a non-string internal_interface) only drops that host and keeps the addresses of the others. The ansible-inventory invocations are pinned as well: --limit rabbitmq is the only thing scoping the listing to the group, and the hostvars lookup must use the non-minified inventory (prefer_minified=False) because the minified variant omits hostvars. - load_rabbitmq_password: missing file, empty, invalid and non-dict secrets, missing key, whitespace stripping, int coercion and loader failure. - RABBITMQ_USER module constant. Tests are grouped into one class per function, with factory fixtures wiring up the collaborators of each function. The lazy redis attribute lives on the osism.utils package and is read via a function-local import, so it is seeded with patch.dict on the package namespace rather than patched on the rabbitmq module (which never gains a utils global). Tests follow the existing mocker + loguru_logs conventions and reach 100% line coverage of the module. Assisted-by: Claude:claude-opus-4-8 Assisted-by: Claude:claude-fable-5 Signed-off-by: Christian Berendt <berendt@osism.tech>
3807e56 to
83da5a2
Compare
Closes #2232
Adds
tests/unit/utils/test_rabbitmq.pycovering both functions inosism/utils/rabbitmq.py, plus one small robustness fix inget_rabbitmq_node_addressesthat the tests pin down.Production change
Skip RabbitMQ hosts that fail address resolution. Previously a
failure while resolving a single host (corrupt facts in the Redis
cache, a failing
ansible-inventory --hostcall, non-JSON hostvars oran
internal_interfacevalue that is not a string) raised out of theper-host loop and made the whole function return
None, discarding theaddresses already collected for other hosts — while the expected data
problems right next to these spots (missing facts, missing interface,
missing IPv4 address) only skipped the affected host. Per-host failures
are now caught, logged and skipped like the other skip branches; the
function still returns
Noneif no host resolves at all.What is covered
Tests are grouped into one class per function
(
TestGetRabbitmqNodeAddresses,TestLoadRabbitmqPassword), withfactory fixtures (
setup_addresses,setup_password) wiring up thecollaborators.
get_rabbitmq_node_addresses()order (happy path returning
[("10.0.0.5","host1"),("10.0.0.6","host2")]);empty rabbitmq group →
None; firstsubprocess.check_outputraisingCalledProcessError; invalid JSON from the group listing(
JSONDecodeError); outer generic exception. The inventoryinvocations are pinned:
--limit rabbitmqis the only thing scopingthe listing to the group, and the hostvars lookup must use the
non-minified inventory (
prefer_minified=False) because the minifiedvariant omits hostvars.
next; partial-success tests (host1 resolves, host2 fails with corrupt
cached facts / failing hostvars query / non-JSON hostvars / non-string
internal_interface) pin that only the failing host is dropped;missing
internal_interface; literal interface name used directly;Jinja2 template resolved via dotted-path traversal of the facts;
template resolving to a non-string (
None/ dict / int); traversalhitting a non-dict mid-walk; interface-name normalization
(
eth0.100→ansible_eth0_100,eth-0→ansible_eth_0); missingnormalized interface key; interface without
ipv4;ipv4without anaddress.None; at least one address → thelist is returned with no error logged.
load_rabbitmq_password()— missing file; empty / invalid / non-dictsecrets; missing
rabbitmq_passwordkey; whitespace stripping; intcoercion; loader raising.
Plus a check that the
RABBITMQ_USERmodule constant is"openstack".Notes for the reviewer
mocker.patch("osism.utils.rabbitmq.utils.redis", ...), but that targetdoes not resolve:
redisis a lazy__getattr__attribute on theosism.utilspackage, and the function reads it via a function-localfrom osism import utils, so therabbitmqmodule never gains autilsglobal (
AttributeError: module 'osism.utils.rabbitmq' has no attribute 'utils'). Instead the cached attribute is seeded on the packagenamespace with
mocker.patch.dict(osism.utils.__dict__, {"redis": ...}),which also bypasses the lazy initializer that would otherwise open a real
Redis connection. Reading the cache key the function builds is asserted in
the happy-path test.
load_yaml_fileandos.path.existsare patched at the call site, as theissue suggests.
mocker+loguru_logsconventions(see
tests/unit/utils/test_init_connections.py).Verification
black --checkandflake8clean on both files; unit tests run in CI.🤖 Generated with Claude Code