From be8359768c1a1cc0a1fbdfc31177d3c23db5dd45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Tue, 14 Jun 2022 15:08:10 -0700 Subject: [PATCH 01/13] Prototype of basic fixture --- .../devtools_testutils/__init__.py | 3 +- .../devtools_testutils/proxy_testcase.py | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/tools/azure-sdk-tools/devtools_testutils/__init__.py b/tools/azure-sdk-tools/devtools_testutils/__init__.py index 02d2e7791178..44a865a73d92 100644 --- a/tools/azure-sdk-tools/devtools_testutils/__init__.py +++ b/tools/azure-sdk-tools/devtools_testutils/__init__.py @@ -19,7 +19,7 @@ from .envvariable_loader import EnvironmentVariableLoader PowerShellPreparer = EnvironmentVariableLoader # Backward compat from .proxy_startup import start_test_proxy, stop_test_proxy, test_proxy -from .proxy_testcase import recorded_by_proxy +from .proxy_testcase import recorded_by_proxy, recorded_test from .sanitizers import ( add_body_key_sanitizer, add_body_regex_sanitizer, @@ -66,6 +66,7 @@ "PowerShellPreparer", "EnvironmentVariableLoader", "recorded_by_proxy", + "recorded_test", "test_proxy", "set_bodiless_matcher", "set_custom_default_matcher", diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 88b2a1f311f4..25c4af56580d 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -197,3 +197,44 @@ def combined_call(*args, **kwargs): return test_output return record_wrap + + +@pytest.fixture +def recorded_test(request): + if sys.version_info.major == 2 and not is_live(): + pytest.skip("Playback testing is incompatible with the azure-sdk-tools test proxy on Python 2") + + def transform_args(*args, **kwargs): + copied_positional_args = list(args) + request = copied_positional_args[1] + + transform_request(request, recording_id) + + return tuple(copied_positional_args), kwargs + + if is_live_and_not_recording(): + return + + test_id = get_test_id() + recording_id, variables = start_record_or_playback(test_id) + original_transport_func = RequestsTransport.send + + def combined_call(*args, **kwargs): + adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) + result = original_transport_func(*adjusted_args, **adjusted_kwargs) + + # make the x-recording-upstream-base-uri the URL of the request + # this makes the request look like it was made to the original endpoint instead of to the proxy + # without this, things like LROPollers can get broken by polling the wrong endpoint + parsed_result = url_parse.urlparse(result.request.url) + upstream_uri = url_parse.urlparse(result.request.headers["x-recording-upstream-base-uri"]) + upstream_uri_dict = {"scheme": upstream_uri.scheme, "netloc": upstream_uri.netloc} + original_target = parsed_result._replace(**upstream_uri_dict).geturl() + + result.request.url = original_target + return result + + RequestsTransport.send = combined_call + yield # test gets run here + RequestsTransport.send = original_transport_func # test finished running -- tear down + stop_record_or_playback(test_id, recording_id, None) # TODO: how do we provide variables to record? From 8591f17515eacf3fa839d406cc0fa7c2f5f6c50e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Wed, 6 Jul 2022 20:23:59 -0700 Subject: [PATCH 02/13] wip; Repo-wide recording and variables fixtures --- sdk/conftest.py | 138 +++++++++++++++++- .../azure-data-tables/tests/preparers.py | 19 +++ .../tests/test_table_batch.py | 20 ++- .../devtools_testutils/__init__.py | 3 +- .../devtools_testutils/proxy_startup.py | 25 ++-- .../devtools_testutils/proxy_testcase.py | 48 +----- 6 files changed, 180 insertions(+), 73 deletions(-) diff --git a/sdk/conftest.py b/sdk/conftest.py index f4cdaaade7ce..9f91a4ec35da 100644 --- a/sdk/conftest.py +++ b/sdk/conftest.py @@ -23,8 +23,23 @@ # IN THE SOFTWARE. # # -------------------------------------------------------------------------- +import logging import os import pytest +import sys +from typing import TYPE_CHECKING +import urllib.parse as url_parse + +from azure.core.exceptions import ResourceNotFoundError +from azure.core.pipeline.policies import ContentDecodePolicy +from azure.core.pipeline.transport import RequestsTransport +from devtools_testutils import test_proxy +from devtools_testutils.helpers import get_test_id, is_live, is_live_and_not_recording +from devtools_testutils.proxy_testcase import start_record_or_playback, stop_record_or_playback, transform_request + +if TYPE_CHECKING: + from typing import Any, Optional + def pytest_configure(config): # register an additional marker @@ -55,4 +70,125 @@ def clean_cached_resources(): yield AbstractPreparer._perform_pending_deletes() except ImportError: - pass \ No newline at end of file + pass + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call) -> None: + """Captures test exception info and makes it available to other fixtures.""" + # execute all other hooks to obtain the report object + outcome = yield + result = outcome.get_result() + if result.outcome == "failed": + error = call.excinfo.value + # set a test_error attribute on the item (available to other fixtures from request.node) + setattr(item, "test_error", error) + + +@pytest.fixture +def start_proxy_session() -> "Optional[tuple[str, str, dict[str, Any]]]": + """Begins a playback or recording session and returns the current test ID, recording ID, and recorded variables. + + This returns a tuple, (a, b, c), where a is the test ID, b is the recording ID, and c is the `variables` dictionary + that maps test variables to values. If no variable dictionary was stored when the test was recorded, c is an empty + dictionary. + """ + if sys.version_info.major == 2 and not is_live(): + pytest.skip("Playback testing is incompatible with the azure-sdk-tools test proxy on Python 2") + + if is_live_and_not_recording(): + return + + test_id = get_test_id() + recording_id, variables = start_record_or_playback(test_id) + return (test_id, recording_id, variables) + + +@pytest.fixture +def recorded_test(test_proxy, start_proxy_session, request) -> "dict[str, Any]": + """Fixture that redirects network requests to target the azure-sdk-tools test proxy. Use with recorded tests. + + For more details and usage examples, refer to + https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md. + """ + test_id, recording_id, variables = start_proxy_session + original_transport_func = RequestsTransport.send + + def transform_args(*args, **kwargs): + copied_positional_args = list(args) + http_request = copied_positional_args[1] + + transform_request(http_request, recording_id) + + return tuple(copied_positional_args), kwargs + + def combined_call(*args, **kwargs): + adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) + result = original_transport_func(*adjusted_args, **adjusted_kwargs) + + # make the x-recording-upstream-base-uri the URL of the request + # this makes the request look like it was made to the original endpoint instead of to the proxy + # without this, things like LROPollers can get broken by polling the wrong endpoint + parsed_result = url_parse.urlparse(result.request.url) + upstream_uri = url_parse.urlparse(result.request.headers["x-recording-upstream-base-uri"]) + upstream_uri_dict = {"scheme": upstream_uri.scheme, "netloc": upstream_uri.netloc} + original_target = parsed_result._replace(**upstream_uri_dict).geturl() + + result.request.url = original_target + return result + + RequestsTransport.send = combined_call + + # store info pertinent to the test in a dictionary that other fixtures can access + variable_recorder = VariableRecorder(variables) + test_info = {"test_id": test_id, "variables": variable_recorder} + yield test_info # yield and allow test to run + + RequestsTransport.send = original_transport_func # test finished running -- tear down + + if hasattr(request.node, "test_error"): + # Exceptions are logged here instead of being raised because of how pytest handles error raising from inside + # fixtures and hooks. Raising from a fixture raises an error in addition to the test failure report, and the + # test proxy error is logged before the test failure output (making it difficult to find in pytest output). + # Raising from a hook isn't allowed, and produces an internal error that disrupts test execution. + # ResourceNotFoundErrors during playback indicate a recording mismatch + error = request.node.test_error + if isinstance(error, ResourceNotFoundError): + error_body = ContentDecodePolicy.deserialize_from_http_generics(error.response) + message = error_body.get("message") or error_body.get("Message") + logger = logging.getLogger() + logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") + + stop_record_or_playback(test_id, recording_id, variables) + + +@pytest.fixture +def variable_recorder(recorded_test) -> "dict[str, Any]": + """Fixture that invokes the `recorded_test` fixture and returns a dictionary of recorded test variables. + + The dictionary returned by this fixture maps test variables to values. If no variable dictionary was stored when the + test was recorded, this returns an empty dictionary. + """ + yield recorded_test["variables"] + + +class VariableRecorder(): + """Interface for fetching recorded test variables and recording new variables.""" + + def __init__(self, variables: "dict[str, Any]") -> None: + self.variables = variables + + def get(self, name: str) -> "Any": + """Returns the value of the recorded variable with the provided name. + + :param str name: The name of the recorded variable. For example, "vault_name". + """ + return self.variables.get(name) + + def record(self, variables: "dict[str, Any]") -> None: + """Records the provided variables in the test recording, making them available for future playback. + + :param variables: A dictionary mapping variable names to their values. + :type variables: dict[str, Any] + """ + self.variables = variables diff --git a/sdk/tables/azure-data-tables/tests/preparers.py b/sdk/tables/azure-data-tables/tests/preparers.py index d4da5573ccfb..0ffab549799f 100644 --- a/sdk/tables/azure-data-tables/tests/preparers.py +++ b/sdk/tables/azure-data-tables/tests/preparers.py @@ -53,6 +53,25 @@ def wrapper(*args, **kwargs): return wrapper +def tables_decorator_with_wraps(func, **kwargs): + @TablesPreparer() + @functools.wraps(func) + def wrapper(*args, **kwargs): + key = kwargs.pop("tables_primary_storage_account_key") + name = kwargs.pop("tables_storage_account_name") + key = AzureNamedKeyCredential(key=key, name=name) + + kwargs["tables_primary_storage_account_key"] = key + kwargs["tables_storage_account_name"] = name + + trimmed_kwargs = {k: v for k, v in kwargs.items()} + trim_kwargs_from_test_function(func, trimmed_kwargs) + + func(*args, **trimmed_kwargs) + + return wrapper + + def cosmos_decorator(func, **kwargs): @CosmosPreparer() def wrapper(*args, **kwargs): diff --git a/sdk/tables/azure-data-tables/tests/test_table_batch.py b/sdk/tables/azure-data-tables/tests/test_table_batch.py index 768f446c1a8a..759fe1f12af2 100644 --- a/sdk/tables/azure-data-tables/tests/test_table_batch.py +++ b/sdk/tables/azure-data-tables/tests/test_table_batch.py @@ -6,6 +6,7 @@ # license information. # -------------------------------------------------------------------------- +from multiprocessing.sharedctypes import Value import pytest from datetime import datetime, timedelta @@ -37,7 +38,7 @@ ) from _shared.testcase import TableTestCase -from preparers import tables_decorator +from preparers import tables_decorator, tables_decorator_with_wraps #------------------------------------------------------------------------------ TEST_TABLE_PREFIX = 'table' @@ -279,13 +280,18 @@ def test_batch_update_if_doesnt_match(self, tables_storage_account_name, tables_ self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_single_op_if_doesnt_match(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_single_op_if_doesnt_match(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers - set_custom_default_matcher( - compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" - ) + # set_custom_default_matcher( + # compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" + # ) + + # Above section is intentionally commented to trigger a playback error + variables = variable_recorder.variables if self.is_live else {"variable_name": "value"} + particular_variable = variable_recorder.get("variable_name") + ... + variable_recorder.record(variables) # Arrange self._set_up(tables_storage_account_name, tables_primary_storage_account_key) diff --git a/tools/azure-sdk-tools/devtools_testutils/__init__.py b/tools/azure-sdk-tools/devtools_testutils/__init__.py index 44a865a73d92..02d2e7791178 100644 --- a/tools/azure-sdk-tools/devtools_testutils/__init__.py +++ b/tools/azure-sdk-tools/devtools_testutils/__init__.py @@ -19,7 +19,7 @@ from .envvariable_loader import EnvironmentVariableLoader PowerShellPreparer = EnvironmentVariableLoader # Backward compat from .proxy_startup import start_test_proxy, stop_test_proxy, test_proxy -from .proxy_testcase import recorded_by_proxy, recorded_test +from .proxy_testcase import recorded_by_proxy from .sanitizers import ( add_body_key_sanitizer, add_body_regex_sanitizer, @@ -66,7 +66,6 @@ "PowerShellPreparer", "EnvironmentVariableLoader", "recorded_by_proxy", - "recorded_test", "test_proxy", "set_bodiless_matcher", "set_custom_default_matcher", diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_startup.py b/tools/azure-sdk-tools/devtools_testutils/proxy_startup.py index 47e1329c0985..a98c3a5881c9 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_startup.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_startup.py @@ -37,8 +37,7 @@ TOOL_ENV_VAR = "PROXY_PID" -def get_image_tag(): - # type: () -> str +def get_image_tag() -> str: """Gets the test proxy Docker image tag from the target_version.txt file in /eng/common/testproxy""" version_file_location = os.path.relpath("eng/common/testproxy/target_version.txt") version_file_location_from_root = os.path.abspath(os.path.join(REPO_ROOT, version_file_location)) @@ -62,8 +61,7 @@ def get_image_tag(): return image_tag -def get_container_info(): - # type: () -> Optional[dict] +def get_container_info() -> "Optional[dict]": """Returns a dictionary containing the test proxy container's information, or None if the container isn't present""" proc = subprocess.Popen( shlex.split("docker container ls -a --format '{{json .}}' --filter name=" + CONTAINER_NAME), @@ -82,23 +80,21 @@ def get_container_info(): return None -def check_availability(): - # type: () -> None +def check_availability() -> None: """Attempts request to /Info/Available. If a test-proxy instance is responding, we should get a response.""" try: response = requests.get(PROXY_CHECK_URL, timeout=60) return response.status_code # We get an SSLError if the container is started but the endpoint isn't available yet except requests.exceptions.SSLError as sslError: - _LOGGER.error(sslError) + _LOGGER.debug(sslError) return 404 except Exception as e: _LOGGER.error(e) return 404 -def check_proxy_availability(): - # type: () -> None +def check_proxy_availability() -> None: """Waits for the availability of the test-proxy.""" start = time.time() now = time.time() @@ -108,8 +104,7 @@ def check_proxy_availability(): now = time.time() -def create_container(): - # type: () -> None +def create_container() -> None: """Creates the test proxy Docker container""" # Most of the time, running this script on a Windows machine will work just fine, as Docker defaults to Linux # containers. However, in CI, Windows images default to _Windows_ containers. We cannot swap them. We can tell @@ -134,8 +129,7 @@ def create_container(): proc.communicate() -def start_test_proxy(): - # type: () -> None +def start_test_proxy() -> None: """Starts the test proxy and returns when the proxy server is ready to receive requests. In regular use cases, this will auto-start the test-proxy docker container. In CI, or when environment variable TF_BUILD is set, this function will start the test-proxy .NET tool.""" @@ -186,8 +180,7 @@ def start_test_proxy(): set_custom_default_matcher(excluded_headers=headers_to_ignore) -def stop_test_proxy(): - # type: () -> None +def stop_test_proxy() -> None: """Stops any running instance of the test proxy""" if not PROXY_MANUALLY_STARTED: @@ -213,7 +206,7 @@ def stop_test_proxy(): @pytest.fixture(scope="session") -def test_proxy(): +def test_proxy() -> None: """Pytest fixture to be used before running any tests that are recorded with the test proxy""" if is_live_and_not_recording(): yield diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 25c4af56580d..57ebc316bf37 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -7,7 +7,6 @@ import requests import six import sys -from typing import TYPE_CHECKING try: # py3 @@ -29,9 +28,6 @@ from .helpers import get_test_id, is_live, is_live_and_not_recording, set_recording_id from .sanitizers import add_remove_header_sanitizer, set_custom_default_matcher -if TYPE_CHECKING: - from typing import Tuple - # To learn about how to migrate SDK tests to the test proxy, please refer to the migration guide at # https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md @@ -43,8 +39,7 @@ PLAYBACK_STOP_URL = "{}/playback/stop".format(PROXY_URL) -def start_record_or_playback(test_id): - # type: (str) -> Tuple(str, dict) +def start_record_or_playback(test_id: str) -> tuple[str, dict]: """Sends a request to begin recording or playing back the provided test. This returns a tuple, (a, b), where a is the recording ID of the test and b is the `variables` dictionary that maps @@ -197,44 +192,3 @@ def combined_call(*args, **kwargs): return test_output return record_wrap - - -@pytest.fixture -def recorded_test(request): - if sys.version_info.major == 2 and not is_live(): - pytest.skip("Playback testing is incompatible with the azure-sdk-tools test proxy on Python 2") - - def transform_args(*args, **kwargs): - copied_positional_args = list(args) - request = copied_positional_args[1] - - transform_request(request, recording_id) - - return tuple(copied_positional_args), kwargs - - if is_live_and_not_recording(): - return - - test_id = get_test_id() - recording_id, variables = start_record_or_playback(test_id) - original_transport_func = RequestsTransport.send - - def combined_call(*args, **kwargs): - adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) - result = original_transport_func(*adjusted_args, **adjusted_kwargs) - - # make the x-recording-upstream-base-uri the URL of the request - # this makes the request look like it was made to the original endpoint instead of to the proxy - # without this, things like LROPollers can get broken by polling the wrong endpoint - parsed_result = url_parse.urlparse(result.request.url) - upstream_uri = url_parse.urlparse(result.request.headers["x-recording-upstream-base-uri"]) - upstream_uri_dict = {"scheme": upstream_uri.scheme, "netloc": upstream_uri.netloc} - original_target = parsed_result._replace(**upstream_uri_dict).geturl() - - result.request.url = original_target - return result - - RequestsTransport.send = combined_call - yield # test gets run here - RequestsTransport.send = original_transport_func # test finished running -- tear down - stop_record_or_playback(test_id, recording_id, None) # TODO: how do we provide variables to record? From 0ee1c5b82f3a92b703853e6d4b1007cf1d059002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Fri, 8 Jul 2022 19:01:15 -0700 Subject: [PATCH 03/13] Multiple tests using fixtures directly --- .../tests/test_table_batch.py | 118 +++++++----------- 1 file changed, 48 insertions(+), 70 deletions(-) diff --git a/sdk/tables/azure-data-tables/tests/test_table_batch.py b/sdk/tables/azure-data-tables/tests/test_table_batch.py index 759fe1f12af2..907658ff6d64 100644 --- a/sdk/tables/azure-data-tables/tests/test_table_batch.py +++ b/sdk/tables/azure-data-tables/tests/test_table_batch.py @@ -46,9 +46,8 @@ class TestTableBatch(AzureRecordedTestCase, TableTestCase): @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_single_insert(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_single_insert(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -83,9 +82,8 @@ def test_batch_single_insert(self, tables_storage_account_name, tables_primary_s self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_single_update(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_single_update(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -124,9 +122,8 @@ def test_batch_single_update(self, tables_storage_account_name, tables_primary_s self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_update(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_update(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -167,9 +164,8 @@ def test_batch_update(self, tables_storage_account_name, tables_primary_storage_ self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_merge(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_merge(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -212,9 +208,8 @@ def test_batch_merge(self, tables_storage_account_name, tables_primary_storage_a self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_update_if_match(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_update_if_match(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -246,9 +241,8 @@ def test_batch_update_if_match(self, tables_storage_account_name, tables_primary self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_update_if_doesnt_match(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_update_if_doesnt_match(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -335,9 +329,8 @@ def test_batch_single_op_if_doesnt_match(self, variable_recorder, tables_storage self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_insert_replace(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_insert_replace(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -371,9 +364,8 @@ def test_batch_insert_replace(self, tables_storage_account_name, tables_primary_ self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_insert_merge(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_insert_merge(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -407,9 +399,8 @@ def test_batch_insert_merge(self, tables_storage_account_name, tables_primary_st self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_delete(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_delete(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -445,9 +436,8 @@ def test_batch_delete(self, tables_storage_account_name, tables_primary_storage_ self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_inserts(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_inserts(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -486,9 +476,8 @@ def test_batch_inserts(self, tables_storage_account_name, tables_primary_storage self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_all_operations_together(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_all_operations_together(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -562,9 +551,8 @@ def test_batch_all_operations_together(self, tables_storage_account_name, tables self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_reuse(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_reuse(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -607,9 +595,8 @@ def test_batch_reuse(self, tables_storage_account_name, tables_primary_storage_a self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_same_row_operations_fail(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_same_row_operations_fail(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -641,9 +628,8 @@ def test_batch_same_row_operations_fail(self, tables_storage_account_name, table self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_different_partition_operations_fail(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_different_partition_operations_fail(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -672,9 +658,8 @@ def test_batch_different_partition_operations_fail(self, tables_storage_account_ self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_too_many_ops(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_too_many_ops(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -701,9 +686,8 @@ def test_batch_too_many_ops(self, tables_storage_account_name, tables_primary_st self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_different_partition_keys(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_different_partition_keys(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -724,9 +708,8 @@ def test_batch_different_partition_keys(self, tables_storage_account_name, table self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_new_non_existent_table(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_new_non_existent_table(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -748,9 +731,8 @@ def test_new_non_existent_table(self, tables_storage_account_name, tables_primar self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_new_invalid_key(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_new_invalid_key(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -771,9 +753,8 @@ def test_new_invalid_key(self, tables_storage_account_name, tables_primary_stora resp = self.table.submit_transaction(batch) @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_new_delete_nonexistent_entity(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_new_delete_nonexistent_entity(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -793,9 +774,8 @@ def test_new_delete_nonexistent_entity(self, tables_storage_account_name, tables self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_delete_batch_with_bad_kwarg(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_delete_batch_with_bad_kwarg(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -828,8 +808,8 @@ def test_delete_batch_with_bad_kwarg(self, tables_storage_account_name, tables_p @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") @pytest.mark.live_test_only - @tables_decorator - def test_batch_sas_auth(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_sas_auth(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -883,8 +863,8 @@ def test_batch_sas_auth(self, tables_storage_account_name, tables_primary_storag @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") @pytest.mark.live_test_only # Request bodies are very large - @tables_decorator - def test_batch_request_too_large(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_request_too_large(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -912,9 +892,8 @@ def test_batch_request_too_large(self, tables_storage_account_name, tables_prima self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_with_mode(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_with_mode(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -962,9 +941,8 @@ def test_batch_with_mode(self, tables_storage_account_name, tables_primary_stora self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator - @recorded_by_proxy - def test_batch_with_specialchar_partitionkey(self, tables_storage_account_name, tables_primary_storage_account_key): + @tables_decorator_with_wraps + def test_batch_with_specialchar_partitionkey(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" From 6a1b3d8a7d61ec4c1aec7739e37a743ebb2ca4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Mon, 11 Jul 2022 16:54:12 -0700 Subject: [PATCH 04/13] Move fixtures; Update variables; Type hints --- sdk/conftest.py | 142 ++-------------- .../tests/test_table_batch.py | 137 +++++++++------- .../devtools_testutils/proxy_testcase.py | 153 ++++++++++++++---- 3 files changed, 219 insertions(+), 213 deletions(-) diff --git a/sdk/conftest.py b/sdk/conftest.py index 9f91a4ec35da..18a293e907c6 100644 --- a/sdk/conftest.py +++ b/sdk/conftest.py @@ -23,52 +23,43 @@ # IN THE SOFTWARE. # # -------------------------------------------------------------------------- -import logging import os import pytest -import sys -from typing import TYPE_CHECKING -import urllib.parse as url_parse -from azure.core.exceptions import ResourceNotFoundError -from azure.core.pipeline.policies import ContentDecodePolicy -from azure.core.pipeline.transport import RequestsTransport -from devtools_testutils import test_proxy -from devtools_testutils.helpers import get_test_id, is_live, is_live_and_not_recording -from devtools_testutils.proxy_testcase import start_record_or_playback, stop_record_or_playback, transform_request - -if TYPE_CHECKING: - from typing import Any, Optional +from devtools_testutils.proxy_testcase import recorded_test, start_proxy_session, variable_recorder def pytest_configure(config): # register an additional marker - config.addinivalue_line( - "markers", "live_test_only: mark test to be a live test only" - ) - config.addinivalue_line( - "markers", "playback_test_only: mark test to be a playback test only" - ) + config.addinivalue_line("markers", "live_test_only: mark test to be a live test only") + config.addinivalue_line("markers", "playback_test_only: mark test to be a playback test only") + def pytest_runtest_setup(item): is_live_only_test_marked = bool([mark for mark in item.iter_markers(name="live_test_only")]) if is_live_only_test_marked: from devtools_testutils import is_live + if not is_live(): pytest.skip("live test only") is_playback_test_marked = bool([mark for mark in item.iter_markers(name="playback_test_only")]) if is_playback_test_marked: from devtools_testutils import is_live - if is_live() and os.environ.get('AZURE_SKIP_LIVE_RECORDING', '').lower() == 'true': + + if is_live() and os.environ.get("AZURE_SKIP_LIVE_RECORDING", "").lower() == "true": pytest.skip("playback test only") + try: from azure_devtools.scenario_tests import AbstractPreparer - @pytest.fixture(scope='session', autouse=True) + + @pytest.fixture(scope="session", autouse=True) def clean_cached_resources(): yield AbstractPreparer._perform_pending_deletes() + + except ImportError: pass @@ -83,112 +74,3 @@ def pytest_runtest_makereport(item, call) -> None: error = call.excinfo.value # set a test_error attribute on the item (available to other fixtures from request.node) setattr(item, "test_error", error) - - -@pytest.fixture -def start_proxy_session() -> "Optional[tuple[str, str, dict[str, Any]]]": - """Begins a playback or recording session and returns the current test ID, recording ID, and recorded variables. - - This returns a tuple, (a, b, c), where a is the test ID, b is the recording ID, and c is the `variables` dictionary - that maps test variables to values. If no variable dictionary was stored when the test was recorded, c is an empty - dictionary. - """ - if sys.version_info.major == 2 and not is_live(): - pytest.skip("Playback testing is incompatible with the azure-sdk-tools test proxy on Python 2") - - if is_live_and_not_recording(): - return - - test_id = get_test_id() - recording_id, variables = start_record_or_playback(test_id) - return (test_id, recording_id, variables) - - -@pytest.fixture -def recorded_test(test_proxy, start_proxy_session, request) -> "dict[str, Any]": - """Fixture that redirects network requests to target the azure-sdk-tools test proxy. Use with recorded tests. - - For more details and usage examples, refer to - https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md. - """ - test_id, recording_id, variables = start_proxy_session - original_transport_func = RequestsTransport.send - - def transform_args(*args, **kwargs): - copied_positional_args = list(args) - http_request = copied_positional_args[1] - - transform_request(http_request, recording_id) - - return tuple(copied_positional_args), kwargs - - def combined_call(*args, **kwargs): - adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) - result = original_transport_func(*adjusted_args, **adjusted_kwargs) - - # make the x-recording-upstream-base-uri the URL of the request - # this makes the request look like it was made to the original endpoint instead of to the proxy - # without this, things like LROPollers can get broken by polling the wrong endpoint - parsed_result = url_parse.urlparse(result.request.url) - upstream_uri = url_parse.urlparse(result.request.headers["x-recording-upstream-base-uri"]) - upstream_uri_dict = {"scheme": upstream_uri.scheme, "netloc": upstream_uri.netloc} - original_target = parsed_result._replace(**upstream_uri_dict).geturl() - - result.request.url = original_target - return result - - RequestsTransport.send = combined_call - - # store info pertinent to the test in a dictionary that other fixtures can access - variable_recorder = VariableRecorder(variables) - test_info = {"test_id": test_id, "variables": variable_recorder} - yield test_info # yield and allow test to run - - RequestsTransport.send = original_transport_func # test finished running -- tear down - - if hasattr(request.node, "test_error"): - # Exceptions are logged here instead of being raised because of how pytest handles error raising from inside - # fixtures and hooks. Raising from a fixture raises an error in addition to the test failure report, and the - # test proxy error is logged before the test failure output (making it difficult to find in pytest output). - # Raising from a hook isn't allowed, and produces an internal error that disrupts test execution. - # ResourceNotFoundErrors during playback indicate a recording mismatch - error = request.node.test_error - if isinstance(error, ResourceNotFoundError): - error_body = ContentDecodePolicy.deserialize_from_http_generics(error.response) - message = error_body.get("message") or error_body.get("Message") - logger = logging.getLogger() - logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") - - stop_record_or_playback(test_id, recording_id, variables) - - -@pytest.fixture -def variable_recorder(recorded_test) -> "dict[str, Any]": - """Fixture that invokes the `recorded_test` fixture and returns a dictionary of recorded test variables. - - The dictionary returned by this fixture maps test variables to values. If no variable dictionary was stored when the - test was recorded, this returns an empty dictionary. - """ - yield recorded_test["variables"] - - -class VariableRecorder(): - """Interface for fetching recorded test variables and recording new variables.""" - - def __init__(self, variables: "dict[str, Any]") -> None: - self.variables = variables - - def get(self, name: str) -> "Any": - """Returns the value of the recorded variable with the provided name. - - :param str name: The name of the recorded variable. For example, "vault_name". - """ - return self.variables.get(name) - - def record(self, variables: "dict[str, Any]") -> None: - """Records the provided variables in the test recording, making them available for future playback. - - :param variables: A dictionary mapping variable names to their values. - :type variables: dict[str, Any] - """ - self.variables = variables diff --git a/sdk/tables/azure-data-tables/tests/test_table_batch.py b/sdk/tables/azure-data-tables/tests/test_table_batch.py index 907658ff6d64..722871d60176 100644 --- a/sdk/tables/azure-data-tables/tests/test_table_batch.py +++ b/sdk/tables/azure-data-tables/tests/test_table_batch.py @@ -6,7 +6,6 @@ # license information. # -------------------------------------------------------------------------- -from multiprocessing.sharedctypes import Value import pytest from datetime import datetime, timedelta @@ -46,8 +45,9 @@ class TestTableBatch(AzureRecordedTestCase, TableTestCase): @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_single_insert(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_single_insert(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -82,8 +82,9 @@ def test_batch_single_insert(self, recorded_test, tables_storage_account_name=No self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_single_update(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_single_update(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -122,8 +123,9 @@ def test_batch_single_update(self, recorded_test, tables_storage_account_name=No self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_update(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_update(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -164,8 +166,9 @@ def test_batch_update(self, variable_recorder, tables_storage_account_name=None, self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_merge(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_merge(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -208,8 +211,9 @@ def test_batch_merge(self, recorded_test, tables_storage_account_name=None, tabl self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_update_if_match(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_update_if_match(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -241,8 +245,9 @@ def test_batch_update_if_match(self, variable_recorder, tables_storage_account_n self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_update_if_doesnt_match(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_update_if_doesnt_match(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -281,11 +286,17 @@ def test_batch_single_op_if_doesnt_match(self, variable_recorder, tables_storage # compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" # ) - # Above section is intentionally commented to trigger a playback error - variables = variable_recorder.variables if self.is_live else {"variable_name": "value"} - particular_variable = variable_recorder.get("variable_name") - ... - variable_recorder.record(variables) + # Above section is intentionally commented to trigger a playback error, to show how error raising is handled + + # variable_recorder directly returns the dictionary containing recorded variables. In live mode, this is an + # empty dictionary; in playback mode, this is populated with any variables that were recorded previously. + # Because the variable_recorder fixture is a function, we unfortunately don't get any autocomplete with the + # parameter whether it's a dictionary or custom type. + + # A custom type could allow for future APIs, but the `setdefault` method is thoroughly sufficient for now. + # Using `setdefault` will either fetch the recorded value for the variable, or record a new value in live mode, + # without having to check the live status of the test or contents of `variable_recorder`. + variable_value = variable_recorder.setdefault("variable_name", "live_generated_value") # Arrange self._set_up(tables_storage_account_name, tables_primary_storage_account_key) @@ -329,8 +340,9 @@ def test_batch_single_op_if_doesnt_match(self, variable_recorder, tables_storage self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_insert_replace(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_insert_replace(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -364,8 +376,9 @@ def test_batch_insert_replace(self, recorded_test, tables_storage_account_name=N self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_insert_merge(self, recorded_test, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_insert_merge(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -399,8 +412,9 @@ def test_batch_insert_merge(self, recorded_test, tables_storage_account_name=Non self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_delete(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_delete(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -436,8 +450,9 @@ def test_batch_delete(self, variable_recorder, tables_storage_account_name=None, self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_inserts(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_inserts(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -476,8 +491,9 @@ def test_batch_inserts(self, variable_recorder, tables_storage_account_name=None self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_all_operations_together(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_all_operations_together(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -551,8 +567,9 @@ def test_batch_all_operations_together(self, variable_recorder, tables_storage_a self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_reuse(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_reuse(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -595,8 +612,9 @@ def test_batch_reuse(self, variable_recorder, tables_storage_account_name=None, self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_same_row_operations_fail(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_same_row_operations_fail(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -628,8 +646,9 @@ def test_batch_same_row_operations_fail(self, variable_recorder, tables_storage_ self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_different_partition_operations_fail(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_different_partition_operations_fail(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -658,8 +677,9 @@ def test_batch_different_partition_operations_fail(self, variable_recorder, tabl self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_too_many_ops(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_too_many_ops(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -686,8 +706,9 @@ def test_batch_too_many_ops(self, variable_recorder, tables_storage_account_name self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_different_partition_keys(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_different_partition_keys(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -708,8 +729,9 @@ def test_batch_different_partition_keys(self, variable_recorder, tables_storage_ self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_new_non_existent_table(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_new_non_existent_table(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -731,8 +753,9 @@ def test_new_non_existent_table(self, variable_recorder, tables_storage_account_ self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_new_invalid_key(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_new_invalid_key(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -753,8 +776,9 @@ def test_new_invalid_key(self, variable_recorder, tables_storage_account_name=No resp = self.table.submit_transaction(batch) @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_new_delete_nonexistent_entity(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_new_delete_nonexistent_entity(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -774,8 +798,9 @@ def test_new_delete_nonexistent_entity(self, variable_recorder, tables_storage_a self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_delete_batch_with_bad_kwarg(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_delete_batch_with_bad_kwarg(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -808,8 +833,9 @@ def test_delete_batch_with_bad_kwarg(self, variable_recorder, tables_storage_acc @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") @pytest.mark.live_test_only - @tables_decorator_with_wraps - def test_batch_sas_auth(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_sas_auth(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -863,8 +889,9 @@ def test_batch_sas_auth(self, variable_recorder, tables_storage_account_name=Non @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") @pytest.mark.live_test_only # Request bodies are very large - @tables_decorator_with_wraps - def test_batch_request_too_large(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_request_too_large(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -892,8 +919,9 @@ def test_batch_request_too_large(self, variable_recorder, tables_storage_account self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_with_mode(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_with_mode(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" @@ -941,8 +969,9 @@ def test_batch_with_mode(self, variable_recorder, tables_storage_account_name=No self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_with_specialchar_partitionkey(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_with_specialchar_partitionkey(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 57ebc316bf37..e0c14a2c07f6 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -6,19 +6,14 @@ import logging import requests import six -import sys - -try: - # py3 - import urllib.parse as url_parse -except: - # py2 - import urlparse as url_parse +from typing import TYPE_CHECKING +import urllib.parse as url_parse import pytest from azure.core.exceptions import HttpResponseError, ResourceNotFoundError from azure.core.pipeline.policies import ContentDecodePolicy + # the functions we patch from azure.core.pipeline.transport import RequestsTransport @@ -26,7 +21,10 @@ from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function from .config import PROXY_URL from .helpers import get_test_id, is_live, is_live_and_not_recording, set_recording_id -from .sanitizers import add_remove_header_sanitizer, set_custom_default_matcher + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Tuple + from azure.core.pipeline.transport import HttpRequest # To learn about how to migrate SDK tests to the test proxy, please refer to the migration guide at # https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md @@ -39,9 +37,9 @@ PLAYBACK_STOP_URL = "{}/playback/stop".format(PROXY_URL) -def start_record_or_playback(test_id: str) -> tuple[str, dict]: +def start_record_or_playback(test_id: str) -> "Tuple[str, Dict[str, str]]": """Sends a request to begin recording or playing back the provided test. - + This returns a tuple, (a, b), where a is the recording ID of the test and b is the `variables` dictionary that maps test variables to values. If no variable dictionary was stored when the test was recorded, b is an empty dictionary. """ @@ -83,32 +81,38 @@ def start_record_or_playback(test_id: str) -> tuple[str, dict]: return (recording_id, variables) -def stop_record_or_playback(test_id, recording_id, test_output): - # type: (str, str, dict) -> None +def stop_record_or_playback(test_id: str, recording_id: str, test_variables: "Dict[str, str]") -> None: if is_live(): - requests.post( + response = requests.post( RECORDING_STOP_URL, headers={ "x-recording-file": test_id, "x-recording-id": recording_id, "x-recording-save": "true", - "Content-Type": "application/json" + "Content-Type": "application/json", }, - json=test_output or {} # tests don't record successfully unless test_output is a dictionary + json=test_variables or {}, # tests don't record successfully unless test_variables is a dictionary ) else: - requests.post( + response = requests.post( PLAYBACK_STOP_URL, headers={"x-recording-id": recording_id}, ) + try: + response.raise_for_status() + except requests.HTTPError as e: + raise HttpResponseError( + "The test proxy ran into an error while ending the session. Make sure any test variables you record have " + "string values." + ) from e -def get_proxy_netloc(): +def get_proxy_netloc() -> "Dict[str, str]": parsed_result = url_parse.urlparse(PROXY_URL) return {"scheme": parsed_result.scheme, "netloc": parsed_result.netloc} -def transform_request(request, recording_id): +def transform_request(request: "HttpRequest", recording_id: str) -> None: """Redirect the request to the test proxy, and store the original request URI in a header""" headers = request.headers @@ -121,7 +125,7 @@ def transform_request(request, recording_id): request.url = updated_target -def recorded_by_proxy(test_func): +def recorded_by_proxy(test_func) -> None: """Decorator that redirects network requests to target the azure-sdk-tools test proxy. Use with recorded tests. For more details and usage examples, refer to @@ -129,9 +133,6 @@ def recorded_by_proxy(test_func): """ def record_wrap(*args, **kwargs): - if sys.version_info.major == 2 and not is_live(): - pytest.skip("Playback testing is incompatible with the azure-sdk-tools test proxy on Python 2") - def transform_args(*args, **kwargs): copied_positional_args = list(args) request = copied_positional_args[1] @@ -168,18 +169,18 @@ def combined_call(*args, **kwargs): RequestsTransport.send = combined_call # call the modified function - # we define test_output before invoking the test so the variable is defined in case of an exception - test_output = None + # we define test_variables before invoking the test so the variable is defined in case of an exception + test_variables = None try: try: - test_output = test_func(*args, variables=variables, **trimmed_kwargs) + test_variables = test_func(*args, variables=variables, **trimmed_kwargs) except TypeError: logger = logging.getLogger() logger.info( "This test can't accept variables as input. The test method should accept `**kwargs` and/or a " "`variables` parameter to make use of recorded test variables." ) - test_output = test_func(*args, **trimmed_kwargs) + test_variables = test_func(*args, **trimmed_kwargs) except ResourceNotFoundError as error: error_body = ContentDecodePolicy.deserialize_from_http_generics(error.response) message = error_body.get("message") or error_body.get("Message") @@ -187,8 +188,102 @@ def combined_call(*args, **kwargs): six.raise_from(error_with_message, error) finally: RequestsTransport.send = original_transport_func - stop_record_or_playback(test_id, recording_id, test_output) + stop_record_or_playback(test_id, recording_id, test_variables) - return test_output + return test_variables return record_wrap + + +@pytest.fixture +def start_proxy_session() -> "Optional[Tuple[str, str, Dict[str, str]]]": + """Begins a playback or recording session and returns the current test ID, recording ID, and recorded variables. + + :returns: A tuple, (a, b, c), where a is the test ID, b is the recording ID, and c is the `variables` dictionary + that maps test variables to string values. If no variable dictionary was stored when the test was recorded, c is + an empty dictionary. If the current test session is live but recording is disabled, this returns None. + """ + if is_live_and_not_recording(): + return + + test_id = get_test_id() + recording_id, variables = start_record_or_playback(test_id) + return (test_id, recording_id, variables) + + +@pytest.fixture +def recorded_test(test_proxy, start_proxy_session, request) -> "Dict[str, Any]": + """Fixture that redirects network requests to target the azure-sdk-tools test proxy. Use with recorded tests. + + For more details and usage examples, refer to + https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md. + + :param function test_proxy: The fixture responsible for starting up the test proxy server. + :param function start_proxy_session: The fixture responsible for starting a recording or playback session. This + should yield a tuple with a test ID, recording ID, and dictionary of recorded test variables. + :param function request: The built-in `request` fixture. + + :yields: A dictionary containing information relevant to the currently executing test. + """ + test_id, recording_id, variables = start_proxy_session + original_transport_func = RequestsTransport.send + + def transform_args(*args, **kwargs): + copied_positional_args = list(args) + http_request = copied_positional_args[1] + + transform_request(http_request, recording_id) + + return tuple(copied_positional_args), kwargs + + def combined_call(*args, **kwargs): + adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) + result = original_transport_func(*adjusted_args, **adjusted_kwargs) + + # make the x-recording-upstream-base-uri the URL of the request + # this makes the request look like it was made to the original endpoint instead of to the proxy + # without this, things like LROPollers can get broken by polling the wrong endpoint + parsed_result = url_parse.urlparse(result.request.url) + upstream_uri = url_parse.urlparse(result.request.headers["x-recording-upstream-base-uri"]) + upstream_uri_dict = {"scheme": upstream_uri.scheme, "netloc": upstream_uri.netloc} + original_target = parsed_result._replace(**upstream_uri_dict).geturl() + + result.request.url = original_target + return result + + RequestsTransport.send = combined_call + + # store info pertinent to the test in a dictionary that other fixtures can access + test_info = {"variables": variables} + yield test_info # yield and allow test to run + + RequestsTransport.send = original_transport_func # test finished running -- tear down + + if hasattr(request.node, "test_error"): + # Exceptions are logged here instead of being raised because of how pytest handles error raising from inside + # fixtures and hooks. Raising from a fixture raises an error in addition to the test failure report, and the + # test proxy error is logged before the test failure output (making it difficult to find in pytest output). + # Raising from a hook isn't allowed, and produces an internal error that disrupts test execution. + # ResourceNotFoundErrors during playback indicate a recording mismatch + error = request.node.test_error + if isinstance(error, ResourceNotFoundError): + error_body = ContentDecodePolicy.deserialize_from_http_generics(error.response) + message = error_body.get("message") or error_body.get("Message") + logger = logging.getLogger() + logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") + + stop_record_or_playback(test_id, recording_id, variables) + + +@pytest.fixture +def variable_recorder(recorded_test) -> "Dict[str, str]": + """Fixture that invokes the `recorded_test` fixture and returns a dictionary of recorded test variables. + + :param function recorded_test: The fixture responsible for redirecting network traffic to target the test proxy. + This should return a dictionary containing information about the current test -- in particular, the variables + that were recorded with the test. + + :returns: A dictionary that maps test variables to string values. If no variable dictionary was stored when the test + was recorded, this returns an empty dictionary. + """ + return recorded_test["variables"] From 87c9f25f2dcc9ddcb5e0c422a5035366d77d4d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Mon, 11 Jul 2022 18:35:57 -0700 Subject: [PATCH 05/13] Clean things up --- sdk/conftest.py | 2 +- tools/azure-sdk-tools/devtools_testutils/__init__.py | 4 +++- .../azure-sdk-tools/devtools_testutils/proxy_testcase.py | 8 +++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sdk/conftest.py b/sdk/conftest.py index 18a293e907c6..f98006995cb3 100644 --- a/sdk/conftest.py +++ b/sdk/conftest.py @@ -26,7 +26,7 @@ import os import pytest -from devtools_testutils.proxy_testcase import recorded_test, start_proxy_session, variable_recorder +from devtools_testutils import recorded_test, test_proxy, variable_recorder def pytest_configure(config): diff --git a/tools/azure-sdk-tools/devtools_testutils/__init__.py b/tools/azure-sdk-tools/devtools_testutils/__init__.py index 02d2e7791178..8aad9bad74dd 100644 --- a/tools/azure-sdk-tools/devtools_testutils/__init__.py +++ b/tools/azure-sdk-tools/devtools_testutils/__init__.py @@ -19,7 +19,7 @@ from .envvariable_loader import EnvironmentVariableLoader PowerShellPreparer = EnvironmentVariableLoader # Backward compat from .proxy_startup import start_test_proxy, stop_test_proxy, test_proxy -from .proxy_testcase import recorded_by_proxy +from .proxy_testcase import recorded_by_proxy, recorded_test, variable_recorder from .sanitizers import ( add_body_key_sanitizer, add_body_regex_sanitizer, @@ -66,12 +66,14 @@ "PowerShellPreparer", "EnvironmentVariableLoader", "recorded_by_proxy", + "recorded_test", "test_proxy", "set_bodiless_matcher", "set_custom_default_matcher", "set_default_settings", "start_test_proxy", "stop_test_proxy", + "variable_recorder", "ResponseCallback", "RetryCounter", "FakeTokenCredential", diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index e0c14a2c07f6..3ed645a6a8db 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -21,6 +21,7 @@ from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function from .config import PROXY_URL from .helpers import get_test_id, is_live, is_live_and_not_recording, set_recording_id +from .proxy_startup import test_proxy if TYPE_CHECKING: from typing import Any, Dict, Optional, Tuple @@ -195,7 +196,6 @@ def combined_call(*args, **kwargs): return record_wrap -@pytest.fixture def start_proxy_session() -> "Optional[Tuple[str, str, Dict[str, str]]]": """Begins a playback or recording session and returns the current test ID, recording ID, and recorded variables. @@ -212,20 +212,18 @@ def start_proxy_session() -> "Optional[Tuple[str, str, Dict[str, str]]]": @pytest.fixture -def recorded_test(test_proxy, start_proxy_session, request) -> "Dict[str, Any]": +def recorded_test(test_proxy, request) -> "Dict[str, Any]": """Fixture that redirects network requests to target the azure-sdk-tools test proxy. Use with recorded tests. For more details and usage examples, refer to https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md. :param function test_proxy: The fixture responsible for starting up the test proxy server. - :param function start_proxy_session: The fixture responsible for starting a recording or playback session. This - should yield a tuple with a test ID, recording ID, and dictionary of recorded test variables. :param function request: The built-in `request` fixture. :yields: A dictionary containing information relevant to the currently executing test. """ - test_id, recording_id, variables = start_proxy_session + test_id, recording_id, variables = start_proxy_session() original_transport_func = RequestsTransport.send def transform_args(*args, **kwargs): From c8b5fd064652a14e7c42c1f2557a1958a45ec58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Thu, 14 Jul 2022 17:31:21 -0700 Subject: [PATCH 06/13] Auto-applying across test class --- .../tests/test_key_client.py | 19 +------------------ .../tests/test_table_batch.py | 8 +++----- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_key_client.py b/sdk/keyvault/azure-keyvault-keys/tests/test_key_client.py index ed0caf5f9f0b..61d1a793812c 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_key_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_key_client.py @@ -62,6 +62,7 @@ def emit(self, record): self.messages.append(record) +@pytest.mark.usefixtures("recorded_test", "variable_recorder") class TestKeyClient(KeyVaultTestCase, KeysTestCase): def _assert_jwks_equal(self, jwk1, jwk2): for field in JsonWebKey._FIELDS: @@ -177,7 +178,6 @@ def _to_bytes(hex): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() - @recorded_by_proxy def test_key_crud_operations(self, client, is_hsm, **kwargs): set_bodiless_matcher() assert client is not None @@ -242,7 +242,6 @@ def test_key_crud_operations(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_hsm) @KeysClientPreparer() - @recorded_by_proxy def test_rsa_public_exponent(self, client, **kwargs): """The public exponent of a Managed HSM RSA key can be specified during creation""" set_bodiless_matcher() @@ -255,7 +254,6 @@ def test_rsa_public_exponent(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() - @recorded_by_proxy def test_backup_restore(self, client, is_hsm, **kwargs): set_bodiless_matcher() assert client is not None @@ -282,7 +280,6 @@ def test_backup_restore(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() - @recorded_by_proxy def test_key_list(self, client, is_hsm, **kwargs): set_bodiless_matcher() @@ -307,7 +304,6 @@ def test_key_list(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() - @recorded_by_proxy def test_list_versions(self, client, is_hsm, **kwargs): assert client is not None @@ -333,7 +329,6 @@ def test_list_versions(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() - @recorded_by_proxy def test_list_deleted_keys(self, client, is_hsm, **kwargs): set_bodiless_matcher() assert client is not None @@ -364,7 +359,6 @@ def test_list_deleted_keys(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() - @recorded_by_proxy def test_recover(self, client, is_hsm, **kwargs): set_bodiless_matcher() assert client is not None @@ -391,7 +385,6 @@ def test_recover(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() - @recorded_by_proxy def test_purge(self, client, is_hsm, **kwargs): set_bodiless_matcher() assert client is not None @@ -423,7 +416,6 @@ def test_purge(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",logging_enabled) @KeysClientPreparer(logging_enable = True) - @recorded_by_proxy def test_logging_enabled(self, client, is_hsm, **kwargs): mock_handler = MockHandler() @@ -458,7 +450,6 @@ def test_logging_enabled(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",logging_enabled) @KeysClientPreparer(logging_enable = False) - @recorded_by_proxy def test_logging_disabled(self, client, is_hsm, **kwargs): mock_handler = MockHandler() @@ -492,7 +483,6 @@ def test_logging_disabled(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_hsm_7_3) @KeysClientPreparer() - @recorded_by_proxy def test_get_random_bytes(self, client, **kwargs): assert client @@ -508,7 +498,6 @@ def test_get_random_bytes(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_7_3) @KeysClientPreparer() - @recorded_by_proxy def test_key_release(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -528,7 +517,6 @@ def test_key_release(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_hsm_7_3) @KeysClientPreparer() - @recorded_by_proxy def test_imported_key_release(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -548,7 +536,6 @@ def test_imported_key_release(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_7_3) @KeysClientPreparer() - @recorded_by_proxy def test_update_release_policy(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -590,7 +577,6 @@ def test_update_release_policy(self, client, **kwargs): #Immutable policies aren't currently supported on Managed HSM @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @KeysClientPreparer() - @recorded_by_proxy def test_immutable_release_policy(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -624,7 +610,6 @@ def test_immutable_release_policy(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @KeysClientPreparer() - @recorded_by_proxy def test_key_rotation(self, client, **kwargs): set_bodiless_matcher() if (not is_public_cloud() and self.is_live): @@ -641,7 +626,6 @@ def test_key_rotation(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @KeysClientPreparer() - @recorded_by_proxy def test_key_rotation_policy(self, client, **kwargs): set_bodiless_matcher() if (not is_public_cloud() and self.is_live): @@ -709,7 +693,6 @@ def test_key_rotation_policy(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() - @recorded_by_proxy def test_get_cryptography_client(self, client, is_hsm, **kwargs): key_name = self.get_resource_name("key-name") key = self._create_rsa_key(client, key_name, hardware_protected=is_hsm) diff --git a/sdk/tables/azure-data-tables/tests/test_table_batch.py b/sdk/tables/azure-data-tables/tests/test_table_batch.py index 722871d60176..07a37618ce26 100644 --- a/sdk/tables/azure-data-tables/tests/test_table_batch.py +++ b/sdk/tables/azure-data-tables/tests/test_table_batch.py @@ -282,11 +282,9 @@ def test_batch_update_if_doesnt_match(self, tables_storage_account_name, tables_ @tables_decorator_with_wraps def test_batch_single_op_if_doesnt_match(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers - # set_custom_default_matcher( - # compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" - # ) - - # Above section is intentionally commented to trigger a playback error, to show how error raising is handled + set_custom_default_matcher( + compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" + ) # variable_recorder directly returns the dictionary containing recorded variables. In live mode, this is an # empty dictionary; in playback mode, this is populated with any variables that were recorded previously. From 9aca122178097815c9bb05d68a482583572a87a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Thu, 14 Jul 2022 18:16:37 -0700 Subject: [PATCH 07/13] Support async tests w/ same fixture --- .../tests/test_keys_async.py | 19 +-- .../devtools_testutils/proxy_testcase.py | 113 ++++++++++++++++-- 2 files changed, 103 insertions(+), 29 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py index 666c5ed63db6..df7e3dabea84 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py @@ -52,6 +52,7 @@ def emit(self, record): self.messages.append(record) +@pytest.mark.usefixtures("recorded_test", "variable_recorder") class TestKeyVaultKey(KeyVaultTestCase, KeysTestCase): def _assert_jwks_equal(self, jwk1, jwk2): @@ -175,7 +176,6 @@ def _to_bytes(hex): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_key_crud_operations(self, client, is_hsm, **kwargs): assert client is not None @@ -242,7 +242,6 @@ async def test_key_crud_operations(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_hsm) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_rsa_public_exponent(self, client, **kwargs): """The public exponent of a Managed HSM RSA key can be specified during creation""" assert client is not None @@ -255,7 +254,6 @@ async def test_rsa_public_exponent(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_backup_restore(self, client, is_hsm, **kwargs): assert client is not None @@ -283,7 +281,6 @@ async def test_backup_restore(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_key_list(self, client, is_hsm, **kwargs): assert client is not None @@ -307,7 +304,6 @@ async def test_key_list(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_list_versions(self, client, is_hsm, **kwargs): assert client is not None @@ -334,7 +330,6 @@ async def test_list_versions(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_list_deleted_keys(self, client, is_hsm, **kwargs): assert client is not None @@ -366,7 +361,6 @@ async def test_list_deleted_keys(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_recover(self, client, is_hsm, **kwargs): assert client is not None @@ -397,7 +391,6 @@ async def test_recover(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_purge(self, client, is_hsm, **kwargs): assert client is not None @@ -425,7 +418,6 @@ async def test_purge(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",logging_enabled) @AsyncKeysClientPreparer(logging_enable = True) - @recorded_by_proxy_async async def test_logging_enabled(self, client, is_hsm, **kwargs): mock_handler = MockHandler() @@ -461,7 +453,6 @@ async def test_logging_enabled(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",logging_disabled) @AsyncKeysClientPreparer(logging_enable = False) - @recorded_by_proxy_async async def test_logging_disabled(self, client, is_hsm, **kwargs): mock_handler = MockHandler() @@ -496,7 +487,6 @@ async def test_logging_disabled(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_hsm_7_3) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_get_random_bytes(self, client, **kwargs): assert client @@ -513,7 +503,6 @@ async def test_get_random_bytes(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_7_3) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_key_release(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -534,7 +523,6 @@ async def test_key_release(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_hsm_7_3) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_imported_key_release(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -555,7 +543,6 @@ async def test_imported_key_release(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_7_3) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_update_release_policy(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -598,7 +585,6 @@ async def test_update_release_policy(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_immutable_release_policy(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -633,7 +619,6 @@ async def test_immutable_release_policy(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_key_rotation(self, client, **kwargs): set_bodiless_matcher() if (not is_public_cloud() and self.is_live): @@ -651,7 +636,6 @@ async def test_key_rotation(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_key_rotation_policy(self, client, **kwargs): set_bodiless_matcher() if (not is_public_cloud() and self.is_live): @@ -724,7 +708,6 @@ async def test_key_rotation_policy(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() - @recorded_by_proxy_async async def test_get_cryptography_client(self, client, is_hsm, **kwargs): key_name = self.get_resource_name("key-name") key = await self._create_rsa_key(client, key_name, hardware_protected=is_hsm) diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 3ed645a6a8db..f37a63b95b8b 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- +from inspect import iscoroutinefunction import logging import requests import six @@ -24,7 +25,7 @@ from .proxy_startup import test_proxy if TYPE_CHECKING: - from typing import Any, Dict, Optional, Tuple + from typing import Any, Callable, Dict, Optional, Tuple from azure.core.pipeline.transport import HttpRequest # To learn about how to migrate SDK tests to the test proxy, please refer to the migration guide at @@ -126,7 +127,7 @@ def transform_request(request: "HttpRequest", recording_id: str) -> None: request.url = updated_target -def recorded_by_proxy(test_func) -> None: +def recorded_by_proxy(test_func: "Callable") -> None: """Decorator that redirects network requests to target the azure-sdk-tools test proxy. Use with recorded tests. For more details and usage examples, refer to @@ -212,10 +213,10 @@ def start_proxy_session() -> "Optional[Tuple[str, str, Dict[str, str]]]": @pytest.fixture -def recorded_test(test_proxy, request) -> "Dict[str, Any]": - """Fixture that redirects network requests to target the azure-sdk-tools test proxy. Use with recorded tests. +async def recorded_test(test_proxy: None, request: pytest.FixtureRequest) -> "Dict[str, Any]": + """Fixture that redirects network requests to target the azure-sdk-tools test proxy. - For more details and usage examples, refer to + Use with recorded tests. For more details and usage examples, refer to https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md. :param function test_proxy: The fixture responsible for starting up the test proxy server. @@ -223,7 +224,67 @@ def recorded_test(test_proxy, request) -> "Dict[str, Any]": :yields: A dictionary containing information relevant to the currently executing test. """ + test_id, recording_id, variables = start_proxy_session() + + # True if the function requesting the fixture is an async test + if iscoroutinefunction(request._pyfuncitem.function): + original_transport_func = await redirect_async_traffic(recording_id) + yield {"variables": variables} # yield relevant test info and allow tests to run + restore_async_traffic(original_transport_func, request) + else: + original_transport_func = redirect_traffic(recording_id) + yield {"variables": variables} # yield relevant test info and allow tests to run + restore_traffic(original_transport_func, request) + + stop_record_or_playback(test_id, recording_id, variables) + + +async def redirect_async_traffic(recording_id: str) -> "Callable": + """Redirects asynchronous network requests to target the test proxy. + + :param str recording_id: Recording ID of the currently executing test. + + :returns: The original transport function used by the currently executing test. + """ + from azure.core.pipeline.transport import AioHttpTransport + + original_transport_func = AioHttpTransport.send + + def transform_args(*args, **kwargs): + copied_positional_args = list(args) + request = copied_positional_args[1] + + transform_request(request, recording_id) + + return tuple(copied_positional_args), kwargs + + async def combined_call(*args, **kwargs): + adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) + result = await original_transport_func(*adjusted_args, **adjusted_kwargs) + + # make the x-recording-upstream-base-uri the URL of the request + # this makes the request look like it was made to the original endpoint instead of to the proxy + # without this, things like LROPollers can get broken by polling the wrong endpoint + parsed_result = url_parse.urlparse(result.request.url) + upstream_uri = url_parse.urlparse(result.request.headers["x-recording-upstream-base-uri"]) + upstream_uri_dict = {"scheme": upstream_uri.scheme, "netloc": upstream_uri.netloc} + original_target = parsed_result._replace(**upstream_uri_dict).geturl() + + result.request.url = original_target + return result + + AioHttpTransport.send = combined_call + return original_transport_func + + +def redirect_traffic(recording_id: str) -> "Callable": + """Redirects network requests to target the test proxy. + + :param str recording_id: Recording ID of the currently executing test. + + :returns: The original transport function used by the currently executing test. + """ original_transport_func = RequestsTransport.send def transform_args(*args, **kwargs): @@ -250,12 +311,20 @@ def combined_call(*args, **kwargs): return result RequestsTransport.send = combined_call + return original_transport_func - # store info pertinent to the test in a dictionary that other fixtures can access - test_info = {"variables": variables} - yield test_info # yield and allow test to run - RequestsTransport.send = original_transport_func # test finished running -- tear down +def restore_async_traffic(original_transport_func: "Callable", request: pytest.FixtureRequest) -> None: + """Resets asynchronous network traffic to no longer target the test proxy. + + :param original_transport_func: The original transport function used by the currently executing test. + :type original_transport_func: Callable + :param request: The built-in `request` pytest fixture. + :type request: ~pytest.FixtureRequest + """ + from azure.core.pipeline.transport import AioHttpTransport + + AioHttpTransport.send = original_transport_func # test finished running -- tear down if hasattr(request.node, "test_error"): # Exceptions are logged here instead of being raised because of how pytest handles error raising from inside @@ -270,11 +339,33 @@ def combined_call(*args, **kwargs): logger = logging.getLogger() logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") - stop_record_or_playback(test_id, recording_id, variables) + +def restore_traffic(original_transport_func: "Callable", request: pytest.FixtureRequest) -> None: + """Resets network traffic to no longer target the test proxy. + + :param original_transport_func: The original transport function used by the currently executing test. + :type original_transport_func: Callable + :param request: The built-in `request` pytest fixture. + :type request: ~pytest.FixtureRequest + """ + RequestsTransport.send = original_transport_func # test finished running -- tear down + + if hasattr(request.node, "test_error"): + # Exceptions are logged here instead of being raised because of how pytest handles error raising from inside + # fixtures and hooks. Raising from a fixture raises an error in addition to the test failure report, and the + # test proxy error is logged before the test failure output (making it difficult to find in pytest output). + # Raising from a hook isn't allowed, and produces an internal error that disrupts test execution. + # ResourceNotFoundErrors during playback indicate a recording mismatch + error = request.node.test_error + if isinstance(error, ResourceNotFoundError): + error_body = ContentDecodePolicy.deserialize_from_http_generics(error.response) + message = error_body.get("message") or error_body.get("Message") + logger = logging.getLogger() + logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") @pytest.fixture -def variable_recorder(recorded_test) -> "Dict[str, str]": +def variable_recorder(recorded_test: "Dict[str, Any]") -> "Dict[str, str]": """Fixture that invokes the `recorded_test` fixture and returns a dictionary of recorded test variables. :param function recorded_test: The fixture responsible for redirecting network traffic to target the test proxy. From 6d5b2ddd8989f3b274e67438591db3102a4ca978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Mon, 18 Jul 2022 14:18:30 -0700 Subject: [PATCH 08/13] Type hints, cspell --- .vscode/cspell.json | 1 + .../devtools_testutils/proxy_testcase.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.vscode/cspell.json b/.vscode/cspell.json index b31e3365ac74..6c3127f7eae3 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -254,6 +254,7 @@ "prebuilts", "pschema", "PSECRET", + "pyfuncitem", "pygobject", "parameterizing", "pytyped", diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index f37a63b95b8b..691c5551b0d3 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -26,6 +26,7 @@ if TYPE_CHECKING: from typing import Any, Callable, Dict, Optional, Tuple + from pytest import FixtureRequest from azure.core.pipeline.transport import HttpRequest # To learn about how to migrate SDK tests to the test proxy, please refer to the migration guide at @@ -213,14 +214,16 @@ def start_proxy_session() -> "Optional[Tuple[str, str, Dict[str, str]]]": @pytest.fixture -async def recorded_test(test_proxy: None, request: pytest.FixtureRequest) -> "Dict[str, Any]": +async def recorded_test(test_proxy: None, request: "FixtureRequest") -> "Dict[str, Any]": """Fixture that redirects network requests to target the azure-sdk-tools test proxy. Use with recorded tests. For more details and usage examples, refer to https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md. - :param function test_proxy: The fixture responsible for starting up the test proxy server. - :param function request: The built-in `request` fixture. + :param test_proxy: The fixture responsible for starting up the test proxy server. + :type test_proxy: None + :param request: The built-in `request` fixture. + :type request: ~pytest.FixtureRequest :yields: A dictionary containing information relevant to the currently executing test. """ @@ -314,7 +317,7 @@ def combined_call(*args, **kwargs): return original_transport_func -def restore_async_traffic(original_transport_func: "Callable", request: pytest.FixtureRequest) -> None: +def restore_async_traffic(original_transport_func: "Callable", request: "FixtureRequest") -> None: """Resets asynchronous network traffic to no longer target the test proxy. :param original_transport_func: The original transport function used by the currently executing test. @@ -340,7 +343,7 @@ def restore_async_traffic(original_transport_func: "Callable", request: pytest.F logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") -def restore_traffic(original_transport_func: "Callable", request: pytest.FixtureRequest) -> None: +def restore_traffic(original_transport_func: "Callable", request: "FixtureRequest") -> None: """Resets network traffic to no longer target the test proxy. :param original_transport_func: The original transport function used by the currently executing test. @@ -368,9 +371,10 @@ def restore_traffic(original_transport_func: "Callable", request: pytest.Fixture def variable_recorder(recorded_test: "Dict[str, Any]") -> "Dict[str, str]": """Fixture that invokes the `recorded_test` fixture and returns a dictionary of recorded test variables. - :param function recorded_test: The fixture responsible for redirecting network traffic to target the test proxy. + :param recorded_test: The fixture responsible for redirecting network traffic to target the test proxy. This should return a dictionary containing information about the current test -- in particular, the variables that were recorded with the test. + :type recorded_test: Dict[str, Any] :returns: A dictionary that maps test variables to string values. If no variable dictionary was stored when the test was recorded, this returns an empty dictionary. From 013c99ba22f24101957a6cd59c01ed07bb1f200c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Wed, 20 Jul 2022 09:07:31 -0700 Subject: [PATCH 09/13] Variable recording class --- .../devtools_testutils/__init__.py | 3 +- .../devtools_testutils/proxy_testcase.py | 15 ------ .../devtools_testutils/variable_recorder.py | 49 +++++++++++++++++++ 3 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 tools/azure-sdk-tools/devtools_testutils/variable_recorder.py diff --git a/tools/azure-sdk-tools/devtools_testutils/__init__.py b/tools/azure-sdk-tools/devtools_testutils/__init__.py index 8aad9bad74dd..9d57cd551d20 100644 --- a/tools/azure-sdk-tools/devtools_testutils/__init__.py +++ b/tools/azure-sdk-tools/devtools_testutils/__init__.py @@ -19,7 +19,7 @@ from .envvariable_loader import EnvironmentVariableLoader PowerShellPreparer = EnvironmentVariableLoader # Backward compat from .proxy_startup import start_test_proxy, stop_test_proxy, test_proxy -from .proxy_testcase import recorded_by_proxy, recorded_test, variable_recorder +from .proxy_testcase import recorded_by_proxy, recorded_test from .sanitizers import ( add_body_key_sanitizer, add_body_regex_sanitizer, @@ -34,6 +34,7 @@ set_custom_default_matcher, set_default_settings, ) +from .variable_recorder import variable_recorder from .helpers import ResponseCallback, RetryCounter from .fake_credentials import FakeTokenCredential diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 691c5551b0d3..cf1cfc7ec3de 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -365,18 +365,3 @@ def restore_traffic(original_transport_func: "Callable", request: "FixtureReques message = error_body.get("message") or error_body.get("Message") logger = logging.getLogger() logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") - - -@pytest.fixture -def variable_recorder(recorded_test: "Dict[str, Any]") -> "Dict[str, str]": - """Fixture that invokes the `recorded_test` fixture and returns a dictionary of recorded test variables. - - :param recorded_test: The fixture responsible for redirecting network traffic to target the test proxy. - This should return a dictionary containing information about the current test -- in particular, the variables - that were recorded with the test. - :type recorded_test: Dict[str, Any] - - :returns: A dictionary that maps test variables to string values. If no variable dictionary was stored when the test - was recorded, this returns an empty dictionary. - """ - return recorded_test["variables"] diff --git a/tools/azure-sdk-tools/devtools_testutils/variable_recorder.py b/tools/azure-sdk-tools/devtools_testutils/variable_recorder.py new file mode 100644 index 000000000000..ba5b5032cacf --- /dev/null +++ b/tools/azure-sdk-tools/devtools_testutils/variable_recorder.py @@ -0,0 +1,49 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from typing import TYPE_CHECKING + +import pytest + +from .proxy_testcase import recorded_test + +if TYPE_CHECKING: + from typing import Any, Dict + + +class VariableRecorder(): + def __init__(self, variables: "Dict[str, str]") -> None: + self.variables = variables + + def get_or_record(self, variable: str, default: str) -> str: + """Returns the recorded value of `variable`, or records and returns `default` as the value for `variable`. + + In recording mode, `get_or_record("a", "b")` will record "b" for the value of the variable `a` and return "b". + In playback, it will return the recorded value of `a`. This is an analogue of a Python dictionary's `setdefault` + method: https://docs.python.org/library/stdtypes.html#dict.setdefault. + + :param str variable: The name of the variable to search the value of, or record a value for. + :param str default: The variable value to record. + + :returns: str + """ + if not isinstance(default, str): + raise ValueError('"default" must be a string. The test proxy cannot record non-string variable values.') + return self.variables.setdefault(variable, default) + + +@pytest.fixture +def variable_recorder(recorded_test: "Dict[str, Any]") -> "Dict[str, str]": + """Fixture that invokes the `recorded_test` fixture and returns a dictionary of recorded test variables. + + :param recorded_test: The fixture responsible for redirecting network traffic to target the test proxy. + This should return a dictionary containing information about the current test -- in particular, the variables + that were recorded with the test. + :type recorded_test: Dict[str, Any] + + :returns: A dictionary that maps test variables to string values. If no variable dictionary was stored when the test + was recorded, this returns an empty dictionary. + """ + return recorded_test["variables"] From 2d60c41a00bab1b99ec18cb591f5baf0fb7ab304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Wed, 20 Jul 2022 12:58:05 -0700 Subject: [PATCH 10/13] Reorganize and rebase --- .../devtools_testutils/__init__.py | 4 +- .../devtools_testutils/proxy_fixtures.py | 233 ++++++++++++++++++ .../devtools_testutils/proxy_testcase.py | 175 +------------ .../devtools_testutils/variable_recorder.py | 49 ---- 4 files changed, 236 insertions(+), 225 deletions(-) create mode 100644 tools/azure-sdk-tools/devtools_testutils/proxy_fixtures.py delete mode 100644 tools/azure-sdk-tools/devtools_testutils/variable_recorder.py diff --git a/tools/azure-sdk-tools/devtools_testutils/__init__.py b/tools/azure-sdk-tools/devtools_testutils/__init__.py index 9d57cd551d20..632e60c2d2d2 100644 --- a/tools/azure-sdk-tools/devtools_testutils/__init__.py +++ b/tools/azure-sdk-tools/devtools_testutils/__init__.py @@ -18,8 +18,9 @@ # cSpell:disable from .envvariable_loader import EnvironmentVariableLoader PowerShellPreparer = EnvironmentVariableLoader # Backward compat +from .proxy_fixtures import recorded_test, variable_recorder from .proxy_startup import start_test_proxy, stop_test_proxy, test_proxy -from .proxy_testcase import recorded_by_proxy, recorded_test +from .proxy_testcase import recorded_by_proxy from .sanitizers import ( add_body_key_sanitizer, add_body_regex_sanitizer, @@ -34,7 +35,6 @@ set_custom_default_matcher, set_default_settings, ) -from .variable_recorder import variable_recorder from .helpers import ResponseCallback, RetryCounter from .fake_credentials import FakeTokenCredential diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_fixtures.py b/tools/azure-sdk-tools/devtools_testutils/proxy_fixtures.py new file mode 100644 index 000000000000..acdddc30fa17 --- /dev/null +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_fixtures.py @@ -0,0 +1,233 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +from inspect import iscoroutinefunction +import logging +from typing import TYPE_CHECKING +import urllib.parse as url_parse + +import pytest + +from azure.core.exceptions import ResourceNotFoundError +from azure.core.pipeline.policies import ContentDecodePolicy + +# the functions we patch +from azure.core.pipeline.transport import RequestsTransport + +from .helpers import get_test_id, is_live_and_not_recording +from .proxy_testcase import start_record_or_playback, stop_record_or_playback, transform_request +from .proxy_startup import test_proxy + +if TYPE_CHECKING: + from typing import Any, Callable, Dict, Optional, Tuple + from pytest import FixtureRequest + + +@pytest.fixture +async def recorded_test(test_proxy: None, request: "FixtureRequest") -> "Dict[str, Any]": + """Fixture that redirects network requests to target the azure-sdk-tools test proxy. + + Use with recorded tests. For more details and usage examples, refer to + https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md. + + :param test_proxy: The fixture responsible for starting up the test proxy server. + :type test_proxy: None + :param request: The built-in `request` fixture. + :type request: ~pytest.FixtureRequest + + :yields: A dictionary containing information relevant to the currently executing test. + """ + + test_id, recording_id, variables = start_proxy_session() + + # True if the function requesting the fixture is an async test + if iscoroutinefunction(request._pyfuncitem.function): + original_transport_func = await redirect_async_traffic(recording_id) + yield {"variables": variables} # yield relevant test info and allow tests to run + restore_async_traffic(original_transport_func, request) + else: + original_transport_func = redirect_traffic(recording_id) + yield {"variables": variables} # yield relevant test info and allow tests to run + restore_traffic(original_transport_func, request) + + stop_record_or_playback(test_id, recording_id, variables) + + +@pytest.fixture +def variable_recorder(recorded_test: "Dict[str, Any]") -> "Dict[str, str]": + """Fixture that invokes the `recorded_test` fixture and returns a dictionary of recorded test variables. + + :param recorded_test: The fixture responsible for redirecting network traffic to target the test proxy. + This should return a dictionary containing information about the current test -- in particular, the variables + that were recorded with the test. + :type recorded_test: Dict[str, Any] + + :returns: A dictionary that maps test variables to string values. If no variable dictionary was stored when the test + was recorded, this returns an empty dictionary. + """ + return VariableRecorder(recorded_test["variables"]) + + +# ----------HELPERS---------- + + +class VariableRecorder(): + def __init__(self, variables: "Dict[str, str]") -> None: + self.variables = variables + + def get_or_record(self, variable: str, default: str) -> str: + """Returns the recorded value of `variable`, or records and returns `default` as the value for `variable`. + + In recording mode, `get_or_record("a", "b")` will record "b" for the value of the variable `a` and return "b". + In playback, it will return the recorded value of `a`. This is an analogue of a Python dictionary's `setdefault` + method: https://docs.python.org/library/stdtypes.html#dict.setdefault. + + :param str variable: The name of the variable to search the value of, or record a value for. + :param str default: The variable value to record. + + :returns: str + """ + if not isinstance(default, str): + raise ValueError('"default" must be a string. The test proxy cannot record non-string variable values.') + return self.variables.setdefault(variable, default) + + +def start_proxy_session() -> "Optional[Tuple[str, str, Dict[str, str]]]": + """Begins a playback or recording session and returns the current test ID, recording ID, and recorded variables. + + :returns: A tuple, (a, b, c), where a is the test ID, b is the recording ID, and c is the `variables` dictionary + that maps test variables to string values. If no variable dictionary was stored when the test was recorded, c is + an empty dictionary. If the current test session is live but recording is disabled, this returns None. + """ + if is_live_and_not_recording(): + return + + test_id = get_test_id() + recording_id, variables = start_record_or_playback(test_id) + return (test_id, recording_id, variables) + + +async def redirect_async_traffic(recording_id: str) -> "Callable": + """Redirects asynchronous network requests to target the test proxy. + + :param str recording_id: Recording ID of the currently executing test. + + :returns: The original transport function used by the currently executing test. + """ + from azure.core.pipeline.transport import AioHttpTransport + + original_transport_func = AioHttpTransport.send + + def transform_args(*args, **kwargs): + copied_positional_args = list(args) + request = copied_positional_args[1] + + transform_request(request, recording_id) + + return tuple(copied_positional_args), kwargs + + async def combined_call(*args, **kwargs): + adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) + result = await original_transport_func(*adjusted_args, **adjusted_kwargs) + + # make the x-recording-upstream-base-uri the URL of the request + # this makes the request look like it was made to the original endpoint instead of to the proxy + # without this, things like LROPollers can get broken by polling the wrong endpoint + parsed_result = url_parse.urlparse(result.request.url) + upstream_uri = url_parse.urlparse(result.request.headers["x-recording-upstream-base-uri"]) + upstream_uri_dict = {"scheme": upstream_uri.scheme, "netloc": upstream_uri.netloc} + original_target = parsed_result._replace(**upstream_uri_dict).geturl() + + result.request.url = original_target + return result + + AioHttpTransport.send = combined_call + return original_transport_func + + +def redirect_traffic(recording_id: str) -> "Callable": + """Redirects network requests to target the test proxy. + + :param str recording_id: Recording ID of the currently executing test. + + :returns: The original transport function used by the currently executing test. + """ + original_transport_func = RequestsTransport.send + + def transform_args(*args, **kwargs): + copied_positional_args = list(args) + http_request = copied_positional_args[1] + + transform_request(http_request, recording_id) + + return tuple(copied_positional_args), kwargs + + def combined_call(*args, **kwargs): + adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) + result = original_transport_func(*adjusted_args, **adjusted_kwargs) + + # make the x-recording-upstream-base-uri the URL of the request + # this makes the request look like it was made to the original endpoint instead of to the proxy + # without this, things like LROPollers can get broken by polling the wrong endpoint + parsed_result = url_parse.urlparse(result.request.url) + upstream_uri = url_parse.urlparse(result.request.headers["x-recording-upstream-base-uri"]) + upstream_uri_dict = {"scheme": upstream_uri.scheme, "netloc": upstream_uri.netloc} + original_target = parsed_result._replace(**upstream_uri_dict).geturl() + + result.request.url = original_target + return result + + RequestsTransport.send = combined_call + return original_transport_func + + +def restore_async_traffic(original_transport_func: "Callable", request: "FixtureRequest") -> None: + """Resets asynchronous network traffic to no longer target the test proxy. + + :param original_transport_func: The original transport function used by the currently executing test. + :type original_transport_func: Callable + :param request: The built-in `request` pytest fixture. + :type request: ~pytest.FixtureRequest + """ + from azure.core.pipeline.transport import AioHttpTransport + + AioHttpTransport.send = original_transport_func # test finished running -- tear down + + if hasattr(request.node, "test_error"): + # Exceptions are logged here instead of being raised because of how pytest handles error raising from inside + # fixtures and hooks. Raising from a fixture raises an error in addition to the test failure report, and the + # test proxy error is logged before the test failure output (making it difficult to find in pytest output). + # Raising from a hook isn't allowed, and produces an internal error that disrupts test execution. + # ResourceNotFoundErrors during playback indicate a recording mismatch + error = request.node.test_error + if isinstance(error, ResourceNotFoundError): + error_body = ContentDecodePolicy.deserialize_from_http_generics(error.response) + message = error_body.get("message") or error_body.get("Message") + logger = logging.getLogger() + logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") + + +def restore_traffic(original_transport_func: "Callable", request: "FixtureRequest") -> None: + """Resets network traffic to no longer target the test proxy. + + :param original_transport_func: The original transport function used by the currently executing test. + :type original_transport_func: Callable + :param request: The built-in `request` pytest fixture. + :type request: ~pytest.FixtureRequest + """ + RequestsTransport.send = original_transport_func # test finished running -- tear down + + if hasattr(request.node, "test_error"): + # Exceptions are logged here instead of being raised because of how pytest handles error raising from inside + # fixtures and hooks. Raising from a fixture raises an error in addition to the test failure report, and the + # test proxy error is logged before the test failure output (making it difficult to find in pytest output). + # Raising from a hook isn't allowed, and produces an internal error that disrupts test execution. + # ResourceNotFoundErrors during playback indicate a recording mismatch + error = request.node.test_error + if isinstance(error, ResourceNotFoundError): + error_body = ContentDecodePolicy.deserialize_from_http_generics(error.response) + message = error_body.get("message") or error_body.get("Message") + logger = logging.getLogger() + logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index cf1cfc7ec3de..9d86ccfbcfe2 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -3,15 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # -------------------------------------------------------------------------- -from inspect import iscoroutinefunction import logging import requests import six from typing import TYPE_CHECKING import urllib.parse as url_parse -import pytest - from azure.core.exceptions import HttpResponseError, ResourceNotFoundError from azure.core.pipeline.policies import ContentDecodePolicy @@ -25,8 +22,7 @@ from .proxy_startup import test_proxy if TYPE_CHECKING: - from typing import Any, Callable, Dict, Optional, Tuple - from pytest import FixtureRequest + from typing import Callable, Dict, Tuple from azure.core.pipeline.transport import HttpRequest # To learn about how to migrate SDK tests to the test proxy, please refer to the migration guide at @@ -196,172 +192,3 @@ def combined_call(*args, **kwargs): return test_variables return record_wrap - - -def start_proxy_session() -> "Optional[Tuple[str, str, Dict[str, str]]]": - """Begins a playback or recording session and returns the current test ID, recording ID, and recorded variables. - - :returns: A tuple, (a, b, c), where a is the test ID, b is the recording ID, and c is the `variables` dictionary - that maps test variables to string values. If no variable dictionary was stored when the test was recorded, c is - an empty dictionary. If the current test session is live but recording is disabled, this returns None. - """ - if is_live_and_not_recording(): - return - - test_id = get_test_id() - recording_id, variables = start_record_or_playback(test_id) - return (test_id, recording_id, variables) - - -@pytest.fixture -async def recorded_test(test_proxy: None, request: "FixtureRequest") -> "Dict[str, Any]": - """Fixture that redirects network requests to target the azure-sdk-tools test proxy. - - Use with recorded tests. For more details and usage examples, refer to - https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md. - - :param test_proxy: The fixture responsible for starting up the test proxy server. - :type test_proxy: None - :param request: The built-in `request` fixture. - :type request: ~pytest.FixtureRequest - - :yields: A dictionary containing information relevant to the currently executing test. - """ - - test_id, recording_id, variables = start_proxy_session() - - # True if the function requesting the fixture is an async test - if iscoroutinefunction(request._pyfuncitem.function): - original_transport_func = await redirect_async_traffic(recording_id) - yield {"variables": variables} # yield relevant test info and allow tests to run - restore_async_traffic(original_transport_func, request) - else: - original_transport_func = redirect_traffic(recording_id) - yield {"variables": variables} # yield relevant test info and allow tests to run - restore_traffic(original_transport_func, request) - - stop_record_or_playback(test_id, recording_id, variables) - - -async def redirect_async_traffic(recording_id: str) -> "Callable": - """Redirects asynchronous network requests to target the test proxy. - - :param str recording_id: Recording ID of the currently executing test. - - :returns: The original transport function used by the currently executing test. - """ - from azure.core.pipeline.transport import AioHttpTransport - - original_transport_func = AioHttpTransport.send - - def transform_args(*args, **kwargs): - copied_positional_args = list(args) - request = copied_positional_args[1] - - transform_request(request, recording_id) - - return tuple(copied_positional_args), kwargs - - async def combined_call(*args, **kwargs): - adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) - result = await original_transport_func(*adjusted_args, **adjusted_kwargs) - - # make the x-recording-upstream-base-uri the URL of the request - # this makes the request look like it was made to the original endpoint instead of to the proxy - # without this, things like LROPollers can get broken by polling the wrong endpoint - parsed_result = url_parse.urlparse(result.request.url) - upstream_uri = url_parse.urlparse(result.request.headers["x-recording-upstream-base-uri"]) - upstream_uri_dict = {"scheme": upstream_uri.scheme, "netloc": upstream_uri.netloc} - original_target = parsed_result._replace(**upstream_uri_dict).geturl() - - result.request.url = original_target - return result - - AioHttpTransport.send = combined_call - return original_transport_func - - -def redirect_traffic(recording_id: str) -> "Callable": - """Redirects network requests to target the test proxy. - - :param str recording_id: Recording ID of the currently executing test. - - :returns: The original transport function used by the currently executing test. - """ - original_transport_func = RequestsTransport.send - - def transform_args(*args, **kwargs): - copied_positional_args = list(args) - http_request = copied_positional_args[1] - - transform_request(http_request, recording_id) - - return tuple(copied_positional_args), kwargs - - def combined_call(*args, **kwargs): - adjusted_args, adjusted_kwargs = transform_args(*args, **kwargs) - result = original_transport_func(*adjusted_args, **adjusted_kwargs) - - # make the x-recording-upstream-base-uri the URL of the request - # this makes the request look like it was made to the original endpoint instead of to the proxy - # without this, things like LROPollers can get broken by polling the wrong endpoint - parsed_result = url_parse.urlparse(result.request.url) - upstream_uri = url_parse.urlparse(result.request.headers["x-recording-upstream-base-uri"]) - upstream_uri_dict = {"scheme": upstream_uri.scheme, "netloc": upstream_uri.netloc} - original_target = parsed_result._replace(**upstream_uri_dict).geturl() - - result.request.url = original_target - return result - - RequestsTransport.send = combined_call - return original_transport_func - - -def restore_async_traffic(original_transport_func: "Callable", request: "FixtureRequest") -> None: - """Resets asynchronous network traffic to no longer target the test proxy. - - :param original_transport_func: The original transport function used by the currently executing test. - :type original_transport_func: Callable - :param request: The built-in `request` pytest fixture. - :type request: ~pytest.FixtureRequest - """ - from azure.core.pipeline.transport import AioHttpTransport - - AioHttpTransport.send = original_transport_func # test finished running -- tear down - - if hasattr(request.node, "test_error"): - # Exceptions are logged here instead of being raised because of how pytest handles error raising from inside - # fixtures and hooks. Raising from a fixture raises an error in addition to the test failure report, and the - # test proxy error is logged before the test failure output (making it difficult to find in pytest output). - # Raising from a hook isn't allowed, and produces an internal error that disrupts test execution. - # ResourceNotFoundErrors during playback indicate a recording mismatch - error = request.node.test_error - if isinstance(error, ResourceNotFoundError): - error_body = ContentDecodePolicy.deserialize_from_http_generics(error.response) - message = error_body.get("message") or error_body.get("Message") - logger = logging.getLogger() - logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") - - -def restore_traffic(original_transport_func: "Callable", request: "FixtureRequest") -> None: - """Resets network traffic to no longer target the test proxy. - - :param original_transport_func: The original transport function used by the currently executing test. - :type original_transport_func: Callable - :param request: The built-in `request` pytest fixture. - :type request: ~pytest.FixtureRequest - """ - RequestsTransport.send = original_transport_func # test finished running -- tear down - - if hasattr(request.node, "test_error"): - # Exceptions are logged here instead of being raised because of how pytest handles error raising from inside - # fixtures and hooks. Raising from a fixture raises an error in addition to the test failure report, and the - # test proxy error is logged before the test failure output (making it difficult to find in pytest output). - # Raising from a hook isn't allowed, and produces an internal error that disrupts test execution. - # ResourceNotFoundErrors during playback indicate a recording mismatch - error = request.node.test_error - if isinstance(error, ResourceNotFoundError): - error_body = ContentDecodePolicy.deserialize_from_http_generics(error.response) - message = error_body.get("message") or error_body.get("Message") - logger = logging.getLogger() - logger.error(f"\n\n-----Test proxy playback error:-----\n\n{message}") diff --git a/tools/azure-sdk-tools/devtools_testutils/variable_recorder.py b/tools/azure-sdk-tools/devtools_testutils/variable_recorder.py deleted file mode 100644 index ba5b5032cacf..000000000000 --- a/tools/azure-sdk-tools/devtools_testutils/variable_recorder.py +++ /dev/null @@ -1,49 +0,0 @@ -# ------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for -# license information. -# -------------------------------------------------------------------------- -from typing import TYPE_CHECKING - -import pytest - -from .proxy_testcase import recorded_test - -if TYPE_CHECKING: - from typing import Any, Dict - - -class VariableRecorder(): - def __init__(self, variables: "Dict[str, str]") -> None: - self.variables = variables - - def get_or_record(self, variable: str, default: str) -> str: - """Returns the recorded value of `variable`, or records and returns `default` as the value for `variable`. - - In recording mode, `get_or_record("a", "b")` will record "b" for the value of the variable `a` and return "b". - In playback, it will return the recorded value of `a`. This is an analogue of a Python dictionary's `setdefault` - method: https://docs.python.org/library/stdtypes.html#dict.setdefault. - - :param str variable: The name of the variable to search the value of, or record a value for. - :param str default: The variable value to record. - - :returns: str - """ - if not isinstance(default, str): - raise ValueError('"default" must be a string. The test proxy cannot record non-string variable values.') - return self.variables.setdefault(variable, default) - - -@pytest.fixture -def variable_recorder(recorded_test: "Dict[str, Any]") -> "Dict[str, str]": - """Fixture that invokes the `recorded_test` fixture and returns a dictionary of recorded test variables. - - :param recorded_test: The fixture responsible for redirecting network traffic to target the test proxy. - This should return a dictionary containing information about the current test -- in particular, the variables - that were recorded with the test. - :type recorded_test: Dict[str, Any] - - :returns: A dictionary that maps test variables to string values. If no variable dictionary was stored when the test - was recorded, this returns an empty dictionary. - """ - return recorded_test["variables"] From b7944cd07a2b89ea452ad3116ef43281a83d4af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Wed, 20 Jul 2022 14:22:36 -0700 Subject: [PATCH 11/13] Remove test modifications --- .../tests/test_key_client.py | 19 ++++++++++++++++++- .../tests/test_keys_async.py | 19 ++++++++++++++++++- .../azure-data-tables/tests/preparers.py | 19 ------------------- .../tests/test_table_batch.py | 19 ++++--------------- 4 files changed, 40 insertions(+), 36 deletions(-) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_key_client.py b/sdk/keyvault/azure-keyvault-keys/tests/test_key_client.py index 61d1a793812c..ed0caf5f9f0b 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_key_client.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_key_client.py @@ -62,7 +62,6 @@ def emit(self, record): self.messages.append(record) -@pytest.mark.usefixtures("recorded_test", "variable_recorder") class TestKeyClient(KeyVaultTestCase, KeysTestCase): def _assert_jwks_equal(self, jwk1, jwk2): for field in JsonWebKey._FIELDS: @@ -178,6 +177,7 @@ def _to_bytes(hex): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() + @recorded_by_proxy def test_key_crud_operations(self, client, is_hsm, **kwargs): set_bodiless_matcher() assert client is not None @@ -242,6 +242,7 @@ def test_key_crud_operations(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_hsm) @KeysClientPreparer() + @recorded_by_proxy def test_rsa_public_exponent(self, client, **kwargs): """The public exponent of a Managed HSM RSA key can be specified during creation""" set_bodiless_matcher() @@ -254,6 +255,7 @@ def test_rsa_public_exponent(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() + @recorded_by_proxy def test_backup_restore(self, client, is_hsm, **kwargs): set_bodiless_matcher() assert client is not None @@ -280,6 +282,7 @@ def test_backup_restore(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() + @recorded_by_proxy def test_key_list(self, client, is_hsm, **kwargs): set_bodiless_matcher() @@ -304,6 +307,7 @@ def test_key_list(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() + @recorded_by_proxy def test_list_versions(self, client, is_hsm, **kwargs): assert client is not None @@ -329,6 +333,7 @@ def test_list_versions(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() + @recorded_by_proxy def test_list_deleted_keys(self, client, is_hsm, **kwargs): set_bodiless_matcher() assert client is not None @@ -359,6 +364,7 @@ def test_list_deleted_keys(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() + @recorded_by_proxy def test_recover(self, client, is_hsm, **kwargs): set_bodiless_matcher() assert client is not None @@ -385,6 +391,7 @@ def test_recover(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() + @recorded_by_proxy def test_purge(self, client, is_hsm, **kwargs): set_bodiless_matcher() assert client is not None @@ -416,6 +423,7 @@ def test_purge(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",logging_enabled) @KeysClientPreparer(logging_enable = True) + @recorded_by_proxy def test_logging_enabled(self, client, is_hsm, **kwargs): mock_handler = MockHandler() @@ -450,6 +458,7 @@ def test_logging_enabled(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",logging_enabled) @KeysClientPreparer(logging_enable = False) + @recorded_by_proxy def test_logging_disabled(self, client, is_hsm, **kwargs): mock_handler = MockHandler() @@ -483,6 +492,7 @@ def test_logging_disabled(self, client, is_hsm, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_hsm_7_3) @KeysClientPreparer() + @recorded_by_proxy def test_get_random_bytes(self, client, **kwargs): assert client @@ -498,6 +508,7 @@ def test_get_random_bytes(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_7_3) @KeysClientPreparer() + @recorded_by_proxy def test_key_release(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -517,6 +528,7 @@ def test_key_release(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_hsm_7_3) @KeysClientPreparer() + @recorded_by_proxy def test_imported_key_release(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -536,6 +548,7 @@ def test_imported_key_release(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_7_3) @KeysClientPreparer() + @recorded_by_proxy def test_update_release_policy(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -577,6 +590,7 @@ def test_update_release_policy(self, client, **kwargs): #Immutable policies aren't currently supported on Managed HSM @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @KeysClientPreparer() + @recorded_by_proxy def test_immutable_release_policy(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -610,6 +624,7 @@ def test_immutable_release_policy(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @KeysClientPreparer() + @recorded_by_proxy def test_key_rotation(self, client, **kwargs): set_bodiless_matcher() if (not is_public_cloud() and self.is_live): @@ -626,6 +641,7 @@ def test_key_rotation(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @KeysClientPreparer() + @recorded_by_proxy def test_key_rotation_policy(self, client, **kwargs): set_bodiless_matcher() if (not is_public_cloud() and self.is_live): @@ -693,6 +709,7 @@ def test_key_rotation_policy(self, client, **kwargs): @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @KeysClientPreparer() + @recorded_by_proxy def test_get_cryptography_client(self, client, is_hsm, **kwargs): key_name = self.get_resource_name("key-name") key = self._create_rsa_key(client, key_name, hardware_protected=is_hsm) diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py index df7e3dabea84..666c5ed63db6 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_keys_async.py @@ -52,7 +52,6 @@ def emit(self, record): self.messages.append(record) -@pytest.mark.usefixtures("recorded_test", "variable_recorder") class TestKeyVaultKey(KeyVaultTestCase, KeysTestCase): def _assert_jwks_equal(self, jwk1, jwk2): @@ -176,6 +175,7 @@ def _to_bytes(hex): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_key_crud_operations(self, client, is_hsm, **kwargs): assert client is not None @@ -242,6 +242,7 @@ async def test_key_crud_operations(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_hsm) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_rsa_public_exponent(self, client, **kwargs): """The public exponent of a Managed HSM RSA key can be specified during creation""" assert client is not None @@ -254,6 +255,7 @@ async def test_rsa_public_exponent(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_backup_restore(self, client, is_hsm, **kwargs): assert client is not None @@ -281,6 +283,7 @@ async def test_backup_restore(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_key_list(self, client, is_hsm, **kwargs): assert client is not None @@ -304,6 +307,7 @@ async def test_key_list(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_list_versions(self, client, is_hsm, **kwargs): assert client is not None @@ -330,6 +334,7 @@ async def test_list_versions(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_list_deleted_keys(self, client, is_hsm, **kwargs): assert client is not None @@ -361,6 +366,7 @@ async def test_list_deleted_keys(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_recover(self, client, is_hsm, **kwargs): assert client is not None @@ -391,6 +397,7 @@ async def test_recover(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_purge(self, client, is_hsm, **kwargs): assert client is not None @@ -418,6 +425,7 @@ async def test_purge(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",logging_enabled) @AsyncKeysClientPreparer(logging_enable = True) + @recorded_by_proxy_async async def test_logging_enabled(self, client, is_hsm, **kwargs): mock_handler = MockHandler() @@ -453,6 +461,7 @@ async def test_logging_enabled(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",logging_disabled) @AsyncKeysClientPreparer(logging_enable = False) + @recorded_by_proxy_async async def test_logging_disabled(self, client, is_hsm, **kwargs): mock_handler = MockHandler() @@ -487,6 +496,7 @@ async def test_logging_disabled(self, client, is_hsm, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_hsm_7_3) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_get_random_bytes(self, client, **kwargs): assert client @@ -503,6 +513,7 @@ async def test_get_random_bytes(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_7_3) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_key_release(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -523,6 +534,7 @@ async def test_key_release(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_hsm_7_3) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_imported_key_release(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -543,6 +555,7 @@ async def test_imported_key_release(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_7_3) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_update_release_policy(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -585,6 +598,7 @@ async def test_update_release_policy(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_immutable_release_policy(self, client, **kwargs): set_bodiless_matcher() attestation_uri = self._get_attestation_uri() @@ -619,6 +633,7 @@ async def test_immutable_release_policy(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_key_rotation(self, client, **kwargs): set_bodiless_matcher() if (not is_public_cloud() and self.is_live): @@ -636,6 +651,7 @@ async def test_key_rotation(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",only_vault_7_3) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_key_rotation_policy(self, client, **kwargs): set_bodiless_matcher() if (not is_public_cloud() and self.is_live): @@ -708,6 +724,7 @@ async def test_key_rotation_policy(self, client, **kwargs): @pytest.mark.asyncio @pytest.mark.parametrize("api_version,is_hsm",all_api_versions) @AsyncKeysClientPreparer() + @recorded_by_proxy_async async def test_get_cryptography_client(self, client, is_hsm, **kwargs): key_name = self.get_resource_name("key-name") key = await self._create_rsa_key(client, key_name, hardware_protected=is_hsm) diff --git a/sdk/tables/azure-data-tables/tests/preparers.py b/sdk/tables/azure-data-tables/tests/preparers.py index 0ffab549799f..d4da5573ccfb 100644 --- a/sdk/tables/azure-data-tables/tests/preparers.py +++ b/sdk/tables/azure-data-tables/tests/preparers.py @@ -53,25 +53,6 @@ def wrapper(*args, **kwargs): return wrapper -def tables_decorator_with_wraps(func, **kwargs): - @TablesPreparer() - @functools.wraps(func) - def wrapper(*args, **kwargs): - key = kwargs.pop("tables_primary_storage_account_key") - name = kwargs.pop("tables_storage_account_name") - key = AzureNamedKeyCredential(key=key, name=name) - - kwargs["tables_primary_storage_account_key"] = key - kwargs["tables_storage_account_name"] = name - - trimmed_kwargs = {k: v for k, v in kwargs.items()} - trim_kwargs_from_test_function(func, trimmed_kwargs) - - func(*args, **trimmed_kwargs) - - return wrapper - - def cosmos_decorator(func, **kwargs): @CosmosPreparer() def wrapper(*args, **kwargs): diff --git a/sdk/tables/azure-data-tables/tests/test_table_batch.py b/sdk/tables/azure-data-tables/tests/test_table_batch.py index 07a37618ce26..768f446c1a8a 100644 --- a/sdk/tables/azure-data-tables/tests/test_table_batch.py +++ b/sdk/tables/azure-data-tables/tests/test_table_batch.py @@ -37,7 +37,7 @@ ) from _shared.testcase import TableTestCase -from preparers import tables_decorator, tables_decorator_with_wraps +from preparers import tables_decorator #------------------------------------------------------------------------------ TEST_TABLE_PREFIX = 'table' @@ -279,23 +279,14 @@ def test_batch_update_if_doesnt_match(self, tables_storage_account_name, tables_ self._tear_down() @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") - @tables_decorator_with_wraps - def test_batch_single_op_if_doesnt_match(self, variable_recorder, tables_storage_account_name=None, tables_primary_storage_account_key=None): + @tables_decorator + @recorded_by_proxy + def test_batch_single_op_if_doesnt_match(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( compare_bodies=False, excluded_headers="Authorization,Content-Length,x-ms-client-request-id,x-ms-request-id" ) - # variable_recorder directly returns the dictionary containing recorded variables. In live mode, this is an - # empty dictionary; in playback mode, this is populated with any variables that were recorded previously. - # Because the variable_recorder fixture is a function, we unfortunately don't get any autocomplete with the - # parameter whether it's a dictionary or custom type. - - # A custom type could allow for future APIs, but the `setdefault` method is thoroughly sufficient for now. - # Using `setdefault` will either fetch the recorded value for the variable, or record a new value in live mode, - # without having to check the live status of the test or contents of `variable_recorder`. - variable_value = variable_recorder.setdefault("variable_name", "live_generated_value") - # Arrange self._set_up(tables_storage_account_name, tables_primary_storage_account_key) try: @@ -832,7 +823,6 @@ def test_delete_batch_with_bad_kwarg(self, tables_storage_account_name, tables_p @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") @pytest.mark.live_test_only @tables_decorator - @recorded_by_proxy def test_batch_sas_auth(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( @@ -888,7 +878,6 @@ def test_batch_sas_auth(self, tables_storage_account_name, tables_primary_storag @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires Python3") @pytest.mark.live_test_only # Request bodies are very large @tables_decorator - @recorded_by_proxy def test_batch_request_too_large(self, tables_storage_account_name, tables_primary_storage_account_key): # this can be reverted to set_bodiless_matcher() after tests are re-recorded and don't contain these headers set_custom_default_matcher( From db8c78f9dd90961e26a5ef628161b354128ba384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Wed, 20 Jul 2022 14:24:55 -0700 Subject: [PATCH 12/13] Remove unused import --- tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py index 9d86ccfbcfe2..a0b49718b4d2 100644 --- a/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py +++ b/tools/azure-sdk-tools/devtools_testutils/proxy_testcase.py @@ -19,7 +19,6 @@ from azure_devtools.scenario_tests.utilities import trim_kwargs_from_test_function from .config import PROXY_URL from .helpers import get_test_id, is_live, is_live_and_not_recording, set_recording_id -from .proxy_startup import test_proxy if TYPE_CHECKING: from typing import Callable, Dict, Tuple From 716cf18af079b54014478f708b411bf216bec1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McCoy=20Pati=C3=B1o?= Date: Wed, 20 Jul 2022 15:05:03 -0700 Subject: [PATCH 13/13] Add azure-sdk-tools dev req to those missing it --- .../azure-mgmt-baremetalinfrastructure/dev_requirements.txt | 3 ++- sdk/keyvault/azure-keyvault/dev_requirements.txt | 3 ++- .../azure-mgmt-streamanalytics/dev_requirements.txt | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/sdk/baremetalinfrastructure/azure-mgmt-baremetalinfrastructure/dev_requirements.txt b/sdk/baremetalinfrastructure/azure-mgmt-baremetalinfrastructure/dev_requirements.txt index fc236b060c0c..92ee26f9fc75 100644 --- a/sdk/baremetalinfrastructure/azure-mgmt-baremetalinfrastructure/dev_requirements.txt +++ b/sdk/baremetalinfrastructure/azure-mgmt-baremetalinfrastructure/dev_requirements.txt @@ -1,2 +1,3 @@ aiohttp>=3.0; python_version >= '3.5' --e ../../../tools/azure-devtools \ No newline at end of file +-e ../../../tools/azure-devtools +-e ../../../tools/azure-sdk-tools \ No newline at end of file diff --git a/sdk/keyvault/azure-keyvault/dev_requirements.txt b/sdk/keyvault/azure-keyvault/dev_requirements.txt index 51436184c10f..a6fc97da776d 100644 --- a/sdk/keyvault/azure-keyvault/dev_requirements.txt +++ b/sdk/keyvault/azure-keyvault/dev_requirements.txt @@ -1 +1,2 @@ --e ../../../tools/azure-devtools \ No newline at end of file +-e ../../../tools/azure-devtools +-e ../../../tools/azure-sdk-tools \ No newline at end of file diff --git a/sdk/streamanalytics/azure-mgmt-streamanalytics/dev_requirements.txt b/sdk/streamanalytics/azure-mgmt-streamanalytics/dev_requirements.txt index fc236b060c0c..92ee26f9fc75 100644 --- a/sdk/streamanalytics/azure-mgmt-streamanalytics/dev_requirements.txt +++ b/sdk/streamanalytics/azure-mgmt-streamanalytics/dev_requirements.txt @@ -1,2 +1,3 @@ aiohttp>=3.0; python_version >= '3.5' --e ../../../tools/azure-devtools \ No newline at end of file +-e ../../../tools/azure-devtools +-e ../../../tools/azure-sdk-tools \ No newline at end of file