Refactor test utils to help testing in other aso charms

This commit is contained in:
Liam Young 2021-10-12 16:21:24 +01:00
parent 0547709b4b
commit 8d556b6a7b
3 changed files with 438 additions and 363 deletions

View File

@ -0,0 +1,208 @@
#!/usr/bin/env python3
# Copyright 2020 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import io
import json
from mock import patch
import unittest
import sys
sys.path.append('lib') # noqa
sys.path.append('src') # noqa
from ops import framework, model
from ops.testing import Harness, _TestingModelBackend, _TestingPebbleClient
class CharmTestCase(unittest.TestCase):
def setUp(self, obj, patches):
super().setUp()
self.patches = patches
self.obj = obj
self.patch_all()
def patch(self, method):
_m = patch.object(self.obj, method)
mock = _m.start()
self.addCleanup(_m.stop)
return mock
def patch_obj(self, obj, method):
_m = patch.object(obj, method)
mock = _m.start()
self.addCleanup(_m.stop)
return mock
def patch_all(self):
for method in self.patches:
setattr(self, method, self.patch(method))
def add_base_amqp_relation(harness):
rel_id = harness.add_relation('amqp', 'rabbitmq')
harness.add_relation_unit(
rel_id,
'rabbitmq/0')
harness.add_relation_unit(
rel_id,
'rabbitmq/0')
harness.update_relation_data(
rel_id,
'rabbitmq/0',
{'ingress-address': '10.0.0.13'})
return rel_id
def add_amqp_relation_credentials(harness, rel_id):
harness.update_relation_data(
rel_id,
'rabbitmq',
{
'hostname': 'rabbithost1.local',
'password': 'rabbit.pass'})
def add_base_identity_service_relation(harness):
rel_id = harness.add_relation('identity-service', 'keystone')
harness.add_relation_unit(
rel_id,
'keystone/0')
harness.add_relation_unit(
rel_id,
'keystone/0')
harness.update_relation_data(
rel_id,
'keystone/0',
{'ingress-address': '10.0.0.33'})
return rel_id
def add_identity_service_relation_response(harness, rel_id):
harness.update_relation_data(
rel_id,
'keystone',
{
'admin-domain-id': 'admindomid1',
'admin-project-id': 'adminprojid1',
'admin-user-id': 'adminuserid1',
'api-version': '3',
'auth-host': 'keystone.local',
'auth-port': '12345',
'auth-protocol': 'http',
'internal-host': 'keystone.internal',
'internal-port': '5000',
'internal-protocol': 'http',
'service-domain': 'servicedom',
'service-domain_id': 'svcdomid1',
'service-host': 'keystone.service',
'service-password': 'svcpass1',
'service-port': '5000',
'service-protocol': 'http',
'service-project': 'svcproj1',
'service-project-id': 'svcprojid1',
'service-username': 'svcuser1'})
def add_base_db_relation(harness):
rel_id = harness.add_relation('my-service-db', 'mysql')
harness.add_relation_unit(
rel_id,
'mysql/0')
harness.add_relation_unit(
rel_id,
'mysql/0')
harness.update_relation_data(
rel_id,
'mysql/0',
{'ingress-address': '10.0.0.3'})
return rel_id
def add_db_relation_credentials(harness, rel_id):
harness.update_relation_data(
rel_id,
'mysql',
{
'databases': json.dumps(['db1']),
'data': json.dumps({
'credentials': {
'username': 'foo',
'password': 'hardpassword',
'address': '10.0.0.10'}})})
def add_api_relations(harness):
add_db_relation_credentials(
harness,
add_base_db_relation(harness))
add_amqp_relation_credentials(
harness,
add_base_amqp_relation(harness))
add_identity_service_relation_response(
harness,
add_base_identity_service_relation(harness))
def get_harness(charm_class, charm_meta, container_calls):
class _OSTestingPebbleClient(_TestingPebbleClient):
def push(
self, path, source, *,
encoding='utf-8', make_dirs=False, permissions=None,
user_id=None, user=None, group_id=None, group=None):
container_calls['push'][path] = {
'source': source,
'permissions': permissions,
'user': user,
'group': group}
def pull(self, path, *, encoding='utf-8'):
container_calls['pull'].append(path)
reader = io.StringIO("0")
return reader
def remove_path(self, path, *, recursive=False):
container_calls['remove_path'].append(path)
class _OSTestingModelBackend(_TestingModelBackend):
def get_pebble(self, socket_path: str):
client = self._pebble_clients.get(socket_path, None)
if client is None:
client = _OSTestingPebbleClient(self)
self._pebble_clients[socket_path] = client
return client
harness = Harness(
charm_class,
meta=charm_meta,
)
harness._backend = _OSTestingModelBackend(
harness._unit_name, harness._meta)
harness._model = model.Model(
harness._meta,
harness._backend)
harness._framework = framework.Framework(
":memory:",
harness._charm_dir,
harness._meta,
harness._model)
# END Workaround
return harness

199
unit_tests/test_charms.py Normal file
View File

@ -0,0 +1,199 @@
#!/usr/bin/env python3
# Copyright 2020 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import tempfile
import sys
sys.path.append('lib') # noqa
sys.path.append('src') # noqa
import advanced_sunbeam_openstack.charm as sunbeam_charm
CHARM_CONFIG = {
'region': 'RegionOne',
'debug': 'true'}
CHARM_METADATA = '''
name: my-service
version: 3
bases:
- name: ubuntu
channel: 20.04/stable
tags:
- openstack
- identity
- misc
subordinate: false
containers:
my-service:
resource: mysvc-image
mounts:
- storage: db
location: /var/lib/mysvc
storage:
logs:
type: filesystem
db:
type: filesystem
resources:
mysvc-image:
type: oci-image
'''
API_CHARM_METADATA = '''
name: my-service
version: 3
bases:
- name: ubuntu
channel: 20.04/stable
tags:
- openstack
- identity
- misc
subordinate: false
requires:
my-service-db:
interface: mysql_datastore
limit: 1
ingress:
interface: ingress
amqp:
interface: rabbitmq
identity-service:
interface: keystone
peers:
peers:
interface: mysvc-peer
containers:
my-service:
resource: mysvc-image
mounts:
- storage: db
location: /var/lib/mysvc
storage:
logs:
type: filesystem
db:
type: filesystem
resources:
mysvc-image:
type: oci-image
'''
class MyCharm(sunbeam_charm.OSBaseOperatorCharm):
openstack_release = 'diablo'
service_name = 'my-service'
def __init__(self, framework):
self.seen_events = []
self.render_calls = []
self._template_dir = self._setup_templates()
super().__init__(framework)
def _log_event(self, event):
self.seen_events.append(type(event).__name__)
def _on_service_pebble_ready(self, event):
super()._on_service_pebble_ready(event)
self._log_event(event)
def _on_config_changed(self, event):
self._log_event(event)
def configure_charm(self, event):
super().configure_charm(event)
self._log_event(event)
@property
def public_ingress_port(self):
return 789
def _setup_templates(self):
tmpdir = tempfile.mkdtemp()
_template_dir = f'{tmpdir}/templates'
os.mkdir(_template_dir)
with open(f'{_template_dir}/my-service.conf.j2', 'w') as f:
f.write("")
return _template_dir
@property
def template_dir(self):
return self._template_dir
TEMPLATE_CONTENTS = """
{{ wsgi_config.wsgi_admin_script }}
{{ my_service_db.database_password }}
{{ options.debug }}
{{ amqp.transport_url }}
{{ amqp.hostname }}
{{ identity_service.service_password }}
"""
class MyAPICharm(sunbeam_charm.OSBaseOperatorAPICharm):
openstack_release = 'diablo'
service_name = 'my-service'
wsgi_admin_script = '/bin/wsgi_admin'
wsgi_public_script = '/bin/wsgi_public'
def __init__(self, framework):
self.seen_events = []
self.render_calls = []
self._template_dir = self._setup_templates()
super().__init__(framework)
def _setup_templates(self):
tmpdir = tempfile.mkdtemp()
_template_dir = f'{tmpdir}/templates'
os.mkdir(_template_dir)
with open(f'{_template_dir}/my-service.conf.j2', 'w') as f:
f.write(TEMPLATE_CONTENTS)
with open(f'{_template_dir}/wsgi-my-service.conf.j2', 'w') as f:
f.write(TEMPLATE_CONTENTS)
return _template_dir
def _log_event(self, event):
self.seen_events.append(type(event).__name__)
def _on_service_pebble_ready(self, event):
super()._on_service_pebble_ready(event)
self._log_event(event)
def _on_config_changed(self, event):
self._log_event(event)
@property
def default_public_ingress_port(self):
return 789
@property
def template_dir(self):
return self._template_dir

View File

@ -15,171 +15,34 @@
# limitations under the License.
import io
import json
import os
import tempfile
from mock import ANY, patch
import unittest
from mock import patch
import sys
sys.path.append('lib') # noqa
sys.path.append('src') # noqa
from ops import framework, model
from ops.testing import Harness, _TestingModelBackend, _TestingPebbleClient
import advanced_sunbeam_openstack.charm as sunbeam_charm
CHARM_CONFIG = {
'region': 'RegionOne',
'debug': 'true'}
CHARM_METADATA = '''
name: my-service
version: 3
bases:
- name: ubuntu
channel: 20.04/stable
tags:
- openstack
- identity
- misc
subordinate: false
containers:
my-service:
resource: mysvc-image
mounts:
- storage: db
location: /var/lib/mysvc
storage:
logs:
type: filesystem
db:
type: filesystem
resources:
mysvc-image:
type: oci-image
'''
API_CHARM_METADATA = '''
name: my-service
version: 3
bases:
- name: ubuntu
channel: 20.04/stable
tags:
- openstack
- identity
- misc
subordinate: false
requires:
my-service-db:
interface: mysql_datastore
limit: 1
ingress:
interface: ingress
amqp:
interface: rabbitmq
identity-service:
interface: keystone
peers:
peers:
interface: mysvc-peer
containers:
my-service:
resource: mysvc-image
mounts:
- storage: db
location: /var/lib/mysvc
storage:
logs:
type: filesystem
db:
type: filesystem
resources:
mysvc-image:
type: oci-image
'''
import advanced_sunbeam_openstack.test_utils as test_utils
from . import test_charms
class CharmTestCase(unittest.TestCase):
def setUp(self, obj, patches):
super().setUp()
self.patches = patches
self.obj = obj
self.patch_all()
def patch(self, method):
_m = patch.object(self.obj, method)
mock = _m.start()
self.addCleanup(_m.stop)
return mock
def patch_obj(self, obj, method):
_m = patch.object(obj, method)
mock = _m.start()
self.addCleanup(_m.stop)
return mock
def patch_all(self):
for method in self.patches:
setattr(self, method, self.patch(method))
class MyCharm(sunbeam_charm.OSBaseOperatorCharm):
openstack_release = 'diablo'
service_name = 'my-service'
def __init__(self, framework):
super().__init__(framework)
self.seen_events = []
self.render_calls = []
def _log_event(self, event):
self.seen_events.append(type(event).__name__)
def _on_service_pebble_ready(self, event):
super()._on_service_pebble_ready(event)
self._log_event(event)
def _on_config_changed(self, event):
self._log_event(event)
def configure_charm(self, event):
super().configure_charm(event)
self._log_event(event)
@property
def public_ingress_port(self):
return 789
class TestOSBaseOperatorCharm(CharmTestCase):
class TestOSBaseOperatorCharm(test_utils.CharmTestCase):
PATCHES = [
]
def setUp(self):
self.container_calls = {
'push': {},
'pull': [],
'remove_path': []}
super().setUp(sunbeam_charm, self.PATCHES)
self.harness = Harness(
MyCharm,
meta=CHARM_METADATA
)
self.harness.update_config(CHARM_CONFIG)
self.harness = test_utils.get_harness(
test_charms.MyCharm,
test_charms.CHARM_METADATA,
self.container_calls)
self.harness.update_config(test_charms.CHARM_CONFIG)
self.harness.begin()
self.addCleanup(self.harness.cleanup)
@ -188,21 +51,16 @@ class TestOSBaseOperatorCharm(CharmTestCase):
# Emit the PebbleReadyEvent
self.harness.charm.on.my_service_pebble_ready.emit(container)
@patch('advanced_sunbeam_openstack.templating.sidecar_config_render')
def test_pebble_ready_handler(self, _renderer):
def test_pebble_ready_handler(self):
self.assertEqual(self.harness.charm.seen_events, [])
self.set_pebble_ready()
self.assertEqual(self.harness.charm.seen_events, ['PebbleReadyEvent'])
@patch('advanced_sunbeam_openstack.templating.sidecar_config_render')
def test_write_config(self, _renderer):
def test_write_config(self):
self.set_pebble_ready()
_renderer.assert_called_once_with(
[self.harness.model.unit.get_container("my-service")],
[],
'src/templates',
'diablo',
ANY)
self.assertEqual(
self.container_calls['push'],
{})
def test_handler_prefix(self):
self.assertEqual(
@ -214,229 +72,39 @@ class TestOSBaseOperatorCharm(CharmTestCase):
self.harness.charm.container_names,
['my-service'])
def test_template_dir(self):
self.assertEqual(
self.harness.charm.template_dir,
'src/templates')
def test_relation_handlers_ready(self):
self.assertTrue(
self.harness.charm.relation_handlers_ready())
TEMPLATE_CONTENTS = """
{{ wsgi_config.wsgi_admin_script }}
{{ my_service_db.database_password }}
{{ options.debug }}
{{ amqp.transport_url }}
{{ amqp.hostname }}
{{ identity_service.service_password }}
"""
class MyAPICharm(sunbeam_charm.OSBaseOperatorAPICharm):
openstack_release = 'diablo'
service_name = 'my-service'
wsgi_admin_script = '/bin/wsgi_admin'
wsgi_public_script = '/bin/wsgi_public'
def __init__(self, framework):
self.seen_events = []
self.render_calls = []
self._template_dir = self._setup_templates()
super().__init__(framework)
def _setup_templates(self):
tmpdir = tempfile.mkdtemp()
_template_dir = f'{tmpdir}/templates'
os.mkdir(_template_dir)
with open(f'{_template_dir}/my-service.conf.j2', 'w') as f:
f.write(TEMPLATE_CONTENTS)
with open(f'{_template_dir}/wsgi-my-service.conf.j2', 'w') as f:
f.write(TEMPLATE_CONTENTS)
return _template_dir
def _log_event(self, event):
self.seen_events.append(type(event).__name__)
def _on_service_pebble_ready(self, event):
super()._on_service_pebble_ready(event)
self._log_event(event)
def _on_config_changed(self, event):
self._log_event(event)
@property
def default_public_ingress_port(self):
return 789
@property
def template_dir(self):
return self._template_dir
class TestOSBaseOperatorAPICharm(CharmTestCase):
class TestOSBaseOperatorAPICharm(test_utils.CharmTestCase):
PATCHES = [
]
def setUp(self):
container_calls = {
self.container_calls = {
'push': {},
'pull': [],
'remove_path': []}
super().setUp(sunbeam_charm, self.PATCHES)
class _OSTestingPebbleClient(_TestingPebbleClient):
def push(
self, path, source, *,
encoding='utf-8', make_dirs=False, permissions=None,
user_id=None, user=None, group_id=None, group=None):
container_calls['push'][path] = {
'source': source,
'permissions': permissions,
'user': user,
'group': group}
def pull(self, path, *, encoding='utf-8'):
container_calls['pull'].append(path)
reader = io.StringIO("0")
return reader
def remove_path(self, path, *, recursive=False):
container_calls['remove_path'].append(path)
class _OSTestingModelBackend(_TestingModelBackend):
def get_pebble(self, socket_path: str):
client = self._pebble_clients.get(socket_path, None)
if client is None:
client = _OSTestingPebbleClient(self)
self._pebble_clients[socket_path] = client
return client
self.container_calls = container_calls
# self.sunbeam_cprocess.ContainerProcessError = Exception
self.harness = Harness(
MyAPICharm,
meta=API_CHARM_METADATA
)
self.harness._backend = _OSTestingModelBackend(
self.harness._unit_name, self.harness._meta)
self.harness._model = model.Model(
self.harness._meta,
self.harness._backend)
self.harness._framework = framework.Framework(
":memory:",
self.harness._charm_dir,
self.harness._meta,
self.harness._model)
# END Workaround
self.harness = test_utils.get_harness(
test_charms.MyAPICharm,
test_charms.API_CHARM_METADATA,
self.container_calls)
self.addCleanup(self.harness.cleanup)
self.harness.update_config(CHARM_CONFIG)
self.harness.update_config(test_charms.CHARM_CONFIG)
self.harness.begin()
def add_base_amqp_relation(self):
rel_id = self.harness.add_relation('amqp', 'rabbitmq')
self.harness.add_relation_unit(
rel_id,
'rabbitmq/0')
self.harness.add_relation_unit(
rel_id,
'rabbitmq/0')
self.harness.update_relation_data(
rel_id,
'rabbitmq/0',
{'ingress-address': '10.0.0.13'})
return rel_id
def add_amqp_relation_credentials(self, rel_id):
self.harness.update_relation_data(
rel_id,
'rabbitmq',
{
'hostname': 'rabbithost1.local',
'password': 'rabbit.pass'})
def add_base_identity_service_relation(self):
rel_id = self.harness.add_relation('identity-service', 'keystone')
self.harness.add_relation_unit(
rel_id,
'keystone/0')
self.harness.add_relation_unit(
rel_id,
'keystone/0')
self.harness.update_relation_data(
rel_id,
'keystone/0',
{'ingress-address': '10.0.0.33'})
return rel_id
def add_identity_service_relation_response(self, rel_id):
self.harness.update_relation_data(
rel_id,
'keystone',
{
'admin-domain-id': 'admindomid1',
'admin-project-id': 'adminprojid1',
'admin-user-id': 'adminuserid1',
'api-version': '3',
'auth-host': 'keystone.local',
'auth-port': '12345',
'auth-protocol': 'http',
'internal-host': 'keystone.internal',
'internal-port': '5000',
'internal-protocol': 'http',
'service-domain': 'servicedom',
'service-domain_id': 'svcdomid1',
'service-host': 'keystone.service',
'service-password': 'svcpass1',
'service-port': '5000',
'service-protocol': 'http',
'service-project': 'svcproj1',
'service-project-id': 'svcprojid1',
'service-username': 'svcuser1'})
def add_base_db_relation(self):
rel_id = self.harness.add_relation('my-service-db', 'mysql')
self.harness.add_relation_unit(
rel_id,
'mysql/0')
self.harness.add_relation_unit(
rel_id,
'mysql/0')
self.harness.update_relation_data(
rel_id,
'mysql/0',
{'ingress-address': '10.0.0.3'})
return rel_id
def add_db_relation_credentials(self, rel_id):
self.harness.update_relation_data(
rel_id,
'mysql',
{
'databases': json.dumps(['db1']),
'data': json.dumps({
'credentials': {
'username': 'foo',
'password': 'hardpassword',
'address': '10.0.0.10'}})})
def set_pebble_ready(self):
self.harness.container_pebble_ready('my-service')
def test_write_config(self):
self.harness.set_leader()
self.set_pebble_ready()
db_rel_id = self.add_base_db_relation()
self.add_db_relation_credentials(db_rel_id)
amqp_rel_id = self.add_base_amqp_relation()
self.add_amqp_relation_credentials(amqp_rel_id)
id_rel_id = self.add_base_identity_service_relation()
self.add_identity_service_relation_response(id_rel_id)
test_utils.add_api_relations(self.harness)
expect_entries = [
'/bin/wsgi_admin',
'hardpassword',
@ -465,10 +133,10 @@ class TestOSBaseOperatorAPICharm(CharmTestCase):
def test__on_database_changed(self, _renderer):
self.harness.set_leader()
self.set_pebble_ready()
rel_id = self.add_base_db_relation()
self.add_db_relation_credentials(rel_id)
db_rel_id = test_utils.add_base_db_relation(self.harness)
test_utils.add_db_relation_credentials(self.harness, db_rel_id)
rel_data = self.harness.get_relation_data(
rel_id,
db_rel_id,
'my-service')
requested_db = json.loads(rel_data['databases'])[0]
self.assertRegex(requested_db, r'^db_.*my_service$')
@ -476,8 +144,8 @@ class TestOSBaseOperatorAPICharm(CharmTestCase):
def test_contexts(self):
self.harness.set_leader()
self.set_pebble_ready()
rel_id = self.add_base_db_relation()
self.add_db_relation_credentials(rel_id)
db_rel_id = test_utils.add_base_db_relation(self.harness)
test_utils.add_db_relation_credentials(self.harness, db_rel_id)
contexts = self.harness.charm.contexts()
self.assertEqual(
contexts.wsgi_config.wsgi_admin_script,