Files

67 lines
3.1 KiB
Python
Raw Permalink Normal View History

"""R1 · R2 · R3 · R6: the engine with fake attempts and a fake clock (no waiting)."""
from tretry.retry import Outcome, Policy, run_with_retry
def failing(times, retryable=True):
calls = []
def attempt(timeout):
calls.append(timeout)
if len(calls) <= times:
return Outcome(False, retryable, f"boom {len(calls)}")
return Outcome(True, output="done")
return attempt, calls
def test_success_after_two_failures_waits_with_backoff(clock):
attempt, calls = failing(2)
result = run_with_retry(attempt, Policy(tries=4, backoff=0.5), sleep=clock.sleep, clock=clock)
assert result.ok and result.output == "done" and len(calls) == 3
assert clock.slept == [0.5, 1.0] # R1: 0.5 → 1.0 (doubles)
def test_wait_has_a_cap_and_jitter_only_shortens():
policy = Policy(tries=10, backoff=1, max_wait=4, jitter=0.5)
assert [policy.wait(n, rand=lambda: 0.0) for n in range(1, 6)] == [1, 2, 4, 4, 4] # R1: cap
assert policy.wait(3, rand=lambda: 1.0) == 2.0 # R2: up to 50% shorter
def test_all_fail_then_fallback(clock):
attempt, _ = failing(9)
result = run_with_retry(attempt, Policy(tries=3), fallback=lambda t: Outcome(True, output="cached"),
sleep=clock.sleep, clock=clock)
assert result.ok and result.used_fallback and result.output == "cached" # R3
assert [a.fallback for a in result.attempts] == [False, False, False, True]
def test_not_retryable_stops_at_once_and_skips_fallback(clock):
attempt, calls = failing(9, retryable=False)
result = run_with_retry(attempt, Policy(tries=5), fallback=lambda t: Outcome(True, output="cached"),
sleep=clock.sleep, clock=clock)
assert not result.ok and len(calls) == 1 and result.stopped == "not-retryable" and not result.used_fallback
def test_deadline_stops_before_a_wait_that_cannot_fit(clock):
def slow(timeout):
clock.now += 3 # every try takes 3 s
return Outcome(False, True, "timeout")
result = run_with_retry(slow, Policy(tries=5, backoff=1, deadline=7), sleep=clock.sleep, clock=clock)
assert result.stopped == "deadline" and len(result.attempts) == 2 # R6: 3 + 1 + 3 = 7 → no third try
assert result.seconds <= 7
def test_deadline_shortens_the_last_timeout(clock):
seen = []
def attempt(timeout):
seen.append(timeout)
clock.now += timeout
return Outcome(False, True, "timeout")
run_with_retry(attempt, Policy(tries=3, backoff=0.5, deadline=5), per_try_timeout=3, sleep=clock.sleep, clock=clock)
assert seen == [3, 1.5] # 3 + 0.5 wait → 1.5 s left for try 2
def test_retry_after_is_respected_up_to_the_cap(clock):
answers = [Outcome(False, True, "HTTP 429", status=429, retry_after=3), Outcome(True, output="ok")]
result = run_with_retry(lambda t: answers.pop(0), Policy(tries=3, backoff=0.5, max_wait=8), sleep=clock.sleep, clock=clock)
assert result.ok and clock.slept == [3] # R4: the server's 3 s, not our 0.5 s