This is an automated email from the ASF dual-hosted git repository.

clewolff pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/libcloud.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 38693a4  Add integration tests for Azure Storage driver (#1572)
38693a4 is described below

commit 38693a4775505364707419447fb9eb788f8f04e9
Author: Clemens Wolff <[email protected]>
AuthorDate: Sun May 2 10:31:08 2021 -0400

    Add integration tests for Azure Storage driver (#1572)
    
    * Fix build
    
    * Namespace compute integration tests
    
    * Add integration tests for Azure Storage driver
---
 .github/workflows/main.yml                   |  38 +++
 integration/README.rst                       |  20 --
 integration/compute/README.rst               |  20 ++
 integration/{driver => compute}/__init__.py  |   0
 integration/{ => compute}/__main__.py        |   4 +-
 integration/{ => compute}/api/__init__.py    |   0
 integration/{ => compute}/api/__main__.py    |   0
 integration/{ => compute}/api/data.py        |   0
 integration/{ => compute}/api/routes.py      |   4 +-
 integration/{ => compute}/api/util.py        |   2 +-
 integration/{ => compute}/config.py          |   0
 integration/{ => compute}/driver/__init__.py |   0
 integration/{ => compute}/driver/test.py     |   0
 integration/{ => compute}/requirements.txt   |   0
 integration/storage/README.rst               |  18 ++
 integration/{driver => storage}/__init__.py  |   0
 integration/{api => storage}/__main__.py     |  11 +-
 integration/storage/base.py                  | 386 +++++++++++++++++++++++++++
 integration/storage/requirements.txt         |   5 +
 integration/storage/test_azure_blobs.py      | 177 ++++++++++++
 tox.ini                                      |  26 +-
 21 files changed, 676 insertions(+), 35 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f9d2750..585bdb7 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -57,6 +57,7 @@ jobs:
 
       - name: Install OS / deb dependencies
         run: |
+          sudo DEBIAN_FRONTEND=noninteractive apt-get update
           sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq gcc 
libvirt-dev
 
       - name: Install Python Dependencies
@@ -67,6 +68,40 @@ jobs:
         run: |
           tox -e py${{ matrix.python_version }}
 
+  integ_tests:
+    name: Run Integration Tests
+    runs-on: ubuntu-latest
+
+    # needs: pre_job
+    # if: ${{ needs.pre_job.outputs.should_skip == 'false' || github.ref == 
'refs/heads/trunk' }}
+
+    strategy:
+      matrix:
+        python_version: [3.7]
+
+    steps:
+      - uses: actions/checkout@master
+        with:
+          fetch-depth: 1
+
+      - name: Use Python ${{ matrix.python_version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python_version }}
+
+      - name: Install OS / deb dependencies
+        run: |
+          sudo DEBIAN_FRONTEND=noninteractive apt-get update
+          sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq gcc 
libvirt-dev
+
+      - name: Install Python Dependencies
+        run: |
+          pip install "tox==3.20.1"
+
+      - name: Run tox target
+        run: |
+          tox -e integration-storage
+
   code_coverage:
     name: Generate Code Coverage
     runs-on: ubuntu-latest
@@ -90,6 +125,7 @@ jobs:
 
       - name: Install OS / deb dependencies
         run: |
+          sudo DEBIAN_FRONTEND=noninteractive apt-get update
           sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq graphviz gcc 
libvirt-dev
 
       - name: Install Python Dependencies
@@ -123,6 +159,7 @@ jobs:
 
       - name: Install OS / deb dependencies
         run: |
+          sudo DEBIAN_FRONTEND=noninteractive apt-get update
           sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq graphviz gcc 
libvirt-dev
 
       - name: Install Python Dependencies
@@ -161,6 +198,7 @@ jobs:
 
       - name: Install OS / deb dependencies
         run: |
+          sudo DEBIAN_FRONTEND=noninteractive apt-get update
           sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq graphviz gcc 
libvirt-dev
 
       - name: Install Python Dependencies
diff --git a/integration/README.rst b/integration/README.rst
deleted file mode 100644
index ed61b5a..0000000
--- a/integration/README.rst
+++ /dev/null
@@ -1,20 +0,0 @@
-Integration Test Module
-=======================
-
-This test suite is for running a live API endpoint and testing the 
apache-libcloud functionality as a full integration test
-
-Running the API service
------------------------
-
-.. code-block:: bash
-
-  pip install -r integration/requirements.txt
-  python -m integration.api
-
-Running the tests
------------------
-
-.. code-block:: bash
-
-   python -m integration
-   
diff --git a/integration/compute/README.rst b/integration/compute/README.rst
new file mode 100644
index 0000000..7c010f1
--- /dev/null
+++ b/integration/compute/README.rst
@@ -0,0 +1,20 @@
+Compute Integration Test Module
+===============================
+
+This test suite is for running a live API endpoint and testing the 
apache-libcloud compute functionality as a full integration test
+
+Running the API service
+-----------------------
+
+.. code-block:: bash
+
+  pip install -r integration/compute/requirements.txt
+  python -m integration.compute.api
+
+Running the tests
+-----------------
+
+.. code-block:: bash
+
+   python -m integration.compute
+   
diff --git a/integration/driver/__init__.py b/integration/compute/__init__.py
similarity index 100%
copy from integration/driver/__init__.py
copy to integration/compute/__init__.py
diff --git a/integration/__main__.py b/integration/compute/__main__.py
similarity index 94%
rename from integration/__main__.py
rename to integration/compute/__main__.py
index 1d04b00..21155bb 100644
--- a/integration/__main__.py
+++ b/integration/compute/__main__.py
@@ -16,9 +16,9 @@
 import unittest
 import sys
 
-from integration.driver.test import TestNodeDriver
+from integration.compute.driver.test import TestNodeDriver
 
-from integration.api.data import NODES, REPORT_DATA
+from integration.compute.api.data import NODES, REPORT_DATA
 
 
 class IntegrationTest(unittest.TestCase):
diff --git a/integration/api/__init__.py b/integration/compute/api/__init__.py
similarity index 100%
rename from integration/api/__init__.py
rename to integration/compute/api/__init__.py
diff --git a/integration/api/__main__.py b/integration/compute/api/__main__.py
similarity index 100%
copy from integration/api/__main__.py
copy to integration/compute/api/__main__.py
diff --git a/integration/api/data.py b/integration/compute/api/data.py
similarity index 100%
rename from integration/api/data.py
rename to integration/compute/api/data.py
diff --git a/integration/api/routes.py b/integration/compute/api/routes.py
similarity index 90%
rename from integration/api/routes.py
rename to integration/compute/api/routes.py
index 05fd0ec..b986ea3 100644
--- a/integration/api/routes.py
+++ b/integration/compute/api/routes.py
@@ -17,8 +17,8 @@ import json
 
 from bottle import route
 
-from integration.api.data import NODES, REPORT_DATA
-from integration.api.util import secure
+from integration.compute.api.data import NODES, REPORT_DATA
+from integration.compute.api.util import secure
 
 
 @route('/compute/nodes', method='GET')
diff --git a/integration/api/util.py b/integration/compute/api/util.py
similarity index 95%
rename from integration/api/util.py
rename to integration/compute/api/util.py
index 679ff98..20c2e0e 100644
--- a/integration/api/util.py
+++ b/integration/compute/api/util.py
@@ -16,7 +16,7 @@
 from bottle import request
 from functools import wraps
 
-from integration.config import EXPECTED_AUTH
+from integration.compute.config import EXPECTED_AUTH
 
 
 def secure(f):
diff --git a/integration/config.py b/integration/compute/config.py
similarity index 100%
rename from integration/config.py
rename to integration/compute/config.py
diff --git a/integration/driver/__init__.py 
b/integration/compute/driver/__init__.py
similarity index 100%
copy from integration/driver/__init__.py
copy to integration/compute/driver/__init__.py
diff --git a/integration/driver/test.py b/integration/compute/driver/test.py
similarity index 100%
rename from integration/driver/test.py
rename to integration/compute/driver/test.py
diff --git a/integration/requirements.txt b/integration/compute/requirements.txt
similarity index 100%
rename from integration/requirements.txt
rename to integration/compute/requirements.txt
diff --git a/integration/storage/README.rst b/integration/storage/README.rst
new file mode 100644
index 0000000..91d6ca8
--- /dev/null
+++ b/integration/storage/README.rst
@@ -0,0 +1,18 @@
+Storage Integration Test Module
+===============================
+
+This test suite is for validating the apache-libcloud storage functionality 
against live storage backends.
+
+Setting up the test suite
+-------------------------
+
+.. code-block:: bash
+
+  pip install -r integration/storage/requirements.txt
+
+Running the tests
+-----------------
+
+.. code-block:: bash
+
+  python -m integration.storage
diff --git a/integration/driver/__init__.py b/integration/storage/__init__.py
similarity index 100%
rename from integration/driver/__init__.py
rename to integration/storage/__init__.py
diff --git a/integration/api/__main__.py b/integration/storage/__main__.py
similarity index 77%
rename from integration/api/__main__.py
rename to integration/storage/__main__.py
index 85d7dc7..f4fba46 100644
--- a/integration/api/__main__.py
+++ b/integration/storage/__main__.py
@@ -13,9 +13,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from bottle import run
+import os
+import sys
+import unittest
 
-import integration.api.routes  # noqa
 
 if __name__ == '__main__':
-    run(host='localhost', port=9898)
+    loader = unittest.TestLoader()
+    tests = loader.discover(os.path.dirname(__file__))
+    runner = unittest.runner.TextTestRunner()
+    result = runner.run(tests)
+    sys.exit(len(result.errors))
diff --git a/integration/storage/base.py b/integration/storage/base.py
new file mode 100644
index 0000000..119e8b1
--- /dev/null
+++ b/integration/storage/base.py
@@ -0,0 +1,386 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import gzip
+import io
+import os
+import random
+import re
+import socket
+import string
+import sys
+import tempfile
+import time
+import unittest
+
+import requests
+
+try:
+    import docker
+except ImportError:
+    docker = None
+
+from libcloud.common.types import LibcloudError
+from libcloud.storage import providers, types
+
+
+MB = 1024 * 1024
+
+
+class Integration:
+    class TestBase(unittest.TestCase):
+        provider = None
+        account = None
+        secret = None
+
+        def setUp(self):
+            for required in 'provider', 'account', 'secret':
+                value = getattr(self, required, None)
+                if value is None:
+                    raise unittest.SkipTest('config {} not 
set'.format(required))
+
+            kwargs = {'key': self.account, 'secret': self.secret}
+
+            for optional in 'host', 'port', 'secure':
+                value = getattr(self, optional, None)
+                if value is not None:
+                    kwargs[optional] = value
+
+            driver_class = providers.get_driver(self.provider)
+            self.driver = driver_class(**kwargs)
+
+        def tearDown(self):
+            for container in self.driver.list_containers():
+                for obj in container.list_objects():
+                    try:
+                        obj.delete()
+                    except LibcloudError as ex:
+                        print(
+                            'Unable to delete object {} in container {}: {}.'
+                            'Delete it manually.'
+                            .format(obj.name, container.name, ex),
+                            file=sys.stderr
+                        )
+
+                try:
+                    container.delete()
+                except LibcloudError as ex:
+                    print(
+                        'Unable to delete container {}: {}.'
+                        'Delete it manually.'
+                        .format(container.name, ex),
+                        file=sys.stderr
+                    )
+
+        def test_containers(self):
+            # make a new container
+            container_name = random_container_name()
+            container = self.driver.create_container(container_name)
+            self.assertEqual(container.name, container_name)
+            container = self.driver.get_container(container_name)
+            self.assertEqual(container.name, container_name)
+
+            # check that an existing container can't be re-created
+            with self.assertRaises(types.ContainerAlreadyExistsError):
+                self.driver.create_container(container_name)
+
+            # check that the new container can be listed
+            containers = self.driver.list_containers()
+            self.assertEqual([c.name for c in containers], [container_name])
+
+            # delete the container
+            self.driver.delete_container(container)
+
+            # check that a deleted container can't be looked up
+            with self.assertRaises(types.ContainerDoesNotExistError):
+                self.driver.get_container(container_name)
+
+            # check that the container is deleted
+            containers = self.driver.list_containers()
+            self.assertEqual([c.name for c in containers], [])
+
+        def _test_objects(self, do_upload, do_download, size=1 * MB):
+            content = os.urandom(size)
+            blob_name = 'testblob'
+            container = self.driver.create_container(random_container_name())
+
+            # upload a file
+            obj = do_upload(container, blob_name, content)
+            self.assertEqual(obj.name, blob_name)
+            obj = self.driver.get_object(container.name, blob_name)
+
+            # check that the file can be listed
+            blobs = self.driver.list_container_objects(container)
+            self.assertEqual([blob.name for blob in blobs], [blob_name])
+
+            # upload another file and check it's excluded in prefix listing
+            do_upload(container, blob_name[::-1], content[::-1])
+            blobs = self.driver.list_container_objects(
+                container, prefix=blob_name[0:3]
+            )
+            self.assertEqual([blob.name for blob in blobs], [blob_name])
+
+            # check that the file can be read back
+            self.assertEqual(do_download(obj), content)
+
+            # delete the file
+            self.driver.delete_object(obj)
+
+            # check that a missing file can't be deleted or looked up
+            with self.assertRaises(types.ObjectDoesNotExistError):
+                self.driver.delete_object(obj)
+            with self.assertRaises(types.ObjectDoesNotExistError):
+                self.driver.get_object(container.name, blob_name)
+
+            # check that the file is deleted
+            blobs = self.driver.list_container_objects(container)
+            self.assertEqual([blob.name for blob in blobs], [blob_name[::-1]])
+
+        def test_objects(self, size=1 * MB):
+            def do_upload(container, blob_name, content):
+                infile = self._create_tempfile(content=content)
+                return self.driver.upload_object(infile, container, blob_name)
+
+            def do_download(obj):
+                outfile = self._create_tempfile()
+                self.driver.download_object(obj, outfile, 
overwrite_existing=True)
+                with open(outfile, 'rb') as fobj:
+                    return fobj.read()
+
+            self._test_objects(do_upload, do_download, size)
+
+        def test_objects_range_downloads(self):
+            blob_name = 'testblob-range'
+            content = b'0123456789'
+            container = self.driver.create_container(random_container_name())
+
+            obj = self.driver.upload_object(
+                self._create_tempfile(content=content),
+                container,
+                blob_name
+            )
+            self.assertEqual(obj.name, blob_name)
+            self.assertEqual(obj.size, len(content))
+
+            obj = self.driver.get_object(container.name, blob_name)
+            self.assertEqual(obj.name, blob_name)
+            self.assertEqual(obj.size, len(content))
+
+            values = [
+                {'start_bytes': 0, 'end_bytes': 1, 'expected_content': b'0'},
+                {'start_bytes': 1, 'end_bytes': 5, 'expected_content': 
b'1234'},
+                {'start_bytes': 5, 'end_bytes': None, 'expected_content': 
b'56789'},
+                {'start_bytes': 5, 'end_bytes': len(content), 
'expected_content': b'56789'},
+                {'start_bytes': 0, 'end_bytes': None, 'expected_content': 
b'0123456789'},
+                {'start_bytes': 0, 'end_bytes': len(content), 
'expected_content': b'0123456789'},
+            ]
+
+            for value in values:
+                # 1. download_object_range
+                start_bytes = value['start_bytes']
+                end_bytes = value['end_bytes']
+                outfile = self._create_tempfile()
+
+                result = self.driver.download_object_range(
+                    obj,
+                    outfile,
+                    start_bytes=start_bytes,
+                    end_bytes=end_bytes,
+                    overwrite_existing=True,
+                )
+                self.assertTrue(result)
+
+                with open(outfile, 'rb') as fobj:
+                    downloaded_content = fobj.read()
+
+                if end_bytes is not None:
+                    expected_content = content[start_bytes:end_bytes]
+                else:
+                    expected_content = content[start_bytes:]
+
+                msg = 'Expected "%s", got "%s" for values: %s' % (
+                    expected_content, downloaded_content, str(value)
+                )
+                self.assertEqual(downloaded_content, expected_content, msg)
+                self.assertEqual(downloaded_content, 
value['expected_content'], msg)
+
+                # 2. download_object_range_as_stream
+                downloaded_content = read_stream(
+                    self.driver.download_object_range_as_stream(
+                        obj, start_bytes=start_bytes, end_bytes=end_bytes
+                    )
+                )
+                self.assertEqual(downloaded_content, expected_content)
+
+        @unittest.skipUnless(os.getenv('LARGE_FILE_SIZE_MB'), 'config not set')
+        def test_objects_large(self):
+            size = int(float(os.environ['LARGE_FILE_SIZE_MB']) * MB)
+            self.test_objects(size)
+
+        def test_objects_stream_io(self):
+            def do_upload(container, blob_name, content):
+                content = io.BytesIO(content)
+                return self.driver.upload_object_via_stream(content, 
container, blob_name)
+
+            def do_download(obj):
+                return read_stream(self.driver.download_object_as_stream(obj))
+
+            self._test_objects(do_upload, do_download)
+
+        def test_objects_stream_iterable(self):
+            def do_upload(container, blob_name, content):
+                content = iter([content[i:i + 1] for i in range(len(content))])
+                return self.driver.upload_object_via_stream(content, 
container, blob_name)
+
+            def do_download(obj):
+                return read_stream(self.driver.download_object_as_stream(obj))
+
+            self._test_objects(do_upload, do_download)
+
+        def test_upload_via_stream_with_content_encoding(self):
+            object_name = 'content_encoding.gz'
+            content = gzip.compress(os.urandom(MB // 100))
+            container = self.driver.create_container(random_container_name())
+            self.driver.upload_object_via_stream(
+                iter(content),
+                container,
+                object_name,
+                headers={'Content-Encoding': 'gzip'},
+            )
+
+            obj = self.driver.get_object(container.name, object_name)
+
+            self.assertEqual(obj.extra.get('content_encoding'), 'gzip')
+
+        def test_cdn_url(self):
+            content = os.urandom(MB // 100)
+            container = self.driver.create_container(random_container_name())
+            obj = self.driver.upload_object_via_stream(iter(content), 
container, 'cdn')
+
+            response = requests.get(self.driver.get_object_cdn_url(obj))
+            response.raise_for_status()
+
+            self.assertEqual(response.content, content)
+
+        def _create_tempfile(self, prefix='', content=b''):
+            fobj, path = tempfile.mkstemp(prefix=prefix, text=False)
+            os.write(fobj, content)
+            os.close(fobj)
+            self.addCleanup(os.remove, path)
+            return path
+
+    class ContainerTestBase(TestBase):
+        image = None
+        version = 'latest'
+        environment = {}
+        ready_message = None
+
+        host = 'localhost'
+        port = None
+        secure = False
+
+        client = None
+        container = None
+        verbose = False
+
+        @classmethod
+        def setUpClass(cls):
+            if docker is None:
+                raise unittest.SkipTest('missing docker library')
+
+            try:
+                cls.client = docker.from_env()
+            except docker.errors.DockerException:
+                raise unittest.SkipTest('unable to create docker client')
+
+            for required in 'image', 'port':
+                value = getattr(cls, required, None)
+                if value is None:
+                    raise unittest.SkipTest('config {} not 
set'.format(required))
+
+            cls.container = cls.client.containers.run(
+                '{}:{}'.format(cls.image, cls.version),
+                detach=True,
+                auto_remove=True,
+                ports={cls.port: cls.port},
+                environment=cls.environment,
+            )
+
+            wait_for(cls.port, cls.host)
+
+            container_ready = cls.ready_message is None
+
+            while not container_ready:
+                time.sleep(1)
+
+                container_ready = any(
+                    cls.ready_message in line
+                    for line in cls.container.logs().splitlines()
+                )
+
+        @classmethod
+        def tearDownClass(cls):
+            if cls.verbose:
+                for line in cls.container.logs().splitlines():
+                    print(line)
+
+            try:
+                cls.container.kill()
+            except docker.errors.DockerException as ex:
+                print(
+                    'Unable to terminate docker container {}: {}.'
+                    'Stop it manually.'
+                    .format(cls.container.short_id, ex),
+                    file=sys.stderr
+                )
+
+
+def wait_for(port, host='localhost', timeout=10):
+    start_time = time.perf_counter()
+
+    while True:
+        try:
+            with socket.create_connection((host, port), timeout=timeout):
+                break
+        except OSError as ex:
+            if time.perf_counter() - start_time >= timeout:
+                raise TimeoutError(
+                    'Waited too long for the port {} on host {} to start 
accepting '
+                    'connections.'.format(port, host)
+                ) from ex
+
+            time.sleep(1)
+
+
+def random_string(length, alphabet=string.ascii_lowercase + string.digits):
+    return ''.join(random.choice(alphabet) for _ in range(length))
+
+
+def random_container_name(prefix='test'):
+    max_length = 63
+    suffix = random_string(max_length)
+    name = prefix + suffix
+    name = re.sub('[^a-z0-9-]', '-', name)
+    name = re.sub('-+', '-', name)
+    name = name[:max_length]
+    name = name.lower()
+    return name
+
+
+def read_stream(stream):
+    buffer = io.BytesIO()
+    buffer.writelines(stream)
+    buffer.seek(0)
+    return buffer.read()
diff --git a/integration/storage/requirements.txt 
b/integration/storage/requirements.txt
new file mode 100644
index 0000000..cbdd599
--- /dev/null
+++ b/integration/storage/requirements.txt
@@ -0,0 +1,5 @@
+azure-identity
+azure-mgmt-resource
+azure-mgmt-storage
+docker
+requests
diff --git a/integration/storage/test_azure_blobs.py 
b/integration/storage/test_azure_blobs.py
new file mode 100644
index 0000000..08a9185
--- /dev/null
+++ b/integration/storage/test_azure_blobs.py
@@ -0,0 +1,177 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the 'License'); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an 'AS IS' BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import os
+import string
+import sys
+import unittest
+
+try:
+    from azure import identity
+    from azure.mgmt import resource
+    from azure.mgmt import storage
+    from azure.mgmt.resource.resources import models as resource_models
+    from azure.mgmt.storage import models as storage_models
+except ImportError:
+    identity = resource = storage = resource_models = storage_models = None
+
+from integration.storage.base import Integration, random_string
+
+DEFAULT_TIMEOUT_SECONDS = 300
+DEFAULT_AZURE_LOCATION = 'EastUS2'
+MAX_STORAGE_ACCOUNT_NAME_LENGTH = 24
+
+
+class AzuriteStorageTest(Integration.ContainerTestBase):
+    provider = 'azure_blobs'
+
+    account = 'devstoreaccount1'
+    secret = 
'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='
+
+    image = 'arafato/azurite'
+    port = 10000
+    environment = {'executable': 'blob'}
+    ready_message = b'Azure Blob Storage Emulator listening'
+
+    has_sas_support = False
+
+    def test_cdn_url(self):
+        if not self.has_sas_support:
+            self.skipTest('Storage backend has no account SAS support')
+
+
+class AzuriteV3StorageTest(AzuriteStorageTest):
+    image = 'mcr.microsoft.com/azure-storage/azurite'
+    ready_message = b'Azurite Blob service is successfully listening'
+
+    has_sas_support = True
+
+    def test_upload_via_stream_with_content_encoding(self):
+        self.skipTest('Possible bug in AzuriteV3, see 
https://github.com/Azure/Azurite/issues/629')
+
+
+class IotedgeStorageTest(Integration.ContainerTestBase):
+    provider = 'azure_blobs'
+
+    account = random_string(10, string.ascii_lowercase)
+    secret = 
base64.b64encode(random_string(20).encode('ascii')).decode('ascii')
+
+    image = 'mcr.microsoft.com/azure-blob-storage'
+    port = 11002
+    environment = {'LOCAL_STORAGE_ACCOUNT_NAME': account, 
'LOCAL_STORAGE_ACCOUNT_KEY': secret}
+    ready_message = b'BlobService - StartAsync completed'
+
+
+class StorageTest(Integration.TestBase):
+    provider = 'azure_blobs'
+
+    kind = storage_models.Kind.STORAGE
+    access_tier = None  # type: storage_models.AccessTier
+
+    @classmethod
+    def setUpClass(cls):
+        if identity is None:
+            raise unittest.SkipTest('missing azure-identity library')
+
+        if resource is None or resource_models is None:
+            raise unittest.SkipTest('missing azure-mgmt-resource library')
+
+        if storage is None or storage_models is None:
+            raise unittest.SkipTest('missing azure-mgmt-storage library')
+
+        config = {
+            key: os.getenv(key)
+            for key in (
+                'AZURE_TENANT_ID',
+                'AZURE_SUBSCRIPTION_ID',
+                'AZURE_CLIENT_ID',
+                'AZURE_CLIENT_SECRET',
+            )
+        }
+
+        for key, value in config.items():
+            if not value:
+                raise unittest.SkipTest('missing environment variable %s' % 
key)
+
+        credentials = identity.ClientSecretCredential(
+            tenant_id=config['AZURE_TENANT_ID'],
+            client_id=config['AZURE_CLIENT_ID'],
+            client_secret=config['AZURE_CLIENT_SECRET'],
+        )
+
+        resource_client = resource.ResourceManagementClient(
+            credentials,
+            config['AZURE_SUBSCRIPTION_ID'],
+        )
+
+        storage_client = storage.StorageManagementClient(
+            credentials,
+            config['AZURE_SUBSCRIPTION_ID'],
+        )
+
+        location = os.getenv('AZURE_LOCATION', DEFAULT_AZURE_LOCATION)
+        name = 'libcloud'
+        name += random_string(MAX_STORAGE_ACCOUNT_NAME_LENGTH - len(name))
+        timeout = float(os.getenv('AZURE_TIMEOUT_SECONDS', 
DEFAULT_TIMEOUT_SECONDS))
+
+        group = resource_client.resource_groups.create_or_update(
+            resource_group_name=name,
+            parameters=resource_models.ResourceGroup(
+                location=location,
+                tags={
+                    'test': cls.__name__,
+                    'run': os.getenv('GITHUB_RUN_ID', '-'),
+                },
+            ),
+            timeout=timeout,
+        )
+
+        cls.addClassCleanup(lambda: resource_client.resource_groups
+                            .begin_delete(group.name)
+                            .result(timeout))
+
+        account = storage_client.storage_accounts.begin_create(
+            resource_group_name=group.name,
+            account_name=name,
+            parameters=storage_models.StorageAccountCreateParameters(
+                
sku=storage_models.Sku(name=storage_models.SkuName.STANDARD_LRS),
+                access_tier=cls.access_tier,
+                kind=cls.kind,
+                location=location,
+            ),
+        ).result(timeout)
+
+        keys = storage_client.storage_accounts.list_keys(
+            resource_group_name=group.name,
+            account_name=account.name,
+            timeout=timeout,
+        )
+
+        cls.account = account.name
+        cls.secret = keys.keys[0].value
+
+
+class StorageV2Test(StorageTest):
+    kind = storage_models.Kind.STORAGE_V2
+
+
+class BlobStorageTest(StorageTest):
+    kind = storage_models.Kind.BLOB_STORAGE
+    access_tier = storage_models.AccessTier.HOT
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())
diff --git a/tox.ini b/tox.ini
index e633fc3..7a22ab9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,9 @@
 [tox]
-envlist = py{pypy3.5,3.5,3.6,3.7,3.8,3.9},checks,lint,pylint,mypy,docs,coverage
+envlist = 
py{pypy3.5,3.5,3.6,3.7,3.8,3.9},checks,lint,pylint,mypy,docs,coverage,integration-storage
 skipsdist = true
 
 [testenv]
-passenv = TERM CI GITHUB_*
+passenv = TERM CI GITHUB_* DOCKER_*
 deps =
     -r{toxinidir}/requirements-tests.txt
     fasteners
@@ -14,7 +14,7 @@ basepython =
     pypypy3: pypy3
     py3.5: python3.5
     py3.6: python3.6
-    
{py3.7,docs,checks,lint,pylint,mypy,coverage,docs,py3.7-dist,py3.7-dist-wheel}: 
python3.7
+    
{py3.7,docs,checks,lint,pylint,mypy,coverage,docs,integration-storage,py3.7-dist,py3.7-dist-wheel}:
 python3.7
     {py3.8,py3.8-windows}: python3.8
     {py3.9}: python3.9
 setenv =
@@ -198,7 +198,7 @@ deps = -r{toxinidir}/requirements-tests.txt
 commands = flake8 libcloud/
            flake8 --max-line-length=160 libcloud/test/
            flake8 demos/
-           flake8 integration/
+           flake8 --max-line-length=160 integration/
            flake8 scripts/
            flake8 --ignore=E402,E902,W503,W504 docs/examples/
            flake8 --ignore=E402,E902,W503,W504 --max-line-length=160 contrib/
@@ -212,10 +212,22 @@ commands =
     bash ./scripts/check_file_names.sh
     python ./scripts/check_asf_license_headers.py .
 
-[testenv:integration]
-deps = -r{toxinidir}/integration/requirements.txt
+[testenv:integration-compute]
+deps = -r{toxinidir}/integration/compute/requirements.txt
 
-commands = python -m integration
+commands = python -m integration.compute
+
+[testenv:integration-storage]
+passenv = AZURE_CLIENT_SECRET
+
+setenv =
+  AZURE_CLIENT_ID=16cd65a3-dfa2-4272-bcdb-842cbbedb1b7
+  AZURE_TENANT_ID=982317c6-fb7e-4e92-abcd-196557e41c5b
+  AZURE_SUBSCRIPTION_ID=d6d608a6-e0c8-42ae-a548-2f41793709d2
+
+deps = -r{toxinidir}/integration/storage/requirements.txt
+
+commands = python -m integration.storage
 
 [testenv:coverage]
 deps =

Reply via email to