Chore: Got code coverage up to 98%
This commit is contained in:
@@ -7,6 +7,7 @@ from types import SimpleNamespace
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.core import retry_patterns as retry_patterns_module
|
||||
from cleveragents.core.exceptions import (
|
||||
NetworkError,
|
||||
RateLimitError,
|
||||
@@ -24,12 +25,13 @@ from cleveragents.core.retry_patterns import (
|
||||
retry_auto_debug,
|
||||
retry_file_operation,
|
||||
retry_network_operation,
|
||||
retry_on_result,
|
||||
retry_provider_operation,
|
||||
retry_with_exponential_backoff,
|
||||
retry_with_jitter,
|
||||
retry_with_timeout,
|
||||
should_retry_result,
|
||||
)
|
||||
from cleveragents.core import retry_patterns as retry_patterns_module
|
||||
|
||||
|
||||
@given("I have the retry patterns module imported")
|
||||
@@ -372,6 +374,28 @@ def step_verify_circuit_closed(context):
|
||||
assert context.circuit_breaker.state == "closed"
|
||||
|
||||
|
||||
@given("the circuit breaker has recorded failures")
|
||||
def step_circuit_breaker_recorded_failures(context):
|
||||
"""Simulate prior failures to exercise closed-state success path."""
|
||||
context.circuit_breaker.failure_count = 3
|
||||
context.circuit_breaker.state = "closed"
|
||||
context.circuit_breaker.last_failure_time = time.time()
|
||||
|
||||
|
||||
@when("I execute a successful call while the circuit is closed")
|
||||
def step_success_call_closed_state(context):
|
||||
"""Execute a successful call without half-open transition."""
|
||||
context.closed_state_result = context.circuit_breaker.call(lambda: "closed success")
|
||||
|
||||
|
||||
@then("the circuit breaker should reset failure tracking after success")
|
||||
def step_verify_closed_state_success(context):
|
||||
"""Ensure closed-state success resets counters."""
|
||||
assert context.closed_state_result == "closed success"
|
||||
assert context.circuit_breaker.failure_count == 0
|
||||
assert context.circuit_breaker.state == "closed"
|
||||
|
||||
|
||||
# Retry context tests
|
||||
@given('I have a retry context for "{operation}"')
|
||||
def step_create_retry_context(context, operation):
|
||||
@@ -475,6 +499,96 @@ def step_verify_async_retry_context_manager_capture(context):
|
||||
assert isinstance(context.retry_context.errors[-1], Exception)
|
||||
|
||||
|
||||
@when("I execute a successful block under the retry context manager")
|
||||
def step_retry_context_manager_success(context):
|
||||
"""Execute a successful block to cover no-error path."""
|
||||
errors_before = len(context.retry_context.errors)
|
||||
with context.retry_context:
|
||||
context.retry_context_success_value = "sync context success"
|
||||
context.retry_context_errors_delta_sync = (
|
||||
len(context.retry_context.errors) - errors_before
|
||||
)
|
||||
|
||||
|
||||
@then("the retry context manager should not record errors")
|
||||
def step_verify_retry_context_manager_success(context):
|
||||
"""Confirm no errors were recorded for successful context block."""
|
||||
assert context.retry_context_success_value == "sync context success"
|
||||
assert context.retry_context_errors_delta_sync == 0
|
||||
|
||||
|
||||
@when("I execute a successful block under the async retry context manager")
|
||||
def step_async_retry_context_manager_success(context):
|
||||
"""Execute a successful async block to cover no-error path."""
|
||||
errors_before = len(context.retry_context.errors)
|
||||
|
||||
async def run_async_block():
|
||||
async with context.retry_context:
|
||||
return "async context success"
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
context.async_retry_context_manager_result = loop.run_until_complete(
|
||||
run_async_block()
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
context.retry_context_errors_delta_async = (
|
||||
len(context.retry_context.errors) - errors_before
|
||||
)
|
||||
|
||||
|
||||
@then("the async retry context manager should not record errors")
|
||||
def step_verify_async_retry_context_manager_success(context):
|
||||
"""Confirm async context manager leaves errors untouched when successful."""
|
||||
assert context.async_retry_context_manager_result == "async context success"
|
||||
assert context.retry_context_errors_delta_async == 0
|
||||
|
||||
|
||||
@when("I execute the async function with the retry context")
|
||||
def step_execute_async_with_retry_context(context):
|
||||
"""Execute async function using RetryContext.async_execute."""
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
context.async_execute_result = loop.run_until_complete(
|
||||
context.retry_context.async_execute(context.test_function)
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
context.async_execute_attempts = context.retry_context.attempt_count
|
||||
|
||||
|
||||
@then("the async retry context execute should succeed after {attempts:d} attempts")
|
||||
def step_verify_async_execute_attempts(context, attempts):
|
||||
"""Verify async execute succeeded after expected attempts."""
|
||||
assert context.async_execute_result == "async success"
|
||||
assert context.async_execute_attempts == attempts
|
||||
|
||||
|
||||
@when("I apply retry with timeout of {max_attempts:d} attempts and {timeout} seconds")
|
||||
def step_apply_retry_with_timeout(context, max_attempts, timeout):
|
||||
"""Apply retry_with_timeout while stubbing out sleep delays."""
|
||||
timeout_seconds = float(timeout)
|
||||
original_sleep = time.sleep
|
||||
time.sleep = lambda _seconds: None
|
||||
|
||||
def restore_sleep():
|
||||
time.sleep = original_sleep
|
||||
|
||||
context.add_cleanup(restore_sleep)
|
||||
decorated_func = retry_with_timeout(
|
||||
max_attempts=max_attempts, timeout_seconds=timeout_seconds
|
||||
)(context.test_function)
|
||||
|
||||
try:
|
||||
context.result = decorated_func()
|
||||
context.retry_succeeded = True
|
||||
except Exception as exc: # pragma: no cover - defensive safety
|
||||
context.retry_error = exc
|
||||
context.retry_succeeded = False
|
||||
|
||||
|
||||
# Retry with jitter tests
|
||||
@given("I have multiple concurrent operations")
|
||||
def step_setup_concurrent_operations(context):
|
||||
@@ -528,6 +642,59 @@ def step_verify_no_simultaneous_retry(context):
|
||||
assert max_in_bucket <= 3, "Too many operations retrying simultaneously"
|
||||
|
||||
|
||||
# Retry on result tests
|
||||
@given("I have sequential result payloads requiring a retry")
|
||||
def step_setup_sequential_retry_results(context):
|
||||
"""Create a deterministic sequence of results for retry_on_result tests."""
|
||||
context.call_count = 0
|
||||
context.result_sequence = [
|
||||
{"retry": True, "value": "try-again"},
|
||||
{"retry": False, "value": "done"},
|
||||
]
|
||||
|
||||
def result_function():
|
||||
context.call_count += 1
|
||||
index = min(context.call_count - 1, len(context.result_sequence) - 1)
|
||||
return context.result_sequence[index]
|
||||
|
||||
context.test_function = result_function
|
||||
|
||||
|
||||
@given("I have a retry predicate that checks for retry flag")
|
||||
def step_setup_retry_predicate(context):
|
||||
"""Define predicate used by retry_on_result decorator."""
|
||||
|
||||
def predicate(result):
|
||||
return isinstance(result, dict) and bool(result.get("retry"))
|
||||
|
||||
context.retry_predicate = predicate
|
||||
|
||||
|
||||
@when("I apply retry on result with max {max_attempts:d} attempts")
|
||||
def step_apply_retry_on_result(context, max_attempts):
|
||||
"""Apply retry_on_result decorator and capture the outcome."""
|
||||
decorated_func = retry_on_result(
|
||||
context.retry_predicate, max_attempts=max_attempts
|
||||
)(context.test_function)
|
||||
|
||||
try:
|
||||
context.retry_on_result_output = decorated_func()
|
||||
context.retry_on_result_succeeded = True
|
||||
except Exception as exc: # pragma: no cover - defensive safety
|
||||
context.retry_on_result_output = exc
|
||||
context.retry_on_result_succeeded = False
|
||||
|
||||
|
||||
@then("the decorated function should return the successful payload")
|
||||
def step_verify_retry_on_result_payload(context):
|
||||
"""Ensure retry_on_result eventually returns the non-retry payload."""
|
||||
assert context.retry_on_result_succeeded, (
|
||||
f"Retry on result failed: {context.retry_on_result_output}"
|
||||
)
|
||||
assert isinstance(context.retry_on_result_output, dict)
|
||||
assert context.retry_on_result_output.get("retry") is False
|
||||
|
||||
|
||||
# Auto-debug retry tests
|
||||
@given("I have a function that fails with errors")
|
||||
def step_create_failing_function_with_errors(context):
|
||||
|
||||
Reference in New Issue
Block a user