← Back to team overview

yellow team mailing list archive

[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')