44layered on it, which cannot be provoked against a backend on demand.
55"""
66
7- import time
87from http import HTTPMethod , HTTPStatus
98
109import pytest
1110import requests
1211import responses
1312from beartype import beartype
13+ from tenacity import Retrying
14+ from tenacity .retry import retry_if_exception_type
15+ from tenacity .stop import stop_after_attempt
16+ from tenacity .wait import wait_none
1417
18+ from tests .mock_vws .fixtures .real_model_target_retries import (
19+ MODEL_TARGET_RETRY_ATTEMPTS ,
20+ REAL_MODEL_TARGET_RETRY_POLICY ,
21+ )
22+ from tests .mock_vws .fixtures .vuforia_backends import VuforiaBackend
1523from tests .mock_vws .utils import ModelTargetEndpoint
1624from tests .mock_vws .utils .model_target_retries import (
17- MODEL_TARGET_RETRY_ATTEMPTS ,
1825 model_target_get ,
19- retrying_transient_real_backend_failures ,
26+ retrying_model_target_requests ,
2027 send_with_transient_retries ,
2128)
2229
@@ -31,25 +38,25 @@ def _response(*, status_code: HTTPStatus) -> requests.Response:
3138 return response
3239
3340
34- @beartype
35- def _do_not_sleep ( seconds : float ) -> None :
36- """Do not sleep ."""
37- del seconds
41+ @pytest . fixture ( name = "retry_policy" )
42+ def fixture_retry_policy ( ) -> Retrying :
43+ """Use the real retry criteria without waiting between attempts ."""
44+ return REAL_MODEL_TARGET_RETRY_POLICY . copy ( wait = wait_none ())
3845
3946
40- @pytest .fixture (autouse = True , name = "no_sleep " )
41- def fixture_no_sleep ( * , monkeypatch : pytest . MonkeyPatch ) -> None :
42- """Do not wait between attempts ."""
43- monkeypatch . setattr ( target = time , name = "sleep" , value = _do_not_sleep )
47+ @pytest .fixture (name = "_model_target_backend " )
48+ def fixture_model_target_backend ( ) -> VuforiaBackend :
49+ """Select the real policy without credential lookup or requests ."""
50+ return VuforiaBackend . REAL
4451
4552
46- def test_transient_response_is_retried () -> None :
53+ def test_transient_response_is_retried (* , retry_policy : Retrying ) -> None :
4754 """A transient gateway response is followed by another attempt,
4855 and the response to that attempt is returned.
4956 """
5057 remaining = [HTTPStatus .BAD_GATEWAY , HTTPStatus .OK ]
5158
52- with retrying_transient_real_backend_failures ( ):
59+ with retrying_model_target_requests ( retrying = retry_policy ):
5360 response = send_with_transient_retries (
5461 method = HTTPMethod .GET ,
5562 send = lambda : _response (status_code = remaining .pop (0 )),
@@ -58,7 +65,7 @@ def test_transient_response_is_retried() -> None:
5865 assert response .status_code == HTTPStatus .OK
5966
6067
61- def test_persistent_transient_response () -> None :
68+ def test_persistent_transient_response (* , retry_policy : Retrying ) -> None :
6269 """The last response is returned rather than hidden, so that the
6370 caller's assertion reports the real status and body.
6471 """
@@ -70,7 +77,7 @@ def send() -> requests.Response:
7077 attempts += 1
7178 return _response (status_code = HTTPStatus .SERVICE_UNAVAILABLE )
7279
73- with retrying_transient_real_backend_failures ( ):
80+ with retrying_model_target_requests ( retrying = retry_policy ):
7481 response = send_with_transient_retries (
7582 method = HTTPMethod .GET ,
7683 send = send ,
@@ -80,7 +87,7 @@ def send() -> requests.Response:
8087 assert attempts == MODEL_TARGET_RETRY_ATTEMPTS
8188
8289
83- def test_persistent_transport_failure () -> None :
90+ def test_persistent_transport_failure (* , retry_policy : Retrying ) -> None :
8491 """A transport failure on the last attempt is not swallowed."""
8592
8693 def send () -> requests .Response :
@@ -93,7 +100,7 @@ def send() -> requests.Response:
93100 raise requests .exceptions .ConnectionError (msg )
94101
95102 with (
96- retrying_transient_real_backend_failures ( ),
103+ retrying_model_target_requests ( retrying = retry_policy ),
97104 pytest .raises (
98105 expected_exception = requests .exceptions .ConnectionError ,
99106 match = r"Connection aborted\." ,
@@ -102,7 +109,7 @@ def send() -> requests.Response:
102109 _ = send_with_transient_retries (method = HTTPMethod .GET , send = send )
103110
104111
105- def test_error_response_is_not_retried () -> None :
112+ def test_error_response_is_not_retried (* , retry_policy : Retrying ) -> None :
106113 """A response which came from Vuforia rather than from the
107114 gateway is returned as it is, however unsuccessful it is.
108115 """
@@ -114,13 +121,13 @@ def send() -> requests.Response:
114121 attempts += 1
115122 return _response (status_code = HTTPStatus .UNAUTHORIZED )
116123
117- with retrying_transient_real_backend_failures ( ):
124+ with retrying_model_target_requests ( retrying = retry_policy ):
118125 _ = send_with_transient_retries (method = HTTPMethod .GET , send = send )
119126
120127 assert attempts == 1
121128
122129
123- def test_mutating_request_is_not_retried () -> None :
130+ def test_mutating_request_is_not_retried (* , retry_policy : Retrying ) -> None :
124131 """A request which is not safe to repeat is sent exactly once,
125132 even when it fails transiently.
126133
@@ -135,13 +142,13 @@ def send() -> requests.Response:
135142 attempts += 1
136143 return _response (status_code = HTTPStatus .BAD_GATEWAY )
137144
138- with retrying_transient_real_backend_failures ( ):
145+ with retrying_model_target_requests ( retrying = retry_policy ):
139146 _ = send_with_transient_retries (method = HTTPMethod .POST , send = send )
140147
141148 assert attempts == 1
142149
143150
144- def test_configured_failure_is_not_retried () -> None :
151+ def test_configured_failure_is_not_retried (* , retry_policy : Retrying ) -> None :
145152 """A 5xx response is returned immediately outside the context.
146153
147154 Tests which configure a mock to fail with a 5xx expect exactly
@@ -157,7 +164,7 @@ def send() -> requests.Response:
157164
158165 # Enter and leave the context first, to check it is switched off
159166 # again for a mock backend test which runs after a real one.
160- with retrying_transient_real_backend_failures ( ):
167+ with retrying_model_target_requests ( retrying = retry_policy ):
161168 pass
162169
163170 response = send_with_transient_retries (
@@ -170,7 +177,7 @@ def send() -> requests.Response:
170177
171178
172179@responses .activate
173- def test_endpoint_send () -> None :
180+ def test_endpoint_send (* , retry_policy : Retrying ) -> None :
174181 """A ``GET`` endpoint retries a transient failure.
175182
176183 This is the path which the cross-cutting Model Target endpoint tests
@@ -193,7 +200,7 @@ def test_endpoint_send() -> None:
193200 takes_json_body = False ,
194201 )
195202
196- with retrying_transient_real_backend_failures ( ):
203+ with retrying_model_target_requests ( retrying = retry_policy ):
197204 response = endpoint .send ()
198205
199206 assert response .status_code == HTTPStatus .UNAUTHORIZED
@@ -202,7 +209,7 @@ def test_endpoint_send() -> None:
202209
203210
204211@responses .activate
205- def test_model_target_get () -> None :
212+ def test_model_target_get (* , retry_policy : Retrying ) -> None :
206213 """``model_target_get`` retries a transient failure."""
207214 gateway_response = responses .Response (
208215 method = responses .GET , url = _URL , status = HTTPStatus .GATEWAY_TIMEOUT
@@ -213,9 +220,77 @@ def test_model_target_get() -> None:
213220 assert responses .add (gateway_response ) is gateway_response
214221 assert responses .add (dataset_response ) is dataset_response
215222
216- with retrying_transient_real_backend_failures ( ):
223+ with retrying_model_target_requests ( retrying = retry_policy ):
217224 response = model_target_get (url = _URL , headers = {}, timeout = 30 )
218225
219226 assert response .content == b"dataset"
220227 assert gateway_response .call_count == 1
221228 assert dataset_response .call_count == 1
229+
230+
231+ @pytest .mark .usefixtures ("verify_model_target_mock_vuforia" )
232+ def test_real_adapter_installs_retry_policy () -> None :
233+ """Real adapter setup installs its policy without account access."""
234+ remaining = [HTTPStatus .BAD_GATEWAY , HTTPStatus .OK ]
235+ response = send_with_transient_retries (
236+ method = HTTPMethod .GET ,
237+ send = lambda : _response (status_code = remaining .pop (0 )),
238+ )
239+ assert response .status_code == HTTPStatus .OK
240+ assert not bool (remaining )
241+
242+
243+ def test_nested_retry_policies () -> None :
244+ """Nested policies restore the outer policy after a raised
245+ exception.
246+ """
247+ outer = Retrying (
248+ retry = retry_if_exception_type (exception_types = ValueError ),
249+ stop = stop_after_attempt (max_attempt_number = 2 ),
250+ wait = wait_none (),
251+ reraise = True ,
252+ )
253+ inner = outer .copy (
254+ retry = retry_if_exception_type (exception_types = KeyError ),
255+ )
256+ error = ValueError ("Controlled failure" )
257+ response = _response (status_code = HTTPStatus .OK )
258+ remaining : list [ValueError | requests .Response ] = [
259+ error ,
260+ response ,
261+ error ,
262+ error ,
263+ response ,
264+ error ,
265+ ]
266+
267+ def send () -> requests .Response :
268+ """Return the next controlled response or raise its exception."""
269+ outcome = remaining .pop (0 )
270+ if isinstance (outcome , ValueError ):
271+ raise outcome
272+ return outcome
273+
274+ with retrying_model_target_requests (retrying = outer ):
275+ assert (
276+ send_with_transient_retries (method = HTTPMethod .GET , send = send )
277+ is response
278+ )
279+ with (
280+ pytest .raises (
281+ expected_exception = ValueError , match = "Controlled failure"
282+ ) as exc ,
283+ retrying_model_target_requests (retrying = inner ),
284+ ):
285+ _ = send_with_transient_retries (method = HTTPMethod .GET , send = send )
286+ assert exc .value is error
287+ assert (
288+ send_with_transient_retries (method = HTTPMethod .GET , send = send )
289+ is response
290+ )
291+ with pytest .raises (
292+ expected_exception = ValueError , match = "Controlled failure"
293+ ) as exc :
294+ _ = send_with_transient_retries (method = HTTPMethod .GET , send = send )
295+ assert exc .value is error
296+ assert not bool (remaining )
0 commit comments