yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #00598
[Merge] lp:~frankban/lpsetup/unittests into lp:lpsetup
Francesco Banconi has proposed merging lp:~frankban/lpsetup/unittests into lp:lpsetup.
Requested reviews:
Launchpad Yellow Squad (yellow)
For more details, see:
https://code.launchpad.net/~frankban/lpsetup/unittests/+merge/97842
== Changes ==
Added unit tests for argparser, handlers and utils.
== Tests ==
$ ./bin/test -vv
Running tests at level 1
Running zope.testrunner.layer.UnitTests tests:
Set up zope.testrunner.layer.UnitTests in 0.000 seconds.
Running:
test_action_flag (lpsetup.tests.test_argparser.ActionsBasedSubCommandTest)
test_action_name (lpsetup.tests.test_argparser.ActionsBasedSubCommandTest)
test_actions (lpsetup.tests.test_argparser.ActionsBasedSubCommandTest)
test_skip_action (lpsetup.tests.test_argparser.ActionsBasedSubCommandTest)
test_failing_action (lpsetup.tests.test_argparser.ActionsBasedSubCommandWithErrorsTest)
test_add_argument (lpsetup.tests.test_argparser.ArgumentParserTest)
test_get_args_from_namespace (lpsetup.tests.test_argparser.ArgumentParserTest)
test_help_subcommand (lpsetup.tests.test_argparser.ArgumentParserTest)
test_missing_help_subcommand (lpsetup.tests.test_argparser.ArgumentParserTest)
test_register_subcommand (lpsetup.tests.test_argparser.ArgumentParserTest)
test_register_subcommand_providing_handler (lpsetup.tests.test_argparser.ArgumentParserTest)
test_arguments (lpsetup.tests.test_argparser.BaseSubCommandTest)
test_failing_validation (lpsetup.tests.test_argparser.BaseSubCommandTest)
test_help (lpsetup.tests.test_argparser.BaseSubCommandTest)
test_name (lpsetup.tests.test_argparser.BaseSubCommandTest)
test_successful_validation (lpsetup.tests.test_argparser.BaseSubCommandTest)
test_directory_containing_spaces (lpsetup.tests.test_handlers.HandleDirectoriesTest)
test_directory_not_in_home (lpsetup.tests.test_handlers.HandleDirectoriesTest)
test_home_is_expanded (lpsetup.tests.test_handlers.HandleDirectoriesTest)
test_lpuser (lpsetup.tests.test_handlers.HandleLPUserTest)
test_key_escaping (lpsetup.tests.test_handlers.HandleSSHKeysTest)
test_no_keys (lpsetup.tests.test_handlers.HandleSSHKeysTest)
test_only_one_key (lpsetup.tests.test_handlers.HandleSSHKeysTest)
test_no_data (lpsetup.tests.test_handlers.HandleUserDataTest)
test_no_whois (lpsetup.tests.test_handlers.HandleUserDataTest)
test_non_existent_user (lpsetup.tests.test_handlers.HandleUserDataTest)
test_only_one_argument (lpsetup.tests.test_handlers.HandleUserDataTest)
test_failure (lpsetup.tests.test_handlers.HandleUserTest)
test_home_dir (lpsetup.tests.test_handlers.HandleUserTest)
test_root_path (lpsetup.tests.test_utils.GetContainerPathTest)
test_with_path (lpsetup.tests.test_utils.GetContainerPathTest)
Ran 31 tests with 0 failures and 0 errors in 0.028 seconds.
Tearing down left over layers:
Tear down zope.testrunner.layer.UnitTests in 0.000 seconds.
--
https://code.launchpad.net/~frankban/lpsetup/unittests/+merge/97842
Your team Launchpad Yellow Squad is requested to review the proposed merge of lp:~frankban/lpsetup/unittests into lp:lpsetup.
=== modified file 'lpsetup/__init__.py'
--- lpsetup/__init__.py 2012-03-12 10:11:31 +0000
+++ lpsetup/__init__.py 2012-03-16 11:17:19 +0000
@@ -9,7 +9,7 @@
'get_version',
]
-VERSION = (0, 1, 0)
+VERSION = (0, 1, 1)
def get_version():
=== modified file 'lpsetup/handlers.py'
--- lpsetup/handlers.py 2012-03-12 14:13:55 +0000
+++ lpsetup/handlers.py 2012-03-16 11:17:19 +0000
@@ -152,7 +152,7 @@
current home directory::
>>> namespace = argparse.Namespace(
- ... private_key=None, home_dir='/tmp/__does_not_exists__')
+ ... private_key=None, home_dir='/tmp/__does_not_exist__')
>>> handle_ssh_keys(namespace) # doctest: +ELLIPSIS
>>> print namespace.private_key
None
@@ -166,7 +166,7 @@
>>> namespace = argparse.Namespace(
... private_key=private, public_key=None,
- ... home_dir='/tmp/__does_not_exists__')
+ ... home_dir='/tmp/__does_not_exist__')
>>> handle_ssh_keys(namespace) # doctest: +ELLIPSIS
Traceback (most recent call last):
ValidationError: arguments private-key...
=== added file 'lpsetup/tests/examples.py'
--- lpsetup/tests/examples.py 1970-01-01 00:00:00 +0000
+++ lpsetup/tests/examples.py 2012-03-16 11:17:19 +0000
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Example objects used in tests."""
+
+__metaclass__ = type
+__all__ = [
+ 'action1',
+ 'action2',
+ 'ActionsBasedSubCommand',
+ 'ActionsBasedSubCommandWithErrors',
+ 'bad_action',
+ 'SubCommand',
+ ]
+
+import subprocess
+
+from lpsetup import argparser
+
+
+def action1(foo):
+ print 'action1 received ' + foo
+
+
+def action2(foo, bar):
+ print 'action2 received {0} and {1}'.format(foo, bar)
+action2.action_name = 'myaction'
+
+
+def bad_action(foo):
+ raise subprocess.CalledProcessError(1, 'command')
+
+
+class SubCommand(argparser.BaseSubCommand):
+ """An example sub command."""
+
+ help = 'Sub command example.'
+
+ def add_arguments(self, parser):
+ super(SubCommand, self).add_arguments(parser)
+ parser.add_argument('--foo')
+
+ def handle(self, namespace):
+ return namespace
+
+
+class ActionsBasedSubCommand(argparser.ActionsBasedSubCommand):
+ """An example action based sub command."""
+
+ actions = (
+ (action1, 'foo'),
+ (action2, 'foo', 'bar'),
+ )
+
+ def add_arguments(self, parser):
+ super(ActionsBasedSubCommand, self).add_arguments(parser)
+ parser.add_argument('--foo')
+ parser.add_argument('--bar')
+
+
+class ActionsBasedSubCommandWithErrors(ActionsBasedSubCommand):
+ """An example action based sub command (containing a failing action)."""
+
+ actions = (
+ (action1, 'foo'),
+ (bad_action, 'foo'),
+ (action2, 'foo', 'bar'),
+ )
=== added file 'lpsetup/tests/test_argparser.py'
--- lpsetup/tests/test_argparser.py 1970-01-01 00:00:00 +0000
+++ lpsetup/tests/test_argparser.py 2012-03-16 11:17:19 +0000
@@ -0,0 +1,190 @@
+#!/usr/bin/env python
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the argparser module."""
+
+import unittest
+
+from lpsetup import argparser
+from lpsetup.exceptions import ValidationError
+from lpsetup.tests import examples
+from lpsetup.tests.utils import (
+ capture_error,
+ capture_output,
+ )
+
+
+class ArgumentParserTest(unittest.TestCase):
+
+ def setUp(self):
+ self.parser = argparser.ArgumentParser()
+
+ def get_sub_command(self):
+ return type(
+ 'SubCommand', (argparser.BaseSubCommand,),
+ {'handle': lambda self, namespace: self.name})
+
+ def test_add_argument(self):
+ # Ensure actions are stored in a "public" instance attribute.
+ self.parser.add_argument('arg1')
+ self.parser.add_argument('arg2')
+ dests = [action.dest for action in self.parser.actions]
+ self.assertListEqual(['help', 'arg1', 'arg2'], dests)
+
+ def test_register_subcommand(self):
+ # The `main` method of the subcommand class is added to namespace,
+ # and can be used to actually handle the sub command execution.
+ name = 'foo'
+ self.parser.register_subcommand(name, self.get_sub_command())
+ namespace = self.parser.parse_args([name])
+ self.assertEqual(name, namespace.main(namespace))
+
+ def test_register_subcommand_providing_handler(self):
+ # Ensure a `handler` callable can also be provided to handle the
+ # subcommand execution.
+ handler = lambda namespace: 'custom handler'
+ self.parser.register_subcommand(
+ 'bar', self.get_sub_command(), handler=handler)
+ namespace = self.parser.parse_args(['bar'])
+ self.assertEqual(handler(namespace), namespace.main(namespace))
+
+ def test_get_args_from_namespace(self):
+ # It is possible to recreate the argument list taking values from
+ # a different namespace.
+ self.parser.add_argument('--foo')
+ self.parser.add_argument('bar')
+ namespace = self.parser.parse_args('--foo eggs spam'.split())
+ namespace.foo = 'changed'
+ args = self.parser.get_args_from_namespace(namespace)
+ self.assertSequenceEqual(['--foo', 'changed', 'spam'], args)
+
+ def test_help_subcommand(self):
+ # Ensure the help sub command is added if other commands exist.
+ self.parser.register_subcommand('foo', self.get_sub_command())
+ namespace = self.parser.parse_args(['help'])
+ with self.assertRaises(SystemExit) as cm:
+ with capture_output() as output:
+ namespace.main(namespace)
+ self.assertEqual(0, cm.exception.code)
+ self.assertIn('usage:', output.getvalue())
+
+ def test_missing_help_subcommand(self):
+ # Ensure the help sub command is missing if no other commands exist.
+ with self.assertRaises(SystemExit) as cm:
+ with capture_error() as error:
+ self.parser.parse_args(['help'])
+ self.assertEqual(2, cm.exception.code)
+ self.assertIn('unrecognized arguments: help', error.getvalue())
+
+
+class SubCommandTestMixin(object):
+
+ sub_command_class = examples.SubCommand
+ sub_command_name = 'subcmd'
+
+ def setUp(self):
+ self.parser = argparser.ArgumentParser()
+ self.sub_command = self.parser.register_subcommand(
+ self.sub_command_name, self.sub_command_class)
+
+ def parse_and_call_main(self, arguments=None):
+ args = [self.sub_command_name]
+ if arguments is not None:
+ args.extend(arguments.split())
+ namespace = self.parser.parse_args(args)
+ return namespace.main(namespace)
+
+ def check_output(self, expected, output):
+ value = filter(None, output.getvalue().split('\n'))
+ self.assertSequenceEqual(expected, value)
+
+
+class BaseSubCommandTest(SubCommandTestMixin, unittest.TestCase):
+
+ def set_validators(self, *args):
+ self.sub_command.validators = args
+
+ def _successful_validator(self, namespace):
+ namespace.bar = True
+
+ def _failing_validator(self, namespace):
+ raise ValidationError('nothing is going on')
+
+ def test_name(self):
+ # Ensure a registered sub command has a name.
+ self.assertEqual(self.sub_command_name, self.sub_command.name)
+
+ def test_arguments(self):
+ # Ensure the sub command arguments are correctly handled.
+ namespace = self.parse_and_call_main('--foo eggs')
+ self.assertEqual('eggs', namespace.foo)
+
+ def test_successful_validation(self):
+ # Ensure attached validators are called by the default handler.
+ self.set_validators(self._successful_validator)
+ namespace = self.parse_and_call_main()
+ self.assertTrue(namespace.bar)
+
+ def test_failing_validation(self):
+ # Ensure `ValidationError` stops the handler execution.
+ self.set_validators(self._failing_validator)
+ with self.assertRaises(SystemExit) as cm:
+ with capture_error() as error:
+ self.parse_and_call_main()
+ self.assertEqual(2, cm.exception.code)
+ self.assertIn('nothing is going on', error.getvalue())
+
+ def test_help(self):
+ # The help attribute of sub command instances is used to generate
+ # the command usage message.
+ help = self.parser.format_help()
+ self.assertIn(self.sub_command.name, help)
+ self.assertIn(self.sub_command.help, help)
+
+
+class ActionsBasedSubCommandTest(SubCommandTestMixin, unittest.TestCase):
+
+ sub_command_class = examples.ActionsBasedSubCommand
+
+ def test_actions(self):
+ # Ensure actions are executed in the order they are provided.
+ with capture_output() as output:
+ self.parse_and_call_main('--foo eggs --bar spam')
+ self.check_output(
+ ['action1 received eggs', 'action2 received eggs and spam'],
+ output)
+
+ def test_action_flag(self):
+ # A special argument `-a` or `--actions` is automatically added to the
+ # parser. It can be used to execute only one or a subset of actions.
+ with capture_output() as output:
+ self.parse_and_call_main('--foo eggs -a action1')
+ self.check_output(['action1 received eggs'], output)
+
+ def test_skip_action(self):
+ # A special argument `--skip-actions` is automatically added to the
+ # parser. It can be used to skip one or more actions.
+ with capture_output() as output:
+ self.parse_and_call_main('--foo eggs --skip-actions action1')
+ self.check_output(['action2 received eggs and None'], output)
+
+ def test_action_name(self):
+ # Ensure the string representation of an action i correctly retrieved.
+ method = self.sub_command._get_action_name
+ self.assertEqual('action1', method(examples.action1))
+ self.assertEqual('myaction', method(examples.action2))
+
+
+class ActionsBasedSubCommandWithErrorsTest(
+ SubCommandTestMixin, unittest.TestCase):
+
+ sub_command_class = examples.ActionsBasedSubCommandWithErrors
+
+ def test_failing_action(self):
+ # Ensure the actions execution is stopped if an action raises
+ # `subprocess.CalledProcessError`.
+ with capture_output() as output:
+ error = self.parse_and_call_main('--foo eggs')
+ self.assertEqual(1, error.returncode)
+ self.check_output(['action1 received eggs'], output)
=== added file 'lpsetup/tests/test_handlers.py'
--- lpsetup/tests/test_handlers.py 1970-01-01 00:00:00 +0000
+++ lpsetup/tests/test_handlers.py 2012-03-16 11:17:19 +0000
@@ -0,0 +1,159 @@
+#!/usr/bin/env python
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the handlers module."""
+
+import argparse
+from contextlib import contextmanager
+import getpass
+import unittest
+
+from lpsetup.exceptions import ValidationError
+from lpsetup.handlers import (
+ handle_directories,
+ handle_lpuser,
+ handle_ssh_keys,
+ handle_user,
+ handle_userdata,
+ )
+
+
+class HandlersTestMixin(object):
+
+ @contextmanager
+ def assertNotValid(self, argument):
+ try:
+ yield
+ except ValidationError as err:
+ self.assertIn(argument, str(err))
+ else:
+ self.fail('ValidationError not raised')
+
+
+class HandleDirectoriesTest(HandlersTestMixin, unittest.TestCase):
+
+ home_dir = '/home/foo'
+ dependencies_dir = '~/launchpad/deps'
+
+ def test_home_is_expanded(self):
+ # Ensure the ~ construction is automatically expanded.
+ namespace = argparse.Namespace(
+ directory='~/launchpad', home_dir=self.home_dir,
+ dependencies_dir=self.dependencies_dir)
+ handle_directories(namespace)
+ self.assertEqual('/home/foo/launchpad', namespace.directory)
+ self.assertEqual(
+ '/home/foo/launchpad/deps', namespace.dependencies_dir)
+
+ def test_directory_not_in_home(self):
+ # The validation fails for directories not residing inside the home.
+ namespace = argparse.Namespace(
+ directory='/tmp/launchpad', home_dir=self.home_dir,
+ dependencies_dir=self.dependencies_dir)
+ with self.assertNotValid('directory'):
+ handle_directories(namespace)
+
+ def test_directory_containing_spaces(self):
+ namespace = argparse.Namespace(directory='directory with spaces')
+ with self.assertNotValid('directory'):
+ handle_directories(namespace)
+
+
+class HandleLPUserTest(unittest.TestCase):
+
+ def test_lpuser(self):
+ # If lpuser is not provided by namespace, the user name is used.
+ username = getpass.getuser()
+ namespace = argparse.Namespace(user=username, lpuser=None)
+ handle_lpuser(namespace)
+ self.assertEqual(username, namespace.lpuser)
+
+
+class HandleSSHKeysTest(HandlersTestMixin, unittest.TestCase):
+
+ private = r'PRIVATE\nKEY'
+ public = r'PUBLIC\nKEY'
+
+ def test_key_escaping(self):
+ # Ensure the keys contained in the namespace are correctly escaped.
+ namespace = argparse.Namespace(
+ private_key=self.private, public_key=self.public)
+ handle_ssh_keys(namespace)
+ self.assertEqual(
+ self.private.decode('string-escape'),
+ namespace.private_key)
+ self.assertEqual(
+ self.public.decode('string-escape'),
+ namespace.public_key)
+ self.assertTrue(namespace.valid_ssh_keys)
+
+ def test_no_keys(self):
+ # Keys are None if they are not provided and can not be found in the
+ # current home directory.
+ namespace = argparse.Namespace(
+ private_key=None, home_dir='/tmp/__does_not_exist__')
+ handle_ssh_keys(namespace)
+ self.assertIsNone(namespace.private_key)
+ self.assertIsNone(namespace.public_key)
+ self.assertFalse(namespace.valid_ssh_keys)
+
+ def test_only_one_key(self):
+ # Ensure a `ValidationError` is raised if only one key is provided.
+ namespace = argparse.Namespace(
+ private_key=self.private, public_key=None,
+ home_dir='/tmp/__does_not_exist__')
+ with self.assertNotValid('private-key'):
+ handle_ssh_keys(namespace)
+
+
+class HandleUserTest(HandlersTestMixin, unittest.TestCase):
+
+ def test_home_dir(self):
+ # Ensure validator populates namespace with `home_dir` name.
+ username = getpass.getuser()
+ namespace = argparse.Namespace(user=username)
+ handle_user(namespace)
+ self.assertEqual('/home/' + username, namespace.home_dir)
+
+ def test_failure(self):
+ # Ensure the validation fails if the current user is root and
+ # no user is provided.
+ namespace = argparse.Namespace(user=None, euid=0)
+ with self.assertNotValid('user'):
+ handle_user(namespace)
+
+
+class HandleUserDataTest(HandlersTestMixin, unittest.TestCase):
+
+ def test_no_data(self):
+ # If full name and email are not provided, the handler tries to
+ # obtain them using the given `whois` callable.
+ namespace = argparse.Namespace(
+ full_name=None, email=None, user='root')
+ email = 'email@xxxxxxxxxxx'
+ handle_userdata(namespace, lambda user: (user, email))
+ self.assertEqual(namespace.user, namespace.full_name)
+ self.assertEqual(email, namespace.email)
+
+ def test_no_whois(self):
+ # Ensure the validation fails if full name or email are not provided
+ # and they can not be obtained using the `whois` callable.
+ namespace = argparse.Namespace(
+ full_name=None, email=None, user='root')
+ with self.assertNotValid('full-name'):
+ handle_userdata(namespace, lambda user: None)
+
+ def test_non_existent_user(self):
+ # Ensure the validation fails if full name or email are not provided
+ # and the system user does not exist.
+ namespace = argparse.Namespace(
+ full_name=None, email=None, user='this_user_does_not_exist')
+ with self.assertNotValid('full-name'):
+ handle_userdata(namespace, lambda user: None)
+
+ def test_only_one_argument(self):
+ # It does not make sense to provide only one argument.
+ namespace = argparse.Namespace(full_name='Foo Bar', email=None)
+ with self.assertNotValid('full-name'):
+ handle_userdata(namespace, lambda user: None)
=== added file 'lpsetup/tests/test_utils.py'
--- lpsetup/tests/test_utils.py 1970-01-01 00:00:00 +0000
+++ lpsetup/tests/test_utils.py 2012-03-16 11:17:19 +0000
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the utils module."""
+
+import unittest
+
+from lpsetup.utils import get_container_path
+
+
+class GetContainerPathTest(unittest.TestCase):
+
+ def test_root_path(self):
+ # Test the correct container root path is returned.
+ self.assertEqual(
+ '/var/lib/lxc/mycontainer/rootfs/',
+ get_container_path('mycontainer'))
+
+ def test_with_path(self):
+ # If a `path` is given, return that path inside the container.
+ self.assertEqual(
+ '/var/lib/lxc/mycontainer/rootfs/etc/apt/',
+ get_container_path('mycontainer', '/etc/apt/'))
+ self.assertEqual(
+ '/var/lib/lxc/mycontainer/rootfs/home',
+ get_container_path('mycontainer', 'home'))
=== added file 'lpsetup/tests/utils.py'
--- lpsetup/tests/utils.py 1970-01-01 00:00:00 +0000
+++ lpsetup/tests/utils.py 2012-03-16 11:17:19 +0000
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test utilities."""
+
+__metaclass__ = type
+__all__ = [
+ 'capture_error',
+ 'capture_output',
+ ]
+
+from contextlib import contextmanager
+from functools import partial
+from StringIO import StringIO
+import sys
+
+
+@contextmanager
+def capture(attr):
+ output = StringIO()
+ backup = getattr(sys, attr)
+ setattr(sys, attr, output)
+ try:
+ yield output
+ finally:
+ setattr(sys, attr, backup)
+
+
+capture_output = partial(capture, 'stdout')
+capture_error = partial(capture, 'stderr')