The following pull request was submitted through Github. It can be accessed and reviewed at: https://github.com/lxc/pylxd/pull/85
This e-mail was sent by the LXC bot, direct replies will not reach the author unless they happen to be subscribed to this list. === Description (from pull-request) === As we've seen significant uptick in the use of pylxd recently, and some demand for good testing, this patch adds unit tests for the newer API. It adds a mock service for requests, so the entire test suite can be run without a LXD instance at all. Currently, it puts the unit test coverage at 70% of the new API. While writing these tests, I also found a bug in the `LXD_DIR` logic of the client, and fixed that (something initial tests would have caught...) This infrastructure is a start, and should make it easier for others to contribute without having to make up some random mock service system. It's definitely not the end of what it should be (and there's even an `XXX` comment to that effect in this patch). While running these tests on various platforms (to make sure they run fine outside of a lxd environment), I found an issue with the default TLS version, where the two separate if statements had some holes in the logic. I _believe_ I fixed those on the platforms that I've tested on, though my gut tells me there still might be an issue with how that default TLS is calculated.
From 150e3ff5e5acd40853a3e56c4c9ed5ff0842177b Mon Sep 17 00:00:00 2001 From: Paul Hummer <p...@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 <p...@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 <p...@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 <p...@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 <p...@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
_______________________________________________ lxc-devel mailing list lxc-devel@lists.linuxcontainers.org http://lists.linuxcontainers.org/listinfo/lxc-devel