diff --git a/README.md b/README.md index 8b1c625..3b9726e 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ This directory contains YAML files where default values for mocked dependencies This directory is the default location for your Touchstone tests. This can optionally be configured for each service in `touchstone.yml`. Touchstone follows a _given_, _when_, _then_ testing pattern. Each test is declared in a Python file prefixed with `test_` containing classes that extend `TouchstoneTest`. By extending this class, you can access Touchstone mocked dependencies to setup and then verify your requirements. For example, we can insert a document into a Mongo DB collection and then verify it exists using the following APIs: ```python -self.mocks.mongodb.setup.insert_document('my_db', 'my_collection', {'foo': 'bar'}) -result: bool = self.mocks.mongodb.verify.document_exists('my_db', 'my_collection', {'foo': 'bar'}) +self.mocks.mongodb.setup().insert_document('my_db', 'my_collection', {'foo': 'bar'}) +result: bool = self.mocks.mongodb.verify().document_exists('my_db', 'my_collection', {'foo': 'bar'}) ``` Important APIs: * `self.mocks` - Hook into Touchstone managed mock dependencies. @@ -84,9 +84,10 @@ Important APIs: * [MySQL](./docs/mocks/mysql.md) * [Rabbit MQ](./docs/mocks/rabbitmq.md) * [S3](./docs/mocks/s3.md) + * [Filesystem](./docs/mocks/filesystem.md) * [Add one!](./docs/add-mock.md) -If a specific mock is not supported, consider building your service independent of the implementation layer. For example, if have a dependency on PostgreSQL, use the MySQL mock as your database implementation during testing. +If a specific mock is not supported, consider building your service independent of the implementation layer. For example, if you have a dependency on PostgreSQL, use the MySQL mock as your database implementation during testing. When running via `touchstone develop`, dev ports for each mock are used. When running touchstone via `touchstone run`, ports are automatically discovered and available to your service containers via the following environment variables: * `TS_{MOCK_NAME}_HOST` - Host where the mock is running. diff --git a/docs/add-mock.md b/docs/add-mock.md index 4fe9a8a..468b77e 100644 --- a/docs/add-mock.md +++ b/docs/add-mock.md @@ -1,7 +1,6 @@ Contribute a New Mock ====== - 1. Create a new directory in [touchstone/lib/mocks](../touchstone/lib/mocks) - 1. Create a new class extending [Mock](../touchstone/lib/mocks/mock.py) + 1. Create a new [Runnable](../touchstone/lib/mocks/runnables) or [Networked Runnable](../touchstone/lib/mocks/networked_runnables) 1. Add a new property to the [Mocks](../touchstone/lib/mocks/mocks.py) class, so your new mock is accessible in user test cases - 1. Build a concrete instance of your new mock in the [Bootstrap](../touchstone/bootstrap.py) `__build_mocks` method with its required dependencies + 1. Build a concrete instance of your new mock in the [Mock Factory](../touchstone/lib/mocks/mock_factory.py) with its required dependencies 1. Write [unit](../tests) and [Touchstone tests](../touchstone-tests) for your new mock diff --git a/docs/mocks/filesystem.md b/docs/mocks/filesystem.md new file mode 100644 index 0000000..489b8d4 --- /dev/null +++ b/docs/mocks/filesystem.md @@ -0,0 +1,44 @@ +Filesystem +====== +Used to mock a filesystem for verifying file creation and contents. + +Note: All paths used in the "setup" and "verify" APIs use the "defaults" directory as a base path. + + +## Specs + * Name: filesystem + + +## Configuration +N/A + + +## Defaults Example +```yaml +--- +directories: + - path: ./filesystem/some-dir + files: + - ./filesystem/foo.csv + - ./filesystem/bar.png + - path: ./filesystem/some-dir/sub-dir + files: + - ./filesystem/foo.csv +``` + + +## Usage Example +```python +# Verify a file exists in a directory +result: bool = self.mocks.filesystem.verify().file_exists('./filesystem/some-dir/foo.csv') + +# Verify a file's content matches as expected +result: bool = self.mocks.filesystem.verify().file_matches('./filesystem/some-dir/foo.csv', given) +``` + +If you are performing filesystem operations in your test code, you must join with `get_base_path` when referring to file paths. This returns the path to the "defaults" folder. For example: +```python +path = os.path.join(self.mocks.filesystem.get_base_path(), './filesystem/foo.csv') +with open(path, 'rb') as data: + return bytes(data.read()) +``` diff --git a/docs/mocks/http.md b/docs/mocks/http.md index fcfed31..87356ab 100644 --- a/docs/mocks/http.md +++ b/docs/mocks/http.md @@ -45,8 +45,8 @@ requests: ## Usage Example ```python # Return JSON when a GET request is made to an endpoint -self.mocks.http.setup.get('/some-endpoint', {'foo': 'bar'}) +self.mocks.http.setup().get('/some-endpoint', {'foo': 'bar'}) # Verify that an endpoint was called -result: bool = self.mocks.http.verify.get_called('/some-endpoint') +result: bool = self.mocks.http.verify().get_called('/some-endpoint') ``` diff --git a/docs/mocks/mongodb.md b/docs/mocks/mongodb.md index c4fbd78..89c3e57 100644 --- a/docs/mocks/mongodb.md +++ b/docs/mocks/mongodb.md @@ -28,8 +28,8 @@ databases: ## Usage Example ```python # Insert a document into a collection -self.mocks.mongodb.setup.insert_document('my_db', 'my_collection', {'foo': 'bar'}) +self.mocks.mongodb.setup().insert_document('my_db', 'my_collection', {'foo': 'bar'}) # Verify that a document exists in a collection -result: bool = self.mocks.mongodb.verify.document_exists('my_db', 'my_collection', {'foo': 'bar'}) +result: bool = self.mocks.mongodb.verify().document_exists('my_db', 'my_collection', {'foo': 'bar'}) ``` diff --git a/docs/mocks/mysql.md b/docs/mocks/mysql.md index 87c5a42..347cad8 100644 --- a/docs/mocks/mysql.md +++ b/docs/mocks/mysql.md @@ -29,8 +29,8 @@ databases: ## Usage Example ```python # Insert a row into a table -self.mocks.mysql.setup.insert_row('my_db', 'my_table', {'foo': 'bar'}) +self.mocks.mysql.setup().insert_row('my_db', 'my_table', {'foo': 'bar'}) # Verify that a row exists in a table -result: bool = self.mocks.mysql.verify.row_exists('my_db', 'my_table', {'foo': 'bar'}) +result: bool = self.mocks.mysql.verify().row_exists('my_db', 'my_table', {'foo': 'bar'}) ``` diff --git a/docs/mocks/rabbitmq.md b/docs/mocks/rabbitmq.md index a8b2ce1..ff7b3ff 100644 --- a/docs/mocks/rabbitmq.md +++ b/docs/mocks/rabbitmq.md @@ -42,11 +42,11 @@ exchanges: ## Usage Example ```python # Publish a message to an exchange -self.mocks.rabbitmq.setup.publish('default-direct.exchange', 'some payload', routing_key='foo') +self.mocks.rabbitmq.setup().publish('default-direct.exchange', 'some payload', routing_key='foo') # Verify that a certain number of messages were published to an exchange and routing key -result: bool = self.mocks.rabbitmq.verify.messages_published('default-direct.exchange', num_expected=3, routing_key='foo') +result: bool = self.mocks.rabbitmq.verify().messages_published('default-direct.exchange', num_expected=3, routing_key='foo') # Verify that a payload was published to an exchange and routing key -result: bool = self.mocks.rabbitmq.verify.payload_published('default-topic.exchange', 'some payload', routing_key='foo') +result: bool = self.mocks.rabbitmq.verify().payload_published('default-topic.exchange', 'some payload', routing_key='foo') ``` diff --git a/docs/mocks/s3.md b/docs/mocks/s3.md index 71fbf49..a697d77 100644 --- a/docs/mocks/s3.md +++ b/docs/mocks/s3.md @@ -22,21 +22,28 @@ buckets: objects: - name: foo.csv content-type: text/csv - path: ./s3-objects/foo.csv + path: ./s3/foo.csv - name: test/bar.png content-type: image/png - path: ./s3-objects/bar.png + path: ./s3/bar.png ``` ## Usage Example ```python # Create a bucket -self.mocks.s3.setup.create_bucket('bucket_name') +self.mocks.s3.setup().create_bucket('bucket_name') # Put an object in a bucket -self.mocks.s3.setup.put_object('bucket_name', 'object_name', data) +self.mocks.s3.setup().put_object('bucket_name', 'object_name', data) # Verify an object exists in a bucket -result: bool = self.mocks.s3.verify.object_exists('bucket_name', 'object_name') +result: bool = self.mocks.s3.verify().object_exists('bucket_name', 'object_name') +``` + +If you are performing filesystem operations in your test code, you must join with `get_base_path` when referring to file paths. This returns the path to the "defaults" folder. For example: +```python +path = os.path.join(self.mocks.s3.get_base_path(), './s3/foo.csv') +with open(path, 'rb') as data: + return bytes(data.read()) ``` diff --git a/examples/java-spring/touchstone/tests/test_order.py b/examples/java-spring/touchstone/tests/test_order.py index 562fde8..c52d069 100644 --- a/examples/java-spring/touchstone/tests/test_order.py +++ b/examples/java-spring/touchstone/tests/test_order.py @@ -23,8 +23,8 @@ def given(self) -> object: } def when(self, given) -> object: - self.mocks.rabbitmq.setup.publish('order-placed.exchange', json.dumps(given)) + self.mocks.rabbitmq.setup().publish('order-placed.exchange', json.dumps(given)) return None def then(self, given, result) -> bool: - return self.mocks.mongodb.verify.document_exists(creds.MONGO_DATABASE, creds.MONGO_COLLECTION, given) + return self.mocks.mongodb.verify().document_exists(creds.MONGO_DATABASE, creds.MONGO_COLLECTION, given) diff --git a/examples/java-spring/touchstone/tests/test_user.py b/examples/java-spring/touchstone/tests/test_user.py index 43e2f43..6b434ca 100644 --- a/examples/java-spring/touchstone/tests/test_user.py +++ b/examples/java-spring/touchstone/tests/test_user.py @@ -22,7 +22,7 @@ def given(self) -> object: 'lastName': 'Brown', 'email': 'jane789@example.com' } - self.mocks.mysql.setup.insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, given) + self.mocks.mysql.setup().insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, given) return given def when(self, given) -> object: @@ -48,7 +48,7 @@ def given(self) -> object: 'lastName': 'Brown', 'email': 'jane789@example.com' } - self.mocks.http.setup.get('/jane-brown/email', given['email']) + self.mocks.http.setup().get('/jane-brown/email', given['email']) return given def when(self, given) -> object: @@ -66,7 +66,7 @@ def then(self, given, result) -> bool: expected_response = given.copy() expected_response['id'] = validation.ANY expected_row = given.copy() - return validation.matches(expected_response, result) and self.mocks.mysql.verify.row_exists( + return validation.matches(expected_response, result) and self.mocks.mysql.verify().row_exists( creds.MYSQL_DATABASE, creds.MYSQL_TABLE, expected_row) @@ -93,7 +93,7 @@ def given(self) -> object: 'lastName': 'Brown', 'email': 'jane789@example.com' } - self.mocks.mysql.setup.insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, existing_user) + self.mocks.mysql.setup().insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, existing_user) return new_info def when(self, given) -> object: @@ -104,7 +104,7 @@ def when(self, given) -> object: return None def then(self, given, result) -> bool: - return self.mocks.mysql.verify.row_exists(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, given) + return self.mocks.mysql.verify().row_exists(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, given) class DeleteUser(TouchstoneTest): @@ -128,7 +128,7 @@ def given(self) -> object: 'lastName': 'Brown', 'email': 'jane789@example.com' } - self.mocks.mysql.setup.insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, user) + self.mocks.mysql.setup().insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, user) return user_id def when(self, given) -> object: @@ -140,6 +140,6 @@ def then(self, given, result) -> bool: where = { 'id': given } - return self.mocks.rabbitmq.verify.payload_published('user.exchange', str(given), - routing_key='user-deleted') and \ - self.mocks.mysql.verify.row_does_not_exist(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, where) + return self.mocks.rabbitmq.verify().payload_published('user.exchange', str(given), + routing_key='user-deleted') and \ + self.mocks.mysql.verify().row_does_not_exist(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, where) diff --git a/examples/java-spring/touchstone/touchstone.yml b/examples/java-spring/touchstone/touchstone.yml index 95c8638..e990e5e 100644 --- a/examples/java-spring/touchstone/touchstone.yml +++ b/examples/java-spring/touchstone/touchstone.yml @@ -1,4 +1,5 @@ --- +touchstone-version: 1.0.0 services: - name: my-app availability_endpoint: "/actuator/health" diff --git a/tests/lib/mocks/test_mocks.py b/tests/lib/mocks/test_mocks.py index 4be0afc..d3fdaf0 100644 --- a/tests/lib/mocks/test_mocks.py +++ b/tests/lib/mocks/test_mocks.py @@ -12,12 +12,6 @@ def setUp(self) -> None: self.mock_mongodb = Mock() self.mocks = Mocks() - self.mocks.http = self.mock_http - self.mocks.register_mock(self.mock_http) - self.mocks.rabbitmq = self.mock_rabbitmq - self.mocks.register_mock(self.mock_rabbitmq) - self.mocks.mongodb = self.mock_mongodb - self.mocks.register_mock(self.mock_mongodb) def test_start_mocksNotRunning_mocksAreRunning(self): # When diff --git a/tests/test_common.py b/tests/test_common.py index 0a372df..33dfd0e 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,34 +1,10 @@ import unittest -from unittest import TestCase, mock +from unittest import TestCase from touchstone import common class TestCommon(TestCase): - @mock.patch('touchstone.common.os') - def test_sanityCheckPasses_requirementsMet_ReturnsTrue(self, mock_os): - # Given - mock_os.getcwd.return_value = 'temp' - mock_os.path.exists.return_value = True - - # When - result = common.sanity_check_passes() - - # Then - self.assertTrue(result) - - @mock.patch('touchstone.common.os') - def test_sanityCheckPasses_requirementsNotMet_ReturnsFalse(self, mock_os): - # Given - mock_os.getcwd.return_value = 'temp' - mock_os.path.exists.return_value = False - - # When - result = common.sanity_check_passes() - - # Then - self.assertFalse(result) - def test_dictMerge_emptyOverride_ReturnsBase(self): # Given base = {'foo': 'bar'} diff --git a/touchstone-tests/touchstone/defaults/filesystem.yml b/touchstone-tests/touchstone/defaults/filesystem.yml new file mode 100644 index 0000000..890bf18 --- /dev/null +++ b/touchstone-tests/touchstone/defaults/filesystem.yml @@ -0,0 +1,9 @@ +--- +directories: + - path: ./filesystem/some-dir + files: + - ./filesystem/foo.csv + - ./filesystem/bar.png + - path: ./filesystem/some-dir/sub-dir + files: + - ./filesystem/foo.csv diff --git a/touchstone-tests/touchstone/defaults/s3-objects/bar.png b/touchstone-tests/touchstone/defaults/filesystem/bar.png similarity index 100% rename from touchstone-tests/touchstone/defaults/s3-objects/bar.png rename to touchstone-tests/touchstone/defaults/filesystem/bar.png diff --git a/touchstone-tests/touchstone/defaults/s3-objects/foo.csv b/touchstone-tests/touchstone/defaults/filesystem/foo.csv similarity index 100% rename from touchstone-tests/touchstone/defaults/s3-objects/foo.csv rename to touchstone-tests/touchstone/defaults/filesystem/foo.csv diff --git a/touchstone-tests/touchstone/defaults/s3.yml b/touchstone-tests/touchstone/defaults/s3.yml index 3e69d03..44cd374 100644 --- a/touchstone-tests/touchstone/defaults/s3.yml +++ b/touchstone-tests/touchstone/defaults/s3.yml @@ -4,7 +4,7 @@ buckets: objects: - name: foo.csv content-type: text/csv - path: ./s3-objects/foo.csv + path: ./s3/foo.csv - name: test/bar.png content-type: image/png - path: ./s3-objects/bar.png + path: ./s3/bar.png diff --git a/touchstone-tests/touchstone/defaults/s3/bar.png b/touchstone-tests/touchstone/defaults/s3/bar.png new file mode 100755 index 0000000..981ae5a Binary files /dev/null and b/touchstone-tests/touchstone/defaults/s3/bar.png differ diff --git a/touchstone-tests/touchstone/defaults/s3/foo.csv b/touchstone-tests/touchstone/defaults/s3/foo.csv new file mode 100644 index 0000000..8bc5dba --- /dev/null +++ b/touchstone-tests/touchstone/defaults/s3/foo.csv @@ -0,0 +1,3 @@ +first name,last name +John ,Smith +Jane ,Brown diff --git a/touchstone-tests/touchstone/tests/test_filesystem.py b/touchstone-tests/touchstone/tests/test_filesystem.py new file mode 100644 index 0000000..fedc4b1 --- /dev/null +++ b/touchstone-tests/touchstone/tests/test_filesystem.py @@ -0,0 +1,27 @@ +import os + +from touchstone.lib.touchstone_test import TouchstoneTest + + +class FileExists(TouchstoneTest): + def given(self) -> object: + return './filesystem/some-dir/foo.csv' + + def when(self, given) -> object: + return None + + def then(self, given, result) -> bool: + return self.mocks.filesystem.verify().file_exists(given) + + +class FileMatches(TouchstoneTest): + def given(self) -> object: + path = os.path.join(self.mocks.filesystem.get_base_path(), './filesystem/some-dir/sub-dir/foo.csv') + with open(path, 'rb') as data: + return bytes(data.read()) + + def when(self, given) -> object: + return None + + def then(self, given, result) -> bool: + return self.mocks.filesystem.verify().file_matches('./filesystem/some-dir/sub-dir/foo.csv', given) diff --git a/touchstone-tests/touchstone/tests/test_http_setup.py b/touchstone-tests/touchstone/tests/test_http_setup.py index f9933f7..a166694 100644 --- a/touchstone-tests/touchstone/tests/test_http_setup.py +++ b/touchstone-tests/touchstone/tests/test_http_setup.py @@ -8,11 +8,11 @@ class Get(TouchstoneTest): def given(self) -> object: given = json.dumps({'foo': 'get'}) - self.mocks.http.setup.get('/get-endpoint', given) + self.mocks.http.setup().get('/get-endpoint', given) return given def when(self, given) -> object: - response = urllib.request.urlopen(f'{self.mocks.http.network.external_url()}/get-endpoint').read() + response = urllib.request.urlopen(f'{self.mocks.http.url()}/get-endpoint').read() return response.decode('utf-8') def then(self, given, result) -> bool: @@ -22,11 +22,11 @@ def then(self, given, result) -> bool: class Post(TouchstoneTest): def given(self) -> object: given = json.dumps({'foo': 'post'}) - self.mocks.http.setup.post('/post-endpoint', given) + self.mocks.http.setup().post('/post-endpoint', given) return given def when(self, given) -> object: - response = urllib.request.urlopen(f'{self.mocks.http.network.external_url()}/post-endpoint', + response = urllib.request.urlopen(f'{self.mocks.http.url()}/post-endpoint', data=bytes()).read() return response.decode('utf-8') @@ -37,11 +37,11 @@ def then(self, given, result) -> bool: class Put(TouchstoneTest): def given(self) -> object: given = json.dumps({'foo': 'put'}) - self.mocks.http.setup.put('/put-endpoint', given) + self.mocks.http.setup().put('/put-endpoint', given) return given def when(self, given) -> object: - request = urllib.request.Request(f'{self.mocks.http.network.external_url()}/put-endpoint', method='PUT') + request = urllib.request.Request(f'{self.mocks.http.url()}/put-endpoint', method='PUT') response = urllib.request.urlopen(request).read() return response.decode('utf-8') @@ -52,11 +52,11 @@ def then(self, given, result) -> bool: class Delete(TouchstoneTest): def given(self) -> object: given = json.dumps({'foo': 'delete'}) - self.mocks.http.setup.delete('/delete-endpoint', given) + self.mocks.http.setup().delete('/delete-endpoint', given) return given def when(self, given) -> object: - request = urllib.request.Request(f'{self.mocks.http.network.external_url()}/delete-endpoint', method='DELETE') + request = urllib.request.Request(f'{self.mocks.http.url()}/delete-endpoint', method='DELETE') response = urllib.request.urlopen(request).read() return response.decode('utf-8') diff --git a/touchstone-tests/touchstone/tests/test_http_verify.py b/touchstone-tests/touchstone/tests/test_http_verify.py index 484c4b4..5262a3a 100644 --- a/touchstone-tests/touchstone/tests/test_http_verify.py +++ b/touchstone-tests/touchstone/tests/test_http_verify.py @@ -5,120 +5,120 @@ class GetCalled(TouchstoneTest): def given(self) -> object: - self.mocks.http.setup.get('/some-endpoint', 'hello http!') + self.mocks.http.setup().get('/some-endpoint', 'hello http!') return None def when(self, given) -> object: - urllib.request.urlopen(f'{self.mocks.http.network.external_url()}/some-endpoint') + urllib.request.urlopen(f'{self.mocks.http.url()}/some-endpoint') return None def then(self, given, result) -> bool: - return self.mocks.http.verify.get_called('/some-endpoint') + return self.mocks.http.verify().get_called('/some-endpoint') class GetCalledWithTimes(TouchstoneTest): def given(self) -> object: - self.mocks.http.setup.get('/some-endpoint', 'hello http!') + self.mocks.http.setup().get('/some-endpoint', 'hello http!') return None def when(self, given) -> object: - urllib.request.urlopen(f'{self.mocks.http.network.external_url()}/some-endpoint') - urllib.request.urlopen(f'{self.mocks.http.network.external_url()}/some-endpoint') - urllib.request.urlopen(f'{self.mocks.http.network.external_url()}/some-endpoint') + urllib.request.urlopen(f'{self.mocks.http.url()}/some-endpoint') + urllib.request.urlopen(f'{self.mocks.http.url()}/some-endpoint') + urllib.request.urlopen(f'{self.mocks.http.url()}/some-endpoint') return None def then(self, given, result) -> bool: expected_calls = 3 - return self.mocks.http.verify.get_called('/some-endpoint', times=expected_calls) + return self.mocks.http.verify().get_called('/some-endpoint', times=expected_calls) class PostCalled(TouchstoneTest): def given(self) -> object: - self.mocks.http.setup.post('/some-endpoint', 'hello http!') + self.mocks.http.setup().post('/some-endpoint', 'hello http!') return None def when(self, given) -> object: - urllib.request.urlopen(f'{self.mocks.http.network.external_url()}/some-endpoint', data=bytes()) + urllib.request.urlopen(f'{self.mocks.http.url()}/some-endpoint', data=bytes()) return None def then(self, given, result) -> bool: - return self.mocks.http.verify.post_called('/some-endpoint') + return self.mocks.http.verify().post_called('/some-endpoint') class PutCalled(TouchstoneTest): def given(self) -> object: - self.mocks.http.setup.put('/some-endpoint', 'hello http!') + self.mocks.http.setup().put('/some-endpoint', 'hello http!') return None def when(self, given) -> object: - request = urllib.request.Request(f'{self.mocks.http.network.external_url()}/some-endpoint', method='PUT') + request = urllib.request.Request(f'{self.mocks.http.url()}/some-endpoint', method='PUT') urllib.request.urlopen(request) return None def then(self, given, result) -> bool: - return self.mocks.http.verify.put_called('/some-endpoint') + return self.mocks.http.verify().put_called('/some-endpoint') class DeleteCalled(TouchstoneTest): def given(self) -> object: - self.mocks.http.setup.delete('/some-endpoint', 'hello http!') + self.mocks.http.setup().delete('/some-endpoint', 'hello http!') return None def when(self, given) -> object: - request = urllib.request.Request(f'{self.mocks.http.network.external_url()}/some-endpoint', method='DELETE') + request = urllib.request.Request(f'{self.mocks.http.url()}/some-endpoint', method='DELETE') urllib.request.urlopen(request) return None def then(self, given, result) -> bool: - return self.mocks.http.verify.delete_called('/some-endpoint') + return self.mocks.http.verify().delete_called('/some-endpoint') class PostContained(TouchstoneTest): def given(self) -> object: - self.mocks.http.setup.post('/some-endpoint', 'hello http!') + self.mocks.http.setup().post('/some-endpoint', 'hello http!') return None def when(self, given) -> object: body = 'foo'.encode('utf8') - request = urllib.request.Request(f'{self.mocks.http.network.external_url()}/some-endpoint', method='POST', + request = urllib.request.Request(f'{self.mocks.http.url()}/some-endpoint', method='POST', data=body) urllib.request.urlopen(request) return None def then(self, given, result) -> bool: expected_body = 'foo' - return self.mocks.http.verify.post_contained('/some-endpoint', expected_body) + return self.mocks.http.verify().post_contained('/some-endpoint', expected_body) class PutContained(TouchstoneTest): def given(self) -> object: - self.mocks.http.setup.put('/some-endpoint', 'hello http!') + self.mocks.http.setup().put('/some-endpoint', 'hello http!') return None def when(self, given) -> object: body = 'foo'.encode('utf8') - request = urllib.request.Request(f'{self.mocks.http.network.external_url()}/some-endpoint', method='PUT', + request = urllib.request.Request(f'{self.mocks.http.url()}/some-endpoint', method='PUT', data=body) urllib.request.urlopen(request) return None def then(self, given, result) -> bool: expected_body = 'foo' - return self.mocks.http.verify.put_contained('/some-endpoint', expected_body) + return self.mocks.http.verify().put_contained('/some-endpoint', expected_body) class DeleteContained(TouchstoneTest): def given(self) -> object: - self.mocks.http.setup.delete('/some-endpoint', 'hello http!') + self.mocks.http.setup().delete('/some-endpoint', 'hello http!') return None def when(self, given) -> object: body = 'foo'.encode('utf8') - request = urllib.request.Request(f'{self.mocks.http.network.external_url()}/some-endpoint', method='DELETE', + request = urllib.request.Request(f'{self.mocks.http.url()}/some-endpoint', method='DELETE', data=body) urllib.request.urlopen(request) return None def then(self, given, result) -> bool: expected_body = 'foo' - return self.mocks.http.verify.delete_contained('/some-endpoint', expected_body) + return self.mocks.http.verify().delete_contained('/some-endpoint', expected_body) diff --git a/touchstone-tests/touchstone/tests/test_mongodb.py b/touchstone-tests/touchstone/tests/test_mongodb.py index fb3141c..dda6e81 100644 --- a/touchstone-tests/touchstone/tests/test_mongodb.py +++ b/touchstone-tests/touchstone/tests/test_mongodb.py @@ -12,11 +12,11 @@ def given(self) -> object: } def when(self, given) -> object: - self.mocks.mongodb.setup.insert_document(mongo_database, mongo_collection, given) + self.mocks.mongodb.setup().insert_document(mongo_database, mongo_collection, given) return None def then(self, given, result) -> bool: - return self.mocks.mongodb.verify.document_exists(mongo_database, mongo_collection, given) + return self.mocks.mongodb.verify().document_exists(mongo_database, mongo_collection, given) class DocumentsInserted(TouchstoneTest): @@ -37,7 +37,7 @@ def given(self) -> object: ] def when(self, given) -> object: - self.mocks.mongodb.setup.insert_documents(mongo_database, mongo_collection, given) + self.mocks.mongodb.setup().insert_documents(mongo_database, mongo_collection, given) return None def then(self, given, result) -> bool: @@ -48,5 +48,6 @@ def then(self, given, result) -> bool: expected_baz = { 'firstName': 'Baz' } - return self.mocks.mongodb.verify.document_exists(mongo_database, mongo_collection, expected_foo, num_expected=2) \ - and self.mocks.mongodb.verify.document_exists(mongo_database, mongo_collection, expected_baz) + return self.mocks.mongodb.verify().document_exists(mongo_database, mongo_collection, expected_foo, + num_expected=2) \ + and self.mocks.mongodb.verify().document_exists(mongo_database, mongo_collection, expected_baz) diff --git a/touchstone-tests/touchstone/tests/test_mysql.py b/touchstone-tests/touchstone/tests/test_mysql.py index 745b4c9..b6892c5 100644 --- a/touchstone-tests/touchstone/tests/test_mysql.py +++ b/touchstone-tests/touchstone/tests/test_mysql.py @@ -12,11 +12,11 @@ def given(self) -> object: } def when(self, given) -> object: - self.mocks.mysql.setup.insert_row(mysql_database, mysql_table, given) + self.mocks.mysql.setup().insert_row(mysql_database, mysql_table, given) return None def then(self, given, result) -> bool: - return self.mocks.mysql.verify.row_exists(mysql_database, mysql_table, given) + return self.mocks.mysql.verify().row_exists(mysql_database, mysql_table, given) class RowsInserted(TouchstoneTest): @@ -37,7 +37,7 @@ def given(self) -> object: ] def when(self, given) -> object: - self.mocks.mysql.setup.insert_rows(mysql_database, mysql_table, given) + self.mocks.mysql.setup().insert_rows(mysql_database, mysql_table, given) return None def then(self, given, result) -> bool: @@ -48,5 +48,20 @@ def then(self, given, result) -> bool: expected_baz = { 'firstName': 'Baz' } - return self.mocks.mysql.verify.row_exists(mysql_database, mysql_table, expected_foo, num_expected=2) \ - and self.mocks.mysql.verify.row_exists(mysql_database, mysql_table, expected_baz) + return self.mocks.mysql.verify().row_exists(mysql_database, mysql_table, expected_foo, num_expected=2) \ + and self.mocks.mysql.verify().row_exists(mysql_database, mysql_table, expected_baz) + + +class NullValueInserted(TouchstoneTest): + def given(self) -> object: + return { + 'firstName': 'Foo', + 'lastName': None + } + + def when(self, given) -> object: + self.mocks.mysql.setup().insert_row(mysql_database, mysql_table, given) + return None + + def then(self, given, result) -> bool: + return self.mocks.mysql.verify().row_exists(mysql_database, mysql_table, given) diff --git a/touchstone-tests/touchstone/tests/test_rabbitmq.py b/touchstone-tests/touchstone/tests/test_rabbitmq.py index 2b44c14..e059fe8 100644 --- a/touchstone-tests/touchstone/tests/test_rabbitmq.py +++ b/touchstone-tests/touchstone/tests/test_rabbitmq.py @@ -9,11 +9,11 @@ def given(self) -> object: return None def when(self, given) -> object: - self.mocks.rabbitmq.setup.publish('default-direct.exchange', 'some payload') + self.mocks.rabbitmq.setup().publish('default-direct.exchange', 'some payload') return None def then(self, given, result) -> bool: - return self.mocks.rabbitmq.verify.messages_published('default-direct.exchange') + return self.mocks.rabbitmq.verify().messages_published('default-direct.exchange') class MessagesPublishedWithTimes(TouchstoneTest): @@ -24,15 +24,15 @@ def given(self) -> object: return None def when(self, given) -> object: - self.mocks.rabbitmq.setup.publish('default-direct.exchange', 'some payload', routing_key='foo') - self.mocks.rabbitmq.setup.publish('default-direct.exchange', 'some payload', routing_key='foo') - self.mocks.rabbitmq.setup.publish('default-direct.exchange', 'some payload', routing_key='foo') - self.mocks.rabbitmq.setup.publish('default-direct.exchange', 'some payload', routing_key='bar') + self.mocks.rabbitmq.setup().publish('default-direct.exchange', 'some payload', routing_key='foo') + self.mocks.rabbitmq.setup().publish('default-direct.exchange', 'some payload', routing_key='foo') + self.mocks.rabbitmq.setup().publish('default-direct.exchange', 'some payload', routing_key='foo') + self.mocks.rabbitmq.setup().publish('default-direct.exchange', 'some payload', routing_key='bar') return None def then(self, given, result) -> bool: - return self.mocks.rabbitmq.verify.messages_published('default-direct.exchange', num_expected=3, - routing_key='foo') + return self.mocks.rabbitmq.verify().messages_published('default-direct.exchange', num_expected=3, + routing_key='foo') class PayloadPublished(TouchstoneTest): @@ -43,8 +43,8 @@ def given(self) -> object: return 'some payload' def when(self, given) -> object: - self.mocks.rabbitmq.setup.publish('default-topic.exchange', given, routing_key='foo') + self.mocks.rabbitmq.setup().publish('default-topic.exchange', given, routing_key='foo') return None def then(self, given, result) -> bool: - return self.mocks.rabbitmq.verify.payload_published('default-topic.exchange', given, routing_key='foo') + return self.mocks.rabbitmq.verify().payload_published('default-topic.exchange', given, routing_key='foo') diff --git a/touchstone-tests/touchstone/tests/test_s3.py b/touchstone-tests/touchstone/tests/test_s3.py index 6cedf64..b9eea92 100644 --- a/touchstone-tests/touchstone/tests/test_s3.py +++ b/touchstone-tests/touchstone/tests/test_s3.py @@ -6,11 +6,11 @@ def given(self) -> object: return 'foo' def when(self, given) -> object: - self.mocks.s3.setup.create_bucket(given) + self.mocks.s3.setup().create_bucket(given) return None def then(self, given, result) -> bool: - return self.mocks.s3.verify.bucket_exists(given) + return self.mocks.s3.verify().bucket_exists(given) class PutObjectExists(TouchstoneTest): @@ -18,15 +18,15 @@ class PutObjectExists(TouchstoneTest): OBJECT_NAME = 'bar' def given(self) -> object: - self.mocks.s3.setup.create_bucket(self.BUCKET_NAME) + self.mocks.s3.setup().create_bucket(self.BUCKET_NAME) return bytes('buzz', encoding='utf-8') def when(self, given) -> object: - self.mocks.s3.setup.put_object(self.BUCKET_NAME, self.OBJECT_NAME, given) + self.mocks.s3.setup().put_object(self.BUCKET_NAME, self.OBJECT_NAME, given) return None def then(self, given, result) -> bool: - return self.mocks.s3.verify.object_exists(self.BUCKET_NAME, self.OBJECT_NAME) + return self.mocks.s3.verify().object_exists(self.BUCKET_NAME, self.OBJECT_NAME) class PutObjectMatches(TouchstoneTest): @@ -34,12 +34,12 @@ class PutObjectMatches(TouchstoneTest): OBJECT_NAME = 'bar' def given(self) -> object: - self.mocks.s3.setup.create_bucket(self.BUCKET_NAME) + self.mocks.s3.setup().create_bucket(self.BUCKET_NAME) return bytes('buzz', encoding='utf-8') def when(self, given) -> object: - self.mocks.s3.setup.put_object(self.BUCKET_NAME, self.OBJECT_NAME, given) + self.mocks.s3.setup().put_object(self.BUCKET_NAME, self.OBJECT_NAME, given) return None def then(self, given, result) -> bool: - return self.mocks.s3.verify.object_matches(self.BUCKET_NAME, self.OBJECT_NAME, given) + return self.mocks.s3.verify().object_matches(self.BUCKET_NAME, self.OBJECT_NAME, given) diff --git a/touchstone-tests/touchstone/touchstone.yml b/touchstone-tests/touchstone/touchstone.yml index 4002d37..c344cee 100644 --- a/touchstone-tests/touchstone/touchstone.yml +++ b/touchstone-tests/touchstone/touchstone.yml @@ -1,4 +1,5 @@ --- +touchstone-version: 1.0.0 services: - name: my-app port: 8080 @@ -9,3 +10,4 @@ mocks: mongodb: mysql: s3: + filesystem: diff --git a/touchstone/bootstrap.py b/touchstone/bootstrap.py index 339625d..ae6d1a2 100644 --- a/touchstone/bootstrap.py +++ b/touchstone/bootstrap.py @@ -1,35 +1,33 @@ +import glob import os +import sys +from pathlib import Path import yaml -from touchstone import common from touchstone.lib import exceptions from touchstone.lib.configs.service_config import ServiceConfig from touchstone.lib.configs.touchstone_config import TouchstoneConfig from touchstone.lib.docker_manager import DockerManager -from touchstone.lib.mocks.http.http import Http -from touchstone.lib.mocks.mock_defaults import MockDefaults +from touchstone.lib.mocks.mock_factory import MockFactory from touchstone.lib.mocks.mocks import Mocks -from touchstone.lib.mocks.mongodb.mongodb import Mongodb -from touchstone.lib.mocks.mysql.mysql import Mysql -from touchstone.lib.mocks.rabbitmq.rabbitmq import Rabbitmq -from touchstone.lib.mocks.s3.s3 import S3 from touchstone.lib.service import Service from touchstone.lib.services import Services from touchstone.lib.tests import Tests -from touchstone.runner import Runner class Bootstrap(object): def __init__(self, is_dev_mode=False): + touchstone_config = self.__build_touchstone_config(os.getcwd()) self.is_dev_mode = is_dev_mode - - docker_manager = DockerManager(should_auto_discover=not self.is_dev_mode) - self.touchstone_config = self.__build_touchstone_config(os.getcwd()) - self.runner = Runner(self.touchstone_config, docker_manager) - self.mocks = self.__build_mocks(self.touchstone_config.config['root'], self.touchstone_config, - self.touchstone_config.config['host'], docker_manager) - self.services = self.__build_services(self.touchstone_config, docker_manager, self.mocks) + self.docker_manager = DockerManager(should_auto_discover=not self.is_dev_mode) + self.mocks = self.__build_mocks(touchstone_config.config['root'], + touchstone_config.config['mocks'], + touchstone_config.config['host']) + self.services = self.__build_services(touchstone_config.config['root'], + touchstone_config.config['host'], + touchstone_config.config['services'], + self.mocks) def __build_touchstone_config(self, root) -> TouchstoneConfig: config = TouchstoneConfig(os.getcwd()) @@ -38,46 +36,47 @@ def __build_touchstone_config(self, root) -> TouchstoneConfig: config.merge(yaml.safe_load(file)) return config - def __build_mocks(self, root, touchstone_config, host, docker_manager) -> Mocks: - mock_defaults = MockDefaults(os.path.join(root, 'defaults')) - mocks = Mocks() - mocks.http = Http(host, mock_defaults, docker_manager) - mocks.rabbitmq = Rabbitmq(host, mock_defaults, docker_manager) - mocks.mongodb = Mongodb(host, mock_defaults, self.is_dev_mode, docker_manager) - mocks.mysql = Mysql(host, mock_defaults, self.is_dev_mode, docker_manager) - mocks.s3 = S3(host, mock_defaults, docker_manager) - potential_mocks = [mocks.http, mocks.rabbitmq, mocks.mongodb, mocks.mysql, mocks.s3] - - if not touchstone_config.config['mocks']: - return mocks + def __build_mocks(self, root, configs, host) -> Mocks: + defaults_paths = {} + default_files = glob.glob(os.path.join(root, 'defaults') + '/*.yml') + for default_file in default_files: + defaults_paths[Path(default_file).stem] = default_file - for mock in touchstone_config.config['mocks']: - user_config = touchstone_config.config['mocks'][mock] - found_mock = False - for potential_mock in potential_mocks: - if potential_mock.name() == mock: - found_mock = True - potential_mock.config = common.dict_merge(potential_mock.default_config(), user_config) - mocks.register_mock(potential_mock) - if not found_mock: - raise exceptions.MockNotSupportedException( - f'"{mock}" is not a supported mock. Please check your touchstone.yml file.') + mocks = Mocks() + for mock_name in configs: + mock_factory = MockFactory(self.is_dev_mode, root, defaults_paths, configs, host, self.docker_manager) + mock = mock_factory.get_mock(mock_name) + if not mock: + raise exceptions.MockNotSupportedException(f'Mock: {mock_name} is not supported.') + setattr(mocks, mock_name, mock.get_runnable()) + mocks.register_mock(mock) return mocks - def __build_services(self, touchstone_config, docker_manager, mocks) -> Services: + def __build_services(self, root, host, user_service_configs, mocks) -> Services: services = [] - for given_service_config in touchstone_config.config['services']: - service_config = ServiceConfig(touchstone_config.config['host']) + for given_service_config in user_service_configs: + service_config = ServiceConfig(host) service_config.merge(given_service_config) - tests_path = os.path.abspath( - os.path.join(touchstone_config.config['root'], service_config.config['tests'])) + tests_path = os.path.abspath(os.path.join(root, service_config.config['tests'])) tests = Tests(mocks, tests_path) - - service = Service(touchstone_config.config['root'], service_config.config['name'], tests, + service = Service(root, service_config.config['name'], tests, service_config.config['dockerfile'], service_config.config['host'], service_config.config['port'], service_config.config['availability_endpoint'], service_config.config['num_retries'], service_config.config['seconds_between_retries'], - docker_manager) - + self.docker_manager) services.append(service) return Services(services) + + def cleanup(self): + self.services.stop() + self.mocks.stop() + self.docker_manager.cleanup() + + def exit(self, is_successful: bool): + print('Shutting down...') + if is_successful: + code = 0 + else: + code = 1 + self.cleanup() + sys.exit(code) diff --git a/touchstone/common.py b/touchstone/common.py index 218bd0c..6fdddea 100644 --- a/touchstone/common.py +++ b/touchstone/common.py @@ -3,6 +3,9 @@ import re from typing import List +import yaml + +from touchstone import __version__ from touchstone.lib import exceptions logger = logging.getLogger('touchstone') @@ -10,16 +13,31 @@ def sanity_check_passes() -> bool: + # Ensure paths and files exist touchstone_path = os.path.join(os.getcwd(), 'touchstone.yml') defaults_path = os.path.join(os.getcwd(), 'defaults') - return os.path.exists(touchstone_path) and os.path.exists(defaults_path) - - -def prep_run(): - if not sanity_check_passes(): + paths_exist = os.path.exists(touchstone_path) and os.path.exists(defaults_path) + if not paths_exist: print('touchstone.yml and the defaults directory could not be found. ' 'If touchstone has not been initialized, run \'touchstone init\'.') - exit(1) + + # Ensure versions are compatible + versions_match = False + with open(touchstone_path, 'r') as file: + touchstone_config = yaml.safe_load(file) + given_version = touchstone_config.get('touchstone-version') + if given_version: + split_given_version = str.split(given_version, '.') + split_current_version = str.split(__version__, '.') + versions_match = split_given_version[0] == split_current_version[0] \ + and split_given_version[1] <= split_current_version[1] + if not versions_match: + print(f'Version defined in touchstone.yml: "{given_version}" is not compatible with system Touchstone ' + f'version:"{__version__}"') + else: + print('A touchstone version number must be defined in "touchstone.yml".') + + return paths_exist and versions_match def dict_merge(base: dict, override: dict) -> dict: diff --git a/touchstone/develop.py b/touchstone/develop.py index 3921f44..c03459c 100644 --- a/touchstone/develop.py +++ b/touchstone/develop.py @@ -7,12 +7,13 @@ def execute(): - common.prep_run() + if not common.sanity_check_passes(): + exit(1) bootstrap = Bootstrap(is_dev_mode=True) print(figlet_format('Touchstone', font='larry3d')) try: - mock_run_contexts = bootstrap.mocks.start() + bootstrap.mocks.start() bootstrap.mocks.print_available_mocks() __print_help() @@ -34,7 +35,7 @@ def execute(): bootstrap.services.run_test(parts[1], parts[2], parts[3]) elif command == 'services start': try: - bootstrap.services.start(mock_run_contexts) + bootstrap.services.start(bootstrap.mocks.environment_vars()) bootstrap.mocks.services_became_available() except KeyboardInterrupt: bootstrap.services.stop() @@ -45,16 +46,14 @@ def execute(): elif command == 'mocks reset': bootstrap.mocks.reset() elif command == 'exit': - bootstrap.services.stop() - bootstrap.mocks.stop() - bootstrap.runner.exit_touchstone(True) + bootstrap.exit(True) elif command == '': pass else: print(f'Unknown Touchstone command "{command}"') except (Exception, KeyboardInterrupt) as e: print('\nTouchstone was interrupted. Cleaning up...') - bootstrap.runner.cleanup() + bootstrap.cleanup() raise e diff --git a/touchstone/init.py b/touchstone/init.py index 1a8c119..b8d34b5 100644 --- a/touchstone/init.py +++ b/touchstone/init.py @@ -1,5 +1,7 @@ import os +from touchstone import __version__ + def execute(): if os.path.exists('touchstone/touchstone.yml'): @@ -9,7 +11,8 @@ def execute(): os.makedirs('touchstone/tests') open('touchstone/tests/__init__.py', 'a').close() with open('touchstone/touchstone.yml', 'w', encoding='utf-8') as file: - data = """--- + data = f"""--- +touchstone-version: {__version__} services: - name: my-app port: 8080 diff --git a/touchstone/lib/mocks/configurers/BasicConfigurer.py b/touchstone/lib/mocks/configurers/BasicConfigurer.py new file mode 100644 index 0000000..dd9c9a9 --- /dev/null +++ b/touchstone/lib/mocks/configurers/BasicConfigurer.py @@ -0,0 +1,13 @@ +from touchstone import common +from touchstone.lib.mocks.configurers.i_configurable import IConfigurable + + +class BasicConfigurer(IConfigurable): + def __init__(self, default_config: dict): + self.__config = default_config + + def get_config(self) -> dict: + return self.__config + + def merge_config(self, other: dict): + self.__config = common.dict_merge(self.__config, other) diff --git a/touchstone/lib/mocks/configurers/FileConfigurer.py b/touchstone/lib/mocks/configurers/FileConfigurer.py new file mode 100644 index 0000000..5dbf903 --- /dev/null +++ b/touchstone/lib/mocks/configurers/FileConfigurer.py @@ -0,0 +1,20 @@ +import yaml + +from touchstone import common +from touchstone.lib.mocks.configurers.i_configurable import IConfigurable + + +class FileConfigurer(IConfigurable): + def __init__(self, config_path: str = None): + self.__config_path = config_path + self.__override_config = {} + + def get_config(self) -> dict: + if not self.__config_path: + return {} + with open(self.__config_path, 'r') as file: + config = yaml.safe_load(file) + return common.dict_merge(config, self.__override_config) + + def merge_config(self, other: dict): + self.__override_config = other diff --git a/touchstone/lib/mocks/http/__init__.py b/touchstone/lib/mocks/configurers/__init__.py similarity index 100% rename from touchstone/lib/mocks/http/__init__.py rename to touchstone/lib/mocks/configurers/__init__.py diff --git a/touchstone/lib/mocks/configurers/i_configurable.py b/touchstone/lib/mocks/configurers/i_configurable.py new file mode 100644 index 0000000..21e9e71 --- /dev/null +++ b/touchstone/lib/mocks/configurers/i_configurable.py @@ -0,0 +1,11 @@ +import abc + + +class IConfigurable(object): + @abc.abstractmethod + def get_config(self) -> dict: + pass + + @abc.abstractmethod + def merge_config(self, other: dict): + pass diff --git a/touchstone/lib/mocks/mongodb/__init__.py b/touchstone/lib/mocks/health_checks/__init__.py similarity index 100% rename from touchstone/lib/mocks/mongodb/__init__.py rename to touchstone/lib/mocks/health_checks/__init__.py diff --git a/touchstone/lib/mocks/health_checks/http_health_check.py b/touchstone/lib/mocks/health_checks/http_health_check.py new file mode 100644 index 0000000..05931d2 --- /dev/null +++ b/touchstone/lib/mocks/health_checks/http_health_check.py @@ -0,0 +1,22 @@ +import urllib.error +import urllib.request + +from touchstone.lib import exceptions +from touchstone.lib.mocks.health_checks.i_url_health_checkable import IUrlHealthCheckable + + +class HttpHealthCheck(IUrlHealthCheckable): + def __init__(self): + self.__url = None + + def set_url(self, url: str): + self.__url = url + + def is_healthy(self) -> bool: + if not self.__url: + raise exceptions.MockException('URL must be set before checking health.') + try: + response = urllib.request.urlopen(self.__url).read() + return False if response is None else True + except (urllib.error.URLError, ConnectionResetError): + return False diff --git a/touchstone/lib/mocks/health_checks/i_health_checkable.py b/touchstone/lib/mocks/health_checks/i_health_checkable.py new file mode 100644 index 0000000..6a2f121 --- /dev/null +++ b/touchstone/lib/mocks/health_checks/i_health_checkable.py @@ -0,0 +1,7 @@ +import abc + + +class IHealthCheckable(object): + @abc.abstractmethod + def is_healthy(self) -> bool: + pass diff --git a/touchstone/lib/mocks/health_checks/i_url_health_checkable.py b/touchstone/lib/mocks/health_checks/i_url_health_checkable.py new file mode 100644 index 0000000..c70b2b2 --- /dev/null +++ b/touchstone/lib/mocks/health_checks/i_url_health_checkable.py @@ -0,0 +1,9 @@ +import abc + +from touchstone.lib.mocks.health_checks.i_health_checkable import IHealthCheckable + + +class IUrlHealthCheckable(IHealthCheckable): + @abc.abstractmethod + def set_url(self, url: str): + pass diff --git a/touchstone/lib/mocks/http/http.py b/touchstone/lib/mocks/http/http.py deleted file mode 100644 index aa8a335..0000000 --- a/touchstone/lib/mocks/http/http.py +++ /dev/null @@ -1,57 +0,0 @@ -import urllib.error -import urllib.parse -import urllib.request -from typing import Optional - -from touchstone.lib.docker_manager import DockerManager -from touchstone.lib.mocks.http.http_setup import HttpSetup -from touchstone.lib.mocks.http.http_verify import HttpVerify -from touchstone.lib.mocks.mock import Mock -from touchstone.lib.mocks.mock_defaults import MockDefaults -from touchstone.lib.mocks.network import Network - - -class Http(Mock): - def __init__(self, host: str, mock_defaults: MockDefaults, docker_manager: DockerManager): - super().__init__(host, mock_defaults) - self.setup: HttpSetup = None - self.verify: HttpVerify = None - self.__docker_manager = docker_manager - self.__container_id: Optional[str] = None - - @staticmethod - def name() -> str: - return 'http' - - @staticmethod - def pretty_name() -> str: - return 'HTTP' - - def run(self) -> Network: - run_result = self.__docker_manager.run_image('holomekc/wiremock-gui:2.25.1', port=8080, exposed_port=9090) - self.__container_id = run_result.container_id - return Network(internal_host=run_result.container_id, - internal_port=run_result.internal_port, - external_port=run_result.external_port, - ui_port=run_result.ui_port, - ui_endpoint='/__admin/webapp', - prefix='http://') - - def is_healthy(self) -> bool: - try: - response = urllib.request.urlopen(self.network.ui_url()).read() - return False if response is None else True - except (urllib.error.URLError, ConnectionResetError): - return False - - def initialize(self): - self.setup: HttpSetup = HttpSetup(self.network.external_url()) - self.verify: HttpVerify = HttpVerify(self.network.external_url()) - self.setup.init(self._mock_defaults.get(self.name())) - - def reset(self): - self.setup.init(self._mock_defaults.get(self.name())) - - def stop(self): - if self.__container_id: - self.__docker_manager.stop_container(self.__container_id) diff --git a/touchstone/lib/mocks/mock.py b/touchstone/lib/mocks/mock.py deleted file mode 100644 index d13e8b9..0000000 --- a/touchstone/lib/mocks/mock.py +++ /dev/null @@ -1,67 +0,0 @@ -import abc -from typing import Optional - -from touchstone.lib import exceptions -from touchstone.lib.mocks.mock_defaults import MockDefaults -from touchstone.lib.mocks.network import Network -from touchstone.lib.mocks.run_context import RunContext - - -class Mock(object): - __metaclass__ = abc.ABCMeta - - def __init__(self, host: str, mock_defaults: MockDefaults): - self.config: dict = {} - self._host = host - self._mock_defaults = mock_defaults - self.__network: Optional[Network] = None - - @property - def network(self) -> Network: - if not self.__network: - raise exceptions.MockException('The mock must be started before its network info can be retrieved.') - return self.__network - - @staticmethod - @abc.abstractmethod - def name() -> str: - """The name of this mock. This is used to match the mock type in touchstone.yml and defaults.""" - - @staticmethod - @abc.abstractmethod - def pretty_name() -> str: - """A pretty, display name for this mock.""" - - def default_config(self) -> dict: - """Optional: A dictionary of configuration values with defaults for this mock. This will be available via - 'self.config'.""" - return {} - - def start(self) -> RunContext: - """Starts this mock.""" - self.__network = self.run() - if not self.network.external_host: - self.__network.external_host = self._host - return RunContext(self.name(), self.__network) - - @abc.abstractmethod - def run(self) -> Network: - """Runs all containers and dependencies needed to run this mock.""" - - @abc.abstractmethod - def is_healthy(self) -> bool: - """Returns True when this mock is in a healthy state and ready to use.""" - - def initialize(self): - """Called when this mock becomes healthy.""" - - def services_available(self): - """Called when all services become available.""" - - @abc.abstractmethod - def reset(self): - """Reset this mock to its default state.""" - - @abc.abstractmethod - def stop(self): - """Stops this mock.""" diff --git a/touchstone/lib/mocks/mock_defaults.py b/touchstone/lib/mocks/mock_defaults.py deleted file mode 100644 index b695ba1..0000000 --- a/touchstone/lib/mocks/mock_defaults.py +++ /dev/null @@ -1,15 +0,0 @@ -import os - -import yaml - - -class MockDefaults(object): - def __init__(self, path: str): - self.path = path - - def get(self, mock_name: str) -> dict: - try: - with open(os.path.join(self.path, f'{mock_name}.yml'), 'r') as file: - return yaml.safe_load(file) - except FileNotFoundError: - return {} diff --git a/touchstone/lib/mocks/mock_factory.py b/touchstone/lib/mocks/mock_factory.py new file mode 100644 index 0000000..36bfd85 --- /dev/null +++ b/touchstone/lib/mocks/mock_factory.py @@ -0,0 +1,99 @@ +import os +from typing import Optional + +from touchstone.lib.docker_manager import DockerManager +from touchstone.lib.mocks.configurers.BasicConfigurer import BasicConfigurer +from touchstone.lib.mocks.configurers.FileConfigurer import FileConfigurer +from touchstone.lib.mocks.health_checks.http_health_check import HttpHealthCheck +from touchstone.lib.mocks.mockables.basic_mock import BasicMock +from touchstone.lib.mocks.mockables.i_mockable import IMockable +from touchstone.lib.mocks.mockables.networked_mock import NetworkedMock +from touchstone.lib.mocks.networked_runnables.http.docker.docker_http_runnable import DockerHttpRunnable +from touchstone.lib.mocks.networked_runnables.http.docker.docker_http_setup import DockerHttpSetup +from touchstone.lib.mocks.networked_runnables.http.docker.docker_http_verify import DockerHttpVerify +from touchstone.lib.mocks.networked_runnables.mongodb.docker.docker_mongo_context import DockerMongoContext +from touchstone.lib.mocks.networked_runnables.mongodb.docker.docker_mongodb_runnable import DockerMongodbRunnable +from touchstone.lib.mocks.networked_runnables.mongodb.docker.docker_mongodb_setup import DockerMongodbSetup +from touchstone.lib.mocks.networked_runnables.mongodb.docker.docker_mongodb_verify import DockerMongodbVerify +from touchstone.lib.mocks.networked_runnables.mysql.docker.docker_mysql_context import DockerMysqlContext +from touchstone.lib.mocks.networked_runnables.mysql.docker.docker_mysql_runnable import DockerMysqlRunnable +from touchstone.lib.mocks.networked_runnables.mysql.docker.docker_mysql_setup import DockerMysqlSetup +from touchstone.lib.mocks.networked_runnables.mysql.docker.docker_mysql_verify import DockerMysqlVerify +from touchstone.lib.mocks.networked_runnables.mysql.i_mysql_behabior import IMysqlBehavior +from touchstone.lib.mocks.networked_runnables.rabbitmq.docker.docker_rabbitmq_context import DockerRabbitmqContext +from touchstone.lib.mocks.networked_runnables.rabbitmq.docker.docker_rabbitmq_runnable import DockerRabbitmqRunnable +from touchstone.lib.mocks.networked_runnables.rabbitmq.docker.docker_rabbitmq_setup import DockerRabbitmqSetup +from touchstone.lib.mocks.networked_runnables.rabbitmq.docker.docker_rabbitmq_verify import DockerRabbitmqVerify +from touchstone.lib.mocks.networked_runnables.rabbitmq.i_rabbitmq_behavior import IRabbitmqBehavior +from touchstone.lib.mocks.networked_runnables.s3.docker.docker_s3_runnable import DockerS3Runnable +from touchstone.lib.mocks.networked_runnables.s3.docker.docker_s3_setup import DockerS3Setup +from touchstone.lib.mocks.networked_runnables.s3.docker.docker_s3_verify import DockerS3Verify +from touchstone.lib.mocks.runnables.filesystem.local.local_filesystem_runnable import LocalFilesystemRunnable +from touchstone.lib.mocks.runnables.filesystem.local.local_filesystem_setup import LocalFilesystemSetup +from touchstone.lib.mocks.runnables.filesystem.local.local_filesystem_verify import LocalFilesystemVerify + + +class MockFactory(object): + def __init__(self, is_dev_mode: bool, root: str, defaults_paths: dict, configs: dict, host: str, + docker_manager: DockerManager): + self.__is_dev_mode = is_dev_mode + self.__root = root + self.__defaults_paths = defaults_paths + self.__configs = configs + self.__host = host + self.__docker_manager = docker_manager + + def get_mock(self, mock_name: str) -> Optional[IMockable]: + config = self.__configs.get(mock_name, {}) + mock_defaults_paths = self.__defaults_paths.get(mock_name, None) + mock = None + + if mock_name == 'http': + defaults_configurer = FileConfigurer(mock_defaults_paths) + runnable = DockerHttpRunnable(defaults_configurer, HttpHealthCheck(), DockerHttpSetup(), DockerHttpVerify(), + self.__docker_manager) + mock = NetworkedMock('http', 'HTTP', self.__host, runnable) + elif mock_name == 'rabbitmq': + defaults_configurer = FileConfigurer(mock_defaults_paths) + configurer = BasicConfigurer(IRabbitmqBehavior.DEFAULT_CONFIG) + configurer.merge_config(config) + context = DockerRabbitmqContext() + setup = DockerRabbitmqSetup(context) + verify = DockerRabbitmqVerify(context) + runnable = DockerRabbitmqRunnable(defaults_configurer, configurer, HttpHealthCheck(), setup, verify, + self.__docker_manager) + mock = NetworkedMock('rabbitmq', 'Rabbit MQ', self.__host, runnable) + elif mock_name == 'mongodb': + defaults_configurer = FileConfigurer(mock_defaults_paths) + context = DockerMongoContext() + setup = DockerMongodbSetup(context) + verify = DockerMongodbVerify(context) + runnable = DockerMongodbRunnable(defaults_configurer, self.__is_dev_mode, setup, verify, + self.__docker_manager) + mock = NetworkedMock('mongodb', 'Mongo DB', self.__host, runnable) + elif mock_name == 'mysql': + defaults_configurer = FileConfigurer(mock_defaults_paths) + configurer = BasicConfigurer(IMysqlBehavior.DEFAULT_CONFIG) + configurer.merge_config(config) + context = DockerMysqlContext() + setup = DockerMysqlSetup(context) + verify = DockerMysqlVerify(context) + runnable = DockerMysqlRunnable(defaults_configurer, self.__is_dev_mode, configurer, setup, verify, + self.__docker_manager) + mock = NetworkedMock('mysql', 'MySQL', self.__host, runnable) + elif mock_name == 's3': + defaults_configurer = FileConfigurer(mock_defaults_paths) + base_objects_path = os.path.join(self.__root, 'defaults') + setup = DockerS3Setup() + verify = DockerS3Verify() + runnable = DockerS3Runnable(defaults_configurer, base_objects_path, HttpHealthCheck(), setup, verify, + self.__docker_manager) + mock = NetworkedMock('s3', 'S3', self.__host, runnable) + elif mock_name == 'filesystem': + defaults_configurer = FileConfigurer(mock_defaults_paths) + base_files_path = os.path.join(self.__root, 'defaults') + setup = LocalFilesystemSetup(base_files_path) + verify = LocalFilesystemVerify(base_files_path) + runnable = LocalFilesystemRunnable(defaults_configurer, base_files_path, setup, verify) + mock = BasicMock('filesystem', 'Filesystem', runnable) + return mock diff --git a/touchstone/lib/mocks/mysql/__init__.py b/touchstone/lib/mocks/mockables/__init__.py similarity index 100% rename from touchstone/lib/mocks/mysql/__init__.py rename to touchstone/lib/mocks/mockables/__init__.py diff --git a/touchstone/lib/mocks/mockables/basic_mock.py b/touchstone/lib/mocks/mockables/basic_mock.py new file mode 100644 index 0000000..8115486 --- /dev/null +++ b/touchstone/lib/mocks/mockables/basic_mock.py @@ -0,0 +1,30 @@ +from touchstone.lib.mocks.mockables.i_mockable import IMockable +from touchstone.lib.mocks.runnables.i_runnable import IRunnable + + +class BasicMock(IMockable): + def __init__(self, name: str, pretty_name: str, runnable: IRunnable): + self.__name = name + self.__pretty_name = pretty_name + self.__runnable = runnable + + def get_name(self): + return self.__name + + def get_pretty_name(self): + return self.__pretty_name + + def get_runnable(self) -> IRunnable: + return self.__runnable + + def start(self): + self.__runnable.start() + + def stop(self): + self.__runnable.stop() + + def reset(self): + self.__runnable.reset() + + def services_available(self): + self.__runnable.services_available() diff --git a/touchstone/lib/mocks/mockables/i_mockable.py b/touchstone/lib/mocks/mockables/i_mockable.py new file mode 100644 index 0000000..ff99530 --- /dev/null +++ b/touchstone/lib/mocks/mockables/i_mockable.py @@ -0,0 +1,17 @@ +import abc + +from touchstone.lib.mocks.runnables.i_runnable import IRunnable + + +class IMockable(IRunnable): + @abc.abstractmethod + def get_name(self): + pass + + @abc.abstractmethod + def get_pretty_name(self): + pass + + @abc.abstractmethod + def get_runnable(self) -> IRunnable: + pass diff --git a/touchstone/lib/mocks/mockables/networked_mock.py b/touchstone/lib/mocks/mockables/networked_mock.py new file mode 100644 index 0000000..8837201 --- /dev/null +++ b/touchstone/lib/mocks/mockables/networked_mock.py @@ -0,0 +1,52 @@ +from touchstone.lib import exceptions +from touchstone.lib.mocks.health_checks.i_health_checkable import IHealthCheckable +from touchstone.lib.mocks.mockables.i_mockable import IMockable +from touchstone.lib.mocks.network import Network +from touchstone.lib.mocks.networked_runnables.i_networked_runnable import INetworkedRunnable +from touchstone.lib.mocks.runnables.i_runnable import IRunnable + + +class NetworkedMock(IMockable, IHealthCheckable): + def __init__(self, name: str, pretty_name: str, localhost: str, networked_runnable: INetworkedRunnable): + self.__name = name + self.__pretty_name = pretty_name + self.__localhost = localhost + self.__networked_runnable = networked_runnable + self.__has_initialized = False + + def get_name(self): + return self.__name + + def get_pretty_name(self): + return self.__pretty_name + + def get_runnable(self) -> IRunnable: + return self.__networked_runnable + + def start(self): + self.__networked_runnable.start() + + def stop(self): + self.__networked_runnable.stop() + + def reset(self): + self.__networked_runnable.reset() + + def services_available(self): + self.__networked_runnable.services_available() + + def is_healthy(self) -> bool: + try: + self.__networked_runnable.get_network() + except exceptions.MockException: + return False + if not self.__networked_runnable.get_network().external_host: + self.__networked_runnable.get_network().external_host = self.__localhost + is_healthy = self.__networked_runnable.is_healthy() + if is_healthy and not self.__has_initialized: + self.__networked_runnable.initialize() + self.__has_initialized = True + return is_healthy + + def get_network(self) -> Network: + return self.__networked_runnable.get_network() diff --git a/touchstone/lib/mocks/mocks.py b/touchstone/lib/mocks/mocks.py index 0b7b786..b9ddd81 100644 --- a/touchstone/lib/mocks/mocks.py +++ b/touchstone/lib/mocks/mocks.py @@ -1,44 +1,43 @@ import time -from typing import List +from typing import List, Tuple from touchstone import common from touchstone.lib import exceptions -from touchstone.lib.mocks.http.http import Http -from touchstone.lib.mocks.mock import Mock -from touchstone.lib.mocks.mongodb.mongodb import Mongodb -from touchstone.lib.mocks.mysql.mysql import Mysql -from touchstone.lib.mocks.rabbitmq.rabbitmq import Rabbitmq -from touchstone.lib.mocks.run_context import RunContext -from touchstone.lib.mocks.s3.s3 import S3 +from touchstone.lib.mocks.health_checks.i_health_checkable import IHealthCheckable +from touchstone.lib.mocks.mockables.i_mockable import IMockable +from touchstone.lib.mocks.mockables.networked_mock import NetworkedMock +from touchstone.lib.mocks.networked_runnables.http.i_http_behavior import IHttpBehavior +from touchstone.lib.mocks.networked_runnables.mongodb.i_mongodb_behavior import IMongodbBehavior +from touchstone.lib.mocks.networked_runnables.mysql.i_mysql_behabior import IMysqlBehavior +from touchstone.lib.mocks.networked_runnables.rabbitmq.i_rabbitmq_behavior import IRabbitmqBehavior +from touchstone.lib.mocks.networked_runnables.s3.i_s3_behavior import IS3Behavior +from touchstone.lib.mocks.runnables.filesystem.i_filesystem_behavior import IFilesystemBehavior class Mocks(object): def __init__(self): - self.http: Http = None - self.rabbitmq: Rabbitmq = None - self.mongodb: Mongodb = None - self.mysql: Mysql = None - self.s3: S3 = None - self.__registered_mocks: List[Mock] = [] + self.http: IHttpBehavior = None + self.rabbitmq: IRabbitmqBehavior = None + self.mongodb: IMongodbBehavior = None + self.mysql: IMysqlBehavior = None + self.s3: IS3Behavior = None + self.filesystem: IFilesystemBehavior = None + self.__registered_mocks: List[IMockable] = [] self.__mocks_running = False - def register_mock(self, mock: Mock): + def register_mock(self, mock: IMockable): self.__registered_mocks.append(mock) - def start(self) -> List[RunContext]: + def start(self): if self.__mocks_running: print('Mocks have already been started. They cannot be started again.') else: - print(f'Starting mocks {[_.pretty_name() for _ in self.__registered_mocks]}...') - run_contexts = [] + print(f'Starting mocks {[_.get_pretty_name() for _ in self.__registered_mocks]}...') for mock in self.__registered_mocks: - run_contexts.append(mock.start()) + mock.start() self.__wait_for_healthy_mocks() - for mock in self.__registered_mocks: - mock.initialize() self.__mocks_running = True print('Finished starting mocks.\n') - return run_contexts def stop(self): print('Stopping mocks...') @@ -57,25 +56,43 @@ def reset(self): for mock in self.__registered_mocks: mock.reset() + def environment_vars(self) -> List[Tuple[str, str]]: + envs = [] + for mock in self.__registered_mocks: + if isinstance(mock, NetworkedMock): + name = mock.get_name().upper() + envs.append((f'TS_{name}_HOST', mock.get_network().internal_host)) + envs.append((f'TS_{name}_PORT', mock.get_network().internal_port)) + envs.append((f'TS_{name}_URL', mock.get_network().internal_url())) + envs.append((f'TS_{name}_USERNAME', mock.get_network().username)) + envs.append((f'TS_{name}_PASSWORD', mock.get_network().password)) + return envs + def print_available_mocks(self): for mock in self.__registered_mocks: - message = f'Mock {mock.pretty_name()} UI running at: {mock.network.ui_url()}' - if mock.network.username: - message += f' Username: "{mock.network.username}", Password: "{mock.network.password}"' + message = f'Mock {mock.get_pretty_name()} running' + if isinstance(mock, NetworkedMock): + message += f' with UI: {mock.get_network().ui_url()}' + if mock.get_network().username: + message += f' and Username: "{mock.get_network().username}", ' \ + f'Password: "{mock.get_network().password}"' print(message) def __wait_for_healthy_mocks(self): for mock in self.__registered_mocks: - attempt = 0 - healthy = False - while not healthy and attempt is not 10: - attempt += 1 - common.logger.debug(f'Waiting for mock: {mock.name()} to become healthy. Attempt {attempt} of 10.') - if mock.is_healthy(): - healthy = True - attempt = 0 - else: - time.sleep(5) - if attempt is 10: - raise exceptions.MockException( - f'Mock {mock.pretty_name()} never became healthy and timed out on initialization.') + health_checkable = mock + if isinstance(health_checkable, IHealthCheckable): + attempt = 0 + healthy = False + while not healthy and attempt is not 10: + attempt += 1 + common.logger.debug( + f'Waiting for mock: {mock.get_name()} to become healthy. Attempt {attempt} of 10.') + if health_checkable.is_healthy(): + healthy = True + attempt = 0 + else: + time.sleep(5) + if attempt is 10: + raise exceptions.MockException( + f'Mock {mock.get_pretty_name()} never became healthy and timed out on initialization.') diff --git a/touchstone/lib/mocks/mongodb/mongodb.py b/touchstone/lib/mocks/mongodb/mongodb.py deleted file mode 100644 index a7ccf7e..0000000 --- a/touchstone/lib/mocks/mongodb/mongodb.py +++ /dev/null @@ -1,73 +0,0 @@ -from typing import Optional - -import pymongo - -from touchstone.lib.docker_manager import DockerManager -from touchstone.lib.mocks.mock import Mock -from touchstone.lib.mocks.mock_defaults import MockDefaults -from touchstone.lib.mocks.mongodb.mongo_context import MongoContext -from touchstone.lib.mocks.mongodb.mongodb_setup import MongodbSetup -from touchstone.lib.mocks.mongodb.mongodb_verify import MongodbVerify -from touchstone.lib.mocks.network import Network - - -class Mongodb(Mock): - def __init__(self, host: str, mock_defaults: MockDefaults, is_dev_mode: bool, docker_manager: DockerManager): - super().__init__(host, mock_defaults) - self.setup: MongodbSetup = None - self.verify: MongodbVerify = None - self.__docker_manager = docker_manager - self.__is_dev_mode = is_dev_mode - self.__container_id: Optional[str] = None - self.__ui_container_id: Optional[str] = None - - @staticmethod - def name() -> str: - return 'mongodb' - - @staticmethod - def pretty_name() -> str: - return 'Mongo DB' - - def run(self) -> Network: - run_result = self.__docker_manager.run_image('mongo:4.0.14', port=27017) - self.__container_id = run_result.container_id - - ui_port = None - if self.__is_dev_mode: - ui_run_result = self.__docker_manager.run_image('mongo-express:0.49.0', - ui_port=8081, - environment_vars=[ - ('ME_CONFIG_MONGODB_PORT', run_result.internal_port), - ('ME_CONFIG_MONGODB_SERVER', run_result.container_id)]) - self.__ui_container_id = ui_run_result.container_id - ui_port = ui_run_result.ui_port - - return Network(internal_host=run_result.container_id, - internal_port=run_result.internal_port, - external_port=run_result.external_port, - ui_port=ui_port) - - def is_healthy(self) -> bool: - try: - client = pymongo.MongoClient(self.network.external_host, self.network.external_port) - status = client.admin.command('serverStatus')['ok'] - return status == 1.0 - except Exception: - return False - - def initialize(self): - mongo_client = pymongo.MongoClient(self.network.external_host, self.network.external_port) - mongo_context = MongoContext() - self.setup = MongodbSetup(mongo_client, mongo_context) - self.verify = MongodbVerify(mongo_client, mongo_context) - self.setup.init(self._mock_defaults.get(self.name())) - - def reset(self): - self.setup.init(self._mock_defaults.get(self.name())) - - def stop(self): - if self.__container_id: - self.__docker_manager.stop_container(self.__container_id) - if self.__ui_container_id: - self.__docker_manager.stop_container(self.__ui_container_id) diff --git a/touchstone/lib/mocks/mysql/mysql.py b/touchstone/lib/mocks/mysql/mysql.py deleted file mode 100644 index c704619..0000000 --- a/touchstone/lib/mocks/mysql/mysql.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import Optional - -import pymysql - -from touchstone.lib.docker_manager import DockerManager -from touchstone.lib.mocks.mock import Mock -from touchstone.lib.mocks.mock_defaults import MockDefaults -from touchstone.lib.mocks.mysql.mysql_context import MysqlContext -from touchstone.lib.mocks.mysql.mysql_setup import MysqlSetup -from touchstone.lib.mocks.mysql.mysql_verify import MysqlVerify -from touchstone.lib.mocks.network import Network - - -class Mysql(Mock): - __USERNAME = 'root' - __PASSWORD = 'root' - - def __init__(self, host: str, mock_defaults: MockDefaults, is_dev_mode: bool, docker_manager: DockerManager): - super().__init__(host, mock_defaults) - self.setup: MysqlSetup = None - self.verify: MysqlVerify = None - self.__is_dev_mode = is_dev_mode - self.__docker_manager = docker_manager - self.__container_id: Optional[str] = None - self.__ui_container_id: Optional[str] = None - - @staticmethod - def name() -> str: - return 'mysql' - - @staticmethod - def pretty_name() -> str: - return 'MySQL' - - def default_config(self) -> dict: - return { - 'convertCamelToSnakeCase': True - } - - def run(self) -> Network: - run_result = self.__docker_manager.run_image('mysql:5.7.29', port=3306, - environment_vars=[('MYSQL_ROOT_PASSWORD', self.__USERNAME)]) - self.__container_id = run_result.container_id - - ui_port = None - if self.__is_dev_mode: - ui_run_result = self.__docker_manager.run_image('adminer:4.7.5-standalone', - ui_port=8080, - environment_vars=[ - ('ADMINER_DEFAULT_SERVER', self.__container_id)]) - self.__ui_container_id = ui_run_result.container_id - ui_port = ui_run_result.ui_port - - return Network(internal_host=run_result.container_id, - internal_port=run_result.internal_port, - external_port=run_result.external_port, - ui_port=ui_port, - username=self.__USERNAME, - password=self.__PASSWORD) - - def is_healthy(self) -> bool: - try: - pymysql.connect(host=self.network.external_host, - port=self.network.external_port, - user=self.__USERNAME, - password=self.__PASSWORD) - return True - except Exception: - return False - - def initialize(self): - connection = pymysql.connect(host=self.network.external_host, - port=self.network.external_port, - user=self.__USERNAME, - password=self.__PASSWORD, - charset='utf8mb4', - autocommit=True, - cursorclass=pymysql.cursors.DictCursor) - cursor = connection.cursor() - mysql_context = MysqlContext() - convert_camel_to_snake = self.config['convertCamelToSnakeCase'] - self.setup = MysqlSetup(cursor, mysql_context, convert_camel_to_snake) - self.verify = MysqlVerify(cursor, mysql_context, convert_camel_to_snake) - self.setup.init(self._mock_defaults.get(self.name())) - - def reset(self): - self.setup.init(self._mock_defaults.get(self.name())) - - def stop(self): - if self.__container_id: - self.__docker_manager.stop_container(self.__container_id) - if self.__ui_container_id: - self.__docker_manager.stop_container(self.__ui_container_id) diff --git a/touchstone/lib/mocks/rabbitmq/__init__.py b/touchstone/lib/mocks/networked_runnables/__init__.py similarity index 100% rename from touchstone/lib/mocks/rabbitmq/__init__.py rename to touchstone/lib/mocks/networked_runnables/__init__.py diff --git a/touchstone/lib/mocks/s3/__init__.py b/touchstone/lib/mocks/networked_runnables/http/__init__.py similarity index 100% rename from touchstone/lib/mocks/s3/__init__.py rename to touchstone/lib/mocks/networked_runnables/http/__init__.py diff --git a/touchstone/lib/mocks/networked_runnables/http/docker/__init__.py b/touchstone/lib/mocks/networked_runnables/http/docker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/networked_runnables/http/docker/docker_http_runnable.py b/touchstone/lib/mocks/networked_runnables/http/docker/docker_http_runnable.py new file mode 100644 index 0000000..58edd50 --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/http/docker/docker_http_runnable.py @@ -0,0 +1,68 @@ +from touchstone.lib import exceptions +from touchstone.lib.docker_manager import DockerManager +from touchstone.lib.mocks.configurers.i_configurable import IConfigurable +from touchstone.lib.mocks.health_checks.i_url_health_checkable import IUrlHealthCheckable +from touchstone.lib.mocks.network import Network +from touchstone.lib.mocks.networked_runnables.http.docker.docker_http_setup import DockerHttpSetup +from touchstone.lib.mocks.networked_runnables.http.docker.docker_http_verify import DockerHttpVerify +from touchstone.lib.mocks.networked_runnables.http.i_http_behavior import IHttpBehavior, IHttpSetup, IHttpVerify +from touchstone.lib.mocks.networked_runnables.i_networked_runnable import INetworkedRunnable + + +class DockerHttpRunnable(INetworkedRunnable, IHttpBehavior): + def __init__(self, defaults_configurer: IConfigurable, health_check: IUrlHealthCheckable, setup: DockerHttpSetup, + verify: DockerHttpVerify, docker_manager: DockerManager): + self.__defaults_configurer = defaults_configurer + self.__health_check = health_check + self.__setup = setup + self.__verify = verify + self.__docker_manager = docker_manager + self.__network = None + self.__container_id = None + + def get_network(self) -> Network: + if not self.__network: + raise exceptions.MockException('Network unavailable. Mock is still starting.') + return self.__network + + def initialize(self): + self.__setup.set_url(self.get_network().external_url()) + self.__verify.set_url(self.get_network().external_url()) + self.__setup.init(self.__defaults_configurer.get_config()) + + def start(self): + run_result = self.__docker_manager.run_image('holomekc/wiremock-gui:2.25.1', port=8080, exposed_port=9090) + self.__container_id = run_result.container_id + self.__network = Network(internal_host=run_result.container_id, + internal_port=run_result.internal_port, + external_port=run_result.external_port, + ui_port=run_result.ui_port, + ui_endpoint='/__admin/webapp', + prefix='http://') + + def stop(self): + if self.__container_id: + self.__docker_manager.stop_container(self.__container_id) + + def reset(self): + self.__setup.init(self.__defaults_configurer.get_config()) + + def services_available(self): + pass + + def is_healthy(self) -> bool: + self.__health_check.set_url(self.get_network().ui_url()) + return self.__health_check.is_healthy() + + def setup(self) -> IHttpSetup: + if not self.__setup: + raise exceptions.MockException('Setup unavailable. Mock is still starting.') + return self.__setup + + def verify(self) -> IHttpVerify: + if not self.__verify: + raise exceptions.MockException('Verify unavailable. Mock is still starting.') + return self.__verify + + def url(self) -> str: + return self.get_network().external_url() diff --git a/touchstone/lib/mocks/http/http_setup.py b/touchstone/lib/mocks/networked_runnables/http/docker/docker_http_setup.py similarity index 79% rename from touchstone/lib/mocks/http/http_setup.py rename to touchstone/lib/mocks/networked_runnables/http/docker/docker_http_setup.py index 3c69c06..d4681c6 100644 --- a/touchstone/lib/mocks/http/http_setup.py +++ b/touchstone/lib/mocks/networked_runnables/http/docker/docker_http_setup.py @@ -5,18 +5,22 @@ import urllib.request from touchstone.lib import exceptions +from touchstone.lib.mocks.networked_runnables.http.i_http_behavior import IHttpSetup -class HttpSetup(object): - def __init__(self, url: str): +class DockerHttpSetup(IHttpSetup): + def __init__(self): super().__init__() - self.url = url + self.__url = None self.mock_ids: list = [] + def set_url(self, url: str): + self.__url = url + def init(self, defaults: dict): # Remove all mocked endpoints for mock_id in self.mock_ids: - request = urllib.request.Request(f'{self.url}/__admin/mappings/{mock_id}', method='DELETE') + request = urllib.request.Request(f'{self.__url}/__admin/mappings/{mock_id}', method='DELETE') try: urllib.request.urlopen(request) except urllib.error.HTTPError: @@ -24,15 +28,14 @@ def init(self, defaults: dict): self.mock_ids = [] # Reset requests journal - request = urllib.request.Request(f'{self.url}/__admin/requests', method='DELETE') + request = urllib.request.Request(f'{self.__url}/__admin/requests', method='DELETE') urllib.request.urlopen(request) - for request in defaults['requests']: + for request in defaults.get('requests', []): self.__submit_mock(request) def get(self, endpoint: str, response: str, response_status: int = 200, response_headers: dict = {'Content-Type': 'application/json'}): - """Returns the given response when a GET request is made to the given endpoint.""" self.__check_mock_response_type(response) mock = { 'request': { @@ -49,7 +52,6 @@ def get(self, endpoint: str, response: str, response_status: int = 200, def post(self, endpoint: str, response: str, response_status: int = 200, response_headers: dict = {'Content-Type': 'application/json'}): - """Returns the given response when a POST request is made to the given endpoint.""" self.__check_mock_response_type(response) mock = { 'request': { @@ -66,7 +68,6 @@ def post(self, endpoint: str, response: str, response_status: int = 200, def put(self, endpoint: str, response: str, response_status: int = 200, response_headers: dict = {'Content-Type': 'application/json'}): - """Returns the given response when a PUT request is made to the given endpoint.""" self.__check_mock_response_type(response) mock = { 'request': { @@ -83,7 +84,6 @@ def put(self, endpoint: str, response: str, response_status: int = 200, def delete(self, endpoint: str, response: str, response_status: int = 200, response_headers: dict = {'Content-Type': 'application/json'}): - """Returns the given response when a DELETE request is made to the given endpoint.""" self.__check_mock_response_type(response) mock = { 'request': { @@ -105,6 +105,6 @@ def __check_mock_response_type(self, response): def __submit_mock(self, mock: dict): data = json.dumps(mock).encode('utf8') request = urllib.request.Request( - f'{self.url}/__admin/mappings', data=data, headers={'Content-Type': 'application/json'}) + f'{self.__url}/__admin/mappings', data=data, headers={'Content-Type': 'application/json'}) response = urllib.request.urlopen(request).read() self.mock_ids.append(json.loads(response)['id']) diff --git a/touchstone/lib/mocks/http/http_verify.py b/touchstone/lib/mocks/networked_runnables/http/docker/docker_http_verify.py similarity index 62% rename from touchstone/lib/mocks/http/http_verify.py rename to touchstone/lib/mocks/networked_runnables/http/docker/docker_http_verify.py index 8cb9bfd..431538b 100644 --- a/touchstone/lib/mocks/http/http_verify.py +++ b/touchstone/lib/mocks/networked_runnables/http/docker/docker_http_verify.py @@ -2,46 +2,36 @@ import urllib.request from touchstone.lib.mocks import validation +from touchstone.lib.mocks.networked_runnables.http.i_http_behavior import IHttpVerify -class HttpVerify(object): - def __init__(self, url: str): +class DockerHttpVerify(IHttpVerify): + def __init__(self): super().__init__() - self.url = url + self.__url = None + + def set_url(self, url: str): + self.__url = url def get_called(self, endpoint: str, times: int = 1) -> bool: - """Returns True if the given endpoint has been called with a GET request the given number of times. - If times is set to None, the endpoint can be called any number of times.""" return self.__count_verification(endpoint, 'GET', times) def post_called(self, endpoint: str, times: int = 1) -> bool: - """Returns True if the given endpoint has been called with a POST request the given number of times. - If times is set to None, the endpoint can be called any number of times.""" return self.__count_verification(endpoint, 'POST', times) def put_called(self, endpoint: str, times: int = 1) -> bool: - """Returns True if the given endpoint has been called with a PUT request the given number of times. - If times is set to None, the endpoint can be called any number of times.""" return self.__count_verification(endpoint, 'PUT', times) def delete_called(self, endpoint: str, times: int = 1) -> bool: - """Returns True if the given endpoint has been called with a DELETE request the given number of times. - If times is set to None, the endpoint can be called any number of times.""" return self.__count_verification(endpoint, 'DELETE', times) def post_contained(self, endpoint: str, expected_body: str) -> bool: - """Returns True if the given endpoint has been called with a POST request containing the given expected - body.""" return self.__contained_verification(endpoint, 'POST', expected_body) def put_contained(self, endpoint: str, expected_body: str) -> bool: - """Returns True if the given endpoint has been called with a PUT request containing the given expected - body.""" return self.__contained_verification(endpoint, 'PUT', expected_body) def delete_contained(self, endpoint: str, expected_body: str) -> bool: - """Returns True if the given endpoint has been called with a DELETE request containing the given expected - body.""" return self.__contained_verification(endpoint, 'DELETE', expected_body) def __count_verification(self, endpoint, http_verb, times): @@ -51,7 +41,7 @@ def __count_verification(self, endpoint, http_verb, times): } data = json.dumps(payload).encode('utf8') request = urllib.request.Request( - f'{self.url}/__admin/requests/count', + f'{self.__url}/__admin/requests/count', data=data, headers={'Content-Type': 'application/json'}) response = urllib.request.urlopen(request).read() @@ -67,7 +57,7 @@ def __contained_verification(self, endpoint, http_verb, expected_body): } data = json.dumps(payload).encode('utf8') request = urllib.request.Request( - f'{self.url}/__admin/requests/find', + f'{self.__url}/__admin/requests/find', data=data, headers={'Content-Type': 'application/json'}) response = urllib.request.urlopen(request).read() diff --git a/touchstone/lib/mocks/networked_runnables/http/i_http_behavior.py b/touchstone/lib/mocks/networked_runnables/http/i_http_behavior.py new file mode 100644 index 0000000..6f6c853 --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/http/i_http_behavior.py @@ -0,0 +1,83 @@ +import abc + + +class IHttpSetup(object): + @abc.abstractmethod + def init(self, defaults: dict): + pass + + @abc.abstractmethod + def get(self, endpoint: str, response: str, response_status: int = 200, + response_headers: dict = {'Content-Type': 'application/json'}): + """Returns the given response when a GET request is made to the given endpoint.""" + pass + + @abc.abstractmethod + def post(self, endpoint: str, response: str, response_status: int = 200, + response_headers: dict = {'Content-Type': 'application/json'}): + """Returns the given response when a POST request is made to the given endpoint.""" + pass + + @abc.abstractmethod + def put(self, endpoint: str, response: str, response_status: int = 200, + response_headers: dict = {'Content-Type': 'application/json'}): + """Returns the given response when a PUT request is made to the given endpoint.""" + pass + + @abc.abstractmethod + def delete(self, endpoint: str, response: str, response_status: int = 200, + response_headers: dict = {'Content-Type': 'application/json'}): + """Returns the given response when a DELETE request is made to the given endpoint.""" + pass + + +class IHttpVerify(object): + def get_called(self, endpoint: str, times: int = 1) -> bool: + """Returns True if the given endpoint has been called with a GET request the given number of times. + If times is set to None, the endpoint can be called any number of times.""" + pass + + def post_called(self, endpoint: str, times: int = 1) -> bool: + """Returns True if the given endpoint has been called with a POST request the given number of times. + If times is set to None, the endpoint can be called any number of times.""" + pass + + def put_called(self, endpoint: str, times: int = 1) -> bool: + """Returns True if the given endpoint has been called with a PUT request the given number of times. + If times is set to None, the endpoint can be called any number of times.""" + pass + + def delete_called(self, endpoint: str, times: int = 1) -> bool: + """Returns True if the given endpoint has been called with a DELETE request the given number of times. + If times is set to None, the endpoint can be called any number of times.""" + pass + + def post_contained(self, endpoint: str, expected_body: str) -> bool: + """Returns True if the given endpoint has been called with a POST request containing the given expected + body.""" + pass + + def put_contained(self, endpoint: str, expected_body: str) -> bool: + """Returns True if the given endpoint has been called with a PUT request containing the given expected + body.""" + pass + + def delete_contained(self, endpoint: str, expected_body: str) -> bool: + """Returns True if the given endpoint has been called with a DELETE request containing the given expected + body.""" + pass + + +class IHttpBehavior(object): + @abc.abstractmethod + def setup(self) -> IHttpSetup: + pass + + @abc.abstractmethod + def verify(self) -> IHttpVerify: + pass + + @abc.abstractmethod + def url(self) -> str: + """Returns the URL of this mocked HTTP resource.""" + pass diff --git a/touchstone/lib/mocks/networked_runnables/i_networked_runnable.py b/touchstone/lib/mocks/networked_runnables/i_networked_runnable.py new file mode 100644 index 0000000..5fcdc61 --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/i_networked_runnable.py @@ -0,0 +1,15 @@ +import abc + +from touchstone.lib.mocks.health_checks.i_health_checkable import IHealthCheckable +from touchstone.lib.mocks.network import Network +from touchstone.lib.mocks.runnables.i_runnable import IRunnable + + +class INetworkedRunnable(IRunnable, IHealthCheckable): + @abc.abstractmethod + def get_network(self) -> Network: + pass + + @abc.abstractmethod + def initialize(self): + pass diff --git a/touchstone/lib/mocks/networked_runnables/mongodb/__init__.py b/touchstone/lib/mocks/networked_runnables/mongodb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/networked_runnables/mongodb/docker/__init__.py b/touchstone/lib/mocks/networked_runnables/mongodb/docker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/mongodb/mongo_context.py b/touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongo_context.py similarity index 97% rename from touchstone/lib/mocks/mongodb/mongo_context.py rename to touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongo_context.py index 708e9f4..3de5035 100644 --- a/touchstone/lib/mocks/mongodb/mongo_context.py +++ b/touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongo_context.py @@ -1,7 +1,7 @@ from typing import KeysView -class MongoContext(object): +class DockerMongoContext(object): def __init__(self): self.__databases = {} diff --git a/touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongodb_runnable.py b/touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongodb_runnable.py new file mode 100644 index 0000000..413f03b --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongodb_runnable.py @@ -0,0 +1,84 @@ +import pymongo + +from touchstone.lib import exceptions +from touchstone.lib.docker_manager import DockerManager +from touchstone.lib.mocks.configurers.i_configurable import IConfigurable +from touchstone.lib.mocks.network import Network +from touchstone.lib.mocks.networked_runnables.i_networked_runnable import INetworkedRunnable +from touchstone.lib.mocks.networked_runnables.mongodb.docker.docker_mongodb_setup import DockerMongodbSetup +from touchstone.lib.mocks.networked_runnables.mongodb.docker.docker_mongodb_verify import DockerMongodbVerify +from touchstone.lib.mocks.networked_runnables.mongodb.i_mongodb_behavior import IMongodbBehavior, IMongodbVerify, \ + IMongodbSetup + + +class DockerMongodbRunnable(INetworkedRunnable, IMongodbBehavior): + def __init__(self, defaults_configurer: IConfigurable, is_dev_mode: bool, setup: DockerMongodbSetup, + verify: DockerMongodbVerify, docker_manager: DockerManager): + self.__defaults_configurer = defaults_configurer + self.__is_dev_mode = is_dev_mode + self.__setup = setup + self.__verify = verify + self.__docker_manager = docker_manager + self.__network = None + self.__container_id = None + self.__ui_container_id = None + + def get_network(self) -> Network: + if not self.__network: + raise exceptions.MockException('Network unavailable. Mock is still starting.') + return self.__network + + def initialize(self): + mongo_client = pymongo.MongoClient(self.get_network().external_host, self.get_network().external_port) + self.__setup.set_mongo_client(mongo_client) + self.__verify.set_mongo_client(mongo_client) + self.__setup.init(self.__defaults_configurer.get_config()) + + def start(self): + run_result = self.__docker_manager.run_image('mongo:4.0.14', port=27017) + self.__container_id = run_result.container_id + + ui_port = None + if self.__is_dev_mode: + ui_run_result = self.__docker_manager.run_image('mongo-express:0.49.0', + ui_port=8081, + environment_vars=[ + ('ME_CONFIG_MONGODB_PORT', run_result.internal_port), + ('ME_CONFIG_MONGODB_SERVER', run_result.container_id)]) + self.__ui_container_id = ui_run_result.container_id + ui_port = ui_run_result.ui_port + + self.__network = Network(internal_host=run_result.container_id, + internal_port=run_result.internal_port, + external_port=run_result.external_port, + ui_port=ui_port) + + def stop(self): + if self.__container_id: + self.__docker_manager.stop_container(self.__container_id) + if self.__ui_container_id: + self.__docker_manager.stop_container(self.__ui_container_id) + + def reset(self): + self.__setup.init(self.__defaults_configurer.get_config()) + + def services_available(self): + pass + + def is_healthy(self) -> bool: + try: + client = pymongo.MongoClient(self.get_network().external_host, self.get_network().external_port) + status = client.admin.command('serverStatus')['ok'] + return status == 1.0 + except Exception: + return False + + def setup(self) -> IMongodbSetup: + if not self.__setup: + raise exceptions.MockException('Setup unavailable. Mock is still starting.') + return self.__setup + + def verify(self) -> IMongodbVerify: + if not self.__verify: + raise exceptions.MockException('Verify unavailable. Mock is still starting.') + return self.__verify diff --git a/touchstone/lib/mocks/mongodb/mongodb_setup.py b/touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongodb_setup.py similarity index 64% rename from touchstone/lib/mocks/mongodb/mongodb_setup.py rename to touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongodb_setup.py index a920081..41726fb 100644 --- a/touchstone/lib/mocks/mongodb/mongodb_setup.py +++ b/touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongodb_setup.py @@ -2,13 +2,17 @@ from pymongo import MongoClient -from touchstone.lib.mocks.mongodb.mongo_context import MongoContext +from touchstone.lib.mocks.networked_runnables.mongodb.docker.docker_mongo_context import DockerMongoContext +from touchstone.lib.mocks.networked_runnables.mongodb.i_mongodb_behavior import IMongodbSetup -class MongodbSetup(object): - def __init__(self, mongo_client: MongoClient, mongo_context: MongoContext): - self.__mongo_client = mongo_client +class DockerMongodbSetup(IMongodbSetup): + def __init__(self, mongo_context: DockerMongoContext): self.__mongo_context = mongo_context + self.__mongo_client = None + + def set_mongo_client(self, mongo_client: MongoClient): + self.__mongo_client = mongo_client def init(self, defaults: dict): for database in self.__mongo_context.databases(): @@ -17,20 +21,17 @@ def init(self, defaults: dict): mongo_database.drop_collection(collection) self.__mongo_context.clear() - for database in defaults['databases']: + for database in defaults.get('databases', []): database_name = database['name'] mongo_database = self.__mongo_client.get_database(database_name) self.__mongo_context.add_database(database_name) - if 'collections' in database: - for collection in database['collections']: - mongo_collection = mongo_database.get_collection(collection['name']) - self.__mongo_context.add_collection(database_name, collection['name']) - if 'documents' in collection: - for document in collection['documents']: - mongo_collection.insert_one(document) + for collection in database.get('collections', []): + mongo_collection = mongo_database.get_collection(collection['name']) + self.__mongo_context.add_collection(database_name, collection['name']) + for document in collection.get('documents', []): + mongo_collection.insert_one(document) def command(self, database: str, command: dict): - """Execute an arbitrary command on the database.""" if self.__mongo_context.database_exists(database): self.__mongo_client.get_database(database).command(command) diff --git a/touchstone/lib/mocks/mongodb/mongodb_verify.py b/touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongodb_verify.py similarity index 68% rename from touchstone/lib/mocks/mongodb/mongodb_verify.py rename to touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongodb_verify.py index 2415449..2616a45 100644 --- a/touchstone/lib/mocks/mongodb/mongodb_verify.py +++ b/touchstone/lib/mocks/networked_runnables/mongodb/docker/docker_mongodb_verify.py @@ -1,16 +1,18 @@ from pymongo import MongoClient -from touchstone.lib.mocks.mongodb.mongo_context import MongoContext +from touchstone.lib.mocks.networked_runnables.mongodb.docker.docker_mongo_context import DockerMongoContext +from touchstone.lib.mocks.networked_runnables.mongodb.i_mongodb_behavior import IMongodbVerify -class MongodbVerify(object): - def __init__(self, mongo_client: MongoClient, mongo_context: MongoContext): - self.__mongo_client = mongo_client +class DockerMongodbVerify(IMongodbVerify): + def __init__(self, mongo_context: DockerMongoContext): self.__mongo_context = mongo_context + self.__mongo_client = None + + def set_mongo_client(self, mongo_client: MongoClient): + self.__mongo_client = mongo_client def document_exists(self, database: str, collection: str, document: dict, num_expected: int = 1) -> bool: - """Returns True if a document exists in the given database and collection. If num_expected is set to None, - any number of documents will be considered passing.""" if not self.__mongo_context.collection_exists(database, collection): return False diff --git a/touchstone/lib/mocks/networked_runnables/mongodb/i_mongodb_behavior.py b/touchstone/lib/mocks/networked_runnables/mongodb/i_mongodb_behavior.py new file mode 100644 index 0000000..cfe7da1 --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/mongodb/i_mongodb_behavior.py @@ -0,0 +1,33 @@ +import abc +from typing import List + + +class IMongodbSetup(object): + def command(self, database: str, command: dict): + """Execute an arbitrary command on the database.""" + pass + + def insert_document(self, database: str, collection: str, document: dict): + """Inserts a document into the given database and collection.""" + pass + + def insert_documents(self, database: str, collection: str, documents: List[dict]): + """Inserts multiple documents into the given database and collection.""" + pass + + +class IMongodbVerify(object): + def document_exists(self, database: str, collection: str, document: dict, num_expected: int = 1) -> bool: + """Returns True if a document exists in the given database and collection. If num_expected is set to None, + any number of documents will be considered passing.""" + pass + + +class IMongodbBehavior(object): + @abc.abstractmethod + def setup(self) -> IMongodbSetup: + pass + + @abc.abstractmethod + def verify(self) -> IMongodbVerify: + pass diff --git a/touchstone/lib/mocks/networked_runnables/mysql/__init__.py b/touchstone/lib/mocks/networked_runnables/mysql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/networked_runnables/mysql/docker/__init__.py b/touchstone/lib/mocks/networked_runnables/mysql/docker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/mysql/mysql_context.py b/touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_context.py similarity index 94% rename from touchstone/lib/mocks/mysql/mysql_context.py rename to touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_context.py index 9919f64..a290a9b 100644 --- a/touchstone/lib/mocks/mysql/mysql_context.py +++ b/touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_context.py @@ -1,4 +1,4 @@ -class MysqlContext(object): +class DockerMysqlContext(object): def __init__(self): self.__databases = [] diff --git a/touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_runnable.py b/touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_runnable.py new file mode 100644 index 0000000..c12d1ff --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_runnable.py @@ -0,0 +1,101 @@ +import pymysql + +from touchstone.lib import exceptions +from touchstone.lib.docker_manager import DockerManager +from touchstone.lib.mocks.configurers.i_configurable import IConfigurable +from touchstone.lib.mocks.network import Network +from touchstone.lib.mocks.networked_runnables.i_networked_runnable import INetworkedRunnable +from touchstone.lib.mocks.networked_runnables.mysql.docker.docker_mysql_setup import DockerMysqlSetup +from touchstone.lib.mocks.networked_runnables.mysql.docker.docker_mysql_verify import DockerMysqlVerify +from touchstone.lib.mocks.networked_runnables.mysql.i_mysql_behabior import IMysqlBehavior, IMysqlVerify, IMysqlSetup + + +class DockerMysqlRunnable(INetworkedRunnable, IMysqlBehavior): + __USERNAME = 'root' + __PASSWORD = 'root' + + def __init__(self, defaults_configurer: IConfigurable, is_dev_mode: bool, configurer: IConfigurable, + setup: DockerMysqlSetup, verify: DockerMysqlVerify, docker_manager: DockerManager): + self.__defaults_configurer = defaults_configurer + self.__is_dev_mode = is_dev_mode + self.__configurer = configurer + self.__setup = setup + self.__verify = verify + self.__docker_manager = docker_manager + self.__network = None + self.__container_id = None + self.__ui_container_id = None + + def get_network(self) -> Network: + if not self.__network: + raise exceptions.MockException('Network unavailable. Mock is still starting.') + return self.__network + + def initialize(self): + connection = pymysql.connect(host=self.get_network().external_host, + port=self.get_network().external_port, + user=self.__USERNAME, + password=self.__PASSWORD, + charset='utf8mb4', + autocommit=True, + cursorclass=pymysql.cursors.DictCursor) + cursor = connection.cursor() + convert_camel_to_snake = self.__configurer.get_config()['convertCamelToSnakeCase'] + self.__setup.set_cursor(cursor) + self.__setup.set_convert_camel_to_snake(convert_camel_to_snake) + self.__verify.set_cursor(cursor) + self.__verify.set_convert_camel_to_snake(convert_camel_to_snake) + self.__setup.init(self.__defaults_configurer.get_config()) + + def start(self): + run_result = self.__docker_manager.run_image('mysql:5.7.29', port=3306, + environment_vars=[('MYSQL_ROOT_PASSWORD', self.__USERNAME)]) + self.__container_id = run_result.container_id + + ui_port = None + if self.__is_dev_mode: + ui_run_result = self.__docker_manager.run_image('adminer:4.7.5-standalone', + ui_port=8080, + environment_vars=[ + ('ADMINER_DEFAULT_SERVER', self.__container_id)]) + self.__ui_container_id = ui_run_result.container_id + ui_port = ui_run_result.ui_port + + self.__network = Network(internal_host=run_result.container_id, + internal_port=run_result.internal_port, + external_port=run_result.external_port, + ui_port=ui_port, + username=self.__USERNAME, + password=self.__PASSWORD) + + def stop(self): + if self.__container_id: + self.__docker_manager.stop_container(self.__container_id) + if self.__ui_container_id: + self.__docker_manager.stop_container(self.__ui_container_id) + + def reset(self): + self.__setup.init(self.__defaults_configurer.get_config()) + + def services_available(self): + pass + + def is_healthy(self) -> bool: + try: + pymysql.connect(host=self.get_network().external_host, + port=self.get_network().external_port, + user=self.__USERNAME, + password=self.__PASSWORD) + return True + except Exception: + return False + + def setup(self) -> IMysqlSetup: + if not self.__setup: + raise exceptions.MockException('Setup unavailable. Mock is still starting.') + return self.__setup + + def verify(self) -> IMysqlVerify: + if not self.__verify: + raise exceptions.MockException('Verify unavailable. Mock is still starting.') + return self.__verify diff --git a/touchstone/lib/mocks/mysql/mysql_setup.py b/touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_setup.py similarity index 77% rename from touchstone/lib/mocks/mysql/mysql_setup.py rename to touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_setup.py index d9d2343..4633e99 100644 --- a/touchstone/lib/mocks/mysql/mysql_setup.py +++ b/touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_setup.py @@ -3,13 +3,20 @@ from pymysql.cursors import Cursor from touchstone import common -from touchstone.lib.mocks.mysql.mysql_context import MysqlContext +from touchstone.lib.mocks.networked_runnables.mysql.docker.docker_mysql_context import DockerMysqlContext +from touchstone.lib.mocks.networked_runnables.mysql.i_mysql_behabior import IMysqlSetup -class MysqlSetup(object): - def __init__(self, cursor: Cursor, mysql_context: MysqlContext, convert_camel_to_snake: bool): - self.__cursor = cursor +class DockerMysqlSetup(IMysqlSetup): + def __init__(self, mysql_context: DockerMysqlContext): self.__mysql_context = mysql_context + self.__cursor = None + self.__convert_camel_to_snake = False + + def set_cursor(self, cursor: Cursor): + self.__cursor = cursor + + def set_convert_camel_to_snake(self, convert_camel_to_snake: bool): self.__convert_camel_to_snake = convert_camel_to_snake def init(self, defaults: dict): @@ -17,12 +24,12 @@ def init(self, defaults: dict): self.__cursor.execute(f'DROP DATABASE {database}') self.__mysql_context.clear() - for database in defaults['databases']: + for database in defaults.get('databases', []): database_name = database['name'] self.__cursor.execute(f'CREATE DATABASE {database_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci') self.__cursor.execute(f'USE {database_name}') self.__mysql_context.add_database(database_name) - for statement in database['statements']: + for statement in database.get('statements', []): self.__cursor.execute(statement) def execute(self, database: str, sql: str): @@ -53,7 +60,10 @@ def insert_rows(self, database: str, table: str, data: List[dict]): def __sql_values_from_dict(self, data: dict) -> str: col_values = [] for value in data.values(): - col_values.append(f"'{value}'") + if not value: + col_values.append('NULL') + else: + col_values.append(f"'{value}'") col_values = ', '.join(col_values) return '(' + col_values + ')' diff --git a/touchstone/lib/mocks/mysql/mysql_verify.py b/touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_verify.py similarity index 68% rename from touchstone/lib/mocks/mysql/mysql_verify.py rename to touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_verify.py index 8f2da04..cffbe5e 100644 --- a/touchstone/lib/mocks/mysql/mysql_verify.py +++ b/touchstone/lib/mocks/networked_runnables/mysql/docker/docker_mysql_verify.py @@ -1,18 +1,23 @@ from pymysql.cursors import Cursor from touchstone import common -from touchstone.lib.mocks.mysql.mysql_context import MysqlContext +from touchstone.lib.mocks.networked_runnables.mysql.docker.docker_mysql_context import DockerMysqlContext +from touchstone.lib.mocks.networked_runnables.mysql.i_mysql_behabior import IMysqlVerify -class MysqlVerify(object): - def __init__(self, cursor: Cursor, mysql_context: MysqlContext, convert_camel_to_snake: bool): - self.__cursor = cursor +class DockerMysqlVerify(IMysqlVerify): + def __init__(self, mysql_context: DockerMysqlContext): self.__mysql_context = mysql_context + self.__cursor = None + self.__convert_camel_to_snake = False + + def set_cursor(self, cursor: Cursor): + self.__cursor = cursor + + def set_convert_camel_to_snake(self, convert_camel_to_snake: bool): self.__convert_camel_to_snake = convert_camel_to_snake def row_exists(self, database: str, table: str, where_conditions: dict, num_expected: int = 1) -> bool: - """Returns True if the given where conditions are found in the given database. If num_expected is set to None, - any number of rows will be considered passing.""" if not self.__mysql_context.database_exists(database): return False @@ -20,7 +25,10 @@ def row_exists(self, database: str, table: str, where_conditions: dict, num_expe where_conditions = common.to_snake(where_conditions) where = [] for key, value in where_conditions.items(): - where.append(f"{key}='{value}'") + if not value: + where.append(f'{key} is NULL') + else: + where.append(f"{key}='{value}'") sql = f"SELECT COUNT(*) FROM {table} WHERE {' AND '.join(where)}" common.logger.debug(f'Executing: {sql}') self.__cursor.execute(f'USE {database}') @@ -36,5 +44,4 @@ def row_exists(self, database: str, table: str, where_conditions: dict, num_expe return False def row_does_not_exist(self, database: str, table: str, where_conditions: dict) -> bool: - """Returns True if the given where conditions are not found in the given database.""" return self.row_exists(database, table, where_conditions, num_expected=0) diff --git a/touchstone/lib/mocks/networked_runnables/mysql/i_mysql_behabior.py b/touchstone/lib/mocks/networked_runnables/mysql/i_mysql_behabior.py new file mode 100644 index 0000000..1829675 --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/mysql/i_mysql_behabior.py @@ -0,0 +1,50 @@ +import abc +from typing import List + + +class IMysqlSetup(object): + @abc.abstractmethod + def execute(self, database: str, sql: str): + """Executes arbitrary SQL on the given database.""" + pass + + @abc.abstractmethod + def insert_row(self, database: str, table: str, data: dict): + """Inserts a dictionary of key-value pairs into the given database and table. If the config option, + "convertCamelToSnakeCase" is set (default True), the dictionary keys will be converted from camel case to + snake case.""" + pass + + @abc.abstractmethod + def insert_rows(self, database: str, table: str, data: List[dict]): + """Inserts a list of dictionaries of key-value pairs into the given database and table. If the config option, + "convertCamelToSnakeCase" is set (default True), the dictionary keys will be converted from camel case to + snake case.""" + pass + + +class IMysqlVerify(object): + @abc.abstractmethod + def row_exists(self, database: str, table: str, where_conditions: dict, num_expected: int = 1) -> bool: + """Returns True if the given where conditions are found in the given database. If num_expected is set to None, + any number of rows will be considered passing.""" + pass + + @abc.abstractmethod + def row_does_not_exist(self, database: str, table: str, where_conditions: dict) -> bool: + """Returns True if the given where conditions are not found in the given database.""" + pass + + +class IMysqlBehavior(object): + DEFAULT_CONFIG = { + 'convertCamelToSnakeCase': True + } + + @abc.abstractmethod + def setup(self) -> IMysqlSetup: + pass + + @abc.abstractmethod + def verify(self) -> IMysqlVerify: + pass diff --git a/touchstone/lib/mocks/networked_runnables/rabbitmq/__init__.py b/touchstone/lib/mocks/networked_runnables/rabbitmq/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/__init__.py b/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/rabbitmq/rmq_context.py b/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_context.py similarity index 97% rename from touchstone/lib/mocks/rabbitmq/rmq_context.py rename to touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_context.py index bd34966..bcb91c8 100644 --- a/touchstone/lib/mocks/rabbitmq/rmq_context.py +++ b/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_context.py @@ -1,7 +1,7 @@ from touchstone import common -class RmqContext(object): +class DockerRabbitmqContext(object): def __init__(self): self.__exchanges: dict = {} diff --git a/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_runnable.py b/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_runnable.py new file mode 100644 index 0000000..23e4473 --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_runnable.py @@ -0,0 +1,84 @@ +import pika + +from touchstone.lib import exceptions +from touchstone.lib.docker_manager import DockerManager +from touchstone.lib.mocks.configurers.i_configurable import IConfigurable +from touchstone.lib.mocks.health_checks.i_url_health_checkable import IUrlHealthCheckable +from touchstone.lib.mocks.network import Network +from touchstone.lib.mocks.networked_runnables.i_networked_runnable import INetworkedRunnable +from touchstone.lib.mocks.networked_runnables.rabbitmq.docker.docker_rabbitmq_setup import DockerRabbitmqSetup +from touchstone.lib.mocks.networked_runnables.rabbitmq.docker.docker_rabbitmq_verify import DockerRabbitmqVerify +from touchstone.lib.mocks.networked_runnables.rabbitmq.i_rabbitmq_behavior import IRabbitmqBehavior, IRabbitmqVerify, \ + IRabbitmqSetup + + +class DockerRabbitmqRunnable(INetworkedRunnable, IRabbitmqBehavior): + __USERNAME = 'guest' + __PASSWORD = 'guest' + + def __init__(self, defaults_configurer: IConfigurable, configurer: IConfigurable, health_check: IUrlHealthCheckable, + setup: DockerRabbitmqSetup, verify: DockerRabbitmqVerify, docker_manager: DockerManager): + self.__defaults_configurer = defaults_configurer + self.__configurer = configurer + self.__health_check = health_check + self.__setup = setup + self.__verify = verify + self.__docker_manager = docker_manager + self.__network = None + self.__container_id = None + + def get_network(self) -> Network: + if not self.__network: + raise exceptions.MockException('Network unavailable. Mock is still starting.') + return self.__network + + def initialize(self): + connection_params = pika.ConnectionParameters( + host=self.get_network().external_host, + port=self.get_network().external_port, + credentials=pika.PlainCredentials(self.__USERNAME, self.__PASSWORD), + heartbeat=0 + ) + connection = pika.BlockingConnection(connection_params) + channel = connection.channel() + self.__setup.set_channel(channel) + self.__setup.set_connection_params(connection_params) + self.__verify.set_blocking_channel(channel) + if self.__configurer.get_config()['autoCreate']: + self.__setup.create_all(self.__defaults_configurer.get_config()) + + def start(self): + run_result = self.__docker_manager.run_image('rabbitmq:3.7.22-management-alpine', port=5672, ui_port=15672) + self.__container_id = run_result.container_id + self.__network = Network(internal_host=run_result.container_id, + internal_port=run_result.internal_port, + external_port=run_result.external_port, + ui_port=run_result.ui_port, + username=self.__USERNAME, + password=self.__PASSWORD) + + def stop(self): + if self.__container_id: + self.__setup.stop_listening() + self.__docker_manager.stop_container(self.__container_id) + + def reset(self): + self.__setup.purge_queues() + + def services_available(self): + if not self.__configurer.get_config()['autoCreate']: + self.__setup.create_shadow_queues(self.__defaults_configurer.get_config()) + + def is_healthy(self) -> bool: + self.__health_check.set_url(self.get_network().ui_url()) + return self.__health_check.is_healthy() + + def setup(self) -> IRabbitmqSetup: + if not self.__setup: + raise exceptions.MockException('Setup unavailable. Mock is still starting.') + return self.__setup + + def verify(self) -> IRabbitmqVerify: + if not self.__verify: + raise exceptions.MockException('Verify unavailable. Mock is still starting.') + return self.__verify diff --git a/touchstone/lib/mocks/rabbitmq/rabbitmq_setup.py b/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_setup.py similarity index 80% rename from touchstone/lib/mocks/rabbitmq/rabbitmq_setup.py rename to touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_setup.py index 9943396..97f86bc 100644 --- a/touchstone/lib/mocks/rabbitmq/rabbitmq_setup.py +++ b/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_setup.py @@ -5,11 +5,12 @@ from pika import spec from pika.adapters.blocking_connection import BlockingChannel -from touchstone.lib.mocks.rabbitmq.rmq_context import RmqContext +from touchstone.lib.mocks.networked_runnables.rabbitmq.docker.docker_rabbitmq_context import DockerRabbitmqContext +from touchstone.lib.mocks.networked_runnables.rabbitmq.i_rabbitmq_behavior import IRabbitmqSetup class MessageConsumer(Thread): - def __init__(self, connection_params: pika.ConnectionParameters, rmq_context: RmqContext): + def __init__(self, connection_params: pika.ConnectionParameters, rmq_context: DockerRabbitmqContext): super().__init__() self.__connection_params = connection_params self.__rmq_context = rmq_context @@ -34,17 +35,22 @@ def message_received(channel: BlockingChannel, method: spec.Basic.Deliver, prope self.channel.basic_consume(queue, message_received) -class RabbitmqSetup(object): - def __init__(self, channel: BlockingChannel, connection_params: pika.ConnectionParameters, rmq_context: RmqContext): +class DockerRabbitmqSetup(IRabbitmqSetup): + def __init__(self, rmq_context: DockerRabbitmqContext): super().__init__() - self.__channel = channel self.__rmq_context = rmq_context - - self.__message_consumer: MessageConsumer = MessageConsumer(connection_params, rmq_context) + self.__channel = None + self.__message_consumer = None self.__exchanges: list = [] self.__queues: list = [] self.__shadow_queues: list = [] + def set_channel(self, channel: BlockingChannel): + self.__channel = channel + + def set_connection_params(self, connection_params: pika.ConnectionParameters): + self.__message_consumer = MessageConsumer(connection_params, self.__rmq_context) + def purge_queues(self): for queue in self.__queues: self.__channel.queue_purge(queue) @@ -53,9 +59,9 @@ def purge_queues(self): self.__rmq_context.clear() def create_all(self, defaults: dict): - for exchange in defaults['exchanges']: + for exchange in defaults.get('exchanges', []): self.__create_exchange(exchange['name'], exchange['type']) - for queue in exchange['queues']: + for queue in exchange.get('queues', []): routing_key = queue.get('routingKey', '') self.__create_queue(queue['name'], exchange['name'], routing_key) self.__create_shadow_queue(queue['name'], exchange['name'], routing_key) @@ -63,8 +69,8 @@ def create_all(self, defaults: dict): self.__message_consumer.start() def create_shadow_queues(self, defaults: dict): - for exchange in defaults['exchanges']: - for queue in exchange['queues']: + for exchange in defaults.get('exchanges', []): + for queue in exchange.get('queues', []): routing_key = queue.get('routingKey', '') self.__create_shadow_queue(queue['name'], exchange['name'], routing_key) if not self.__message_consumer.is_alive(): @@ -74,7 +80,7 @@ def stop_listening(self): def callback(): self.__message_consumer.channel.stop_consuming() - if self.__message_consumer.is_alive(): + if self.__message_consumer and self.__message_consumer.is_alive(): self.__message_consumer.channel.connection.add_callback_threadsafe(callback) self.__message_consumer.join() diff --git a/touchstone/lib/mocks/rabbitmq/rabbitmq_verify.py b/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_verify.py similarity index 63% rename from touchstone/lib/mocks/rabbitmq/rabbitmq_verify.py rename to touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_verify.py index 50f35f9..ec3a1b5 100644 --- a/touchstone/lib/mocks/rabbitmq/rabbitmq_verify.py +++ b/touchstone/lib/mocks/networked_runnables/rabbitmq/docker/docker_rabbitmq_verify.py @@ -1,18 +1,20 @@ from pika.adapters.blocking_connection import BlockingChannel from touchstone.lib.mocks import validation -from touchstone.lib.mocks.rabbitmq.rmq_context import RmqContext +from touchstone.lib.mocks.networked_runnables.rabbitmq.docker.docker_rabbitmq_context import DockerRabbitmqContext +from touchstone.lib.mocks.networked_runnables.rabbitmq.i_rabbitmq_behavior import IRabbitmqVerify -class RabbitmqVerify(object): - def __init__(self, channel: BlockingChannel, rmq_context: RmqContext): +class DockerRabbitmqVerify(IRabbitmqVerify): + def __init__(self, rmq_context: DockerRabbitmqContext): super().__init__() - self.__channel: BlockingChannel = channel self.__rmq_context = rmq_context + self.__channel = None + + def set_blocking_channel(self, channel: BlockingChannel): + self.__channel = channel def messages_published(self, exchange: str, num_expected: int = 1, routing_key: str = '') -> bool: - """Returns True if messages have been published the given number of times to the given exchange and - routing-key. If num_expected is set to None, any number of messages will be considered passing.""" if not self.__rmq_context.exchange_is_tracked(exchange, routing_key): return False num_messages = self.__rmq_context.messages_published(exchange, routing_key) @@ -21,7 +23,6 @@ def messages_published(self, exchange: str, num_expected: int = 1, routing_key: return validation.matches(num_expected, num_messages) def payload_published(self, exchange: str, expected_payload: str, routing_key: str = '') -> bool: - """Returns True if a message with the given payload has been published to the given exchange and routing key.""" if not self.__rmq_context.exchange_is_tracked(exchange, routing_key): return False payloads = self.__rmq_context.payloads_published(exchange, routing_key) diff --git a/touchstone/lib/mocks/networked_runnables/rabbitmq/i_rabbitmq_behavior.py b/touchstone/lib/mocks/networked_runnables/rabbitmq/i_rabbitmq_behavior.py new file mode 100644 index 0000000..16b0e49 --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/rabbitmq/i_rabbitmq_behavior.py @@ -0,0 +1,34 @@ +import abc + + +class IRabbitmqSetup(object): + def publish(self, exchange: str, payload: str, routing_key: str = ''): + """Publish a message with a payload to the given exchange and optional routing-key.""" + pass + + +class IRabbitmqVerify(object): + @abc.abstractmethod + def messages_published(self, exchange: str, num_expected: int = 1, routing_key: str = '') -> bool: + """Returns True if messages have been published the given number of times to the given exchange and + routing-key. If num_expected is set to None, any number of messages will be considered passing.""" + pass + + @abc.abstractmethod + def payload_published(self, exchange: str, expected_payload: str, routing_key: str = '') -> bool: + """Returns True if a message with the given payload has been published to the given exchange and routing key.""" + pass + + +class IRabbitmqBehavior(object): + DEFAULT_CONFIG = { + 'autoCreate': True + } + + @abc.abstractmethod + def setup(self) -> IRabbitmqSetup: + pass + + @abc.abstractmethod + def verify(self) -> IRabbitmqVerify: + pass diff --git a/touchstone/lib/mocks/networked_runnables/s3/__init__.py b/touchstone/lib/mocks/networked_runnables/s3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/networked_runnables/s3/docker/__init__.py b/touchstone/lib/mocks/networked_runnables/s3/docker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/networked_runnables/s3/docker/docker_s3_runnable.py b/touchstone/lib/mocks/networked_runnables/s3/docker/docker_s3_runnable.py new file mode 100644 index 0000000..7abd3b2 --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/s3/docker/docker_s3_runnable.py @@ -0,0 +1,81 @@ +from minio import Minio + +from touchstone.lib import exceptions +from touchstone.lib.docker_manager import DockerManager +from touchstone.lib.mocks.configurers.i_configurable import IConfigurable +from touchstone.lib.mocks.health_checks.i_url_health_checkable import IUrlHealthCheckable +from touchstone.lib.mocks.network import Network +from touchstone.lib.mocks.networked_runnables.i_networked_runnable import INetworkedRunnable +from touchstone.lib.mocks.networked_runnables.s3.docker.docker_s3_setup import DockerS3Setup +from touchstone.lib.mocks.networked_runnables.s3.docker.docker_s3_verify import DockerS3Verify +from touchstone.lib.mocks.networked_runnables.s3.i_s3_behavior import IS3Behavior, IS3Verify, IS3Setup + + +class DockerS3Runnable(INetworkedRunnable, IS3Behavior): + __USERNAME = 'admin123' + __PASSWORD = 'admin123' + + def __init__(self, defaults_configurer: IConfigurable, base_objects_path: str, health_check: IUrlHealthCheckable, + setup: DockerS3Setup, verify: DockerS3Verify, docker_manager: DockerManager): + self.__defaults_configurer = defaults_configurer + self.__base_objects_path = base_objects_path + self.__health_check = health_check + self.__setup = setup + self.__verify = verify + self.__docker_manager = docker_manager + self.__network = None + self.__container_id = None + + def get_network(self) -> Network: + if not self.__network: + raise exceptions.MockException('Network unavailable. Mock is still starting.') + return self.__network + + def initialize(self): + s3_client = Minio(self.get_network().external_url(), + access_key=self.__USERNAME, + secret_key=self.__PASSWORD, + secure=False) + self.__setup.set_s3_client(s3_client) + self.__verify.set_s3_client(s3_client) + self.__setup.init(self.__base_objects_path, self.__defaults_configurer.get_config()) + + def start(self): + run_result = self.__docker_manager.run_image('minio/minio:RELEASE.2020-02-27T00-23-05Z server /data', + port=9000, + environment_vars=[('MINIO_ACCESS_KEY', self.__USERNAME), + ('MINIO_SECRET_KEY', self.__PASSWORD)]) + self.__container_id = run_result.container_id + self.__network = Network(internal_host=run_result.container_id, + internal_port=run_result.internal_port, + external_port=run_result.external_port, + ui_endpoint='/minio', + username=self.__USERNAME, + password=self.__PASSWORD) + + def stop(self): + if self.__container_id: + self.__docker_manager.stop_container(self.__container_id) + + def reset(self): + self.__setup.init(self.__base_objects_path, self.__defaults_configurer.get_config()) + + def services_available(self): + pass + + def is_healthy(self) -> bool: + self.__health_check.set_url(self.get_network().ui_url() + '/health/ready') + return self.__health_check.is_healthy() + + def setup(self) -> IS3Setup: + if not self.__setup: + raise exceptions.MockException('Setup unavailable. Mock is still starting.') + return self.__setup + + def verify(self) -> IS3Verify: + if not self.__verify: + raise exceptions.MockException('Verify unavailable. Mock is still starting.') + return self.__verify + + def get_base_path(self) -> str: + return self.__base_objects_path diff --git a/touchstone/lib/mocks/s3/s3_setup.py b/touchstone/lib/mocks/networked_runnables/s3/docker/docker_s3_setup.py similarity index 80% rename from touchstone/lib/mocks/s3/s3_setup.py rename to touchstone/lib/mocks/networked_runnables/s3/docker/docker_s3_setup.py index b0c5144..714ff2b 100644 --- a/touchstone/lib/mocks/s3/s3_setup.py +++ b/touchstone/lib/mocks/networked_runnables/s3/docker/docker_s3_setup.py @@ -3,9 +3,14 @@ from minio import Minio +from touchstone.lib.mocks.networked_runnables.s3.i_s3_behavior import IS3Setup -class S3Setup(object): - def __init__(self, s3_client: Minio): + +class DockerS3Setup(IS3Setup): + def __init__(self): + self.__s3_client = None + + def set_s3_client(self, s3_client: Minio): self.__s3_client = s3_client def init(self, path: str, defaults: dict): @@ -15,10 +20,10 @@ def init(self, path: str, defaults: dict): self.__s3_client.remove_object(bucket.name, m_object.object_name) self.__s3_client.remove_bucket(bucket.name) - for bucket in defaults['buckets']: + for bucket in defaults.get('buckets', []): bucket_name = bucket['name'] self.__s3_client.make_bucket(bucket_name) - for m_object in bucket['objects']: + for m_object in bucket.get('objects', []): object_path = os.path.join(path, m_object['path']) file_stat = os.stat(object_path) with open(object_path, 'rb') as data: diff --git a/touchstone/lib/mocks/s3/s3_verify.py b/touchstone/lib/mocks/networked_runnables/s3/docker/docker_s3_verify.py similarity index 78% rename from touchstone/lib/mocks/s3/s3_verify.py rename to touchstone/lib/mocks/networked_runnables/s3/docker/docker_s3_verify.py index 57dc753..16e9cb0 100644 --- a/touchstone/lib/mocks/s3/s3_verify.py +++ b/touchstone/lib/mocks/networked_runnables/s3/docker/docker_s3_verify.py @@ -1,8 +1,13 @@ from minio import Minio +from touchstone.lib.mocks.networked_runnables.s3.i_s3_behavior import IS3Verify -class S3Verify(object): - def __init__(self, s3_client: Minio): + +class DockerS3Verify(IS3Verify): + def __init__(self): + self.__s3_client = None + + def set_s3_client(self, s3_client: Minio): self.__s3_client = s3_client def bucket_exists(self, name: str) -> bool: diff --git a/touchstone/lib/mocks/networked_runnables/s3/i_s3_behavior.py b/touchstone/lib/mocks/networked_runnables/s3/i_s3_behavior.py new file mode 100644 index 0000000..aaf9974 --- /dev/null +++ b/touchstone/lib/mocks/networked_runnables/s3/i_s3_behavior.py @@ -0,0 +1,46 @@ +import abc + + +class IS3Setup(object): + @abc.abstractmethod + def create_bucket(self, name: str): + """Creates a new bucket with the given name.""" + pass + + @abc.abstractmethod + def put_object(self, bucket_name: str, object_name: str, data: bytes, + content_type: str = 'application/octet-stream'): + """Puts a the given bytes into an object and bucket.""" + pass + + +class IS3Verify(object): + @abc.abstractmethod + def bucket_exists(self, name: str) -> bool: + """Return True if the given bucket name exists.""" + pass + + @abc.abstractmethod + def object_exists(self, bucket_name: str, object_name: str) -> bool: + """Returns True if the given bucket and object name exists.""" + pass + + @abc.abstractmethod + def object_matches(self, bucket_name: str, object_name: str, expected: bytes) -> bool: + """Returns True if the given object in a bucket matches the expected bytes.""" + pass + + +class IS3Behavior(object): + @abc.abstractmethod + def setup(self) -> IS3Setup: + pass + + @abc.abstractmethod + def verify(self) -> IS3Verify: + pass + + @abc.abstractmethod + def get_base_path(self) -> str: + """Returns the "defaults" base directory path.""" + pass diff --git a/touchstone/lib/mocks/rabbitmq/rabbitmq.py b/touchstone/lib/mocks/rabbitmq/rabbitmq.py deleted file mode 100644 index 68063e0..0000000 --- a/touchstone/lib/mocks/rabbitmq/rabbitmq.py +++ /dev/null @@ -1,82 +0,0 @@ -import urllib.error -import urllib.request -from typing import Optional - -import pika - -from touchstone.lib.docker_manager import DockerManager -from touchstone.lib.mocks.mock import Mock -from touchstone.lib.mocks.mock_defaults import MockDefaults -from touchstone.lib.mocks.network import Network -from touchstone.lib.mocks.rabbitmq.rabbitmq_setup import RabbitmqSetup -from touchstone.lib.mocks.rabbitmq.rabbitmq_verify import RabbitmqVerify -from touchstone.lib.mocks.rabbitmq.rmq_context import RmqContext - - -class Rabbitmq(Mock): - __USERNAME = 'guest' - __PASSWORD = 'guest' - - def __init__(self, host: str, mock_defaults: MockDefaults, docker_manager: DockerManager): - super().__init__(host, mock_defaults) - self.setup: RabbitmqSetup = None - self.verify: RabbitmqVerify = None - self.__docker_manager = docker_manager - self.__container_id: Optional[str] = None - - @staticmethod - def name() -> str: - return 'rabbitmq' - - @staticmethod - def pretty_name() -> str: - return 'Rabbit MQ' - - def default_config(self) -> dict: - return { - 'autoCreate': True - } - - def run(self) -> Network: - run_result = self.__docker_manager.run_image('rabbitmq:3.7.22-management-alpine', port=5672, ui_port=15672) - self.__container_id = run_result.container_id - return Network(internal_host=run_result.container_id, - internal_port=run_result.internal_port, - external_port=run_result.external_port, - ui_port=run_result.ui_port, - username=self.__USERNAME, - password=self.__PASSWORD) - - def is_healthy(self) -> bool: - try: - response = urllib.request.urlopen(f'{self.network.ui_url()}').read() - return response is not None - except (urllib.error.URLError, ConnectionResetError): - return False - - def initialize(self): - connection_params = pika.ConnectionParameters( - host=self.network.external_host, - port=self.network.external_port, - credentials=pika.PlainCredentials(self.__USERNAME, self.__PASSWORD), - heartbeat=0 - ) - connection = pika.BlockingConnection(connection_params) - rmq_context = RmqContext() - channel = connection.channel() - self.setup = RabbitmqSetup(channel, connection_params, rmq_context) - self.verify = RabbitmqVerify(channel, rmq_context) - if self.config['autoCreate']: - self.setup.create_all(self._mock_defaults.get(self.name())) - - def services_available(self): - if not self.config['autoCreate']: - self.setup.create_shadow_queues(self._mock_defaults.get(self.name())) - - def reset(self): - self.setup.purge_queues() - - def stop(self): - if self.__container_id: - self.setup.stop_listening() - self.__docker_manager.stop_container(self.__container_id) diff --git a/touchstone/lib/mocks/run_context.py b/touchstone/lib/mocks/run_context.py deleted file mode 100644 index 4b89e2f..0000000 --- a/touchstone/lib/mocks/run_context.py +++ /dev/null @@ -1,7 +0,0 @@ -from touchstone.lib.mocks.network import Network - - -class RunContext(object): - def __init__(self, name: str, network: Network): - self.name = name - self.network = network diff --git a/touchstone/lib/mocks/runnables/__init__.py b/touchstone/lib/mocks/runnables/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/runnables/filesystem/__init__.py b/touchstone/lib/mocks/runnables/filesystem/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/runnables/filesystem/i_filesystem_behavior.py b/touchstone/lib/mocks/runnables/filesystem/i_filesystem_behavior.py new file mode 100644 index 0000000..00628df --- /dev/null +++ b/touchstone/lib/mocks/runnables/filesystem/i_filesystem_behavior.py @@ -0,0 +1,33 @@ +import abc + + +class IFilesystemSetup(object): + pass + + +class IFilesystemVerify(object): + @abc.abstractmethod + def file_exists(self, path: str) -> bool: + """Returns True if the given path to a file exists. Assumes "defaults" as a base directory.""" + pass + + @abc.abstractmethod + def file_matches(self, path: str, expected: bytes) -> bool: + """Returns True if the given path to a file matches the expected bytes. + Assumes "defaults" as a base directory.""" + pass + + +class IFilesystemBehavior(object): + @abc.abstractmethod + def setup(self) -> IFilesystemSetup: + pass + + @abc.abstractmethod + def verify(self) -> IFilesystemVerify: + pass + + @abc.abstractmethod + def get_base_path(self) -> str: + """Returns the "defaults" base directory path.""" + pass diff --git a/touchstone/lib/mocks/runnables/filesystem/local/__init__.py b/touchstone/lib/mocks/runnables/filesystem/local/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/touchstone/lib/mocks/runnables/filesystem/local/local_filesystem_runnable.py b/touchstone/lib/mocks/runnables/filesystem/local/local_filesystem_runnable.py new file mode 100644 index 0000000..cb06623 --- /dev/null +++ b/touchstone/lib/mocks/runnables/filesystem/local/local_filesystem_runnable.py @@ -0,0 +1,41 @@ +from touchstone.lib import exceptions +from touchstone.lib.mocks.configurers.i_configurable import IConfigurable +from touchstone.lib.mocks.runnables.filesystem.i_filesystem_behavior import IFilesystemBehavior, IFilesystemVerify, \ + IFilesystemSetup +from touchstone.lib.mocks.runnables.filesystem.local.local_filesystem_setup import LocalFilesystemSetup +from touchstone.lib.mocks.runnables.filesystem.local.local_filesystem_verify import LocalFilesystemVerify +from touchstone.lib.mocks.runnables.i_runnable import IRunnable + + +class LocalFilesystemRunnable(IRunnable, IFilesystemBehavior): + def __init__(self, defaults_configurer: IConfigurable, base_files_path: str, setup: LocalFilesystemSetup, + verify: LocalFilesystemVerify): + self.__defaults_configurer = defaults_configurer + self.__base_files_path = base_files_path + self.__setup = setup + self.__verify = verify + + def start(self): + self.__setup.init(self.__defaults_configurer.get_config()) + + def stop(self): + self.__setup.delete_defaults(self.__defaults_configurer.get_config()) + + def reset(self): + self.__setup.init(self.__defaults_configurer.get_config()) + + def services_available(self): + pass + + def setup(self) -> IFilesystemSetup: + if not self.__setup: + raise exceptions.MockException('Setup unavailable. Mock is still starting.') + return self.__setup + + def verify(self) -> IFilesystemVerify: + if not self.__verify: + raise exceptions.MockException('Verify unavailable. Mock is still starting.') + return self.__verify + + def get_base_path(self) -> str: + return self.__base_files_path diff --git a/touchstone/lib/mocks/runnables/filesystem/local/local_filesystem_setup.py b/touchstone/lib/mocks/runnables/filesystem/local/local_filesystem_setup.py new file mode 100644 index 0000000..d72fb56 --- /dev/null +++ b/touchstone/lib/mocks/runnables/filesystem/local/local_filesystem_setup.py @@ -0,0 +1,23 @@ +import os +import shutil + +from touchstone.lib.mocks.runnables.filesystem.i_filesystem_behavior import IFilesystemSetup + + +class LocalFilesystemSetup(IFilesystemSetup): + def __init__(self, base_files_path: str): + self.__base_files_path = base_files_path + + def init(self, defaults: dict): + self.delete_defaults(defaults) + for directory in defaults.get('directories', []): + directory_path = os.path.join(self.__base_files_path, directory['path']) + os.mkdir(directory_path) + for file in directory.get('files', []): + file_path = os.path.join(self.__base_files_path, file) + shutil.copyfile(file_path, os.path.join(directory_path, os.path.basename(file_path))) + + def delete_defaults(self, defaults: dict): + for directory in defaults.get('directories', []): + path = os.path.join(self.__base_files_path, directory['path']) + shutil.rmtree(path, ignore_errors=True) diff --git a/touchstone/lib/mocks/runnables/filesystem/local/local_filesystem_verify.py b/touchstone/lib/mocks/runnables/filesystem/local/local_filesystem_verify.py new file mode 100644 index 0000000..5ab2cf9 --- /dev/null +++ b/touchstone/lib/mocks/runnables/filesystem/local/local_filesystem_verify.py @@ -0,0 +1,23 @@ +import os + +from touchstone.lib.mocks.runnables.filesystem.i_filesystem_behavior import IFilesystemVerify + + +class LocalFilesystemVerify(IFilesystemVerify): + def __init__(self, base_files_path: str): + self.__base_files_path = base_files_path + + def file_exists(self, path: str) -> bool: + path = os.path.join(self.__base_files_path, path) + exists = os.path.isfile(path) + if not exists: + print(f'Path: "{path}" does not exist.') + return exists + + def file_matches(self, path: str, expected: bytes) -> bool: + path = os.path.join(self.__base_files_path, path) + with open(path, 'rb') as data: + data_matches = bytes(data.read()) == expected + if not data_matches: + print('Expected data does not match data in object.') + return data_matches diff --git a/touchstone/lib/mocks/runnables/i_runnable.py b/touchstone/lib/mocks/runnables/i_runnable.py new file mode 100644 index 0000000..9f601f3 --- /dev/null +++ b/touchstone/lib/mocks/runnables/i_runnable.py @@ -0,0 +1,19 @@ +import abc + + +class IRunnable(object): + @abc.abstractmethod + def start(self): + pass + + @abc.abstractmethod + def stop(self): + pass + + @abc.abstractmethod + def reset(self): + pass + + @abc.abstractmethod + def services_available(self): + pass diff --git a/touchstone/lib/mocks/s3/s3.py b/touchstone/lib/mocks/s3/s3.py deleted file mode 100644 index 84ccf0a..0000000 --- a/touchstone/lib/mocks/s3/s3.py +++ /dev/null @@ -1,71 +0,0 @@ -import urllib.error -import urllib.request -from typing import Optional - -from minio import Minio - -from touchstone.lib.docker_manager import DockerManager -from touchstone.lib.mocks.mock import Mock -from touchstone.lib.mocks.mock_defaults import MockDefaults -from touchstone.lib.mocks.network import Network -from touchstone.lib.mocks.s3.s3_setup import S3Setup -from touchstone.lib.mocks.s3.s3_verify import S3Verify - - -class S3(Mock): - __USERNAME = 'admin123' - __PASSWORD = 'admin123' - - def __init__(self, host: str, mock_defaults: MockDefaults, docker_manager: DockerManager): - super().__init__(host, mock_defaults) - self.setup: S3Setup = None - self.verify: S3Verify = None - self.__docker_manager = docker_manager - self.__container_id: Optional[str] = None - - @staticmethod - def name() -> str: - return 's3' - - @staticmethod - def pretty_name() -> str: - return 'S3' - - def run(self) -> Network: - run_result = self.__docker_manager.run_image('minio/minio:RELEASE.2020-02-27T00-23-05Z server /data', - port=9000, - environment_vars=[('MINIO_ACCESS_KEY', self.__USERNAME), - ('MINIO_SECRET_KEY', self.__PASSWORD)]) - self.__container_id = run_result.container_id - return Network(internal_host=run_result.container_id, - internal_port=run_result.internal_port, - external_port=run_result.external_port, - username=self.__USERNAME, - password=self.__PASSWORD) - - def is_healthy(self) -> bool: - try: - response = urllib.request.urlopen(f'http://{self.network.external_url()}/minio/health/ready').read() - return response is not None - except (urllib.error.URLError, ConnectionResetError): - return False - - def initialize(self): - s3_client = Minio(self.network.external_url(), - access_key=self.__USERNAME, - secret_key=self.__PASSWORD, - secure=False) - self.setup = S3Setup(s3_client) - self.verify = S3Verify(s3_client) - path = self._mock_defaults.path - defaults = self._mock_defaults.get(self.name()) - self.setup.init(path, defaults) - - def reset(self): - path = self._mock_defaults.path - defaults = self._mock_defaults.get(self.name()) - self.setup.init(path, defaults) - - def stop(self): - if self.__container_id: - self.__docker_manager.stop_container(self.__container_id) diff --git a/touchstone/lib/service.py b/touchstone/lib/service.py index e1383e2..6954a5c 100644 --- a/touchstone/lib/service.py +++ b/touchstone/lib/service.py @@ -7,7 +7,6 @@ from touchstone.lib import exceptions from touchstone.lib.docker_manager import DockerManager -from touchstone.lib.mocks.run_context import RunContext from touchstone.lib.tests import Tests @@ -27,12 +26,11 @@ def __init__(self, root: str, name: str, tests: Tests, dockerfile: str, host: st self.__docker_manager = docker_manager self.__container_id: Optional[str] = None - def start(self, run_contexts: List[RunContext]): + def start(self, environment_vars: List[Tuple[str, str]] = []): if self.__dockerfile is not None: self.__log('Building and running Dockerfile...') dockerfile_path = os.path.abspath(os.path.join(self.__root, self.__dockerfile)) tag = self.__docker_manager.build_dockerfile(dockerfile_path) - environment_vars = self.__environment_vars_from_run_contexts(run_contexts) run_result = self.__docker_manager.run_image(tag, self.__port, environment_vars=environment_vars) self.__container_id = run_result.container_id self.__port = run_result.external_port @@ -77,14 +75,3 @@ def is_running(self) -> bool: def __log(self, message: str): print(f'{self.name} :: {message}') - - def __environment_vars_from_run_contexts(self, run_contexts: List[RunContext]) -> List[Tuple[str, str]]: - envs = [] - for run_context in run_contexts: - name = run_context.name.upper() - envs.append((f'TS_{name}_HOST', run_context.network.internal_host)) - envs.append((f'TS_{name}_PORT', run_context.network.internal_port)) - envs.append((f'TS_{name}_URL', run_context.network.internal_url())) - envs.append((f'TS_{name}_USERNAME', run_context.network.username)) - envs.append((f'TS_{name}_PASSWORD', run_context.network.password)) - return envs diff --git a/touchstone/lib/services.py b/touchstone/lib/services.py index d95c57d..41af388 100644 --- a/touchstone/lib/services.py +++ b/touchstone/lib/services.py @@ -1,6 +1,5 @@ -from typing import List +from typing import List, Tuple -from touchstone.lib.mocks.run_context import RunContext from touchstone.lib.service import Service @@ -9,13 +8,13 @@ def __init__(self, services: List[Service]): self.__services = services self.__services_running = False - def start(self, run_contexts: List[RunContext]): + def start(self, environment_vars: List[Tuple[str, str]] = []): if self.__services_running: print('Services have already been started. They cannot be started again.') else: print(f'Starting services {[_.name for _ in self.__services]}...') for service in self.__services: - service.start(run_contexts) + service.start(environment_vars) self.__services_running = True for service in self.__services: service.wait_for_availability() diff --git a/touchstone/run.py b/touchstone/run.py index 76b01ec..fd1a866 100644 --- a/touchstone/run.py +++ b/touchstone/run.py @@ -5,24 +5,22 @@ def execute(): - common.prep_run() + if not common.sanity_check_passes(): + exit(1) bootstrap = Bootstrap() print(figlet_format('Touchstone', font='larry3d')) try: - mock_run_contexts = bootstrap.mocks.start() - bootstrap.services.start(mock_run_contexts) + bootstrap.mocks.start() + bootstrap.services.start(bootstrap.mocks.environment_vars()) bootstrap.mocks.services_became_available() tests_did_pass = bootstrap.services.run_all_tests() if tests_did_pass: print('All Touchstone tests passed successfully!') else: print('One or more Touchstone tests failed.') - - bootstrap.services.stop() - bootstrap.mocks.stop() - bootstrap.runner.exit_touchstone(tests_did_pass) + bootstrap.exit(tests_did_pass) except (Exception, KeyboardInterrupt) as e: print('\nTouchstone was interrupted. Cleaning up...') - bootstrap.runner.cleanup() + bootstrap.cleanup() raise e diff --git a/touchstone/runner.py b/touchstone/runner.py deleted file mode 100644 index f06f00e..0000000 --- a/touchstone/runner.py +++ /dev/null @@ -1,22 +0,0 @@ -import sys - -from touchstone.lib.configs.touchstone_config import TouchstoneConfig -from touchstone.lib.docker_manager import DockerManager - - -class Runner(object): - def __init__(self, touchstone_config: TouchstoneConfig, docker_manager: DockerManager): - self.__touchstone_config = touchstone_config - self.__docker_manager = docker_manager - - def exit_touchstone(self, is_successful: bool): - print('Shutting down...') - if is_successful: - code = 0 - else: - code = 1 - self.cleanup() - sys.exit(code) - - def cleanup(self): - self.__docker_manager.cleanup() diff --git a/touchstone/temp.py b/touchstone/temp.py deleted file mode 100644 index 8faf30a..0000000 --- a/touchstone/temp.py +++ /dev/null @@ -1,44 +0,0 @@ -import glob -import importlib.util -import os -import sys -import time - -package_dir = '/Users/s560879/Documents/GitRepos/tindr-service/touchstone/tests' - - -def load_package(): - pkg_spec = importlib.util.spec_from_file_location(os.path.basename(package_dir), package_dir + '/__init__.py') - pkg_module = importlib.util.module_from_spec(pkg_spec) - sys.modules[pkg_spec.name] = pkg_module - pkg_spec.loader.exec_module(pkg_module) - - -def load_test(): - spec = importlib.util.spec_from_file_location(package_dir + '/test_data_emit.py', - package_dir + '/test_data_emit.py') - module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module - spec.loader.exec_module(module) - - -def reload_support_modules(): - package_len = len(package_dir) - len(os.path.basename(package_dir)) - files = glob.glob(package_dir + '/**/*.py', recursive=True) - for file in files: - file_name = os.path.basename(file) - if 'test_' not in file_name and '__init__' not in file_name: - module = file[package_len:].replace('/', '.')[:-3] - if module in sys.modules: - importlib.reload(sys.modules[module]) - - -load_package() -reload_support_modules() -load_test() - -time.sleep(5) - -load_package() -reload_support_modules() -load_test()