Skip to content

Commit 396fdf0

Browse files
committed
Add unit tests for retry decorator
1 parent 3d990ea commit 396fdf0

File tree

1 file changed

+117
-0
lines changed

1 file changed

+117
-0
lines changed

tests/unit/test_utils_retry.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import random
2+
from time import monotonic, sleep
3+
from typing import List, NoReturn, Tuple, Type
4+
from unittest.mock import Mock
5+
6+
import pytest
7+
8+
from pip._internal.utils.retry import retry
9+
10+
11+
def test_retry_no_error() -> None:
12+
function = Mock(return_value="daylily")
13+
wrapped = retry(wait=0, stop_after_delay=0.01)(function)
14+
assert wrapped("eggs", alternative="spam") == "daylily"
15+
function.assert_called_once_with("eggs", alternative="spam")
16+
17+
18+
def test_retry_no_error_after_retry() -> None:
19+
raised = False
20+
21+
def _raise_once() -> str:
22+
nonlocal raised
23+
if not raised:
24+
raised = True
25+
raise RuntimeError("ham")
26+
return "daylily"
27+
28+
function = Mock(wraps=_raise_once)
29+
wrapped = retry(wait=0, stop_after_delay=0.01)(function)
30+
assert wrapped() == "daylily"
31+
assert function.call_count == 2
32+
33+
34+
def test_retry_last_error_is_reraised() -> None:
35+
errors = []
36+
37+
def _raise_error() -> NoReturn:
38+
error = RuntimeError(random.random())
39+
errors.append(error)
40+
raise error
41+
42+
function = Mock(wraps=_raise_error)
43+
wrapped = retry(wait=0, stop_after_delay=0.01)(function)
44+
try:
45+
wrapped()
46+
except Exception as e:
47+
assert isinstance(e, RuntimeError)
48+
assert e is errors[-1]
49+
else:
50+
assert False, "unexpected return"
51+
52+
assert function.call_count > 1, "expected at least one retry"
53+
54+
55+
@pytest.mark.parametrize("exc", [KeyboardInterrupt, SystemExit])
56+
def test_retry_ignores_base_exception(exc: Type[BaseException]) -> None:
57+
function = Mock(side_effect=exc())
58+
wrapped = retry(wait=0, stop_after_delay=0.01)(function)
59+
with pytest.raises(exc):
60+
wrapped()
61+
function.assert_called_once()
62+
63+
64+
def create_timestamped_callable(sleep_per_call: float = 0) -> Tuple[Mock, List[float]]:
65+
timestamps = []
66+
67+
def _raise_error() -> NoReturn:
68+
timestamps.append(monotonic())
69+
if sleep_per_call:
70+
sleep(sleep_per_call)
71+
raise RuntimeError
72+
73+
return Mock(wraps=_raise_error), timestamps
74+
75+
76+
# Use multiple of 15ms as Windows' sleep is only accurate to 15ms.
77+
@pytest.mark.parametrize("wait_duration", [0.015, 0.045, 0.15])
78+
def test_retry_wait(wait_duration: float) -> None:
79+
function, timestamps = create_timestamped_callable()
80+
# Only the first retry will be scheduled before the time limit is exceeded.
81+
wrapped = retry(wait=wait_duration, stop_after_delay=0.01)(function)
82+
start_time = monotonic()
83+
with pytest.raises(RuntimeError):
84+
wrapped()
85+
assert len(timestamps) == 2
86+
assert timestamps[1] - start_time >= wait_duration
87+
88+
89+
@pytest.mark.parametrize(
90+
"call_duration, max_allowed_calls", [(0.01, 10), (0.04, 3), (0.15, 1)]
91+
)
92+
def test_retry_time_limit(call_duration: float, max_allowed_calls: int) -> None:
93+
function, timestamps = create_timestamped_callable(sleep_per_call=call_duration)
94+
wrapped = retry(wait=0, stop_after_delay=0.1)(function)
95+
96+
start_time = monotonic()
97+
with pytest.raises(RuntimeError):
98+
wrapped()
99+
assert len(timestamps) <= max_allowed_calls
100+
assert all(t - start_time <= 0.1 for t in timestamps)
101+
102+
103+
def test_retry_method() -> None:
104+
class MyClass:
105+
def __init__(self) -> None:
106+
self.calls = 0
107+
108+
@retry(wait=0, stop_after_delay=0.01)
109+
def method(self, string: str) -> str:
110+
self.calls += 1
111+
if self.calls >= 5:
112+
return string
113+
raise RuntimeError
114+
115+
o = MyClass()
116+
assert o.method("orange") == "orange"
117+
assert o.calls == 5

0 commit comments

Comments
 (0)