summaryrefslogtreecommitdiff
diff options
authorBilly Olsen <billy.olsen@canonical.com>2014-12-16 23:39:57 -0700
committerBilly Olsen <billy.olsen@canonical.com>2014-12-16 23:39:57 -0700
commit37c13bbb3939e0c66e8c4c624fd2b20ce0e36ee8 (patch)
treedada81308f17ffa4cebc539d9ceb16cc88d402f5
parent0396bd9803c0aa376cfb4dc0cf012d66755d79f3 (diff)
Add more unit test coverage.
-rw-r--r--.bzrignore2
-rwxr-xr-xhooks/hooks.py26
-rw-r--r--setup.cfg2
-rw-r--r--unit_tests/test_hooks.py165
4 files changed, 175 insertions, 20 deletions
diff --git a/.bzrignore b/.bzrignore
index cffa634..6adea0a 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -3,3 +3,5 @@
.pydevproject
bin/*
scripts/charm-helpers-sync.py
+.venv/*
+.coverage
diff --git a/hooks/hooks.py b/hooks/hooks.py
index 0e06317..8a105ee 100755
--- a/hooks/hooks.py
+++ b/hooks/hooks.py
@@ -83,6 +83,11 @@ default_mongos_list = "/etc/mongos.list"
default_wait_for = 10
default_max_tries = 5
+INSTALL_PACKAGES = ['mongodb-server',
+ 'python-yaml',
+ 'python-pymongo',
+ 'python-portalocker']
+
mongoinit_replset_lockfile = '/tmp/mongodb-charm.lock'
###############################################################################
@@ -184,6 +189,9 @@ def process_check_pidfile(pidfile=None):
return((None, None))
+class TimeoutException(Exception):
+ pass
+
###############################################################################
# Charm support functions
###############################################################################
@@ -420,7 +428,7 @@ def init_replset(master_node=None):
c = MongoClient('localhost')
while True:
try:
- r = c.admin.command('replSetGetStatus')
+ r = admin_command(c, 'replSetGetStatus')
juju_log('init_replset: myState: %s' % r['myState'])
if r['myState'] == 1: # we're primary!
break
@@ -439,6 +447,13 @@ def init_replset(master_node=None):
return(retVal)
+def admin_command(client, cmdstr):
+ """Runs an admin command against the client. Primary purpose is to
+ simplify the unit testing.
+ """
+ return client.admin.command(cmdstr)
+
+
def join_replset(master_node=None, host=None):
juju_log("join_replset: master_node: %s, host: %s" %
(master_node, host))
@@ -843,9 +858,7 @@ def install_hook():
juju_log("Installing mongodb")
add_source(config('source'), config('key'))
apt_update(fatal=True)
- apt_install(packages=['mongodb-server', 'python-yaml', 'python-pymongo',
- 'python-portalocker'],
- fatal=True)
+ apt_install(packages=INSTALL_PACKAGES, fatal=True)
@hooks.hook('config-changed')
@@ -1102,7 +1115,7 @@ def am_i_primary():
c = MongoClient('localhost')
for i in xrange(1, 10):
try:
- r = c.admin.command('replSetGetStatus')
+ r = admin_command(c, 'replSetGetStatus')
juju_log('am_i_primary: replSetGetStatus returned: %s' % str(r),
'DEBUG')
if r['myState'] == 1:
@@ -1124,6 +1137,9 @@ def am_i_primary():
raise
finally:
time.sleep(1.5)
+
+ # Raise an error if we exhausted the maximum amount of trials
+ raise TimeoutException('Unable to determine if local unit is primary')
@hooks.hook('replicaset-relation-changed')
diff --git a/setup.cfg b/setup.cfg
index 29c8a62..3f7bd91 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,5 +2,5 @@
verbosity=2
with-coverage=1
cover-erase=1
-;cover-package=hooks
+cover-package=hooks
diff --git a/unit_tests/test_hooks.py b/unit_tests/test_hooks.py
index 8836087..4e40bdf 100644
--- a/unit_tests/test_hooks.py
+++ b/unit_tests/test_hooks.py
@@ -3,6 +3,7 @@ from mock import patch
import hooks
from test_utils import CharmTestCase
+from pymongo.errors import OperationFailure
# Defines a set of functions to patch on the hooks object. Any of these
# methods will be patched by default on the default invocations of the
@@ -29,31 +30,20 @@ class MongoHooksTest(CharmTestCase):
# test_config dictionary
self.config.side_effect = self.test_config.get
- # Note: if we need to mock a specific class of an object, we need to
- # invoke the patch.object rather than simply mocking the module itself.
- # This is typically recommended for patching any object which belongs
- # to the object under test (lookup partial-mocks for more information).
@patch.object(hooks, 'restart_mongod')
@patch.object(hooks, 'enable_replset')
# Note: patching the os.environ dictionary in-line here so there's no
# additional parameter sent into the function
@patch.dict('os.environ', JUJU_UNIT_NAME='fake-unit/0')
- def test_replica_set_relation_joined(self, mock_enable, mock_restart):
- # only have 1 invocation of the unit_get, so we'll just tell it what
- # to return. Can change to be more sophisticated when necessary.
+ def test_replica_set_relation_joined(self, mock_enable_replset,
+ mock_restart):
self.unit_get.return_value = 'private.address'
self.test_config.set('port', '1234')
self.test_config.set('replicaset', 'fake-replicaset')
self.relation_id.return_value = 'fake-relation-id'
- # Partial mock to control the flow around the if check for the enable
- # restart.
- mock_enable.return_value = False
+ mock_enable_replset.return_value = False
- # This tests the trigger of firing the relation-joined as the python
- # invocation works. However, it doesn't seem to accept all the mocking
- # in this case, so I'll just invoke the function directly.
- # hooks.hooks.execute(['hooks/replicaset-relation-joined'])
hooks.replica_set_relation_joined()
# Verify that mongodb was NOT restarted since the replicaset we claimed
@@ -67,3 +57,150 @@ class MongoHooksTest(CharmTestCase):
'type': 'replset'}
# Check that the relation data was set as we expect it to be set.
self.relation_set.assert_called_with('fake-relation-id', exp_rel_vals)
+
+ mock_enable_replset.reset_mock()
+ self.relation_set.reset_mock()
+ mock_enable_replset.return_value = True
+
+ hooks.replica_set_relation_joined()
+
+ self.assertTrue(mock_restart.called)
+ self.relation_set.assert_called_with('fake-relation-id', exp_rel_vals)
+
+ def test_init_replset_no_master_node(self):
+ retval = hooks.init_replset(master_node=None)
+ self.assertFalse(retval)
+
+ @patch.object(hooks, 'admin_command')
+ @patch.object(hooks, 'MongoClient')
+ @patch.object(hooks, 'config')
+ @patch.object(hooks, 'mongo_client')
+ @patch('time.sleep')
+ def test_init_repl_set(self, mock_sleep, mock_mongo_client_fn,
+ mock_config, mock_mongo_client, mock_admin_command):
+ master_node = 'mongo.unit.private.address'
+ mock_mongo_client_fn.return_value = False
+
+ mock_config.return_value = {'replicaset': 'foo',
+ 'private-address': 'mongo.local',
+ 'port': '12345'}
+
+ # Put the OK state (1) at the end and check the loop.
+ ret_values = [{'myState': x} for x in [0, 2, 3, 4, 1]]
+ mock_admin_command.side_effect = ret_values
+
+ hooks.init_replset(master_node)
+
+ mock_admin_command.assert_called()
+ self.assertEqual(len(ret_values), mock_admin_command.call_count)
+ self.assertEqual(len(ret_values) + 1, mock_sleep.call_count)
+
+ mock_admin_command.reset_mock()
+ exc = [OperationFailure('Received replSetInitiate'),
+ OperationFailure('unhandled')]
+ mock_admin_command.side_effect = exc
+
+ try:
+ hooks.init_replset(master_node)
+ self.assertTrue(False, msg="Expected error")
+ except OperationFailure:
+ pass
+
+ mock_admin_command.assert_called()
+ self.assertEqual(2, mock_admin_command.call_count)
+
+ @patch.object(hooks, 'mongo_client_smart')
+ def test_join_replset(self, mock_mongo_client):
+ hooks.join_replset()
+ self.assertFalse(mock_mongo_client.called)
+
+ mock_mongo_client.reset_mock()
+ hooks.join_replset(master_node='mongo.local')
+ self.assertFalse(mock_mongo_client.called)
+
+ mock_mongo_client.reset_mock()
+ hooks.join_replset(host='fake-host')
+ self.assertFalse(mock_mongo_client.called)
+
+ mock_mongo_client.reset_mock()
+ hooks.join_replset(master_node='mongo.local', host='fake-host')
+ mock_mongo_client.assert_called_with('localhost',
+ 'rs.add("fake-host")')
+
+ @patch.object(hooks, 'mongo_client')
+ def test_leave_replset(self, mock_mongo_client):
+ hooks.leave_replset()
+ self.assertFalse(mock_mongo_client.called)
+
+ mock_mongo_client.reset_mock()
+ hooks.leave_replset(master_node='mongo.local')
+ self.assertFalse(mock_mongo_client.called)
+
+ mock_mongo_client.reset_mock()
+ hooks.leave_replset(host='fake-host')
+ self.assertFalse(mock_mongo_client.called)
+
+ mock_mongo_client.reset_mock()
+ hooks.leave_replset('mongo.local', 'fake-host')
+ mock_mongo_client.assert_called_with('mongo.local',
+ 'rs.remove("fake-host")')
+
+ @patch.object(hooks, 'apt_install')
+ @patch.object(hooks, 'apt_update')
+ @patch.object(hooks, 'add_source')
+ def test_install_hook(self, mock_add_source, mock_apt_update,
+ mock_apt_install):
+ self.test_config.set('source', 'fake-source')
+ self.test_config.set('key', 'fake-key')
+
+ hooks.install_hook()
+ mock_add_source.assert_called_with('fake-source', 'fake-key')
+ mock_apt_update.assert_called_with(fatal=True)
+ mock_apt_install.assert_called_with(packages=hooks.INSTALL_PACKAGES,
+ fatal=True)
+
+ @patch.object(hooks, 'admin_command')
+ @patch.object(hooks, 'MongoClient')
+ @patch('time.sleep')
+ def test_am_i_primary(self, mock_sleep, mock_mongo_client, mock_admin_cmd):
+ mock_admin_cmd.side_effect = [{'myState': x} for x in xrange(5)]
+ expected_results = [True if x == 1 else False for x in xrange(5)]
+
+ # Check expected return values each time...
+ for exp in expected_results:
+ rv = hooks.am_i_primary()
+ self.assertEqual(exp, rv)
+
+ @patch.object(hooks, 'admin_command')
+ @patch.object(hooks, 'MongoClient')
+ @patch('time.sleep')
+ def test_am_i_primary_too_many_attempts(self, mock_sleep, mock_mongo_client,
+ mock_admin_cmd):
+ msg = 'replSetInitiate - should come online shortly'
+ mock_admin_cmd.side_effect = [OperationFailure(msg) for x in xrange(10)]
+
+ try:
+ rv = hooks.am_i_primary()
+ self.assertTrue(False, 'Expected failure.')
+ except hooks.TimeoutException:
+ self.assertEqual(mock_admin_cmd.call_count, 9)
+ pass
+
+ @patch.object(hooks, 'admin_command')
+ @patch.object(hooks, 'MongoClient')
+ @patch('time.sleep')
+ def test_am_i_primary_operation_failures(self, mock_sleep,
+ mock_mongo_client, mock_admin_cmd):
+ mock_admin_cmd.side_effect = OperationFailure('EMPTYCONFIG')
+
+ rv = hooks.am_i_primary()
+ mock_admin_cmd.assert_called()
+ self.assertFalse(rv)
+
+ mock_admin_cmd.reset_mock()
+ mock_admin_cmd.side_effect = OperationFailure('unexpected failure')
+ try:
+ hooks.am_i_primary()
+ except OperationFailure:
+ mock_admin_cmd.assert_called()
+