Skip to content

Commit b54bba9

Browse files
authored
[Test proxy] Update troubleshooting guide, link to it in failure messages (#42555)
1 parent d43fe29 commit b54bba9

File tree

2 files changed

+136
-37
lines changed

2 files changed

+136
-37
lines changed

doc/dev/test_proxy_troubleshooting.md

Lines changed: 129 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,22 @@ GitHub repository, but this isn't necessary to read for Python testing.
77

88
## Table of contents
99

10-
- [Guide for test proxy troubleshooting](#guide-for-test-proxy-troubleshooting)
11-
- [Table of contents](#table-of-contents)
12-
- [Debugging tip](#debugging-tip)
13-
- [Test collection failure](#test-collection-failure)
14-
- [Errors in tests using resource preparers](#errors-in-tests-using-resource-preparers)
15-
- [Test failure during `record/start` or `playback/start` requests](#test-failure-during-recordstart-or-playbackstart-requests)
16-
- [Playback failures from body matching errors](#playback-failures-from-body-matching-errors)
17-
- [Playback failures from inconsistent line breaks](#playback-failures-from-inconsistent-line-breaks)
18-
- [Playback failures from URL mismatches](#playback-failures-from-url-mismatches)
19-
- [Recordings not being produced](#recordings-not-being-produced)
20-
- [ConnectionError during tests](#connectionerror-during-tests)
21-
- [Different error than expected when using proxy](#different-error-than-expected-when-using-proxy)
22-
- [Test setup failure in test pipeline](#test-setup-failure-in-test-pipeline)
23-
- [Fixture not found error](#fixture-not-found-error)
24-
- [PermissionError during startup](#permissionerror-during-startup)
10+
- [Debugging tip](#debugging-tip)
11+
- [ServiceRequestError: Cannot connect to host](#servicerequesterror-cannot-connect-to-host)
12+
- [ResourceNotFoundError: Playback failure](#resourcenotfounderror-playback-failure)
13+
- [Test collection failure](#test-collection-failure)
14+
- [Errors in tests using resource preparers](#errors-in-tests-using-resource-preparers)
15+
- [Test failure during `record/start` or `playback/start` requests](#test-failure-during-recordstart-or-playbackstart-requests)
16+
- [Playback failures from body matching errors](#playback-failures-from-body-matching-errors)
17+
- [Playback failures from inconsistent line breaks](#playback-failures-from-inconsistent-line-breaks)
18+
- [Playback failures from URL mismatches](#playback-failures-from-url-mismatches)
19+
- [Playback failures from inconsistent test values](#playback-failures-from-inconsistent-test-values)
20+
- [Recordings not being produced](#recordings-not-being-produced)
21+
- [ConnectionError during tests](#connectionerror-during-tests)
22+
- [Different error than expected when using proxy](#different-error-than-expected-when-using-proxy)
23+
- [Test setup failure in test pipeline](#test-setup-failure-in-test-pipeline)
24+
- [Fixture not found error](#fixture-not-found-error)
25+
- [PermissionError during startup](#permissionerror-during-startup)
2526

2627
## Debugging tip
2728

@@ -39,6 +40,53 @@ containing the strings `test_delete` or `test_upload`.
3940

4041
For more information about `pytest` invocations, refer to [Usage and Invocations][pytest_commands].
4142

43+
## ServiceRequestError: Cannot connect to host
44+
45+
Tests may fail during startup with the following exception:
46+
47+
```text
48+
azure.core.exceptions.ServiceRequestError: Cannot connect to host localhost:5001
49+
ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate
50+
verify failed: self signed certificate (_ssl.c:1123)')]
51+
```
52+
53+
This is caused by the test proxy's certificate being incorrectly configured. First, update your branch to include the
54+
latest changes from `main` -- this ensures you have the latest certificate version (it needs to be occasionally
55+
rotated).
56+
57+
If tests continue to fail, this is likely due to an async-specific environment issue. The certificate is
58+
[automatically configured][cert_setup] during proxy startup, but async environments can still nondeterministically fail.
59+
60+
To work around this, set the following environment variable in your `.env` file:
61+
62+
```text
63+
PROXY_URL='http://localhost:5000'
64+
```
65+
66+
This will target an HTTP endpoint for the test proxy that doesn't require certificates. Service requests will still be
67+
sent securely from your client; this change only affects test proxy interactions.
68+
69+
## ResourceNotFoundError: Playback failure
70+
71+
Test playback errors typically raise with a message similar to the following:
72+
73+
```text
74+
FAILED test_client.py::TestClient::test_client_method - azure.core.exceptions.ResourceNotFoundError:
75+
Playback failure -- for help resolving, see https://aka.ms/azsdk/python/test-proxy/troubleshoot. Error details:
76+
Unable to find a record for the request POST https://fake_resource.service.azure.net?api-version=2025-09-01
77+
```
78+
79+
This means that the test recording didn't contain a match for the incoming playback request. This usually just means
80+
that the test needs to be re-recorded to pick up library updates (e.g. a new service API version).
81+
82+
If playback errors persist after re-recording, you may need to modify session sanitizers or matchers. The following
83+
sections of this guide describe common scenarios:
84+
85+
- [Playback failures from body matching errors](#playback-failures-from-body-matching-errors)
86+
- [Playback failures from inconsistent line breaks](#playback-failures-from-inconsistent-line-breaks)
87+
- [Playback failures from URL mismatches](#playback-failures-from-url-mismatches)
88+
- [Playback failures from inconsistent test values](#playback-failures-from-inconsistent-test-values)
89+
4290
## Test collection failure
4391

4492
Make sure that all test class names begin with "Test", and that all test method names begin with "test_". For more
@@ -111,9 +159,9 @@ Resource preparers need a management client to function, so test classes that us
111159

112160
## Test failure during `record/start` or `playback/start` requests
113161

114-
If your library uses out-of-repo recordings and tests fail during startup, logs might indicate that POST requests to
115-
`record/start` or `playback/start` endpoints are returning 500 responses. In a stack trace, these errors might be raised
116-
[here][record_request_failure] or [here][playback_request_failure], respectively.
162+
If tests fail during startup, logs might indicate that POST requests to `record/start` or `playback/start` endpoints
163+
are returning 500 responses. In a stack trace, these errors might be raised [here][record_request_failure] or
164+
[here][playback_request_failure], respectively.
117165

118166
This suggests that the test proxy failed to fetch recordings from the assets repository. This likely comes from a
119167
corrupted `git` configuration in `azure-sdk-for-python/.assets`. To resolve this:
@@ -139,11 +187,9 @@ These folders will be freshly recreated the next time you run tests.
139187

140188
## Playback failures from body matching errors
141189

142-
In the old, `vcrpy`-based testing system, request and response bodies weren't compared in playback mode by default in
143-
most packages. The test proxy system enables body matching by default, which can introduce failures for tests that
144-
passed in the old system. For example, if a test sends a request that includes the current Unix time in its body, the
145-
body will contain a new value when run in playback mode at a later time. This request might still match the recording if
146-
body matching is disabled, but not if it's enabled.
190+
The test proxy system enables body matching by default. For example, if a test sends a request that includes the
191+
current Unix time in its body, the body will contain a new value when run in playback mode at a later time -- this
192+
request won't match the recording if body matching is enabled.
147193

148194
Body matching can be turned off with the test proxy by calling the `set_bodiless_matcher` method from
149195
[devtools_testutils/sanitizers.py][py_sanitizers] at the very start of a test method. This matcher applies only to the
@@ -152,13 +198,12 @@ matching enabled by default.
152198

153199
## Playback failures from inconsistent line breaks
154200

155-
Some tests require recording content to completely match, including line breaks (for example, when sending the content of
156-
a test file in a request body). Line breaks can vary between OSes and cause tests to fail on certain platforms, in which
157-
case it can help to specify a particular format for test files by using [`.gitattributes`][gitattributes].
201+
Line breaks can vary between OSes and cause tests to fail on certain platforms, in which case it can help to specify a
202+
particular format for test files by using [`.gitattributes`][gitattributes].
158203

159-
A `.gitattributes` file can be placed at the root of a directory to apply git settings to each file under that directory.
160-
If a test directory contains files that need to have consistent line breaks, for example LF breaks instead of CRLF ones,
161-
you can create a `.gitattributes` file in the directory with the following content:
204+
A `.gitattributes` file can be placed at the root of a directory to apply git settings to each file under that
205+
directory. If a test directory contains files that need to have consistent line breaks, for example LF breaks instead
206+
of CRLF ones, you can create a `.gitattributes` file in the directory with the following content:
162207

163208
```text
164209
# Force git to checkout text files with LF (line feed) as the ending (vs CRLF)
@@ -209,11 +254,16 @@ that test, which is recommended.
209254

210255
### Sanitization impacting request URL/body/headers
211256

212-
In some cases, a value in a response body is used in the following request as part of the URL, body, or headers. If this value is sanitized, the recorded request might differ than what is expected during playback. Common culprits include sanitization of "name", "id", and "Location" fields. To resolve this, you can either opt out of specific sanitization or add another sanitizer to align with the sanitized value.
257+
In some cases, a value in a response body is used in the following request as part of the URL, body, or headers. If
258+
this value is sanitized, the recorded request might differ than what is expected during playback. Common culprits
259+
include sanitization of "name", "id", and "Location" fields. To resolve this, you can either opt out of specific
260+
sanitization or add another sanitizer to align with the sanitized value.
213261

214262
#### Opt out
215263

216-
You can opt out of sanitization for the fields that are used for your requests by calling the `remove_batch_sanitizer` method from `devtools_testutils` with the [sanitizer IDs][test_proxy_sanitizers] to exclude. Generally, this is done in the `conftest.py` file, in the one of the session-scoped fixtures. Example:
264+
You can opt out of sanitization for the fields that are used for your requests by calling the `remove_batch_sanitizer`
265+
method from `devtools_testutils` with the [sanitizer IDs][test_proxy_sanitizers] to exclude. Generally, this is done in
266+
the `conftest.py` file, in the one of the session-scoped fixtures. Example:
217267

218268
```python
219269
from devtools_testutils import remove_batch_sanitizers, test_proxy
@@ -245,6 +295,48 @@ from devtools_testutils import add_uri_regex_sanitizer
245295
add_uri_regex_sanitizer(regex="(?<=https://.+/foo/bar/)(?<id>[^/?\\.]+)", group_for_replace="id", value="Sanitized")
246296
```
247297

298+
## Playback failures from inconsistent test values
299+
300+
To run recorded tests successfully when recorded values are inconsistent or random and can't be sanitized, the test
301+
proxy provides a `variables` API. This makes it possible for a test to record the values of variables that were used
302+
during recording and use the same values in playback mode without a sanitizer.
303+
304+
For example, imagine that a test uses a randomized `table_uuid` variable when creating resources. The same random value
305+
for `table_uuid` can be used in playback mode by using this `variables` API.
306+
307+
There are two requirements for a test to use recorded variables. First, the test method should accept `**kwargs`.
308+
Second, the test method should `return` a dictionary with any test variables that it wants to record. This dictionary
309+
will be stored in the recording when the test is run live, and will be passed to the test as a `variables` keyword
310+
argument when the test is run in playback.
311+
312+
Below is a code example of how a test method could use recorded variables:
313+
314+
```python
315+
from devtools_testutils import AzureRecordedTestCase, recorded_by_proxy
316+
317+
class TestExample(AzureRecordedTestCase):
318+
319+
@recorded_by_proxy
320+
def test_example(self, **kwargs):
321+
# In live mode, variables is an empty dictionary
322+
# In playback mode, the value of variables is {"table_uuid": "random-value"}
323+
variables = kwargs.pop("variables", {})
324+
325+
# To fetch variable values, use the `setdefault` method to look for a key ("table_uuid")
326+
# and set a real value for that key if it's not present ("random-value")
327+
table_uuid = variables.setdefault("table_uuid", "random-value")
328+
329+
# use variables["table_uuid"] when using the table UUID throughout the test
330+
...
331+
332+
# return the variables at the end of the test to record them
333+
return variables
334+
```
335+
336+
> **Note:** `variables` will be passed as a named argument to any test that accepts `kwargs` by the test proxy. In
337+
> environments that don't use the test proxy, though -- like live test pipelines -- `variables` won't be provided.
338+
> To avoid a KeyError, providing an empty dictionary as the default value to `kwargs.pop` is recommended.
339+
248340
## Recordings not being produced
249341

250342
Ensure the environment variable `AZURE_SKIP_LIVE_RECORDING` **isn't** set to "true", and that `AZURE_TEST_RUN_LIVE`
@@ -321,8 +413,8 @@ skipped, so the `TestProxy` parameter doesn't need to be set in `tests.yml`.
321413

322414
Tests that aren't recorded should omit the `recorded_by_proxy` decorator. However, if these unrecorded tests accept
323415
parameters that are provided by a preparer like the `devtools_testutils` [EnvironmentVariableLoader][env_var_loader],
324-
you may see a new test setup error after migrating to the test proxy. For example, imagine a test is decorated with a
325-
preparer that provides a Key Vault URL as a `azure_keyvault_url` parameter:
416+
you may see a test setup error. For example, imagine a test is decorated with a preparer that provides a Key Vault URL
417+
as a `azure_keyvault_url` parameter:
326418

327419
```python
328420
class TestExample(AzureRecordedTestCase):
@@ -369,7 +461,8 @@ Alternatively, you can delete the installed tool and re-run your tests to automa
369461

370462
<!-- Links -->
371463

372-
[custom_default_matcher]: https://github.com/Azure/azure-sdk-for-python/blob/497f5f3435162c4f2086d1429fc1bba4f31a4354/eng/tools/azure-sdk-tools/devtools_testutils/sanitizers.py#L85
464+
[cert_setup]: https://github.com/Azure/azure-sdk-for-python/blob/9958caf6269247f940c697a3f982bbbf0a47a19b/eng/tools/azure-sdk-tools/devtools_testutils/proxy_startup.py#L210
465+
[custom_default_matcher]: https://github.com/Azure/azure-sdk-for-python/blob/9958caf6269247f940c697a3f982bbbf0a47a19b/eng/tools/azure-sdk-tools/devtools_testutils/sanitizers.py#L90
373466
[detailed_docs]: https://github.com/Azure/azure-sdk-tools/tree/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md
374467
[env_var_loader]: https://github.com/Azure/azure-sdk-for-python/blob/main/eng/tools/azure-sdk-tools/devtools_testutils/envvariable_loader.py
375468
[gitattributes]: https://git-scm.com/docs/gitattributes
@@ -379,10 +472,10 @@ Alternatively, you can delete the installed tool and re-run your tests to automa
379472
[parametrize_example]: https://github.com/Azure/azure-sdk-for-python/blob/aa607b3b8c3e646928375ebcc6339d68e4e90a49/sdk/keyvault/azure-keyvault-keys/tests/test_key_client.py#L190
380473
[pipelines_ci]: https://github.com/Azure/azure-sdk-for-python/blob/5ba894966ed6b0e1ee8d854871f8c2da36a73d79/sdk/eventgrid/ci.yml#L30
381474
[pipelines_live]: https://github.com/Azure/azure-sdk-for-python/blob/e2b5852deaef04752c1323d2ab0958f83b98858f/sdk/textanalytics/tests.yml#L26-L27
382-
[playback_request_failure]: https://github.com/Azure/azure-sdk-for-python/blob/e23d9a6b1edcc1127ded40b9993029495b4ad08c/eng/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py#L108
475+
[playback_request_failure]: https://github.com/Azure/azure-sdk-for-python/blob/9958caf6269247f940c697a3f982bbbf0a47a19b/eng/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py#L102
383476
[py_sanitizers]: https://github.com/Azure/azure-sdk-for-python/blob/main/eng/tools/azure-sdk-tools/devtools_testutils/sanitizers.py
384477
[pytest_collection]: https://docs.pytest.org/latest/goodpractices.html#test-discovery
385478
[pytest_commands]: https://docs.pytest.org/latest/usage.html
386-
[record_request_failure]: https://github.com/Azure/azure-sdk-for-python/blob/e23d9a6b1edcc1127ded40b9993029495b4ad08c/eng/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py#L97
479+
[record_request_failure]: https://github.com/Azure/azure-sdk-for-python/blob/9958caf6269247f940c697a3f982bbbf0a47a19b/eng/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py#L91
387480
[test_proxy_sanitizers]: https://github.com/Azure/azure-sdk-tools/blob/57382d5dc00b10a2f9cfd597293eeee0c2dbd8fd/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/SanitizerDictionary.cs#L65
388481
[wrong_exception]: https://github.com/Azure/azure-sdk-tools/issues/2907

eng/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,14 @@ def combined_call(*args, **kwargs):
237237

238238
except ResourceNotFoundError as error:
239239
error_body = ContentDecodePolicy.deserialize_from_http_generics(error.response)
240+
troubleshoot = (
241+
"Playback failure -- for help resolving, see https://aka.ms/azsdk/python/test-proxy/troubleshoot."
242+
)
240243
message = error_body.get("message") or error_body.get("Message")
241-
error_with_message = ResourceNotFoundError(message=message, response=error.response)
244+
error_with_message = ResourceNotFoundError(
245+
message=f"{troubleshoot} Error details:\n{message}",
246+
response=error.response,
247+
)
242248
six.raise_from(error_with_message, error)
243249

244250
finally:

0 commit comments

Comments
 (0)