s/charm_testing/charm_lifecycle/ and unit tests

This commit is contained in:
Liam Young
2018-03-27 12:33:20 +00:00
parent 5d61c46a81
commit 353559528b
22 changed files with 453 additions and 68 deletions

View File

@@ -8,9 +8,10 @@ from setuptools.command.test import test as TestCommand
version = "0.0.1.dev1"
install_require = [
'hvac',
'juju',
'juju-wait',
'hvac'
'PyYAML',
]
tests_require = [

View File

@@ -1,3 +1,5 @@
juju_wait
PyYAML
flake8>=2.2.4,<=2.4.1
mock>=1.2
nose>=1.3.7
nose>=1.3.7

View File

@@ -0,0 +1,27 @@
import mock
import zaza.charm_lifecycle.configure as lc_configure
import unit_tests.utils as ut_utils
class TestCharmLifecycleConfigure(ut_utils.BaseTestCase):
def test_run_configure_list(self):
self.patch_object(lc_configure.utils, 'get_class')
self.get_class.side_effect = lambda x: x
mock1 = mock.MagicMock()
mock2 = mock.MagicMock()
lc_configure.run_configure_list([mock1, mock2])
self.assertTrue(mock1.called)
self.assertTrue(mock2.called)
def test_configure(self):
self.patch_object(lc_configure, 'run_configure_list')
mock1 = mock.MagicMock()
mock2 = mock.MagicMock()
lc_configure.configure([mock1, mock2])
self.run_configure_list.assert_called_once_with([mock1, mock2])
def test_parser(self):
args = lc_configure.parse_args(['-c', 'my.func1', 'my.func2'])
self.assertEqual(args.configfuncs, ['my.func1', 'my.func2'])

View File

@@ -0,0 +1,40 @@
import zaza.charm_lifecycle.deploy as lc_deploy
import unit_tests.utils as ut_utils
class TestCharmLifecycleDeploy(ut_utils.BaseTestCase):
def test_deploy_bundle(self):
self.patch_object(lc_deploy.subprocess, 'check_call')
lc_deploy.deploy_bundle('bun.yaml', 'newmodel')
self.check_call.assert_called_once_with(
['juju', 'deploy', '-m', 'newmodel', 'bun.yaml'])
def test_deploy(self):
self.patch_object(lc_deploy, 'deploy_bundle')
self.patch_object(lc_deploy.juju_wait, 'wait')
lc_deploy.deploy('bun.yaml', 'newmodel')
self.deploy_bundle.assert_called_once_with('bun.yaml', 'newmodel')
self.wait.assert_called_once_with()
def test_deploy_nowait(self):
self.patch_object(lc_deploy, 'deploy_bundle')
self.patch_object(lc_deploy.juju_wait, 'wait')
lc_deploy.deploy('bun.yaml', 'newmodel', wait=False)
self.deploy_bundle.assert_called_once_with('bun.yaml', 'newmodel')
self.assertFalse(self.wait.called)
def test_parser(self):
args = lc_deploy.parse_args([
'-m', 'mymodel',
'-b', 'bun.yaml'])
self.assertEqual(args.model, 'mymodel')
self.assertEqual(args.bundle, 'bun.yaml')
self.assertTrue(args.wait)
def test_parser_nowait(self):
args = lc_deploy.parse_args([
'-m', 'mymodel',
'-b', 'bun.yaml',
'--no-wait'])
self.assertFalse(args.wait)

View File

@@ -0,0 +1,20 @@
import zaza.charm_lifecycle.destroy as lc_destroy
import unit_tests.utils as ut_utils
class TestCharmLifecycleDestroy(ut_utils.BaseTestCase):
def test_destroy_model(self):
self.patch_object(lc_destroy.subprocess, 'check_call')
lc_destroy.destroy_model('doomed')
self.check_call.assert_called_once_with(
['juju', 'destroy-model', '--yes', 'doomed'])
def test_destroy(self):
self.patch_object(lc_destroy, 'destroy_model')
lc_destroy.destroy('doomed')
self.destroy_model.assert_called_once_with('doomed')
def test_parser(self):
args = lc_destroy.parse_args(['-m', 'doomed'])
self.assertEqual(args.model_name, 'doomed')

View File

@@ -0,0 +1,55 @@
import mock
import zaza.charm_lifecycle.func_test_runner as lc_func_test_runner
import unit_tests.utils as ut_utils
class TestCharmLifecycleFuncTestRunner(ut_utils.BaseTestCase):
def test_func_test_runner(self):
self.patch_object(lc_func_test_runner.utils, 'get_charm_config')
self.patch_object(lc_func_test_runner, 'generate_model_name')
self.patch_object(lc_func_test_runner.prepare, 'prepare')
self.patch_object(lc_func_test_runner.deploy, 'deploy')
self.patch_object(lc_func_test_runner.configure, 'configure')
self.patch_object(lc_func_test_runner.test, 'test')
self.patch_object(lc_func_test_runner.destroy, 'destroy')
self.generate_model_name.return_value = 'newmodel'
self.get_charm_config.return_value = {
'charm_name': 'mycharm',
'gate_bundles': ['bundle1', 'bundle2'],
'configure': [
'zaza.charm_tests.mycharm.setup.basic_setup'
'zaza.charm_tests.othercharm.setup.setup'],
'tests': [
'zaza.charm_tests.mycharm.tests.SmokeTest',
'zaza.charm_tests.mycharm.tests.ComplexTest']}
lc_func_test_runner.func_test_runner()
prepare_calls = [
mock.call('newmodel'),
mock.call('newmodel')]
deploy_calls = [
mock.call('./tests/bundles/bundle1.yaml', 'newmodel'),
mock.call('./tests/bundles/bundle2.yaml', 'newmodel')]
configure_calls = [
mock.call([
'zaza.charm_tests.mycharm.setup.basic_setup'
'zaza.charm_tests.othercharm.setup.setup']),
mock.call([
'zaza.charm_tests.mycharm.setup.basic_setup'
'zaza.charm_tests.othercharm.setup.setup'])]
test_calls = [
mock.call([
'zaza.charm_tests.mycharm.tests.SmokeTest',
'zaza.charm_tests.mycharm.tests.ComplexTest']),
mock.call([
'zaza.charm_tests.mycharm.tests.SmokeTest',
'zaza.charm_tests.mycharm.tests.ComplexTest'])]
destroy_calls = [
mock.call('newmodel'),
mock.call('newmodel')]
self.prepare.assert_has_calls(prepare_calls)
self.deploy.assert_has_calls(deploy_calls)
self.configure.assert_has_calls(configure_calls)
self.test.assert_has_calls(test_calls)
self.destroy.assert_has_calls(destroy_calls)

View File

@@ -0,0 +1,20 @@
import zaza.charm_lifecycle.prepare as lc_prepare
import unit_tests.utils as ut_utils
class TestCharmLifecyclePrepare(ut_utils.BaseTestCase):
def test_add_model(self):
self.patch_object(lc_prepare.subprocess, 'check_call')
lc_prepare.add_model('newmodel')
self.check_call.assert_called_once_with(
['juju', 'add-model', 'newmodel'])
def test_prepare(self):
self.patch_object(lc_prepare, 'add_model')
lc_prepare.add_model('newmodel')
self.add_model.assert_called_once_with('newmodel')
def test_parser(self):
args = lc_prepare.parse_args(['-m', 'newmodel'])
self.assertEqual(args.model_name, 'newmodel')

View File

@@ -0,0 +1,36 @@
import mock
import zaza.charm_lifecycle.test as lc_test
import unit_tests.utils as ut_utils
class TestCharmLifecycleTest(ut_utils.BaseTestCase):
def test_run_test_list(self):
loader_mock = mock.MagicMock()
runner_mock = mock.MagicMock()
self.patch_object(lc_test.unittest, 'TestLoader')
self.patch_object(lc_test.unittest, 'TextTestRunner')
self.TestLoader.return_value = loader_mock
self.TextTestRunner.return_value = runner_mock
self.patch_object(lc_test.utils, 'get_class')
self.get_class.side_effect = lambda x: x
test_class1_mock = mock.MagicMock()
test_class2_mock = mock.MagicMock()
lc_test.run_test_list([test_class1_mock, test_class2_mock])
loader_calls = [
mock.call(test_class1_mock),
mock.call(test_class2_mock)]
loader_mock.loadTestsFromTestCase.assert_has_calls(loader_calls)
def test_test(self):
self.patch_object(lc_test, 'run_test_list')
lc_test.run_test_list(['test_class1', 'test_class2'])
self.run_test_list.assert_called_once_with(
['test_class1', 'test_class2'])
def test_parser(self):
args = lc_test.parse_args(['-t', 'my.test_class1', 'my.test_class2'])
self.assertEqual(
args.tests,
['my.test_class1', 'my.test_class2'])

View File

@@ -0,0 +1,24 @@
import os
import tempfile
import yaml
import zaza.charm_lifecycle.utils as lc_utils
import unit_tests.utils as ut_utils
class TestCharmLifecycleUtils(ut_utils.BaseTestCase):
def test_get_charm_config(self):
f = tempfile.NamedTemporaryFile(delete=False, mode='w')
f.write(yaml.dump({'test_config': 'someconfig'}))
f.close()
charm_config = lc_utils.get_charm_config(yaml_file=f.name)
os.unlink(f.name)
self.assertEqual(charm_config, {'test_config': 'someconfig'})
def test_get_class(self):
self.assertEqual(
type(lc_utils.get_class('unit_tests.'
'test_zaza_charm_lifecycle_utils.'
'TestCharmLifecycleUtils')()),
type(self))

82
unit_tests/utils.py Normal file
View File

@@ -0,0 +1,82 @@
# 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.
# Note that the unit_tests/__init__.py also mocks out two charmhelpers imports
# that have side effects that try to apt install modules:
# sys.modules['charmhelpers.contrib.openstack.utils'] = mock.MagicMock()
# sys.modules['charmhelpers.contrib.network.ip'] = mock.MagicMock()
import contextlib
import io
import mock
import unittest
@contextlib.contextmanager
def patch_open():
'''Patch open() to allow mocking both open() itself and the file that is
yielded.
Yields the mock for "open" and "file", respectively.'''
mock_open = mock.MagicMock(spec=open)
mock_file = mock.MagicMock(spec=io.FileIO)
@contextlib.contextmanager
def stub_open(*args, **kwargs):
mock_open(*args, **kwargs)
yield mock_file
with mock.patch('builtins.open', stub_open):
yield mock_open, mock_file
class BaseTestCase(unittest.TestCase):
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_object(self, obj, attr, return_value=None, name=None, new=None,
**kwargs):
if name is None:
name = attr
if new is not None:
mocked = mock.patch.object(obj, attr, new=new, **kwargs)
else:
mocked = mock.patch.object(obj, attr, **kwargs)
self._patches[name] = mocked
started = mocked.start()
if new is None:
started.return_value = return_value
self._patches_start[name] = started
setattr(self, name, started)
def patch(self, item, return_value=None, name=None, new=None, **kwargs):
if name is None:
raise RuntimeError("Must pass 'name' to .patch()")
if new is not None:
mocked = mock.patch(item, new=new, **kwargs)
else:
mocked = mock.patch(item, **kwargs)
self._patches[name] = mocked
started = mocked.start()
if new is None:
started.return_value = return_value
self._patches_start[name] = started
setattr(self, name, started)

View File

@@ -1,7 +1,8 @@
import argparse
import logging
import sys
import zaza.charm_testing.utils as utils
import zaza.charm_lifecycle.utils as utils
def run_configure_list(functions):
@@ -23,15 +24,26 @@ def configure(functions):
run_configure_list(functions)
def parse_args(args):
"""Parse command line arguments
:param args: List of configure functions functions
:type list: [str1, str2,...] List of command line arguments
:returns: Parsed arguments
:rtype: Namespace
"""
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--configfuncs', nargs='+',
help='Space sperated list of config functions',
required=False)
return parser.parse_args(args)
def main():
"""Run the configuration defined by the command line args or if none were
provided read the configuration functions from the charms tests.yaml
config file"""
logging.basicConfig(level=logging.INFO)
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--configfuncs', nargs='+',
help='Space sperated list of config functions',
required=False)
args = parser.parse_args()
args = parse_args(sys.argv[1:])
funcs = args.configfuncs or utils.get_charm_config()['configure']
configure(funcs)

View File

@@ -1,11 +1,12 @@
import argparse
import logging
import subprocess
import sys
import juju_wait
def deploy_bundle(bundle, model, wait=True):
def deploy_bundle(bundle, model):
"""Deploy the given bundle file in the specified model
:param bundle: Path to bundle file
@@ -15,14 +16,32 @@ def deploy_bundle(bundle, model, wait=True):
"""
logging.info("Deploying bundle {}".format(bundle))
subprocess.check_call(['juju', 'deploy', '-m', model, bundle])
def deploy(bundle, model, wait=True):
"""Run all steps to complete deployment
:param bundle: Path to bundle file
:type bundle: str
:param model: Name of model to deploy bundle in
:type model: str
:param wait: Whether to wait until deployment completes
:type model: bool
"""
deploy_bundle(bundle, model)
if wait:
logging.info("Waiting for environment to settle")
juju_wait.wait()
def main():
"""Deploy bundle"""
logging.basicConfig(level=logging.INFO)
def parse_args(args):
"""Parse command line arguments
:param args: List of configure functions functions
:type list: [str1, str2,...] List of command line arguments
:returns: Parsed arguments
:rtype: Namespace
"""
parser = argparse.ArgumentParser()
parser.add_argument('-m', '--model',
help='Model to deploy to',
@@ -34,5 +53,11 @@ def main():
help='Do not wait for deployment to settle',
action='store_false')
parser.set_defaults(wait=True)
args = parser.parse_args()
return parser.parse_args(args)
def main():
"""Deploy bundle"""
logging.basicConfig(level=logging.INFO)
args = parse_args(sys.argv[1:])
deploy_bundle(args.bundle, args.model, wait=args.wait)

View File

@@ -1,6 +1,7 @@
import argparse
import logging
import subprocess
import sys
def destroy_model(model_name):
@@ -13,7 +14,7 @@ def destroy_model(model_name):
subprocess.check_call(['juju', 'destroy-model', '--yes', model_name])
def clean_up(model_name):
def destroy(model_name):
"""Run all steps to cleaup after a test run
:param model: Name of model to remove
@@ -22,11 +23,22 @@ def clean_up(model_name):
destroy_model(model_name)
def main():
"""Cleanup after test run"""
logging.basicConfig(level=logging.INFO)
def parse_args(args):
"""Parse command line arguments
:param args: List of configure functions functions
:type list: [str1, str2,...] List of command line arguments
:returns: Parsed arguments
:rtype: Namespace
"""
parser = argparse.ArgumentParser()
parser.add_argument('-m', '--model-name', help='Name of model to remove',
required=True)
args = parser.parse_args()
clean_up(args.model_name)
return parser.parse_args(args)
def main():
"""Cleanup after test run"""
logging.basicConfig(level=logging.INFO)
args = parse_args(sys.argv[1:])
destroy(args.model_name)

View File

@@ -0,0 +1,38 @@
import datetime
import os
import zaza.charm_lifecycle.configure as configure
import zaza.charm_lifecycle.destroy as destroy
import zaza.charm_lifecycle.utils as utils
import zaza.charm_lifecycle.prepare as prepare
import zaza.charm_lifecycle.deploy as deploy
import zaza.charm_lifecycle.test as test
def generate_model_name(charm_name, bundle_name):
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
return '{}{}{}'.format('charm_name', bundle_name, timestamp)
def func_test_runner():
"""Deploy the bundles and run the tests as defined by the charms tests.yaml
"""
test_config = utils.get_charm_config()
for t in test_config['gate_bundles']:
model_name = generate_model_name(test_config['charm_name'], t)
# Prepare
prepare.prepare(model_name)
# Deploy
deploy.deploy(
os.path.join(utils.BUNDLE_DIR, '{}.yaml'.format(t)),
model_name)
# Configure
configure.configure(test_config['configure'])
# Test
test.test(test_config['tests'])
# Destroy
destroy.destroy(model_name)
def main():
func_test_runner()

View File

@@ -1,6 +1,7 @@
import argparse
import logging
import subprocess
import sys
def add_model(model_name):
@@ -22,11 +23,22 @@ def prepare(model_name):
add_model(model_name)
def parse_args(args):
"""Parse command line arguments
:param args: List of configure functions functions
:type list: [str1, str2,...] List of command line arguments
:returns: Parsed arguments
:rtype: Namespace
"""
parser = argparse.ArgumentParser()
parser.add_argument('-m', '--model-name', help='Name of model to remove',
required=True)
return parser.parse_args(args)
def main():
"""Add a new model"""
logging.basicConfig(level=logging.INFO)
parser = argparse.ArgumentParser()
parser.add_argument('-m', '--model-name', help='Name of new model',
required=True)
args = parser.parse_args()
args = parse_args(sys.argv[1:])
prepare(args.model_name)

View File

@@ -1,8 +1,9 @@
import argparse
import logging
import unittest
import sys
import zaza.charm_testing.utils as utils
import zaza.charm_lifecycle.utils as utils
def run_test_list(tests):
@@ -24,14 +25,25 @@ def test(tests):
run_test_list(tests)
def main():
"""Run the tests defined by the command line args or if none were provided
read the tests from the charms tests.yaml config file"""
logging.basicConfig(level=logging.INFO)
def parse_args(args):
"""Parse command line arguments
:param args: List of configure functions functions
:type list: [str1, str2,...] List of command line arguments
:returns: Parsed arguments
:rtype: Namespace
"""
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--tests', nargs='+',
help='Space sperated list of test classes',
required=False)
args = parser.parse_args()
return parser.parse_args(args)
def main():
"""Run the tests defined by the command line args or if none were provided
read the tests from the charms tests.yaml config file"""
logging.basicConfig(level=logging.INFO)
args = parse_args(sys.argv[1:])
tests = args.tests or utils.get_charm_config()['tests']
test(tests)

View File

@@ -5,13 +5,17 @@ BUNDLE_DIR = "./tests/bundles/"
DEFAULT_TEST_CONFIG = "./tests/tests.yaml"
def get_charm_config():
def get_charm_config(yaml_file=None):
"""Read the yaml test config file and return the resulting config
:param yaml_file: File to be read
:type yaml_file: str
:returns: Config dictionary
:rtype: dict
"""
with open(DEFAULT_TEST_CONFIG, 'r') as stream:
if not yaml_file:
yaml_file = DEFAULT_TEST_CONFIG
with open(yaml_file, 'r') as stream:
return yaml.load(stream)

View File

@@ -1,34 +0,0 @@
import datetime
import os
import zaza.charm_testing.configure as configure
import zaza.charm_testing.destroy as destroy
import zaza.charm_testing.utils as utils
import zaza.charm_testing.prepare as prepare
import zaza.charm_testing.deploy as deploy
import zaza.charm_testing.test as test
def func_test_runner():
"""Deploy the bundles and run the tests as defined by the charms tests.yaml
"""
test_config = utils.get_charm_config()
for t in test_config['gate_bundles']:
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
model_name = '{}{}{}'.format(test_config['charm_name'], t, timestamp)
# Prepare
prepare.add_model(model_name)
# Deploy
deploy.deploy_bundle(
os.path.join(utils.BUNDLE_DIR, '{}.yaml'.format(t)),
model_name)
# Configure
configure.run_configure_list(test_config['configure'])
# Test
test.run_test_list(test_config['tests'])
# Destroy
destroy.clean_up(model_name)
def main():
func_test_runner()

View File

@@ -1,6 +1,3 @@
import logging
import sys
from juju import loop
from juju.model import Model