Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

{testsdk} Move azure-devtools's code to azure-cli-testsdk #20601

Merged
merged 8 commits into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/authoring_tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ class StorageAccountTests(ScenarioTest):
Note:

1. Like `ResourceGroupPreparer`, you can use `StorageAccountPreparer` to prepare a disposable storage account for the test. The account is deleted along with the resource group during test teardown.
2. Creation of a storage account requires a resource group. Therefore `ResourceGroupPrepare` must be placed above `StorageAccountPreparer`, since preparers are designed to be executed from top to bottom. (The core preparer implementation is in the [AbstractPreparer](https://github.com/Azure/azure-python-devtools/blob/master/src/azure_devtools/scenario_tests/preparers.py) class in the [azure-devtools](https://pypi.python.org/pypi/azure-devtools) package.)
2. Creation of a storage account requires a resource group. Therefore `ResourceGroupPrepare` must be placed above `StorageAccountPreparer`, since preparers are designed to be executed from top to bottom. (The core preparer implementation is in the `azure.cli.testsdk.scenario_tests.preparers.AbstractPreparer`.)
3. The preparers communicate among themselves by adding values to the `kwargs` of the decorated methods. Therefore the `StorageAccountPreparer` uses the resource group created in the preceding `ResourceGroupPreparer`.
4. The `StorageAccountPreparer` can be further customized to modify the parameters of the created storage account:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from azure.cli.testsdk import live_only, MOCKED_USER_NAME
from azure.cli.testsdk.constants import AUX_SUBSCRIPTION, AUX_TENANT

from azure_devtools.scenario_tests.const import MOCKED_SUBSCRIPTION_ID, MOCKED_TENANT_ID
from azure.cli.testsdk.scenario_tests.const import MOCKED_SUBSCRIPTION_ID, MOCKED_TENANT_ID

mock_subscriptions = [
{
Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli-testsdk/azure/cli/testsdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure_devtools.scenario_tests import live_only, record_only, get_sha1_hash
from .scenario_tests import live_only, record_only, get_sha1_hash

from .base import ScenarioTest, LiveScenarioTest, LocalContextScenarioTest
from .preparers import (StorageAccountPreparer, ResourceGroupPreparer, RoleBasedServicePrincipalPreparer,
Expand Down
10 changes: 5 additions & 5 deletions src/azure-cli-testsdk/azure/cli/testsdk/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
import unittest
import tempfile

from azure_devtools.scenario_tests import (IntegrationTestBase, ReplayableTest, SubscriptionRecordingProcessor,
LargeRequestBodyProcessor,
LargeResponseBodyProcessor, LargeResponseBodyReplacer, RequestUrlNormalizer,
live_only, DeploymentNameReplacer, patch_time_sleep_api, create_random_name)
from .scenario_tests import (IntegrationTestBase, ReplayableTest, SubscriptionRecordingProcessor,
LargeRequestBodyProcessor,
LargeResponseBodyProcessor, LargeResponseBodyReplacer, RequestUrlNormalizer,
live_only, DeploymentNameReplacer, patch_time_sleep_api, create_random_name)

from azure_devtools.scenario_tests.const import MOCKED_SUBSCRIPTION_ID, ENV_SKIP_ASSERT
from .scenario_tests.const import MOCKED_SUBSCRIPTION_ID, ENV_SKIP_ASSERT

from .patches import (patch_load_cached_subscriptions, patch_main_exception_handler,
patch_retrieve_token_for_user, patch_long_run_operation_delay,
Expand Down
4 changes: 2 additions & 2 deletions src/azure-cli-testsdk/azure/cli/testsdk/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure_devtools.scenario_tests import mock_in_unit_test
from azure_devtools.scenario_tests.const import MOCKED_SUBSCRIPTION_ID, MOCKED_TENANT_ID
from .scenario_tests import mock_in_unit_test
from .scenario_tests.const import MOCKED_SUBSCRIPTION_ID, MOCKED_TENANT_ID

from .exceptions import CliExecutionError

Expand Down
2 changes: 1 addition & 1 deletion src/azure-cli-testsdk/azure/cli/testsdk/preparers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import os
from datetime import datetime

from azure_devtools.scenario_tests import AbstractPreparer, SingleValueReplacer
from .scenario_tests import AbstractPreparer, SingleValueReplacer

from .base import LiveScenarioTest
from .exceptions import CliTestError
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

"""
This module is vendored from
https://github.com/Azure/azure-python-devtools/tree/1.2.0/src/azure_devtools/scenario_tests

More info: https://github.com/Azure/azure-cli/pull/20601
Comment on lines +7 to +10
Copy link
Member Author

@jiasli jiasli Dec 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments for where this module comes from.

"""

from .base import IntegrationTestBase, ReplayableTest, LiveTest
from .exceptions import AzureTestError
from .decorators import live_only, record_only, AllowLargeResponse
from .patches import mock_in_unit_test, patch_time_sleep_api, patch_long_run_operation_delay
from .preparers import AbstractPreparer, SingleValueReplacer
from .recording_processors import (
RecordingProcessor, SubscriptionRecordingProcessor,
LargeRequestBodyProcessor, LargeResponseBodyProcessor, LargeResponseBodyReplacer,
OAuthRequestResponsesFilter, DeploymentNameReplacer, GeneralNameReplacer, AccessTokenReplacer, RequestUrlNormalizer,
)
from .utilities import create_random_name, get_sha1_hash

__all__ = ['IntegrationTestBase', 'ReplayableTest', 'LiveTest',
'AzureTestError',
'mock_in_unit_test', 'patch_time_sleep_api', 'patch_long_run_operation_delay',
'AbstractPreparer', 'SingleValueReplacer', 'AllowLargeResponse',
'RecordingProcessor', 'SubscriptionRecordingProcessor',
'LargeRequestBodyProcessor', 'LargeResponseBodyProcessor', 'LargeResponseBodyReplacer',
'OAuthRequestResponsesFilter', 'DeploymentNameReplacer', 'GeneralNameReplacer',
'AccessTokenReplacer', 'RequestUrlNormalizer',
'live_only', 'record_only',
'create_random_name', 'get_sha1_hash']
__version__ = '0.5.2'
219 changes: 219 additions & 0 deletions src/azure-cli-testsdk/azure/cli/testsdk/scenario_tests/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from __future__ import print_function
import unittest
import os
import inspect
import tempfile
import shutil
import logging
import threading
import six
import vcr

from .config import TestConfig
from .const import ENV_TEST_DIAGNOSE
from .utilities import create_random_name
from .decorators import live_only


class IntegrationTestBase(unittest.TestCase):
def __init__(self, method_name):
super(IntegrationTestBase, self).__init__(method_name)
self.diagnose = os.environ.get(ENV_TEST_DIAGNOSE, None) == 'True'
self.logger = logging.getLogger('azure.cli.testsdk.scenario_tests')

def create_random_name(self, prefix, length): # pylint: disable=no-self-use
return create_random_name(prefix=prefix, length=length)

def create_temp_file(self, size_kb, full_random=False):
"""
Create a temporary file for testing. The test harness will delete the file during tearing down.
:param float size_kb: specify the generated file size in kb.
"""
fd, path = tempfile.mkstemp()
os.close(fd)
self.addCleanup(lambda: os.remove(path))

with open(path, mode='r+b') as f:
if full_random:
chunk = os.urandom(1024)
else:
chunk = bytearray([0] * 1024)
for _ in range(int(size_kb)):
f.write(chunk)
chunk = os.urandom(int(1024 * (size_kb % 1)))
f.write(chunk)

return path

def create_temp_dir(self):
"""
Create a temporary directory for testing. The test harness will delete the directory during tearing down.
"""
temp_dir = tempfile.mkdtemp()
self.addCleanup(lambda: shutil.rmtree(temp_dir, ignore_errors=True))

return temp_dir

@classmethod
def set_env(cls, key, val):
os.environ[key] = val

@classmethod
def pop_env(cls, key):
return os.environ.pop(key, None)


@live_only()
class LiveTest(IntegrationTestBase):
pass


class ReplayableTest(IntegrationTestBase): # pylint: disable=too-many-instance-attributes
FILTER_HEADERS = [
'authorization',
'client-request-id',
'retry-after',
'x-ms-client-request-id',
'x-ms-correlation-request-id',
'x-ms-ratelimit-remaining-subscription-reads',
'x-ms-request-id',
'x-ms-routing-request-id',
'x-ms-gateway-service-instanceid',
'x-ms-ratelimit-remaining-tenant-reads',
'x-ms-served-by',
'x-ms-authorization-auxiliary'
]

def __init__(self, # pylint: disable=too-many-arguments
method_name, config_file=None, recording_dir=None, recording_name=None, recording_processors=None,
replay_processors=None, recording_patches=None, replay_patches=None):
super(ReplayableTest, self).__init__(method_name)

self.recording_processors = recording_processors or []
self.replay_processors = replay_processors or []

self.recording_patches = recording_patches or []
self.replay_patches = replay_patches or []

self.config = TestConfig(config_file=config_file)

self.disable_recording = False

test_file_path = inspect.getfile(self.__class__)
recording_dir = recording_dir or os.path.join(os.path.dirname(test_file_path), 'recordings')
self.is_live = self.config.record_mode

self.vcr = vcr.VCR(
cassette_library_dir=recording_dir,
before_record_request=self._process_request_recording,
before_record_response=self._process_response_recording,
decode_compressed_response=True,
record_mode='once' if not self.is_live else 'all',
filter_headers=self.FILTER_HEADERS
)
self.vcr.register_matcher('query', self._custom_request_query_matcher)

self.recording_file = os.path.join(
recording_dir,
'{}.yaml'.format(recording_name or method_name)
)
if self.is_live and os.path.exists(self.recording_file):
os.remove(self.recording_file)

self.in_recording = self.is_live or not os.path.exists(self.recording_file)
self.test_resources_count = 0
self.original_env = os.environ.copy()

def setUp(self):
super(ReplayableTest, self).setUp()

# set up cassette
cm = self.vcr.use_cassette(self.recording_file)
self.cassette = cm.__enter__()
self.addCleanup(cm.__exit__)

# set up mock patches
if self.in_recording:
for patch in self.recording_patches:
patch(self)
else:
for patch in self.replay_patches:
patch(self)

def tearDown(self):
os.environ = self.original_env
# Autorest.Python 2.x
assert not [t for t in threading.enumerate() if t.name.startswith("AzureOperationPoller")], \
"You need to call 'result' or 'wait' on all AzureOperationPoller you have created"
# Autorest.Python 3.x
assert not [t for t in threading.enumerate() if t.name.startswith("LROPoller")], \
"You need to call 'result' or 'wait' on all LROPoller you have created"

def _process_request_recording(self, request):
if self.disable_recording:
return None

if self.in_recording:
for processor in self.recording_processors:
request = processor.process_request(request)
if not request:
break
else:
for processor in self.replay_processors:
request = processor.process_request(request)
if not request:
break

return request

def _process_response_recording(self, response):
from .utilities import is_text_payload
if self.in_recording:
# make header name lower case and filter unwanted headers
headers = {}
for key in response['headers']:
if key.lower() not in self.FILTER_HEADERS:
headers[key.lower()] = response['headers'][key]
response['headers'] = headers

body = response['body']['string']
if is_text_payload(response) and body and not isinstance(body, six.string_types):
response['body']['string'] = body.decode('utf-8')

for processor in self.recording_processors:
response = processor.process_response(response)
if not response:
break
else:
for processor in self.replay_processors:
response = processor.process_response(response)
if not response:
break

return response

@classmethod
def _custom_request_query_matcher(cls, r1, r2):
""" Ensure method, path, and query parameters match. """
from six.moves.urllib_parse import urlparse, parse_qs # pylint: disable=import-error, relative-import

url1 = urlparse(r1.uri)
url2 = urlparse(r2.uri)

q1 = parse_qs(url1.query)
q2 = parse_qs(url2.query)
shared_keys = set(q1.keys()).intersection(set(q2.keys()))

if len(shared_keys) != len(q1) or len(shared_keys) != len(q2):
return False

for key in shared_keys:
if q1[key][0].lower() != q2[key][0].lower():
return False

return True
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import configargparse

from .const import ENV_LIVE_TEST


class TestConfig(object): # pylint: disable=too-few-public-methods
def __init__(self, parent_parsers=None, config_file=None):
parent_parsers = parent_parsers or []
self.parser = configargparse.ArgumentParser(parents=parent_parsers)
self.parser.add_argument(
'-c', '--config', is_config_file=True, default=config_file,
help='Path to a configuration file in YAML format.'
)
self.parser.add_argument(
'-l', '--live-mode', action='store_true', dest='live_mode',
env_var=ENV_LIVE_TEST,
help='Activate "live" recording mode for tests.'
)
self.args = self.parser.parse_args([])

@property
def record_mode(self):
return self.args.live_mode
14 changes: 14 additions & 0 deletions src/azure-cli-testsdk/azure/cli/testsdk/scenario_tests/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

# Replaced mock values
MOCKED_SUBSCRIPTION_ID = '00000000-0000-0000-0000-000000000000'
MOCKED_TENANT_ID = '00000000-0000-0000-0000-000000000000'

# Configuration environment variable
ENV_COMMAND_COVERAGE = 'AZURE_TEST_COMMAND_COVERAGE'
ENV_LIVE_TEST = 'AZURE_TEST_RUN_LIVE'
ENV_SKIP_ASSERT = 'AZURE_TEST_SKIP_ASSERT'
ENV_TEST_DIAGNOSE = 'AZURE_TEST_DIAGNOSE'
Loading