[lxc-devel] [pylxd/master] Mock LXD services

rockstar on Github lxc-bot at linuxcontainers.org
Mon May 23 01:50:14 UTC 2016


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 1398 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20160523/e35f123c/attachment.bin>
-------------- next part --------------
From 150e3ff5e5acd40853a3e56c4c9ed5ff0842177b Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Fri, 20 May 2016 22:26:09 -0600
Subject: [PATCH 1/5] Fix a bug with LXD_DIR in the lxd client.

---
 pylxd/client.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pylxd/client.py b/pylxd/client.py
index 43db745..13fe182 100644
--- a/pylxd/client.py
+++ b/pylxd/client.py
@@ -115,7 +115,7 @@ def __init__(self, endpoint=None, version='1.0'):
         else:
             if 'LXD_DIR' in os.environ:
                 path = os.path.join(
-                    os.environ.get['LXD_DIR'], 'unix.socket')
+                    os.environ.get('LXD_DIR'), 'unix.socket')
             else:
                 path = '/var/lib/lxd/unix.socket'
             self.api = _APINode('http+unix://{}'.format(

From d0187dbd1d362ff9c763c9edf5ee950ada53fb4e Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Fri, 20 May 2016 22:26:39 -0600
Subject: [PATCH 2/5] Add tests for pylxd.client

---
 pylxd/tests/__init__.py    |   0
 pylxd/tests/test_client.py | 108 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 108 insertions(+)
 create mode 100644 pylxd/tests/__init__.py
 create mode 100644 pylxd/tests/test_client.py

diff --git a/pylxd/tests/__init__.py b/pylxd/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pylxd/tests/test_client.py b/pylxd/tests/test_client.py
new file mode 100644
index 0000000..ecd4698
--- /dev/null
+++ b/pylxd/tests/test_client.py
@@ -0,0 +1,108 @@
+import os
+import unittest
+
+import mock
+import requests
+import requests_unixsocket
+
+from pylxd import client
+
+
+class TestClient(unittest.TestCase):
+    """Tests for pylxd.client.Client."""
+
+    def test_create(self):
+        """Client creation sets default API endpoint."""
+        expected = 'http+unix://%2Fvar%2Flib%2Flxd%2Funix.socket/1.0'
+
+        an_client = client.Client()
+
+        self.assertEqual(expected, an_client.api._api_endpoint)
+
+    def test_create_LXD_DIR(self):
+        """When LXD_DIR is set, use it in the client."""
+        os.environ['LXD_DIR'] = '/lxd'
+        expected = 'http+unix://%2Flxd%2Funix.socket/1.0'
+
+        an_client = client.Client()
+
+        self.assertEqual(expected, an_client.api._api_endpoint)
+
+    def test_create_endpoint(self):
+        """Explicitly set the client endpoint."""
+        endpoint = 'http://lxd'
+        expected = 'http://lxd/1.0'
+
+        an_client = client.Client(endpoint=endpoint)
+
+        self.assertEqual(expected, an_client.api._api_endpoint)
+
+
+class TestAPINode(unittest.TestCase):
+    """Tests for pylxd.client._APINode."""
+
+    def test_getattr(self):
+        """API Nodes can use object notation for nesting."""
+        node = client._APINode('http://test.com')
+
+        new_node = node.test
+
+        self.assertEqual(
+            'http://test.com/test', new_node._api_endpoint)
+
+    def test_getitem(self):
+        """API Nodes can use dict notation for nesting."""
+        node = client._APINode('http://test.com')
+
+        new_node = node['test']
+
+        self.assertEqual(
+            'http://test.com/test', new_node._api_endpoint)
+
+    def test_session_http(self):
+        """HTTP nodes return the default requests session."""
+        node = client._APINode('http://test.com')
+
+        self.assertEqual(requests, node.session)
+
+    def test_session_unix_socket(self):
+        """HTTP nodes return a requests_unixsocket session."""
+        node = client._APINode('http+unix://test.com')
+
+        self.assertIsInstance(node.session, requests_unixsocket.Session)
+
+    @mock.patch('pylxd.client.requests.get')
+    def test_get(self, get):
+        """Perform a session get."""
+        node = client._APINode('http://test.com')
+
+        node.get()
+
+        get.assert_called_once_with('http://test.com')
+
+    @mock.patch('pylxd.client.requests.post')
+    def test_post(self, post):
+        """Perform a session post."""
+        node = client._APINode('http://test.com')
+
+        node.post()
+
+        post.assert_called_once_with('http://test.com')
+
+    @mock.patch('pylxd.client.requests.put')
+    def test_put(self, put):
+        """Perform a session put."""
+        node = client._APINode('http://test.com')
+
+        node.put()
+
+        put.assert_called_once_with('http://test.com')
+
+    @mock.patch('pylxd.client.requests.delete')
+    def test_delete(self, delete):
+        """Perform a session delete."""
+        node = client._APINode('http://test.com')
+
+        node.delete()
+
+        delete.assert_called_once_with('http://test.com')

From e94353a7bca5ebbebf6b1153147839dfcf6659b7 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Sun, 22 May 2016 19:06:12 -0600
Subject: [PATCH 3/5] Fix the DEFAULT_TLS_VERSION holes.

---
 pylxd/deprecated/connection.py | 17 ++++++-----------
 1 file changed, 6 insertions(+), 11 deletions(-)

diff --git a/pylxd/deprecated/connection.py b/pylxd/deprecated/connection.py
index 544c8fd..65adbf0 100644
--- a/pylxd/deprecated/connection.py
+++ b/pylxd/deprecated/connection.py
@@ -31,19 +31,14 @@
 
 if hasattr(ssl, 'SSLContext'):
     # For Python >= 2.7.9 and Python 3.x
-    USE_STDLIB_SSL = True
+    if hasattr(ssl, 'PROTOCOL_TLSv1_2'):
+        DEFAULT_TLS_VERSION = ssl.PROTOCOL_TLSv1_2
+    else:
+        DEFAULT_TLS_VERSION = ssl.PROTOCOL_TLSv1
 else:
     # For Python 2.6 and <= 2.7.8
-    USE_STDLIB_SSL = False
-
-if not USE_STDLIB_SSL:
-    import OpenSSL.SSL
-
-# Detect SSL tls version
-if hasattr(ssl, 'PROTOCOL_TLSv1_2'):
-    DEFAULT_TLS_VERSION = ssl.PROTOCOL_TLSv1_2
-else:
-    DEFAULT_TLS_VERSION = OpenSSL.SSL.TLSv1_2_METHOD
+    from OpenSSL import SSL
+    DEFAULT_TLS_VERSION = SSL.TLSv1_2_METHOD
 
 
 class UnixHTTPConnection(http_client.HTTPConnection):

From 626940d03855b65bec3481cfdbd20783fece0057 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Sun, 22 May 2016 19:11:04 -0600
Subject: [PATCH 4/5] Add more coverage settings.

---
 .coveragerc | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/.coveragerc b/.coveragerc
index e4f4eb4..dedcde7 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -3,5 +3,6 @@ branch = True
 source = pylxd
 
 [report]
-ignore-errors = True
-omit = pylxd/tests/*
+omit =
+    pylxd/tests/*
+    pylxd/deprecated/*

From 31d2a1f3df4774d503bac1d72600d5c46af87e29 Mon Sep 17 00:00:00 2001
From: Paul Hummer <paul at eventuallyanyway.com>
Date: Sun, 22 May 2016 19:24:19 -0600
Subject: [PATCH 5/5] Add new pylxd unittests.

This brings test coverage of the new pylxd api up to 70%, which
is tolerable for now. It also exposed a bug that has now been
fixed.
---
 pylxd/tests/mock_lxd.py       | 115 ++++++++++++++++++++++++++++++++++++++++++
 pylxd/tests/test_container.py |  88 ++++++++++++++++++++++++++++++++
 pylxd/tests/test_image.py     |  22 ++++++++
 pylxd/tests/test_profile.py   |  20 ++++++++
 pylxd/tests/testing.py        |  19 +++++++
 test-requirements.txt         |   2 +
 6 files changed, 266 insertions(+)
 create mode 100644 pylxd/tests/mock_lxd.py
 create mode 100644 pylxd/tests/test_container.py
 create mode 100644 pylxd/tests/test_image.py
 create mode 100644 pylxd/tests/test_profile.py
 create mode 100644 pylxd/tests/testing.py

diff --git a/pylxd/tests/mock_lxd.py b/pylxd/tests/mock_lxd.py
new file mode 100644
index 0000000..76fa2a2
--- /dev/null
+++ b/pylxd/tests/mock_lxd.py
@@ -0,0 +1,115 @@
+import json
+
+
+def containers_POST(request, context):
+    context.status_code = 201
+    return json.dumps({'operation': 'operation-abc'})
+
+
+def container_GET(request, context):
+    if request.path.endswith('an-container'):
+        response_text = json.dumps({'metadata': {
+            'name': 'an-container',
+            'ephemeral': True,
+        }})
+        context.status_code = 200
+        return response_text
+    else:
+        context.status_code = 404
+
+
+def profile_GET(request, context):
+    name = request.path.split('/')[-1]
+    if name in ('an-profile', 'an-new-profile'):
+        return json.dumps({
+            'metadata': {
+                'name': name,
+            },
+        })
+    else:
+        context.status_code = 404
+
+
+RULES = [
+    # Containers
+    {
+        'text': json.dumps({'metadata': [
+            'http://pylxd.test/1.0/containers/an-container',
+        ]}),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/containers$',
+    },
+    {
+        'text': containers_POST,
+        'method': 'POST',
+        'url': r'^http://pylxd.test/1.0/containers$',
+    },
+    {
+        'text': container_GET,
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$',
+    },
+    {
+        'text': json.dumps({'operation': 'operation-abc'}),
+        'method': 'POST',
+        'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$',
+    },
+    {
+        'text': json.dumps({'operation': 'operation-abc'}),
+        'method': 'PUT',
+        'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$',
+    },
+    {
+        'text': json.dumps({'operation': 'operation-abc'}),
+        'method': 'DELETE',
+        'url': r'^http://pylxd.test/1.0/containers/(?P<container_name>.*)$',
+    },
+
+    # Images
+    {
+        'text': json.dumps({'metadata': [
+            'http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$',  # NOQA
+        ]}),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/images$',
+    },
+    {
+        'text': json.dumps({'metadata': {}}),
+        'method': 'POST',
+        'url': r'^http://pylxd.test/1.0/images$',
+    },
+    {
+        'text': json.dumps({
+            'metadata': {
+                'fingerprint': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',  # NOQA
+            },
+        }),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/images/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855$',  # NOQA
+    },
+
+    # Profiles
+    {
+        'text': json.dumps({'metadata': [
+            'http://pylxd.test/1.0/profiles/an-profile',
+        ]}),
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/profiles$',
+    },
+    {
+        'method': 'POST',
+        'url': r'^http://pylxd.test/1.0/profiles$',
+    },
+    {
+        'text': profile_GET,
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/profiles/(?P<container_name>.*)$',
+    },
+
+    # Operations
+    {
+        'text': '{"metadata": {"id": "operation-abc"}}',
+        'method': 'GET',
+        'url': r'^http://pylxd.test/1.0/operations/(?P<operation_id>.*)$',
+    },
+]
diff --git a/pylxd/tests/test_container.py b/pylxd/tests/test_container.py
new file mode 100644
index 0000000..a7a0c99
--- /dev/null
+++ b/pylxd/tests/test_container.py
@@ -0,0 +1,88 @@
+from pylxd import container
+from pylxd.tests import testing
+
+
+class TestContainer(testing.PyLXDTestCase):
+    """Tests for pylxd.container.Container."""
+
+    def test_all(self):
+        """A list of all containers are returned."""
+        containers = container.Container.all(self.client)
+
+        self.assertEqual(1, len(containers))
+
+    def test_get(self):
+        """Return a container."""
+        name = 'an-container'
+
+        an_container = container.Container.get(self.client, name)
+
+        self.assertEqual(name, an_container.name)
+
+    def test_get_not_found(self):
+        """NameError is raised when the container doesn't exist."""
+        name = 'an-missing-container'
+
+        self.assertRaises(
+            NameError, container.Container.get, self.client, name)
+
+    def test_create(self):
+        """A new container is created."""
+        config = {'name': 'an-new-container'}
+
+        an_new_container = container.Container.create(
+            self.client, config, wait=True)
+
+        self.assertEqual(config['name'], an_new_container.name)
+
+    def test_reload(self):
+        """A reload updates the properties of a container."""
+        an_container = container.Container(
+            name='an-container', _client=self.client)
+
+        an_container.reload()
+
+        self.assertTrue(an_container.ephemeral)
+
+    def test_reload_not_found(self):
+        """NameError is raised on a 404 for updating container."""
+        an_container = container.Container(
+            name='an-missing-container', _client=self.client)
+
+        self.assertRaises(NameError, an_container.reload)
+
+    def test_update(self):
+        """A container is updated."""
+        an_container = container.Container(
+            name='an-container', _client=self.client)
+        an_container.architecture = 1
+        an_container.config = {}
+        an_container.created_at = 1
+        an_container.devices = {}
+        an_container.ephemeral = 1
+        an_container.expanded_config = {}
+        an_container.expanded_devices = {}
+        an_container.profiles = 1
+        an_container.status = 1
+
+        an_container.update(wait=True)
+
+        self.assertTrue(an_container.ephemeral)
+
+    def test_rename(self):
+        an_container = container.Container(
+            name='an-container', _client=self.client)
+
+        an_container.rename('an-renamed-container', wait=True)
+
+        self.assertEqual('an-renamed-container', an_container.name)
+
+    def test_delete(self):
+        """A container is deleted."""
+        # XXX: rockstar (21 May 2016) - This just executes
+        # a code path. There should be an assertion here, but
+        # it's not clear how to assert that, just yet.
+        an_container = container.Container(
+            name='an-container', _client=self.client)
+
+        an_container.delete(wait=True)
diff --git a/pylxd/tests/test_image.py b/pylxd/tests/test_image.py
new file mode 100644
index 0000000..7e97997
--- /dev/null
+++ b/pylxd/tests/test_image.py
@@ -0,0 +1,22 @@
+import hashlib
+
+from pylxd import image
+from pylxd.tests import testing
+
+
+class TestImage(testing.PyLXDTestCase):
+    """Tests for pylxd.image.Image."""
+
+    def test_all(self):
+        """A list of all images is returned."""
+        images = image.Image.all(self.client)
+
+        self.assertEqual(1, len(images))
+
+    def test_create(self):
+        """An image is created."""
+        fingerprint = hashlib.sha256(b'').hexdigest()
+        a_image = image.Image.create(self.client, b'')
+
+        self.assertIsInstance(a_image, image.Image)
+        self.assertEqual(fingerprint, a_image.fingerprint)
diff --git a/pylxd/tests/test_profile.py b/pylxd/tests/test_profile.py
new file mode 100644
index 0000000..f8ea7e1
--- /dev/null
+++ b/pylxd/tests/test_profile.py
@@ -0,0 +1,20 @@
+from pylxd import profile
+from pylxd.tests import testing
+
+
+class TestProfile(testing.PyLXDTestCase):
+    """Tests for pylxd.profile.Profile."""
+
+    def test_all(self):
+        """A list of all profiles is returned."""
+        profiles = profile.Profile.all(self.client)
+
+        self.assertEqual(1, len(profiles))
+
+    def test_create(self):
+        """A new profile is created."""
+        an_profile = profile.Profile.create(
+            self.client, name='an-new-profile', config={})
+
+        self.assertIsInstance(an_profile, profile.Profile)
+        self.assertEqual('an-new-profile', an_profile.name)
diff --git a/pylxd/tests/testing.py b/pylxd/tests/testing.py
new file mode 100644
index 0000000..88cde8c
--- /dev/null
+++ b/pylxd/tests/testing.py
@@ -0,0 +1,19 @@
+import unittest
+
+import mock_services
+
+from pylxd.client import Client
+from pylxd.tests import mock_lxd
+
+
+class PyLXDTestCase(unittest.TestCase):
+    """A test case for handling mocking of LXD services."""
+
+    def setUp(self):
+        mock_services.update_http_rules(mock_lxd.RULES)
+        mock_services.start_http_mock()
+
+        self.client = Client(endpoint='http://pylxd.test')
+
+    def tearDown(self):
+        mock_services.stop_http_mock()
diff --git a/test-requirements.txt b/test-requirements.txt
index 5c10f8d..f76850a 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -2,3 +2,5 @@ ddt>=0.7.0
 nose>=1.3.7
 mock>=1.3.0
 flake8>=2.5.0
+# See https://github.com/novafloss/mock-services/pull/15
+-e git://github.com/rockstar/mock-services.git@aba3977d1a3f43afd77d99f241ee1111c20deeed#egg=mock-services


More information about the lxc-devel mailing list