retry makes too few attempts and hides the error
Done- Plan (done)
- Code (done)
- Test (done)
- Review (done)
- Finish (done)
Cost
$0.0051
Tests
Passed
Iterations
1
Re-plans
0
LLM calls
11
Prompt read from cache
57%
Usage by agent
- Cache read
- Cache write
- Input
- Output
View as table
| Agent | Cost | Cache read | Cache write | Input | Output |
|---|---|---|---|---|---|
| planner | $0.0010 | 3,877 | 2,270 | 8 | 1,395 |
| coder | $0.0016 | 10,905 | 5,160 | 10 | 1,698 |
| tester | $0.0016 | 9,742 | 6,286 | 8 | 1,405 |
| reviewer | $0.0009 | 0 | 4,648 | 4 | 583 |
Timeline
-
planner iteration 1 Finished 3 calls · 7,550 tokens · $0.0010 · 12.0s
Loading…
-
coder iteration 1 Finished 4 calls · 17.8K tokens · $0.0016 · 12.1s
Loading…
-
tester iteration 1 Finished 3 calls · 17.4K tokens · $0.0016 · 10.7s
Loading…
-
verify iteration 1 Passed 0.5s
.............. [100%] 14 passed in 0.02s
-
reviewer iteration 1 Finished 1 calls · 5,235 tokens · $0.0009 · 4.6s
Loading…
Final diff
diff --git a/retrying.py b/retrying.py index a49ffae..e4e6447 100644 --- a/retrying.py +++ b/retrying.py @@ -1,7 +1,10 @@ def retry(fn, attempts: int = 3): - for _ in range(attempts - 1): + if attempts < 1: + raise ValueError("attempts must be at least 1") + last_exc = None + for _ in range(attempts): try: return fn() - except Exception: - pass - return None + except Exception as e: + last_exc = e + raise last_exc diff --git a/test_retrying.py b/test_retrying.py index 409b641..5fae042 100644 --- a/test_retrying.py +++ b/test_retrying.py @@ -1,5 +1,118 @@ +import pytest + from retrying import retry def test_success_first_time(): assert retry(lambda: 5) == 5 + + +@pytest.mark.parametrize("n", [1, 2, 3, 5]) +def test_succeeds_on_nth_attempt(n): + calls = {"count": 0} + + def fn(): + calls["count"] += 1 + if calls["count"] < n: + raise ValueError("fail") + return "ok" + + assert retry(fn, attempts=n) == "ok" + assert calls["count"] == n + + +def test_succeeds_on_last_attempt_after_failures(): + attempts = 4 + calls = {"count": 0} + + def fn(): + calls["count"] += 1 + if calls["count"] < attempts: + raise RuntimeError("boom") + return 42 + + assert retry(fn, attempts=attempts) == 42 + assert calls["count"] == attempts + + +def test_always_fails_calls_exactly_attempts_times_and_raises_last(): + calls = {"count": 0} + + def fn(): + calls["count"] += 1 + raise ValueError(f"n{calls['count']}") + + with pytest.raises(ValueError, match="^n3$"): + retry(fn, attempts=3) + assert calls["count"] == 3 + + +def test_stops_after_first_success(): + calls = {"count": 0} + + def fn(): + calls["count"] += 1 + return "done" + + assert retry(fn, attempts=5) == "done" + assert calls["count"] == 1 + + +@pytest.mark.parametrize("attempts", [0, -1]) +def test_attempts_less_than_one_raises_value_error(attempts): + with pytest.raises(ValueError): + retry(lambda: 5, attempts=attempts) + + +def test_raises_exact_instance_from_last_attempt(): + calls = {"count": 0} + excs = [] + + def fn(): + calls["count"] += 1 + exc = KeyError(calls["count"]) + excs.append(exc) + raise exc + + with pytest.raises(KeyError) as info: + retry(fn, attempts=4) + assert calls["count"] == 4 + assert info.value is excs[-1] + + +def test_last_exception_type_wins_over_earlier_types(): + errors = [ValueError("first"), TypeError("second"), OSError("third")] + calls = {"count": 0} + + def fn(): + exc = errors[calls["count"]] + calls["count"] += 1 + raise exc + + with pytest.raises(OSError, match="^third$"): + retry(fn, attempts=3) + assert calls["count"] == 3 + + +def test_base_exception_propagates_immediately(): + calls = {"count": 0} + + def fn(): + calls["count"] += 1 + raise KeyboardInterrupt() + + with pytest.raises(KeyboardInterrupt): + retry(fn, attempts=5) + assert calls["count"] == 1 + + +def test_attempts_one_calls_once_and_raises(): + calls = {"count": 0} + + def fn(): + calls["count"] += 1 + raise ValueError("only") + + with pytest.raises(ValueError, match="^only$"): + retry(fn, attempts=1) + assert calls["count"] == 1