retry makes too few attempts and hides the error
Done- Plan (done)
- Code (done)
- Test (done)
- Review (done)
- Finish (done)
Cost
$0.0037
Tests
Passed
Iterations
1
Re-plans
0
LLM calls
10
Prompt read from cache
80%
Usage by agent
- Cache read
- Cache write
- Input
- Output
View as table
| Agent | Cost | Cache read | Cache write | Input | Output |
|---|---|---|---|---|---|
| planner | $0.0007 | 5,545 | 614 | 8 | 1,208 |
| coder | $0.0016 | 13,386 | 3,305 | 10 | 2,086 |
| tester | $0.0014 | 11,290 | 3,827 | 8 | 1,624 |
Timeline
-
planner iteration 1 Finished 3 calls · 7,375 tokens · $0.0007 · 9.6s
Loading…
-
coder iteration 1 Finished 4 calls · 18.8K tokens · $0.0016 · 15.0s
Loading…
-
tester iteration 1 Finished 3 calls · 16.7K tokens · $0.0014 · 12.8s
Loading…
-
verify iteration 1 Passed 0.5s
......... [100%] 9 passed in 0.03s
Final diff
diff --git a/retrying.py b/retrying.py index a49ffae..483f943 100644 --- a/retrying.py +++ b/retrying.py @@ -1,7 +1,14 @@ def retry(fn, attempts: int = 3): - for _ in range(attempts - 1): + """Call ``fn`` up to ``attempts`` times and return its first successful result. + + If every attempt raises an ``Exception``, the exception from the last + attempt is re-raised. ``attempts`` values below 1 are treated as 1, so + ``fn`` is always called at least once. No delay is applied between attempts. + """ + last_exc = None + for _ in range(max(1, attempts)): try: return fn() - except Exception: - pass - return None + except Exception as exc: + last_exc = exc + raise last_exc diff --git a/test_retrying.py b/test_retrying.py index 409b641..238130c 100644 --- a/test_retrying.py +++ b/test_retrying.py @@ -1,5 +1,106 @@ +import pytest + from retrying import retry def test_success_first_time(): assert retry(lambda: 5) == 5 + + +def test_succeeds_after_failures(): + calls = [] + + def fn(): + calls.append(1) + if len(calls) < 3: + raise ValueError("boom") + return "ok" + + assert retry(fn, attempts=3) == "ok" + assert len(calls) == 3 + + +def test_always_failing_called_attempts_times_and_raises_last(): + calls = [] + + def fn(): + calls.append(1) + raise RuntimeError(f"attempt {len(calls)}") + + with pytest.raises(RuntimeError, match="attempt 3"): + retry(fn, attempts=3) + assert len(calls) == 3 + + +def test_success_first_time_calls_once(): + calls = [] + + def fn(): + calls.append(1) + return 42 + + assert retry(fn, attempts=3) == 42 + assert len(calls) == 1 + + +def test_stops_calling_after_success_on_attempt_k(): + calls = [] + + def fn(): + calls.append(1) + if len(calls) < 2: + raise ValueError("first") + return "done" + + assert retry(fn, attempts=5) == "done" + assert len(calls) == 2 + + +def test_raises_exception_object_from_last_attempt(): + excs = [] + + def fn(): + exc = KeyError(len(excs)) + excs.append(exc) + raise exc + + with pytest.raises(KeyError) as info: + retry(fn, attempts=4) + assert len(excs) == 4 + assert info.value is excs[-1] + assert info.value is not excs[0] + + +def test_attempts_zero_runs_once(): + calls = [] + + def fn(): + calls.append(1) + raise ValueError("nope") + + with pytest.raises(ValueError): + retry(fn, attempts=0) + assert len(calls) == 1 + + +def test_negative_attempts_runs_once(): + calls = [] + + def fn(): + calls.append(1) + return "x" + + assert retry(fn, attempts=-2) == "x" + assert len(calls) == 1 + + +def test_base_exception_propagates_immediately(): + calls = [] + + def fn(): + calls.append(1) + raise KeyboardInterrupt() + + with pytest.raises(KeyboardInterrupt): + retry(fn, attempts=3) + assert len(calls) == 1