"""Step definitions for retry_service_patterns_coverage.feature. Targets uncovered lines in retry_service_patterns.py: - Line 82: _is_async_callable partial(async coroutine) → True (unreachable on Python 3.13 where iscoroutinefunction handles partials natively; covered indirectly via partial(callable_obj)) - Line 85: _is_async_callable partial(callable_obj_with_async_call) → True - Line 86: _is_async_callable partial(SomeClass) → False - Line 90: _is_async_callable(non_callable) → False - Line 254: async retry_service_operation CircuitBreakerOpen with no breaker - Lines 444-446: RetryContext.execute rejects async callables - Lines 452-453: RetryContext.execute nesting guard - Lines 507-508: RetryContext.async_execute nesting guard - Line 482: RetryContext.execute RuntimeError safeguard - Line 537: RetryContext.async_execute RuntimeError safeguard - Lines 575-577: retry_auto_debug rejects sync callables - Line 639: retry_auto_debug returns None with zero attempts """ import asyncio from functools import partial from unittest.mock import patch from behave import given, then, when from cleveragents.core.retry_patterns import CircuitBreakerOpen from cleveragents.core.retry_service_patterns import ( RetryContext, _is_async_callable, _retry_depth, retry_auto_debug, retry_service_operation, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _run_async(coro): """Run a coroutine in a fresh event loop.""" loop = asyncio.new_event_loop() try: return loop.run_until_complete(coro) finally: loop.close() # --------------------------------------------------------------------------- # Scenario: _is_async_callable detects partial wrapping async callable object # (line 85 — partial wrapping a non-type callable with async __call__) # --------------------------------------------------------------------------- @given("I have a functools.partial wrapping a callable object with async __call__") def step_partial_wrapping_async_callable_obj(context): class AsyncCallableObj: async def __call__(self, x): return x context.partial_async_callable = partial(AsyncCallableObj(), 42) @when("I check if the partial wrapping async callable is async") def step_check_partial_async_callable(context): context.is_async_callable_result = _is_async_callable( context.partial_async_callable ) @then("the async callable partial result should be True") def step_assert_partial_async_callable_true(context): assert context.is_async_callable_result is True, ( f"Expected True, got {context.is_async_callable_result}" ) # --------------------------------------------------------------------------- # Scenario: _is_async_callable returns False for partial wrapping a class type # (line 86) # --------------------------------------------------------------------------- @given("I have a functools.partial wrapping a class type") def step_partial_wrapping_class(context): class MyClass: def __init__(self, x=10): self.x = x context.partial_type = partial(MyClass, x=5) @when("I check if the partial-wrapped type is async callable") def step_check_partial_type_async(context): context.is_async_result_type = _is_async_callable(context.partial_type) @then("rspcov the result should be False for partial type detection") def step_assert_partial_type_false(context): assert context.is_async_result_type is False, ( f"Expected False, got {context.is_async_result_type}" ) # --------------------------------------------------------------------------- # Scenario: _is_async_callable returns False for non-callable (line 90) # --------------------------------------------------------------------------- @given("I have a plain integer value") def step_plain_integer(context): context.non_callable = 42 @when("I check if the integer is async callable") def step_check_integer_async(context): context.is_async_integer_result = _is_async_callable(context.non_callable) @then("rspcov the result should be False for the non-callable value") def step_assert_integer_false(context): assert context.is_async_integer_result is False, ( f"Expected False, got {context.is_async_integer_result}" ) # --------------------------------------------------------------------------- # Scenario: Async retry_service_operation with CircuitBreakerOpen, no breaker # (line 254) # --------------------------------------------------------------------------- @given( "I have an async function decorated with retry_service_operation" " and no circuit breaker" ) def step_async_decorated_no_breaker(context): @retry_service_operation( service_name="test-svc", operation_name="test-op", max_attempts=2, base_delay=0.01, max_delay=0.02, circuit_breaker=None, total_timeout=5.0, ) async def _failing_op(): raise CircuitBreakerOpen("circuit is open") context.async_cb_func = _failing_op @given("the async function raises CircuitBreakerOpen") def step_noop_marker(_context): pass # Configured in the previous step @when("I call the decorated async service operation") def step_call_async_service_op(context): context.cb_error = None try: _run_async(context.async_cb_func()) except CircuitBreakerOpen as exc: context.cb_error = exc @then("CircuitBreakerOpen should propagate with failure count zero") def step_assert_cb_open_propagated(context): assert context.cb_error is not None, "Expected CircuitBreakerOpen to be raised" assert isinstance(context.cb_error, CircuitBreakerOpen) # --------------------------------------------------------------------------- # Scenario: RetryContext.execute rejects async callables (lines 444-446) # --------------------------------------------------------------------------- @given('I have a RetryContext for operation "{name}"') def step_create_retry_context(context, name): context.retry_ctx = RetryContext(operation_name=name, max_attempts=2) @given("I have an async callable to pass to sync execute") def step_create_async_for_sync(context): async def _async_fn(): return "async_result" context.async_for_sync = _async_fn @when("I call execute with the async callable") def step_call_execute_with_async(context): context.type_error = None try: context.retry_ctx.execute(context.async_for_sync) except TypeError as exc: context.type_error = exc @then("a TypeError should be raised indicating async_execute is needed") def step_assert_type_error_async(context): assert context.type_error is not None, "Expected TypeError to be raised" assert "async_execute" in str(context.type_error) # --------------------------------------------------------------------------- # Scenario: RetryContext.execute nesting guard (lines 452-453) # --------------------------------------------------------------------------- @given("the retry depth is already at maximum") def step_set_max_depth(context): # Set depth to 1 (the max nesting depth) so the guard kicks in context.depth_token = _retry_depth.set(1) def restore(): _retry_depth.reset(context.depth_token) context.add_cleanup(restore) @when("I call execute at max nesting depth with a sync function") def step_call_execute_at_max_depth(context): context.nesting_call_count = 0 def _sync_fn(): context.nesting_call_count += 1 return "nested_result" context.nesting_result = context.retry_ctx.execute(_sync_fn) @then("the function should execute once without retry wrapping") def step_assert_executed_once(context): assert context.nesting_result == "nested_result" assert context.nesting_call_count == 1 @then("the attempt count should be 1") def step_assert_attempt_count_1(context): assert context.retry_ctx.attempt_count == 1 # --------------------------------------------------------------------------- # Scenario: RetryContext.async_execute nesting guard (lines 507-508) # --------------------------------------------------------------------------- @given("the retry depth is already at maximum for async") def step_set_max_depth_async(context): context.depth_token_async = _retry_depth.set(1) def restore(): _retry_depth.reset(context.depth_token_async) context.add_cleanup(restore) @when("I call async_execute at max nesting depth") def step_call_async_execute_at_max_depth(context): context.async_nesting_call_count = 0 async def _async_fn(): context.async_nesting_call_count += 1 return "async_nested_result" context.async_nesting_result = _run_async( context.retry_ctx.async_execute(_async_fn) ) @then("the async function should execute once without retry wrapping") def step_assert_async_executed_once(context): assert context.async_nesting_result == "async_nested_result" assert context.async_nesting_call_count == 1 @then("the async attempt count should be 1") def step_assert_async_attempt_count_1(context): assert context.retry_ctx.attempt_count == 1 # --------------------------------------------------------------------------- # Scenario: RetryContext.execute RuntimeError safeguard (line 482) # --------------------------------------------------------------------------- @given("Retrying is patched to yield zero iterations") def step_patch_retrying_empty(context): # Patch is applied in the When step; this step documents intent. pass @when("I call execute expecting a RuntimeError from empty retrying") def step_call_execute_empty_retrying(context): """Patch Retrying to return an empty iterator, then call execute.""" class _EmptyRetrying: def __init__(self, **kwargs): pass def __iter__(self): return iter([]) context.execute_runtime_error = None with patch("cleveragents.core.retry_service_patterns.Retrying", _EmptyRetrying): try: context.retry_ctx.execute(lambda: "should_not_run") except RuntimeError as exc: context.execute_runtime_error = exc @then("a RuntimeError about Retrying executing at least once should be raised") def step_assert_retrying_runtime_error(context): assert context.execute_runtime_error is not None, ( "Expected RuntimeError to be raised" ) assert "at least once" in str(context.execute_runtime_error) # --------------------------------------------------------------------------- # Scenario: RetryContext.async_execute RuntimeError safeguard (line 537) # --------------------------------------------------------------------------- @given("AsyncRetrying is patched to yield zero async iterations") def step_patch_async_retrying_empty(context): pass # Patch applied in the When step @when("I call async_execute expecting a RuntimeError from empty async retrying") def step_call_async_execute_empty_retrying(context): """Patch AsyncRetrying to return an empty async iterator.""" class _EmptyAsyncRetrying: def __init__(self, **kwargs): pass def __aiter__(self): return self async def __anext__(self): raise StopAsyncIteration async def _async_fn(): return "should_not_run" context.async_execute_runtime_error = None with patch( "cleveragents.core.retry_service_patterns.AsyncRetrying", _EmptyAsyncRetrying, ): try: _run_async(context.retry_ctx.async_execute(_async_fn)) except RuntimeError as exc: context.async_execute_runtime_error = exc @then("a RuntimeError about AsyncRetrying executing at least once should be raised") def step_assert_async_retrying_runtime_error(context): assert context.async_execute_runtime_error is not None, ( "Expected RuntimeError to be raised" ) assert "at least once" in str(context.async_execute_runtime_error) # --------------------------------------------------------------------------- # Scenario: retry_auto_debug rejects sync callables (lines 575-577) # --------------------------------------------------------------------------- @given("I have a synchronous function decorated with retry_auto_debug") def step_sync_func_with_auto_debug(context): def _sync_fn(): return "sync_result" context.auto_debug_sync_wrapper = retry_auto_debug(max_debug_attempts=3)(_sync_fn) @when("I invoke the retry_auto_debug wrapper for the sync function") def step_invoke_auto_debug_sync(context): context.auto_debug_type_error = None try: _run_async(context.auto_debug_sync_wrapper()) except TypeError as exc: context.auto_debug_type_error = exc @then("a TypeError about async callables should be raised") def step_assert_auto_debug_type_error(context): assert context.auto_debug_type_error is not None, "Expected TypeError to be raised" assert "async" in str(context.auto_debug_type_error).lower() # --------------------------------------------------------------------------- # Scenario: retry_auto_debug returns None with zero attempts (line 639) # --------------------------------------------------------------------------- @given("I have an async function decorated with retry_auto_debug with zero attempts") def step_auto_debug_zero_attempts(context): async def _async_fn(): return "should_not_be_called" context.auto_debug_zero_wrapper = retry_auto_debug(max_debug_attempts=0)(_async_fn) @when("I invoke the retry_auto_debug wrapper with zero attempts") def step_invoke_auto_debug_zero(context): context.auto_debug_zero_result = _run_async(context.auto_debug_zero_wrapper()) @then("rspcov the result should be None from exhausted attempts") def step_assert_auto_debug_none(context): assert context.auto_debug_zero_result is None, ( f"Expected None, got {context.auto_debug_zero_result!r}" )