summaryrefslogtreecommitdiffstats
path: root/charms/trusty/cassandra/testing
diff options
context:
space:
mode:
authorStuart Mackie <wsmackie@juniper.net>2016-11-22 14:00:39 -0800
committerStuart Mackie <wsmackie@juniper.net>2016-11-22 14:00:39 -0800
commit5de4c9f0c126b984da72128a3ca084eda6b4087a (patch)
tree0be55aa0809cc395e45baeae63db660b4e72fe83 /charms/trusty/cassandra/testing
parentf02da72993eb8e5a34ed049bad442c6d6db4701a (diff)
Add local cassandra charm from Tony Liu's machine
Change-Id: I56478233f7498861f95a0c12be983f9a1307853e Signed-off-by: Stuart Mackie <wsmackie@juniper.net>
Diffstat (limited to 'charms/trusty/cassandra/testing')
-rw-r--r--charms/trusty/cassandra/testing/__init__.py15
-rw-r--r--charms/trusty/cassandra/testing/amuletfixture.py234
-rw-r--r--charms/trusty/cassandra/testing/mocks.py182
3 files changed, 431 insertions, 0 deletions
diff --git a/charms/trusty/cassandra/testing/__init__.py b/charms/trusty/cassandra/testing/__init__.py
new file mode 100644
index 0000000..b1b7fcd
--- /dev/null
+++ b/charms/trusty/cassandra/testing/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2015 Canonical Ltd.
+#
+# This file is part of the Cassandra Charm for Juju.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranties of
+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+# PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
diff --git a/charms/trusty/cassandra/testing/amuletfixture.py b/charms/trusty/cassandra/testing/amuletfixture.py
new file mode 100644
index 0000000..988267f
--- /dev/null
+++ b/charms/trusty/cassandra/testing/amuletfixture.py
@@ -0,0 +1,234 @@
+# Copyright 2015 Canonical Ltd.
+#
+# This file is part of the Cassandra Charm for Juju.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranties of
+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+# PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from functools import wraps
+import json
+import os
+import shutil
+import subprocess
+import tempfile
+import time
+
+import amulet
+import yaml
+
+
+class AmuletFixture(amulet.Deployment):
+ def __init__(self, series, verbose=False):
+ if verbose:
+ super(AmuletFixture, self).__init__(series=series)
+ else:
+ # We use a wrapper around juju-deployer so we can fix how it is
+ # invoked. In particular, turn off all the noise so we can
+ # actually read our test output.
+ juju_deployer = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), os.pardir, 'lib',
+ 'juju-deployer-wrapper.py'))
+ super(AmuletFixture, self).__init__(series=series,
+ juju_deployer=juju_deployer)
+ assert self.series == series
+
+ def setUp(self):
+ self._temp_dirs = []
+
+ self.reset_environment(force=True)
+
+ # Repackage our charm to a temporary directory, allowing us
+ # to strip our virtualenv symlinks that would otherwise cause
+ # juju to abort. We also strip the .bzr directory, working
+ # around Bug #1394078.
+ self.repackage_charm()
+
+ # Fix amulet.Deployment so it doesn't depend on environment
+ # variables or the current working directory, but rather the
+ # environment we have introspected.
+ with open(os.path.join(self.charm_dir, 'metadata.yaml'), 'r') as s:
+ self.charm_name = yaml.safe_load(s)['name']
+ self.charm_cache.test_charm = None
+ self.charm_cache.fetch(self.charm_name, self.charm_dir,
+ series=self.series)
+
+ # Explicitly reset $JUJU_REPOSITORY to ensure amulet and
+ # juju-deployer does not mess with the real one, per Bug #1393792
+ self.org_repo = os.environ.get('JUJU_REPOSITORY', None)
+ temp_repo = tempfile.mkdtemp(suffix='.repo')
+ self._temp_dirs.append(temp_repo)
+ os.environ['JUJU_REPOSITORY'] = temp_repo
+ os.makedirs(os.path.join(temp_repo, self.series), mode=0o700)
+
+ def tearDown(self, reset_environment=True):
+ if reset_environment:
+ self.reset_environment()
+ if self.org_repo is None:
+ del os.environ['JUJU_REPOSITORY']
+ else:
+ os.environ['JUJU_REPOSITORY'] = self.org_repo
+
+ def deploy(self, timeout=None):
+ '''Deploying or updating the configured system.
+
+ Invokes amulet.Deployer.setup with a nicer name and standard
+ timeout handling.
+ '''
+ if timeout is None:
+ timeout = int(os.environ.get('AMULET_TIMEOUT', 900))
+
+ # juju-deployer is buried under here, and has race conditions.
+ # Sleep a bit before invoking it, so its cached view of the
+ # environment matches reality.
+ time.sleep(15)
+
+ # If setUp fails, tearDown is never called leaving the
+ # environment setup. This is useful for debugging.
+ self.setup(timeout=timeout)
+ self.wait(timeout=timeout)
+
+ def __del__(self):
+ for temp_dir in self._temp_dirs:
+ if os.path.exists(temp_dir):
+ shutil.rmtree(temp_dir, ignore_errors=True)
+
+ def get_status(self):
+ try:
+ raw = subprocess.check_output(['juju', 'status', '--format=json'],
+ universal_newlines=True)
+ except subprocess.CalledProcessError as x:
+ print(x.output)
+ raise
+ if raw:
+ return json.loads(raw)
+ return None
+
+ def wait(self, timeout=None):
+ '''Wait until the environment has reached a stable state.'''
+ if timeout is None:
+ timeout = int(os.environ.get('AMULET_TIMEOUT', 900))
+ cmd = ['timeout', str(timeout), 'juju', 'wait', '-q']
+ try:
+ subprocess.check_output(cmd, universal_newlines=True)
+ except subprocess.CalledProcessError as x:
+ print(x.output)
+ raise
+
+ def reset_environment(self, force=False):
+ if force:
+ status = self.get_status()
+ machines = [m for m in status.get('machines', {}).keys()
+ if m != '0']
+ if machines:
+ subprocess.call(['juju', 'destroy-machine',
+ '--force'] + machines,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL)
+ fails = dict()
+ while True:
+ status = self.get_status()
+ service_items = status.get('services', {}).items()
+ if not service_items:
+ break
+ for service_name, service in service_items:
+ if service.get('life', '') not in ('dying', 'dead'):
+ subprocess.call(['juju', 'destroy-service', service_name],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+ for unit_name, unit in service.get('units', {}).items():
+ if unit.get('agent-state', None) == 'error':
+ if force:
+ # If any units have failed hooks, unstick them.
+ # This should no longer happen now we are
+ # using the 'destroy-machine --force' command
+ # earlier.
+ try:
+ subprocess.check_output(
+ ['juju', 'resolved', unit_name],
+ stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError:
+ # A previous 'resolved' call make cause a
+ # subsequent one to fail if it is still
+ # being processed. However, we need to keep
+ # retrying because after a successful
+ # resolution a subsequent hook may cause an
+ # error state.
+ pass
+ else:
+ fails[unit_name] = unit
+ time.sleep(1)
+
+ harvest_machines = []
+ for machine, state in status.get('machines', {}).items():
+ if machine != "0" and state.get('life') not in ('dying', 'dead'):
+ harvest_machines.append(machine)
+
+ if harvest_machines:
+ cmd = ['juju', 'remove-machine', '--force'] + harvest_machines
+ subprocess.check_output(cmd, stderr=subprocess.STDOUT)
+
+ if fails:
+ raise Exception("Teardown failed", fails)
+
+ def repackage_charm(self):
+ """Mirror the charm into a staging area.
+
+ We do this to work around issues with Amulet, juju-deployer
+ and juju. In particular:
+ - symlinks in the Python virtual env pointing outside of the
+ charm directory.
+ - odd bzr interactions, such as tests being run on the committed
+ version of the charm, rather than the working tree.
+
+ Returns the test charm directory.
+ """
+ # Find the charm_dir we are testing
+ src_charm_dir = os.path.dirname(__file__)
+ while True:
+ if os.path.exists(os.path.join(src_charm_dir,
+ 'metadata.yaml')):
+ break
+ assert src_charm_dir != os.sep, 'metadata.yaml not found'
+ src_charm_dir = os.path.abspath(os.path.join(src_charm_dir,
+ os.pardir))
+
+ with open(os.path.join(src_charm_dir, 'metadata.yaml'), 'r') as s:
+ self.charm_name = yaml.safe_load(s)['name']
+
+ repack_root = tempfile.mkdtemp(suffix='.charm')
+ self._temp_dirs.append(repack_root)
+ # juju-deployer now requires the series in the path when
+ # deploying from an absolute path.
+ repack_root = os.path.join(repack_root, self.series)
+ os.makedirs(repack_root, mode=0o700)
+
+ self.charm_dir = os.path.join(repack_root, self.charm_name)
+
+ # Ignore .bzr to work around weird bzr interactions with
+ # juju-deployer, per Bug #1394078, and ignore .venv
+ # due to a) it containing symlinks juju will reject and b) to avoid
+ # infinite recursion.
+ shutil.copytree(src_charm_dir, self.charm_dir, symlinks=True,
+ ignore=shutil.ignore_patterns('.venv?', '.bzr'))
+
+
+# Bug #1417097 means we need to monkey patch Amulet for now.
+real_juju = amulet.helpers.juju
+
+
+@wraps(real_juju)
+def patched_juju(args, env=None):
+ args = [str(a) for a in args]
+ return real_juju(args, env)
+
+amulet.helpers.juju = patched_juju
+amulet.deployer.juju = patched_juju
diff --git a/charms/trusty/cassandra/testing/mocks.py b/charms/trusty/cassandra/testing/mocks.py
new file mode 100644
index 0000000..7d03f23
--- /dev/null
+++ b/charms/trusty/cassandra/testing/mocks.py
@@ -0,0 +1,182 @@
+# Copyright 2015 Canonical Ltd.
+#
+# This file is part of the Cassandra Charm for Juju.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranties of
+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+# PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+charm-helpers mocks.
+'''
+import os.path
+import shutil
+import tempfile
+from unittest.mock import patch
+
+import yaml
+
+from charmhelpers import fetch
+from charmhelpers.core import hookenv
+
+
+CHARM_DIR = os.path.abspath(os.path.join(
+ os.path.dirname(__file__), os.pardir))
+
+
+def mock_charmhelpers(test_case):
+ hookenv.cache.clear() # Clear the hookenv memorisation.
+
+ mocks = []
+
+ # Mock environment
+ charm_dir = tempfile.TemporaryDirectory()
+ test_case.addCleanup(charm_dir.cleanup)
+ mock_env = patch.dict(os.environ, dict(CHARM_DIR=charm_dir.name))
+ mock_env.start()
+ test_case.addCleanup(mock_env.stop)
+ shutil.copyfile(os.path.join(CHARM_DIR, 'metadata.yaml'),
+ os.path.join(charm_dir.name, 'metadata.yaml'))
+
+ # Mock config.
+ # Set items:
+ # hookenv.config()['foo'] = 'bar'
+ # Reset 'previous' state:
+ # hookenv.config().save();
+ # hookenv.config().load_previous()
+ config = hookenv.Config()
+ tmp = tempfile.NamedTemporaryFile(suffix='.config')
+ config.CONFIG_FILE_NAME = tmp.name
+ test_case.addCleanup(tmp.close)
+ with open(os.path.join(CHARM_DIR, 'config.yaml'), 'rb') as f:
+ defaults = yaml.safe_load(f)['options']
+ for k, v in defaults.items():
+ opt_type = v.get('type', 'string')
+ opt_val = v.get('default', None)
+ if opt_val is None:
+ config[k] = None
+ elif opt_type == 'string':
+ config[k] = str(opt_val)
+ elif opt_type == 'int':
+ config[k] = int(opt_val)
+ elif opt_type == 'boolean':
+ config[k] = bool(opt_val)
+
+ def mock_config(scope=None):
+ if scope is None:
+ return config
+ return config.get(scope, None)
+ mocks.append(patch('charmhelpers.core.hookenv.config',
+ side_effect=mock_config, autospec=True))
+
+ # Magic mocks.
+ methods = [
+ 'charmhelpers.core.hookenv.log',
+ 'charmhelpers.core.hookenv.hook_name',
+ 'charmhelpers.core.hookenv.related_units',
+ 'charmhelpers.core.hookenv.relation_get',
+ 'charmhelpers.core.hookenv.relation_set',
+ 'charmhelpers.core.hookenv.relation_ids',
+ 'charmhelpers.core.hookenv.relation_type',
+ 'charmhelpers.core.hookenv.service_name',
+ 'charmhelpers.core.hookenv.local_unit',
+ 'charmhelpers.core.hookenv.unit_private_ip',
+ 'charmhelpers.core.hookenv.unit_public_ip',
+ 'charmhelpers.core.host.log',
+ 'charmhelpers.fetch.filter_installed_packages',
+ 'os.chown', 'os.fchown',
+ ]
+ for m in methods:
+ mocks.append(patch(m, autospec=True))
+
+ for mock in mocks:
+ mock.start()
+ test_case.addCleanup(mock.stop)
+
+ hookenv.local_unit.return_value = 'service/1'
+
+ def mock_unit_private_ip():
+ return '10.20.0.{}'.format(hookenv.local_unit().split('/')[-1])
+ hookenv.unit_private_ip.side_effect = mock_unit_private_ip
+
+ def mock_unit_public_ip():
+ return '10.30.0.{}'.format(hookenv.local_unit().split('/')[-1])
+ hookenv.unit_public_ip.side_effect = mock_unit_public_ip
+
+ def mock_service_name():
+ return hookenv.local_unit().split('/')[0]
+ hookenv.service_name.side_effect = mock_service_name
+
+ hookenv.relation_ids.side_effect = (
+ lambda x: ['{}:1'.format(x)] if x else [])
+ hookenv.related_units.return_value = ('service/2', 'service/3')
+
+ relinfos = dict()
+
+ def mock_relation_set(relation_id=None, relation_settings=None, **kwargs):
+ if relation_id is None:
+ relation_id = hookenv.relation_id()
+ unit = hookenv.local_unit()
+ relinfo = mock_relation_get(unit=unit, rid=relation_id)
+ if relation_settings is not None:
+ relinfo.update(relation_settings)
+ relinfo.update(kwargs)
+ return None
+ hookenv.relation_set.side_effect = mock_relation_set
+
+ def mock_relation_get(attribute=None, unit=None, rid=None):
+ if rid is None:
+ rid = hookenv.relation_id()
+ if unit is None:
+ unit = hookenv.remove_unit()
+ service, unit_num = unit.split('/')
+ unit_num = int(unit_num)
+ relinfos.setdefault(rid, {})
+ relinfos[rid].setdefault(
+ unit, {'private-address': '10.20.0.{}'.format(unit_num)})
+ relinfo = relinfos[rid][unit]
+ if attribute is None or attribute == '-':
+ return relinfo
+ return relinfo.get(attribute)
+ hookenv.relation_get.side_effect = mock_relation_get
+
+ def mock_chown(target, uid, gid):
+ assert uid == 0
+ assert gid == 0
+ assert os.path.exists(target)
+ os.chown.side_effect = mock_chown
+
+ def mock_fchown(fd, uid, gid):
+ assert uid == 0
+ assert gid == 0
+ os.fchown.side_effect = mock_fchown
+
+ fetch.filter_installed_packages.side_effect = lambda pkgs: list(pkgs)
+
+ def mock_relation_for_unit(unit=None, rid=None):
+ if unit is None:
+ unit = hookenv.remote_unit()
+ service, unit_num = unit.split('/')
+ unit_num = int(unit_num)
+ return {'private-address': '10.20.0.{}'.format(unit_num)}
+ hookenv.relation_for_unit.side_effect = mock_relation_for_unit
+
+ def mock_chown(target, uid, gid):
+ assert uid == 0
+ assert gid == 0
+ assert os.path.exists(target)
+ os.chown.side_effect = mock_chown
+
+ def mock_fchown(fd, uid, gid):
+ assert uid == 0
+ assert gid == 0
+ os.fchown.side_effect = mock_fchown
+
+ fetch.filter_installed_packages.side_effect = lambda pkgs: list(pkgs)