From 9250cd841457e5a68a068addd1b00bd68b9a0579 Mon Sep 17 00:00:00 2001 From: James Page Date: Mon, 9 Mar 2020 09:52:17 +0000 Subject: [PATCH] Add baseline unit tests --- .gitignore | 2 + .stestr.conf | 3 + tox.ini | 85 +++++++---- unit_tests/__init__.py | 47 +----- ...=> test_lib_charm_openstack_trilio_wlm.py} | 5 +- unit_tests/test_openvswitch_odl_handlers.py | 43 ------ unit_tests/test_trilio_wlm_handlers.py | 143 ++++++++++++++++++ 7 files changed, 209 insertions(+), 119 deletions(-) create mode 100644 .stestr.conf rename unit_tests/{test_lib_charm_openstack_openvswitch_odl.py => test_lib_charm_openstack_trilio_wlm.py} (93%) delete mode 100644 unit_tests/test_openvswitch_odl_handlers.py create mode 100644 unit_tests/test_trilio_wlm_handlers.py diff --git a/.gitignore b/.gitignore index 452c6f1..1e1d2dc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ build/ .tox/ func-results.json test-charm/ +**/__pycache__ +.stestr diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..5fcccac --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./unit_tests +top_dir=./ diff --git a/tox.ini b/tox.ini index ee7e92e..5b41c1d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,16 @@ # Source charm: ./tox.ini # This file is managed centrally by release-tools and should not be modified -# within individual charm repos. +# within individual charm repos. See the 'global' dir contents for available +# choices of tox.ini for OpenStack Charms: +# https://github.com/openstack-charmers/release-tools + [tox] skipsdist = True -envlist = pep8,py34,py35,py27 -skip_missing_interpreters = True - -[bundleenv] -setenv = VIRTUAL_ENV={envdir} - PYTHONHASHSEED=0 - TERM=linux - LAYER_PATH={toxinidir}/layers - INTERFACE_PATH={toxinidir}/interfaces - JUJU_REPOSITORY={toxinidir}/build -install_command = - pip install {opts} {packages} -deps = - -r{toxinidir}/requirements.txt +envlist = pep8,py3 +# NOTE: Avoid build/test env pollution by not enabling sitepackages. +sitepackages = False +# NOTE: Avoid false positives by not skipping missing interpreters. +skip_missing_interpreters = False [testenv] setenv = VIRTUAL_ENV={envdir} @@ -25,47 +19,74 @@ setenv = VIRTUAL_ENV={envdir} LAYER_PATH={toxinidir}/layers INTERFACE_PATH={toxinidir}/interfaces JUJU_REPOSITORY={toxinidir}/build -passenv = http_proxy https_proxy CHARM_TEMPLATE_LOCAL_BRANCH +passenv = http_proxy https_proxy INTERFACE_PATH LAYER_PATH JUJU_REPOSITORY install_command = pip install {opts} {packages} deps = -r{toxinidir}/requirements.txt [testenv:build] -basepython = python2.7 +basepython = python3 commands = charm-build --log-level DEBUG -o {toxinidir}/build src {posargs} -[testenv:py34] -basepython = python3.4 +[testenv:py3] +basepython = python3 deps = -r{toxinidir}/test-requirements.txt -commands = ostestr {posargs} +commands = stestr run --slowest {posargs} [testenv:py35] basepython = python3.5 deps = -r{toxinidir}/test-requirements.txt -commands = ostestr {posargs} +commands = stestr run --slowest {posargs} + +[testenv:py36] +basepython = python3.6 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run --slowest {posargs} + +[testenv:py37] +basepython = python3.7 +deps = -r{toxinidir}/test-requirements.txt +commands = stestr run --slowest {posargs} [testenv:pep8] -basepython = python2.7 +basepython = python3 deps = -r{toxinidir}/test-requirements.txt commands = flake8 {posargs} src unit_tests -[testenv:test_create] -# This tox target is used for template generation testing and can be removed -# from a generated source charm or built charm -basepython = python2.7 -deps = -r{toxinidir}/test-generate-requirements.txt +[testenv:cover] +# Technique based heavily upon +# https://github.com/openstack/nova/blob/master/tox.ini +basepython = python3 +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt setenv = - CHARM_TEMPLATE_ALT_REPO = {toxinidir} + {[testenv]setenv} + PYTHON=coverage run commands = - charm-create -t openstack-api -a congress test-charm - /bin/cp test-artifacts/congress.conf.sample {toxinidir}/test-charm/congress/src/templates/congress.conf -# charm-build --log-level DEBUG -o {toxinidir}/test-charm/congress/build {toxinidir}/test-charm/congress/src {posargs} + coverage erase + stestr run --slowest {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report + +[coverage:run] +branch = True +concurrency = multiprocessing +parallel = True +source = + . +omit = + .tox/* + */charmhelpers/* + unit_tests/* [testenv:venv] +basepython = python3 commands = {posargs} [flake8] # E402 ignore necessary for path append before sys module import in actions -ignore = E402 \ No newline at end of file +ignore = E402,W504 diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py index 1ca5eb6..64c49f5 100644 --- a/unit_tests/__init__.py +++ b/unit_tests/__init__.py @@ -1,46 +1,9 @@ -# Copyright 2016 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 sys -import mock -sys.path.append('src') -sys.path.append('src/lib') +sys.path.append("src") +sys.path.append("src/lib") # Mock out charmhelpers so that we can test without it. -# also stops sideeffects from occuring. -charmhelpers = mock.MagicMock() -apt_pkg = mock.MagicMock() -sys.modules['apt_pkg'] = apt_pkg -sys.modules['charmhelpers'] = charmhelpers -sys.modules['charmhelpers.core'] = charmhelpers.core -sys.modules['charmhelpers.core.decorators'] = charmhelpers.core.decorators -sys.modules['charmhelpers.core.hookenv'] = charmhelpers.core.hookenv -sys.modules['charmhelpers.core.host'] = charmhelpers.core.host -sys.modules['charmhelpers.core.unitdata'] = charmhelpers.core.unitdata -sys.modules['charmhelpers.core.templating'] = charmhelpers.core.templating -sys.modules['charmhelpers.contrib'] = charmhelpers.contrib -sys.modules['charmhelpers.contrib.openstack'] = charmhelpers.contrib.openstack -sys.modules['charmhelpers.contrib.openstack.utils'] = ( - charmhelpers.contrib.openstack.utils) -sys.modules['charmhelpers.contrib.openstack.templating'] = ( - charmhelpers.contrib.openstack.templating) -sys.modules['charmhelpers.contrib.network'] = charmhelpers.contrib.network -sys.modules['charmhelpers.contrib.network.ip'] = ( - charmhelpers.contrib.network.ip) -sys.modules['charmhelpers.fetch'] = charmhelpers.fetch -sys.modules['charmhelpers.cli'] = charmhelpers.cli -sys.modules['charmhelpers.contrib.hahelpers'] = charmhelpers.contrib.hahelpers -sys.modules['charmhelpers.contrib.hahelpers.cluster'] = ( - charmhelpers.contrib.hahelpers.cluster) \ No newline at end of file +import charms_openstack.test_mocks # noqa + +charms_openstack.test_mocks.mock_charmhelpers() diff --git a/unit_tests/test_lib_charm_openstack_openvswitch_odl.py b/unit_tests/test_lib_charm_openstack_trilio_wlm.py similarity index 93% rename from unit_tests/test_lib_charm_openstack_openvswitch_odl.py rename to unit_tests/test_lib_charm_openstack_trilio_wlm.py index f8780dc..911de32 100644 --- a/unit_tests/test_lib_charm_openstack_openvswitch_odl.py +++ b/unit_tests/test_lib_charm_openstack_trilio_wlm.py @@ -16,7 +16,7 @@ import mock import unittest -import charm.openstack.sdn_charm as sdn_charm +import charm.openstack.trilio_wlm as trilio_wlm class Helper(unittest.TestCase): @@ -41,4 +41,5 @@ class Helper(unittest.TestCase): setattr(self, attr, started) -class TestSDNCharm(Helper): \ No newline at end of file +class TestSDNCharm(Helper): + pass \ No newline at end of file diff --git a/unit_tests/test_openvswitch_odl_handlers.py b/unit_tests/test_openvswitch_odl_handlers.py deleted file mode 100644 index f107ee6..0000000 --- a/unit_tests/test_openvswitch_odl_handlers.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2016 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. - -from __future__ import absolute_import -from __future__ import print_function - -import mock - -import reactive.sdn_charm_handlers as handlers - -import charms_openstack.test_utils as test_utils - - -class TestRegisteredHooks(test_utils.TestRegisteredHooks): - - def test_hooks(self): - defaults = [ - 'charm.installed', - 'config.changed', - 'update-status'] - hook_set = { - 'when': { - }, - 'when_not': { - } - } - # test that the hooks were registered via the - # reactive.barbican_handlers - self.registered_hooks_test_helper(handlers, hook_set, defaults) - - -class TestSDNCharmHandles(test_utils.PatchHelper): \ No newline at end of file diff --git a/unit_tests/test_trilio_wlm_handlers.py b/unit_tests/test_trilio_wlm_handlers.py new file mode 100644 index 0000000..bcb2d17 --- /dev/null +++ b/unit_tests/test_trilio_wlm_handlers.py @@ -0,0 +1,143 @@ +# 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 mock + +import reactive.trilio_wlm_handlers as handlers + +import charms_openstack.test_utils as test_utils + +_when_args = {} +_when_not_args = {} + + +def mock_hook_factory(d): + def mock_hook(*args, **kwargs): + def inner(f): + # remember what we were passed. Note that we can't actually + # determine the class we're attached to, as the decorator only gets + # the function. + try: + d[f.__name__].append(dict(args=args, kwargs=kwargs)) + except KeyError: + d[f.__name__] = [dict(args=args, kwargs=kwargs)] + return f + + return inner + + return mock_hook + + +class TestDmapiHandlers(test_utils.PatchHelper): + @classmethod + def setUpClass(cls): + cls._patched_when = mock.patch( + "charms.reactive.when", mock_hook_factory(_when_args) + ) + cls._patched_when_started = cls._patched_when.start() + cls._patched_when_not = mock.patch( + "charms.reactive.when_not", mock_hook_factory(_when_not_args) + ) + cls._patched_when_not_started = cls._patched_when_not.start() + # force requires to rerun the mock_hook decorator: + # try except is Python2/Python3 compatibility as Python3 has moved + # reload to importlib. + try: + reload(handlers) + except NameError: + import importlib + + importlib.reload(handlers) + + @classmethod + def tearDownClass(cls): + cls._patched_when.stop() + cls._patched_when_started = None + cls._patched_when = None + cls._patched_when_not.stop() + cls._patched_when_not_started = None + cls._patched_when_not = None + # and fix any breakage we did to the module + try: + reload(handlers) + except NameError: + import importlib + + importlib.reload(handlers) + + def setUp(self): + self._patches = {} + self._patches_start = {} + + def tearDown(self): + for k, v in self._patches.items(): + v.stop() + setattr(self, k, None) + self._patches = None + self._patches_start = None + + def patch(self, obj, attr, return_value=None, side_effect=None): + mocked = mock.patch.object(obj, attr) + self._patches[attr] = mocked + started = mocked.start() + started.return_value = return_value + started.side_effect = side_effect + self._patches_start[attr] = started + setattr(self, attr, started) + + def test_registered_hooks(self): + # test that the hooks actually registered the relation expressions that + # are meaningful for this interface: this is to handle regressions. + # The keys are the function names that the hook attaches to. + when_patterns = { + "render_config": ( + "shared-db.available", + "identity-service.available", + "amqp.available", + ), + "init_db": ("config.rendered",), + "cluster_connected": ("ha.connected",), + "request_endpoint_notification": + ("identity-service.connected",) + } + when_not_patterns = {} + # check the when hooks are attached to the expected functions + for t, p in [ + (_when_args, when_patterns), + (_when_not_args, when_not_patterns), + ]: + for f, args in t.items(): + # check that function is in patterns + self.assertTrue(f in p.keys(), "{} not found".format(f)) + # check that the lists are equal + lst = [] + for a in args: + lst += a["args"][:] + self.assertEqual( + sorted(lst), + sorted(p[f]), + "{}: incorrect state registration".format(f), + ) + + def test_render(self): + wlm_charm = mock.MagicMock() + self.patch_object( + handlers.charm, "provide_charm_instance", new=mock.MagicMock() + ) + self.provide_charm_instance().__enter__.return_value = wlm_charm + self.provide_charm_instance().__exit__.return_value = None + args = "args" + handlers.render_config(args) + wlm_charm.upgrade_if_available.assert_called_once_with((args,)) + wlm_charm.render_with_interfaces.assert_called_once_with((args,)) + wlm_charm.configure_tls.assert_called_once_with() + wlm_charm.assess_status.assert_called_once_with()