From 9df7a100eb4258dbc853247b787beb52c3a36bde Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 12 May 2016 12:48:44 -0500 Subject: [PATCH 01/14] Add missing requirements for tests. Without these additional requirements the `make test` command will fail. * Added Django<1.9; Django 1.9 is not supported * Added django-rq; this is optional in usage, but required for testing/devevelopment. --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 5bcf8d9..3e8815e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,8 @@ requests>=2.0.0 # Tests +Django<1.9 +django-rq==0.9.0 ipdb==0.8 ipython==2.4.1 mock==1.0.1 From 90e2048a9109799b9e0582ae26bf81e79388cab6 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 12 May 2016 12:51:47 -0500 Subject: [PATCH 02/14] Update commands for testing --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index bc407c0..d99f7bd 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,9 @@ Also see the [example response data](./tests/fixtures/ok.json). ### Testing +The following commands should be run in a _virtualenv_. + + pip install -r requirements.txt make test open coverage/index.html From be1321f95a3d501a8bcb7eba119381c88e161ac3 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 12 May 2016 13:52:38 -0500 Subject: [PATCH 03/14] Update README with steps to successfully execute make test --- README.md | 1 + requirements.txt | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index d99f7bd..8d1d568 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Also see the [example response data](./tests/fixtures/ok.json). The following commands should be run in a _virtualenv_. pip install -r requirements.txt + pip install "django<1.9" django-rq make test open coverage/index.html diff --git a/requirements.txt b/requirements.txt index 3e8815e..5bcf8d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,6 @@ requests>=2.0.0 # Tests -Django<1.9 -django-rq==0.9.0 ipdb==0.8 ipython==2.4.1 mock==1.0.1 From 691190f5e229e8a5cfc4d95f263d71e1cdb07894 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 12 May 2016 14:04:19 -0500 Subject: [PATCH 04/14] Add python3.5 and django1.8 to the tox envlist --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4eb0d08..d40d945 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py27-django{15,16,17,18,rq} + py27-django{15,16,17,18,rq}, py35-django18 [testenv] setenv = From 1ab190cc9f188e837f6ea75c8a3efa243f106825 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 12 May 2016 14:27:31 -0500 Subject: [PATCH 05/14] Update README section on Testing --- README.md | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8d1d568..e7c67e8 100644 --- a/README.md +++ b/README.md @@ -49,16 +49,42 @@ Also see the [example response data](./tests/fixtures/ok.json). ### Testing -The following commands should be run in a _virtualenv_. +There are several ways to run the projects tests. The recommended approach +is to run `tox`, becuase your testing environment will be properly set-up. - pip install -r requirements.txt - pip install "django<1.9" django-rq - make test - open coverage/index.html +For a thorough multi-Django version testing use: -For more thorough multi-Django version testing use: + $ tox - tox +To list the configured environments: + + $ tox -l + py27-django15 + py27-django16 + py27-django17 + py27-django18 + py27-djangorq + py35-django18 + +You can execute a single test environment: + + $ tox -e py35-django18 + +If you manage your own environment, you can run the `make test` command +yourself. It is recommened that you do this in a _virtualenv_. + + $ pip install -r requirements.txt # Normally tox would install these + $ pip install "django<1.9" django-rq # Normally tox would install these + $ make test + $ open coverage/index.html + +You can run a subset of tests by setting the `PACKAGES` variable: + + $ make PACKAGES="tests.test_endpoint tests.test_service_resources" + +To run a single test: + + $ make PACKAGES=tests.test_endpoint:EndpointTestCase.test_status_endpoint_returns_200_on_success ### License From 7087a9d221b73eaa2f3857ef048853960b1bb745 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 12 May 2016 14:27:48 -0500 Subject: [PATCH 06/14] Remove debugging print statements --- tests/test_endpoint.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_endpoint.py b/tests/test_endpoint.py index ccdef6d..0e83bc0 100644 --- a/tests/test_endpoint.py +++ b/tests/test_endpoint.py @@ -19,7 +19,6 @@ def assert_content(self, content, status='ok', **overrides): expected_data = self.expected_data.copy() expected_data['status'] = status expected_data['resources'].update(**overrides) - # print data; print '*' * 80; print expected_data # DEBUG self.assertEqual(data, expected_data) # Assertions @@ -28,7 +27,6 @@ def assert_content(self, content, status='ok', **overrides): def test_status_endpoint_returns_200_on_success(self): response = self.client.get('/_status/') self.assertEqual(response.status_code, 200) - # print response.content # DEBUG uncomment when updating ok.json self.assert_content(response.content) def test_status_endpoint_returns_200_with_warning_on_timeout(self): From 8983cf46f9e90fdf3dcf61d50ad58557cbf19f70 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 12 May 2016 16:56:12 -0500 Subject: [PATCH 07/14] Assert JSON decoding error message in both python 2 and 3 --- tests/test_service_resources.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_service_resources.py b/tests/test_service_resources.py index 8f79302..b49bff6 100644 --- a/tests/test_service_resources.py +++ b/tests/test_service_resources.py @@ -40,7 +40,12 @@ def test_error_result_reports_error_on_success(self): def test_invalid_json_reports_error_on_success(self): self.mock_ok_service(data=None) + json_decoding_error_messages = { + 'python2_error': 'No JSON object could be decoded', + 'python3_error': 'Expecting value: line 1 column 1 (char 0)', + } + result = self.service.check() self.assertEqual(result['status'], ERROR) self.assertEqual(result['result'], None) - self.assertEqual(result['error'], 'No JSON object could be decoded') + self.assertIn(result['error'], json_decoding_error_messages.values()) From a4f30ef5c8631af77d662fedc0f7eff63739c791 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 12 May 2016 17:03:11 -0500 Subject: [PATCH 08/14] Decode bytes from python3 response content --- tests/test_endpoint.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_endpoint.py b/tests/test_endpoint.py index 0e83bc0..aea909f 100644 --- a/tests/test_endpoint.py +++ b/tests/test_endpoint.py @@ -14,8 +14,9 @@ def setUp(self): self.mock_ok_elasticsearch() self.expected_data = self.get_fixture('ok.json') - def assert_content(self, content, status='ok', **overrides): - data = json.loads(content) + def assert_response_content(self, response, status='ok', **overrides): + encoding = getattr(response, 'charset', 'utf-8') + data = json.loads(response.content.decode(encoding)) expected_data = self.expected_data.copy() expected_data['status'] = status expected_data['resources'].update(**overrides) @@ -27,7 +28,7 @@ def assert_content(self, content, status='ok', **overrides): def test_status_endpoint_returns_200_on_success(self): response = self.client.get('/_status/') self.assertEqual(response.status_code, 200) - self.assert_content(response.content) + self.assert_response_content(response) def test_status_endpoint_returns_200_with_warning_on_timeout(self): self.mock_duration(step=1.0) @@ -40,7 +41,7 @@ def test_status_endpoint_returns_200_with_warning_on_timeout(self): for resource in self.expected_data['resources'].values(): resource.update(status='warning', latency=1.0) - self.assert_content(response.content, status='warning') + self.assert_response_content(response, status='warning') def test_status_endpoint_returns_503_on_http_error(self): # redo requests mocks @@ -58,7 +59,7 @@ def test_status_endpoint_returns_503_on_http_error(self): bar_data = dict(resources['bar'], status=u'error', error=bar_error) bar_data['result']['status'] = u'warning' overrides = {'foo': foo_data, 'bar': bar_data} - self.assert_content(response.content, status=u'error', **overrides) + self.assert_response_content(response, status=u'error', **overrides) def test_status_endpoint_returns_503_on_database_error(self): message = 'mock error' @@ -70,7 +71,7 @@ def test_status_endpoint_returns_503_on_database_error(self): database_data = self.expected_data['resources']['database'].copy() expected_data = dict(database_data, status='error', error=message) overrides = {'database': expected_data} - self.assert_content(response.content, status='error', **overrides) + self.assert_response_content(response, status='error', **overrides) def test_status_endpoint_returns_503_on_elasticsearch_error(self): # redo requests mocks @@ -84,4 +85,4 @@ def test_status_endpoint_returns_503_on_elasticsearch_error(self): es_data = self.expected_data['resources']['es_resource'].copy() expected_data = dict(es_data, status='error', error='Some ES error') overrides = {'es_resource': expected_data} - self.assert_content(response.content, status='error', **overrides) + self.assert_response_content(response, status='error', **overrides) From 299a0f672793eee6649240461f6c9ee222eaeea9 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 12 May 2016 17:46:46 -0500 Subject: [PATCH 09/14] In py3, map() returns an iterator and these statements weren't being executed --- canary_endpoint/resources/databases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canary_endpoint/resources/databases.py b/canary_endpoint/resources/databases.py index 016b8b3..06d61b1 100644 --- a/canary_endpoint/resources/databases.py +++ b/canary_endpoint/resources/databases.py @@ -31,7 +31,7 @@ def check(self): result = super(Database, self).check() try: cursor = self.connection.cursor() - map(cursor.execute, self.statements) + [cursor.execute(sql) for sql in self.statements] return dict(result, status=OK) except (DatabaseError, MySQLError) as e: return dict(result, status=ERROR, error=str(e)) From bbb59b5997cd50f96303dfc5913e311cbdd9ff60 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 12 May 2016 17:49:29 -0500 Subject: [PATCH 10/14] Bump version number --- canary_endpoint/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canary_endpoint/version.py b/canary_endpoint/version.py index ef7eb44..a71c5c7 100644 --- a/canary_endpoint/version.py +++ b/canary_endpoint/version.py @@ -1 +1 @@ -__version__ = '0.6.0' +__version__ = '0.7.0' From 56ca033b01d04c47ca021180814978a8b88a6525 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 19 May 2016 21:00:42 -0500 Subject: [PATCH 11/14] Add language classifiers to setup.py --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 63ffcb0..37786b2 100644 --- a/setup.py +++ b/setup.py @@ -28,5 +28,7 @@ classifiers=[ 'Environment :: Web Environment', 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.5', ], ) From e9e3b31bf30fe8f9454d97d3cf8d973c2ed671c3 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 19 May 2016 21:09:02 -0500 Subject: [PATCH 12/14] Use for loop instead of comprehension when executing sql --- canary_endpoint/resources/databases.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/canary_endpoint/resources/databases.py b/canary_endpoint/resources/databases.py index 06d61b1..90f3642 100644 --- a/canary_endpoint/resources/databases.py +++ b/canary_endpoint/resources/databases.py @@ -31,7 +31,9 @@ def check(self): result = super(Database, self).check() try: cursor = self.connection.cursor() - [cursor.execute(sql) for sql in self.statements] + for sql in self.statements: + cursor.execute(sql) + return dict(result, status=OK) except (DatabaseError, MySQLError) as e: return dict(result, status=ERROR, error=str(e)) From 22b8b60b7bd7d7757a07904d59f69deee70dcdaa Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Thu, 19 May 2016 21:34:32 -0500 Subject: [PATCH 13/14] Consistent error message across Python versions --- canary_endpoint/resources/services.py | 8 ++++++++ tests/test_service_resources.py | 7 +------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/canary_endpoint/resources/services.py b/canary_endpoint/resources/services.py index 58ec438..161f08f 100644 --- a/canary_endpoint/resources/services.py +++ b/canary_endpoint/resources/services.py @@ -1,4 +1,9 @@ import requests +try: + from json.decoder import JSONDecodeError +except ImportError: + class JSONDecodeError(ValueError): + pass from ..constants import OK, WARNING, ERROR from ..decorators import timed @@ -57,5 +62,8 @@ def check(self): elif service_status == ERROR: status = ERROR return dict(result, status=status, result=service_result) + except JSONDecodeError: + error_message = 'No JSON object could be decoded' + return dict(result, status=ERROR, result=None, error=error_message) except (AttributeError, ValueError) as e: return dict(result, status=ERROR, result=None, error=str(e)) diff --git a/tests/test_service_resources.py b/tests/test_service_resources.py index b49bff6..8f79302 100644 --- a/tests/test_service_resources.py +++ b/tests/test_service_resources.py @@ -40,12 +40,7 @@ def test_error_result_reports_error_on_success(self): def test_invalid_json_reports_error_on_success(self): self.mock_ok_service(data=None) - json_decoding_error_messages = { - 'python2_error': 'No JSON object could be decoded', - 'python3_error': 'Expecting value: line 1 column 1 (char 0)', - } - result = self.service.check() self.assertEqual(result['status'], ERROR) self.assertEqual(result['result'], None) - self.assertIn(result['error'], json_decoding_error_messages.values()) + self.assertEqual(result['error'], 'No JSON object could be decoded') From 053a3199f7574df7ebebce398868a0d33964a8e2 Mon Sep 17 00:00:00 2001 From: Scott Sullivan Date: Fri, 20 May 2016 21:21:55 -0500 Subject: [PATCH 14/14] Use a concrete value for deterministic behavior --- tests/test_endpoint.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_endpoint.py b/tests/test_endpoint.py index aea909f..769c57d 100644 --- a/tests/test_endpoint.py +++ b/tests/test_endpoint.py @@ -14,9 +14,8 @@ def setUp(self): self.mock_ok_elasticsearch() self.expected_data = self.get_fixture('ok.json') - def assert_response_content(self, response, status='ok', **overrides): - encoding = getattr(response, 'charset', 'utf-8') - data = json.loads(response.content.decode(encoding)) + def assert_content(self, content, status='ok', **overrides): + data = json.loads(content.decode('utf-8')) expected_data = self.expected_data.copy() expected_data['status'] = status expected_data['resources'].update(**overrides) @@ -28,7 +27,7 @@ def assert_response_content(self, response, status='ok', **overrides): def test_status_endpoint_returns_200_on_success(self): response = self.client.get('/_status/') self.assertEqual(response.status_code, 200) - self.assert_response_content(response) + self.assert_content(response.content) def test_status_endpoint_returns_200_with_warning_on_timeout(self): self.mock_duration(step=1.0) @@ -41,7 +40,7 @@ def test_status_endpoint_returns_200_with_warning_on_timeout(self): for resource in self.expected_data['resources'].values(): resource.update(status='warning', latency=1.0) - self.assert_response_content(response, status='warning') + self.assert_content(response.content, status='warning') def test_status_endpoint_returns_503_on_http_error(self): # redo requests mocks @@ -59,7 +58,7 @@ def test_status_endpoint_returns_503_on_http_error(self): bar_data = dict(resources['bar'], status=u'error', error=bar_error) bar_data['result']['status'] = u'warning' overrides = {'foo': foo_data, 'bar': bar_data} - self.assert_response_content(response, status=u'error', **overrides) + self.assert_content(response.content, status=u'error', **overrides) def test_status_endpoint_returns_503_on_database_error(self): message = 'mock error' @@ -71,7 +70,7 @@ def test_status_endpoint_returns_503_on_database_error(self): database_data = self.expected_data['resources']['database'].copy() expected_data = dict(database_data, status='error', error=message) overrides = {'database': expected_data} - self.assert_response_content(response, status='error', **overrides) + self.assert_content(response.content, status='error', **overrides) def test_status_endpoint_returns_503_on_elasticsearch_error(self): # redo requests mocks @@ -85,4 +84,4 @@ def test_status_endpoint_returns_503_on_elasticsearch_error(self): es_data = self.expected_data['resources']['es_resource'].copy() expected_data = dict(es_data, status='error', error='Some ES error') overrides = {'es_resource': expected_data} - self.assert_response_content(response, status='error', **overrides) + self.assert_content(response.content, status='error', **overrides)