From 3bf5576103dc7ca859c9961a75f315e339dc8e4b Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Thu, 23 Jan 2020 18:42:24 -0800 Subject: [PATCH] Starting point of changes. --- .azure-devops/merge.yml | 2 +- .../templates/install-azure-cli-edge.yml | 1 - .../templates/install-azure-cli-released.yml | 3 +- .azure-devops/templates/run-tests.yml | 27 ++++---- .azure-devops/templates/set-pythonpath.yml | 12 ++++ azext_iot/_factory.py | 8 ++- azext_iot/_validators.py | 8 +-- azext_iot/common/deps.py | 16 +++-- azext_iot/common/pip.py | 2 +- azext_iot/constants.py | 4 +- azext_iot/operations/events3/_builders.py | 3 +- azext_iot/operations/events3/_events.py | 4 +- azext_iot/tests/__init__.py | 10 +++ azext_iot/tests/settings.py | 19 ++++-- azext_iot/tests/test_iot_ext_int.py | 54 ++++++++------- azext_iot/tests/test_iot_utility_unit.py | 68 +++++++++++++++++-- pytest.ini.example | 4 +- setup.cfg | 3 + 18 files changed, 174 insertions(+), 74 deletions(-) create mode 100644 .azure-devops/templates/set-pythonpath.yml diff --git a/.azure-devops/merge.yml b/.azure-devops/merge.yml index 50bf9af66..3e58293a9 100644 --- a/.azure-devops/merge.yml +++ b/.azure-devops/merge.yml @@ -89,7 +89,7 @@ jobs: - job: 'Run_Tests_Mac' dependsOn: ['Build_Publish_Azure_CLI_Test_SDK','Build_Publish_Azure_IoT_CLI_Extension'] pool: - vmImage: 'macOS-10.13' + vmImage: 'macOS-10.14' steps: - template: templates/run-tests.yml diff --git a/.azure-devops/templates/install-azure-cli-edge.yml b/.azure-devops/templates/install-azure-cli-edge.yml index 246a6372c..fa61d5baf 100644 --- a/.azure-devops/templates/install-azure-cli-edge.yml +++ b/.azure-devops/templates/install-azure-cli-edge.yml @@ -1,4 +1,3 @@ steps: - script: 'pip install --pre azure-cli --extra-index-url https://azurecliprod.blob.core.windows.net/edge' displayName: 'Install Azure CLI edge' - diff --git a/.azure-devops/templates/install-azure-cli-released.yml b/.azure-devops/templates/install-azure-cli-released.yml index 5746b109a..1f609819f 100644 --- a/.azure-devops/templates/install-azure-cli-released.yml +++ b/.azure-devops/templates/install-azure-cli-released.yml @@ -1,4 +1,3 @@ - steps: - script: pip install azure-cli - displayName: 'Install Azure CLI released' \ No newline at end of file + displayName: 'Install Azure CLI released' diff --git a/.azure-devops/templates/run-tests.yml b/.azure-devops/templates/run-tests.yml index 3b08ab8f1..52c57d641 100644 --- a/.azure-devops/templates/run-tests.yml +++ b/.azure-devops/templates/run-tests.yml @@ -19,34 +19,37 @@ steps: - template: setup-ci-machine.yml - - template: download-install-local-azure-iot-cli-extension-with-pip.yml + - template: download-install-local-azure-iot-cli-extension.yml + + - template: set-pythonpath.yml - ${{ if eq(parameters.runUnitTestsOnly, 'false') }}: - script: pytest --junitxml "TEST-results.xml" displayName: 'Execute all Tests' - + - ${{ if eq(parameters.runUnitTestsOnly, 'true') }}: - - script: pytest -v azext_iot/tests/test_iot_ext_unit.py --junitxml "TEST-iothub-unit-results.xml" + - script: pytest -v azext_iot/tests/test_iot_ext_unit.py --junitxml=junit/test-iothub-unit-results.xml displayName: 'Execute IoT Hub unit tests' - - script: pytest -v azext_iot/tests/test_iot_dps_unit.py --junitxml "TEST-dps-unit-results.xml" + - script: pytest -v azext_iot/tests/test_iot_dps_unit.py --junitxml=junit/test-dps-unit-results.xml displayName: 'Execute DPS unit tests' - - script: pytest -v azext_iot/tests/test_iot_utility_unit.py --junitxml "TEST-utility-unit-results.xml" + - script: pytest -v azext_iot/tests/test_iot_utility_unit.py --junitxml=junit/test-utility-unit-results.xml displayName: 'Execute Utility unit tests' - - script: pytest -v azext_iot/tests/test_iot_central_unit.py --junitxml "TEST-central-unit-results.xml" + - script: pytest -v azext_iot/tests/test_iot_central_unit.py --junitxml=junit/test-central-unit-results.xml displayName: 'Execute IoT Central unit tests' - - script: pytest -v azext_iot/tests/test_iot_pnp_unit.py --junitxml "TEST-pnp-unit-results.xml" + - script: pytest -v azext_iot/tests/test_iot_pnp_unit.py --junitxml=junit/test-pnp-unit-results.xml displayName: 'Execute IoT PnP unit tests' - - script: pytest -v azext_iot/tests/test_iot_digitaltwin_unit.py --junitxml "TEST-dt-unit-results.xml" + - script: pytest -v azext_iot/tests/test_iot_digitaltwin_unit.py --junitxml=junit/test-dt-unit-results.xml displayName: 'Execute IoT DigitalTwin unit tests' - - script: pytest -v azext_iot/tests/configurations/test_iot_config_unit.py --junitxml "TEST-config-unit-results.xml" + - script: pytest -v azext_iot/tests/configurations/test_iot_config_unit.py --junitxml=junit/test-config-unit-results.xml displayName: 'Execute IoT Configuration unit tests' - - script: pytest -v azext_iot/tests/jobs/test_iothub_jobs_unit.py --junitxml "TEST-jobs-unit-results.xml" + - script: pytest -v azext_iot/tests/jobs/test_iothub_jobs_unit.py --junitxml=junit/test-jobs-unit-results.xml displayName: 'Execute IoT Hub job unit tests' - task: PublishTestResults@2 + condition: succeededOrFailed() displayName: 'Publish Test Results' inputs: testResultsFormat: 'JUnit' - testResultsFiles: '**TEST-*.xml' + testResultsFiles: '**/test-*.xml' + testRunTitle: 'Publish test results for Python ${{ parameters.pythonVersion }} on OS $(Agent.OS)' searchFolder: '$(System.DefaultWorkingDirectory)' - condition: succeededOrFailed() diff --git a/.azure-devops/templates/set-pythonpath.yml b/.azure-devops/templates/set-pythonpath.yml new file mode 100644 index 000000000..49258dc59 --- /dev/null +++ b/.azure-devops/templates/set-pythonpath.yml @@ -0,0 +1,12 @@ +steps: + - task: PythonScript@0 + displayName : 'Extract extension path' + name: 'extractExtensionPath' + inputs: + scriptSource: 'inline' + script: | + from azure.cli.core.extension import get_extension_path + from six import print_ + extension_path = get_extension_path("azure-cli-iot-ext") + print_("Extension path is " + extension_path) + print_("##vso[task.setvariable variable=PYTHONPATH;]"+extension_path) diff --git a/azext_iot/_factory.py b/azext_iot/_factory.py index efd505c0f..03e0eae1a 100644 --- a/azext_iot/_factory.py +++ b/azext_iot/_factory.py @@ -25,7 +25,13 @@ def iot_hub_service_factory(cli_ctx, *_): working with IoT Hub. """ from azure.cli.core.commands.client_factory import get_mgmt_service_client - from azure.mgmt.iothub.iot_hub_client import IotHubClient + + # To support newer and older IotHubClient. 0.9.0+ has breaking changes. + try: + from azure.mgmt.iothub import IotHubClient + except: + # For <0.9.0 + from azure.mgmt.iothub.iot_hub_client import IotHubClient return get_mgmt_service_client(cli_ctx, IotHubClient).iot_hub_resource diff --git a/azext_iot/_validators.py b/azext_iot/_validators.py index 6fedaf516..8b70452d4 100644 --- a/azext_iot/_validators.py +++ b/azext_iot/_validators.py @@ -13,18 +13,18 @@ def mode2_iot_login_handler(cmd, namespace): args = vars(namespace) arg_keys = args.keys() if 'login' in arg_keys: - login_value = args.get('login') + login_value = args['login'] iot_cmd_type = None entity_value = None if 'hub_name' in arg_keys: iot_cmd_type = 'IoT Hub' - entity_value = args.get('hub_name') + entity_value = args['hub_name'] elif 'dps_name' in arg_keys: iot_cmd_type = 'DPS' - entity_value = args.get('dps_name') + entity_value = args['dps_name'] elif 'repo_endpoint' in arg_keys: iot_cmd_type = 'PnP' - entity_value = args.get('repo_endpoint') + entity_value = args['repo_endpoint'] if not any([login_value, entity_value]): raise CLIError(error_no_hub_or_login_on_input(iot_cmd_type)) diff --git a/azext_iot/common/deps.py b/azext_iot/common/deps.py index 165af184a..97abccb57 100644 --- a/azext_iot/common/deps.py +++ b/azext_iot/common/deps.py @@ -8,6 +8,7 @@ from os import linesep import six from six.moves import input +from knack.util import CLIError from azext_iot.constants import EVENT_LIB, VERSION from azext_iot.common.utility import test_import from azext_iot.common.config import get_uamqp_ext_version, update_uamqp_ext_version @@ -18,9 +19,8 @@ def ensure_uamqp(config, yes=False, repair=False): if get_uamqp_ext_version(config) != EVENT_LIB[1] or repair or not test_import(EVENT_LIB[0]): if not yes: - input_txt = ('Dependency update required for IoT extension version: {}. {}' - 'Updated dependency must be compatible with {} {}. ' - 'Continue? (y/n) -> ').format(VERSION, linesep, EVENT_LIB[0], EVENT_LIB[1]) + input_txt = ('Dependency update ({} {}) required for IoT extension version: {}. {}' + 'Continue? (y/n) -> ').format(EVENT_LIB[0], EVENT_LIB[1], VERSION, linesep) i = input(input_txt) if i.lower() != 'y': sys.exit('User has declined update...') @@ -28,8 +28,10 @@ def ensure_uamqp(config, yes=False, repair=False): six.print_('Updating required dependency...') with HomebrewPipPatch(): # The version range defined in this custom_version parameter should be stable - if install(EVENT_LIB[0], custom_version='>={},<{}'.format(EVENT_LIB[1], EVENT_LIB[2])): + try: + install(EVENT_LIB[0], compatible_version='{}'.format(EVENT_LIB[1])) update_uamqp_ext_version(config, EVENT_LIB[1]) - six.print_('Update appears to have worked. Executing command...') - else: - sys.exit('Failure updating {} {}. Aborting...'.format(EVENT_LIB[0], EVENT_LIB[1])) + six.print_('Update complete. Executing command...') + except RuntimeError as e: + six.print_('Failure updating {}. Aborting...'.format(EVENT_LIB[0])) + raise CLIError(e) diff --git a/azext_iot/common/pip.py b/azext_iot/common/pip.py index fc08f3dda..e7b9bbda5 100644 --- a/azext_iot/common/pip.py +++ b/azext_iot/common/pip.py @@ -39,4 +39,4 @@ def install(package, exact_version=None, compatible_version=None, custom_version except subprocess.CalledProcessError as e: logger.debug(e.output) logger.debug(e) - return False + raise(RuntimeError(e.output)) diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 33122d340..bf6b78ef4 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,7 +7,7 @@ import os -VERSION = "0.8.8" +VERSION = "0.8.9" EXTENSION_NAME = "azure-cli-iot-ext" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" @@ -44,7 +44,7 @@ USER_AGENT = "IoTPlatformCliExtension/{}".format(VERSION) # (Lib name, minimum version (including), maximum version (excluding)) -EVENT_LIB = ("uamqp", "1.1.0", "1.1.1") +EVENT_LIB = ("uamqp", "1.2", "1.3") # Config Key's CONFIG_KEY_UAMQP_EXT_VERSION = "uamqp_ext_version" diff --git a/azext_iot/operations/events3/_builders.py b/azext_iot/operations/events3/_builders.py index 5392b40db..4ea2daf33 100644 --- a/azext_iot/operations/events3/_builders.py +++ b/azext_iot/operations/events3/_builders.py @@ -4,7 +4,8 @@ from azext_iot.common.sas_token_auth import SasTokenAuthentication from azext_iot.common.utility import (parse_entity, unicode_binary_map, url_encode_str) -DEBUG = True +# To provide amqp frame trace +DEBUG = False class AmqpBuilder(): diff --git a/azext_iot/operations/events3/_events.py b/azext_iot/operations/events3/_events.py index 3eed2ff38..939ec70b1 100644 --- a/azext_iot/operations/events3/_events.py +++ b/azext_iot/operations/events3/_events.py @@ -18,8 +18,8 @@ from azext_iot.common.utility import parse_entity, unicode_binary_map, process_json_arg from azext_iot.operations.events3._builders import AmqpBuilder - -DEBUG = True +# To provide amqp frame trace +DEBUG = False logger = get_logger(__name__) diff --git a/azext_iot/tests/__init__.py b/azext_iot/tests/__init__.py index 3e8d2de94..f2e50768e 100644 --- a/azext_iot/tests/__init__.py +++ b/azext_iot/tests/__init__.py @@ -65,6 +65,7 @@ def __init__(self, test_scenario, entity_name, entity_rg, entity_cs): os.environ["AZURE_CORE_COLLECT_TELEMETRY"] = "no" super(IoTLiveScenarioTest, self).__init__(test_scenario) + self.region = self.get_region() def generate_device_names(self, count=1, edge=False): names = [ @@ -144,6 +145,15 @@ def tearDown(self): checks=self.is_empty(), ) + def get_region(self): + result = self.cmd( + "iot hub show -n {}".format(self.entity_name) + ).get_output_in_json() + locations_set = result["properties"]["locations"] + for loc in locations_set: + if loc["role"] == "primary": + return loc["location"] + def disable_telemetry(test_function): def wrapper(*args, **kwargs): diff --git a/azext_iot/tests/settings.py b/azext_iot/tests/settings.py index cf51c07bb..3b03560c1 100644 --- a/azext_iot/tests/settings.py +++ b/azext_iot/tests/settings.py @@ -17,15 +17,22 @@ class Setting(object): # Example of a dynamic class # TODO: Evaluate moving this to the extension prime time class DynamoSettings(object): - def __init__(self, env_set): - if not isinstance(env_set, list): - raise TypeError("env_set must be a list") + def __init__(self, req_env_set, opt_env_set=None): + if not isinstance(req_env_set, list): + raise TypeError("req_env_set must be a list") + self.env = Setting() - self._build_config(env_set) + self._build_config(req_env_set) + + if opt_env_set: + if not isinstance(opt_env_set, list): + raise TypeError("opt_env_set must be a list") + self._build_config(opt_env_set, optional=True) - def _build_config(self, env_set): + def _build_config(self, env_set, optional=False): for key in env_set: value = environ.get(key) if not value: - raise RuntimeError("'{}' environment variable required.") + if not optional: + raise RuntimeError("'{}' environment variable required.".format(key)) setattr(self.env, key, value) diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 771422e9b..bd196988e 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -6,28 +6,27 @@ import os import pytest +import warnings from azext_iot.common.utility import read_file_content from . import IoTLiveScenarioTest +from .settings import DynamoSettings, ENV_SET_TEST_IOTHUB_BASIC from azext_iot.constants import DEVICE_DEVICESCOPE_PREFIX +opt_env_set = ["azext_iot_teststorageuri"] -# Set these to the proper IoT Hub, IoT Hub Cstring and Resource Group for Live Integration Tests. -LIVE_HUB = os.environ.get("azext_iot_testhub") -LIVE_RG = os.environ.get("azext_iot_testrg") -LIVE_HUB_CS = os.environ.get("azext_iot_testhub_cs") +settings = DynamoSettings(req_env_set=ENV_SET_TEST_IOTHUB_BASIC, opt_env_set=opt_env_set) + +LIVE_HUB = settings.env.azext_iot_testhub +LIVE_RG = settings.env.azext_iot_testrg +LIVE_HUB_CS = settings.env.azext_iot_testhub_cs LIVE_HUB_MIXED_CASE_CS = LIVE_HUB_CS.replace("HostName", "hostname", 1) # Set this environment variable to your empty blob container sas uri to test device export and enable file upload test. # For file upload, you will need to have configured your IoT Hub before running. -LIVE_STORAGE = os.environ.get("azext_iot_teststorageuri") +LIVE_STORAGE = settings.env.azext_iot_teststorageuri LIVE_CONSUMER_GROUPS = ["test1", "test2", "test3"] -if not all([LIVE_HUB, LIVE_HUB_CS, LIVE_RG]): - raise ValueError( - "Set azext_iot_testhub, azext_iot_testhub_cs and azext_iot_testrg to run IoT Hub integration tests." - ) - CWD = os.path.dirname(os.path.abspath(__file__)) PRIMARY_THUMBPRINT = "A361EA6A7119A8B0B7BBFFA2EAFDAD1F9D5BED8C" @@ -606,23 +605,26 @@ def test_hub_device_twins(self): ], ) - # TODO move distributed tracing tests - self.cmd( - "iot hub distributed-tracing show -d {} -n {} -g {}".format( - device_ids[2], LIVE_HUB, LIVE_RG - ), - checks=self.is_empty(), - ) - - result = self.cmd( - "iot hub distributed-tracing update -d {} -n {} -g {} --sm on --sr 50".format( - device_ids[2], LIVE_HUB, LIVE_RG + # Region specific test + if self.region not in ["West US 2", "North Europe", "Southeast Asia"]: + warnings.warn("Skipping distributed-tracing tests. IoT Hub not in supported region!") + else: + self.cmd( + "iot hub distributed-tracing show -d {} -n {} -g {}".format( + device_ids[2], LIVE_HUB, LIVE_RG + ), + checks=self.is_empty(), ) - ).get_output_in_json() - assert result["deviceId"] == device_ids[2] - assert result["samplingMode"] == "enabled" - assert result["samplingRate"] == "50%" - assert not result["isSynced"] + + result = self.cmd( + "iot hub distributed-tracing update -d {} -n {} -g {} --sm on --sr 50".format( + device_ids[2], LIVE_HUB, LIVE_RG + ) + ).get_output_in_json() + assert result["deviceId"] == device_ids[2] + assert result["samplingMode"] == "enabled" + assert result["samplingRate"] == "50%" + assert not result["isSynced"] class TestIoTHubModules(IoTLiveScenarioTest): diff --git a/azext_iot/tests/test_iot_utility_unit.py b/azext_iot/tests/test_iot_utility_unit.py index f085df424..d3750e965 100644 --- a/azext_iot/tests/test_iot_utility_unit.py +++ b/azext_iot/tests/test_iot_utility_unit.py @@ -1,10 +1,11 @@ import pytest import json from knack.util import CLIError +from azure.cli.core.extension import get_extension_path from azext_iot.common.utility import validate_min_python_version from azext_iot.common.deps import ensure_uamqp from azext_iot._validators import mode2_iot_login_handler -from azext_iot.constants import EVENT_LIB +from azext_iot.constants import EVENT_LIB, EXTENSION_NAME from azext_iot.common.utility import process_json_arg, read_file_content, logger @@ -110,13 +111,13 @@ def uamqp_scenario(self, mocker): def test_ensure_uamqp_version( self, mocker, uamqp_scenario, case, extra_input, external_input ): + from functools import partial + if case == "importerror": uamqp_scenario["test_import"].return_value = False elif case == "compatibility": uamqp_scenario["get_uamqp"].return_value = "0.0.0" - from functools import partial - kwargs = {} user_cancelled = True if extra_input and "yes;" in extra_input: @@ -138,9 +139,64 @@ def test_ensure_uamqp_version( else: install_args = uamqp_scenario["installer"].call_args assert install_args[0][0] == EVENT_LIB[0] - assert install_args[1]["custom_version"] == ">={},<{}".format( - EVENT_LIB[1], EVENT_LIB[2] - ) + assert install_args[1]["compatible_version"] == EVENT_LIB[1] + + +class TestInstallPipPackage(object): + @pytest.fixture() + def subprocess_scenario(self, mocker): + return mocker.patch("azext_iot.common.pip.subprocess") + + @pytest.fixture() + def subprocess_error(self, mocker): + from subprocess import CalledProcessError + + patch_check_output = mocker.patch( + "azext_iot.common.pip.subprocess.check_output" + ) + patch_check_output.side_effect = CalledProcessError( + returncode=1, cmd="cmd", output=None + ) + return patch_check_output + + @pytest.mark.parametrize( + "install_type, package_name, expected", + [ + ({"exact_version": "1.2"}, "uamqp", "uamqp==1.2"), + ({"compatible_version": "1.2"}, "uamqp", "uamqp~=1.2"), + ({"custom_version": ">=1.2,<1.3"}, "uamqp", "uamqp>=1.2,<1.3"), + ], + ) + def test_pip_install( + self, subprocess_scenario, install_type, package_name, expected + ): + from azext_iot.common.pip import install + from sys import executable + + install(package_name, **install_type) + + assert subprocess_scenario.check_output.call_count == 1 + + call = subprocess_scenario.check_output.call_args[0][0] + + assert call == [ + executable, + "-m", + "pip", + "--disable-pip-version-check", + "--no-cache-dir", + "install", + "-U", + "--target", + get_extension_path(EXTENSION_NAME), + expected, + ] + + def test_pip_error(self, subprocess_error): + from azext_iot.common.pip import install + + with pytest.raises(RuntimeError): + install("uamqp") class TestProcessJsonArg(object): diff --git a/pytest.ini.example b/pytest.ini.example index c0898c389..8708ac4aa 100644 --- a/pytest.ini.example +++ b/pytest.ini.example @@ -1,5 +1,5 @@ [pytest] -junit_family = legacy +junit_family = xunit1 addopts = -v -p no:warnings @@ -9,7 +9,7 @@ norecursedirs = build testpaths = - tests + azext_iot/tests env = AZURE_TEST_RUN_LIVE=True diff --git a/setup.cfg b/setup.cfg index d8c2e8ca5..d80e75ff3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,3 +20,6 @@ ignore = per-file-ignores = # ignore line length for help content azext_iot/*_help.py:E501 + +[tool:pytest] +junit_family = xunit1