From f4036fb547ced12e704545d37068ef940ac2aee2 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Tue, 5 May 2026 03:50:58 +0000 Subject: [PATCH] test(resource): add coverage for remove_resource exception rollback path Add Behave scenario to cover the except Exception rollback path in ResourceInstanceMixin.remove_resource() in _resource_registry_ops.py. This uncovered branch was causing the CI coverage gate to fail below the 97% threshold. ISSUES CLOSED: #6329 --- ...ce_registry_service_coverage_boost.feature | 8 +++ ...e_registry_service_coverage_boost_steps.py | 66 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/features/resource_registry_service_coverage_boost.feature b/features/resource_registry_service_coverage_boost.feature index efffcff8d..4f74402c8 100644 --- a/features/resource_registry_service_coverage_boost.feature +++ b/features/resource_registry_service_coverage_boost.feature @@ -29,6 +29,14 @@ Feature: Resource registry service coverage boost Then the unlink_child session should have been rolled back And the generic exception from unlink_child should propagate + # ── remove_resource generic exception rollback ─────────────────── + + Scenario: remove_resource rolls back on unexpected generic exception + Given a resource registry service prepared for remove_resource with a faulty session + When I attempt remove_resource and a generic exception occurs + Then the remove_resource session should have been rolled back + And the generic exception from remove_resource should propagate + # ── _build_tree_node type_filter filtering ─────────────────────── Scenario: get_resource_tree with type_filter excludes non-matching children diff --git a/features/steps/resource_registry_service_coverage_boost_steps.py b/features/steps/resource_registry_service_coverage_boost_steps.py index 8883bdff0..1d87f629e 100644 --- a/features/steps/resource_registry_service_coverage_boost_steps.py +++ b/features/steps/resource_registry_service_coverage_boost_steps.py @@ -5,6 +5,9 @@ Covers the remaining uncovered lines in resource_registry_service.py: - Lines 646-648: link_child generic exception rollback - Lines 686-688: unlink_child generic exception rollback - Line 785: _build_tree_node type_filter skips non-matching children + +Also covers _resource_registry_ops.py: +- remove_resource generic exception rollback path """ from __future__ import annotations @@ -51,6 +54,9 @@ _TREE_PARENT = "01JK00000000000000000000KK" _TREE_CHILD1 = "01JM00000000000000000000MM" _TREE_CHILD2 = "01JN00000000000000000000NN" +# Scenario 5: remove_resource generic error +_REMOVE_RES_5 = "01JP00000000000000000000PP" + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -400,6 +406,66 @@ def step_verify_unlink_child_error_propagates(context: Any) -> None: assert "disk I/O error in unlink_child" in str(context.boost_unlink_generic_error) +# --------------------------------------------------------------------------- +# Scenario: remove_resource rolls back on unexpected generic exception +# (targets _resource_registry_ops.py except Exception path) +# --------------------------------------------------------------------------- + + +@given("a resource registry service prepared for remove_resource with a faulty session") +def step_setup_faulty_remove_resource(context: Any) -> None: + """Set up a service whose session blows up during remove_resource DB ops.""" + resource = Resource( + resource_id=_REMOVE_RES_5, + name="test/remove-me", + resource_type_name="git-checkout", + classification=PhysVirt.PHYSICAL, + description=None, + properties={}, + location="/tmp/rm", + content_hash=None, + sandbox_strategy=None, + ) + + # Mock session: count() returns 0 (no links/edges), but delete() on the + # resource row blows up with a generic exception. + mock_session = MagicMock() + mock_session.query.return_value.filter.return_value.count.return_value = 0 + mock_session.query.return_value.filter_by.return_value.first.return_value = ( + MagicMock() # non-None row so delete() is called + ) + mock_session.delete.side_effect = OSError("disk I/O error in remove_resource") + + context.boost_remove_mock_session = mock_session + + svc = ResourceRegistryService.__new__(ResourceRegistryService) + svc._session_factory = lambda: mock_session + svc.show_resource = lambda name_or_id: resource # type: ignore[assignment] + context.boost_remove_svc = svc + + +@when("I attempt remove_resource and a generic exception occurs") +def step_attempt_remove_resource_generic_error(context: Any) -> None: + context.boost_remove_generic_error = None + try: + context.boost_remove_svc.remove_resource("test/remove-me") + except OSError as exc: + context.boost_remove_generic_error = exc + + +@then("the remove_resource session should have been rolled back") +def step_verify_remove_resource_rollback(context: Any) -> None: + context.boost_remove_mock_session.rollback.assert_called() + + +@then("the generic exception from remove_resource should propagate") +def step_verify_remove_resource_error_propagates(context: Any) -> None: + assert context.boost_remove_generic_error is not None + assert "disk I/O error in remove_resource" in str( + context.boost_remove_generic_error + ) + + # --------------------------------------------------------------------------- # Scenario: get_resource_tree with type_filter excludes non-matching children # (targets line 785)