[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