Merge lp:~frankban/lpsetup/commands-unittests into lp:lpsetup

Proposed by Francesco Banconi
Status: Merged
Approved by: Francesco Banconi
Approved revision: 43
Merged at revision: 34
Proposed branch: lp:~frankban/lpsetup/commands-unittests
Merge into: lp:lpsetup
Diff against target: 627 lines (+376/-51)
11 files modified
lpsetup/argparser.py (+39/-24)
lpsetup/subcommands/get.py (+0/-1)
lpsetup/subcommands/install.py (+4/-2)
lpsetup/subcommands/lxcinstall.py (+7/-10)
lpsetup/tests/subcommands/test_get.py (+66/-0)
lpsetup/tests/subcommands/test_inithost.py (+50/-0)
lpsetup/tests/subcommands/test_install.py (+55/-0)
lpsetup/tests/subcommands/test_lxcinstall.py (+75/-0)
lpsetup/tests/subcommands/test_version.py (+0/-1)
lpsetup/tests/test_argparser.py (+14/-8)
lpsetup/tests/utils.py (+66/-5)
To merge this branch: bzr merge lp:~frankban/lpsetup/commands-unittests
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+112094@code.launchpad.net

Commit message

Added sub commands unit tests.

Description of the change

== Changes ==

sub command tests: added unit tests for the get, inithost, install and lxcinstall sub commands. Each TestCase is a subclass of `tests.utils.StepsBasedSubCommandTestMixin`: this way handlers, steps and needs_root are automatically tested in a declarative way. Note that arguments parsing is also tested for each sub command: see *StepsBasedSubCommandTestMixin* doctest for a more detailed explanation.
Note that update and branch sub commands are not tested: they are obsolete and will either go away or be heavily modified before the end of the process.

argparser.StepsBasedSubCommand: the steps are no longer handled by *__init__*. Added a new *get_steps* method returning a list of *(step_name, step_callable, step_args)* tuples. This way 1) it is easier for a sub command subclassing another command to dynamically modify steps, and 2) we have a single method to call if we want to check the steps that a sub command wants to run.

argparser.StepsBasedSubCommand: added *_include_step* to decouple steps filtering from command execution. Now we have a single method that handle `--skip-steps` or `--steps` options, and the inclusion/exclusion is performed when the steps are retrieved and not when the sub command runs (*subcommand.handle()*).

get: removed *needs_root* to avoid confusion: *needs_root* is unnecessary since the command overrides *get_needs_root()*.

lxcinstall: now the command subclasses install and reuses inithost, install and get steps. A consequence of this change is that now the command correctly handles `directory` and `dependencies_dir` arguments.

tests.utils.SubCommandTestMixin: implemented a *parse* method that parsed command line arguments and returns an initialized namespace. Changed *parse_and_call_main* to reuse the *parse* method and to accept variable positional arguments.

tests.utils: added a helper function returning a random string.

tests.test_argparser: added a test for namespace initialization.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch is really good. These tests will help us quite a bit while we
implement the LEP. Here are my thoughts from reading through the diff:

The combination of the negative test for step_name ("not in steps_to_skip") and
the ternary operator in _include_step is really hard for me to understand.
Maybe this instead:

def _include_step(self, step_name, namespace):
    """Return True if the given *step_name* must be run, False otherwise.

    A step is included in the command execution if the step included in
    `--steps` (or steps is None) and is not included in `--skip-steps`.
    """
    steps_to_skip = namespace.skip_steps or []
    steps_to_run = namespace.steps
    # If explicitly told to skip a step, then skip it.
    if step_name in steps_to_skip:
        return False
    # If no list of steps was provided then any non-skipped are to be run.
    if steps_to_run is None:
        return True
    # A list of steps to run was provided, so the step has to be included in
    # order to be run.
    return step_name in steps_to_run

I am not entirely sure the above is right, which bolsters my feeling that the
original was hard to understand.

Since the subcommand tests always have a get_arguments function which is called
and assigned to expected_arguments, it might be slightly better to just have a
required get_arguments method instead.

I don't feel strongly about it, but for the sake of diversity: here is a
version of get_random_string that is a bit shorter and more direct:

def get_random_string(size=10):
    """Return a random string to be used in tests."""
    return ''.join(random.sample(string.ascii_letters, size))

I really like the explanation in the StepsBasedSubCommandTestMixin docstring of
how to use the class. I wonder if it would be good to leave the "must define"
attributes undefined so AttributeErrors are generated if a subclass leaves them
out.

Revision history for this message
Francesco Banconi (frankban) wrote :

Thanks Benji, I've updated the branch following your suggestions, except for the *get_arguments* method as we discussed in IRC.

Revision history for this message
Benji York (benji) wrote :

Looks great.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lpsetup/argparser.py'
--- lpsetup/argparser.py 2012-03-30 11:02:11 +0000
+++ lpsetup/argparser.py 2012-06-26 15:04:24 +0000
@@ -416,15 +416,15 @@
416416
417 steps = ()417 steps = ()
418418
419 def __init__(self, *args, **kwargs):419 def add_arguments(self, parser):
420 super(StepsBasedSubCommand, self).__init__(*args, **kwargs)420 super(StepsBasedSubCommand, self).add_arguments(parser)
421 self._step_names = []421 step_names = [self._get_step_name(i[0]) for i in self.steps]
422 self._steps = {}422 parser.add_argument(
423 for step_args in self.steps:423 '-s', '--steps', nargs='+', choices=step_names,
424 step, args = step_args[0], step_args[1:]424 help='Call one or more internal functions.')
425 step_name = self._get_step_name(step)425 parser.add_argument(
426 self._step_names.append(step_name)426 '--skip-steps', nargs='+', choices=step_names,
427 self._steps[step_name] = (step, args)427 help='Skip one or more internal functions.')
428428
429 def _get_step_name(self, step):429 def _get_step_name(self, step):
430 """Return the string representation of a step callable.430 """Return the string representation of a step callable.
@@ -450,28 +450,43 @@
450 except AttributeError:450 except AttributeError:
451 return step.__name__451 return step.__name__
452452
453 def add_arguments(self, parser):453 def _include_step(self, step_name, namespace):
454 super(StepsBasedSubCommand, self).add_arguments(parser)454 """Return True if the given *step_name* must be run, False otherwise.
455 parser.add_argument(455
456 '-s', '--steps', nargs='+', choices=self._step_names,456 A step is included in the command execution if the step included in
457 help='Call one or more internal functions.')457 `--steps` (or steps is None) and is not included in `--skip-steps`.
458 parser.add_argument(458 """
459 '--skip-steps', nargs='+', choices=self._step_names,459 steps_to_skip = namespace.skip_steps or []
460 help='Skip one or more internal functions.')460 steps_to_run = namespace.steps
461 # If explicitly told to skip a step, then skip it.
462 if step_name in steps_to_skip:
463 return False
464 # If no list of steps was provided then any non-skipped are to be run.
465 if steps_to_run is None:
466 return True
467 # A list of steps to run was provided, so the step has to be included
468 # in order to be run.
469 return step_name in steps_to_run
470
471 def get_steps(self, namespace):
472 """Return a list of *(step_name, step_callable, step_args)* tuples."""
473 steps = []
474 for step_arg_names in self.steps:
475 step, arg_names = step_arg_names[0], step_arg_names[1:]
476 step_name = self._get_step_name(step)
477 if self._include_step(step_name, namespace):
478 args = [getattr(namespace, i) for i in arg_names]
479 steps.append((step_name, step, args))
480 return steps
461481
462 def _call_step(self, namespace, step, args):482 def _call_step(self, namespace, step, args):
463 """Default callable used to run a `step`, using given `args`."""483 """Default callable used to run a `step`, using given `args`."""
464 return step(*args)484 return step(*args)
465485
466 def handle(self, namespace):486 def handle(self, namespace):
467 skip_steps = namespace.skip_steps or []
468 step_names = filter(
469 lambda step_name: step_name not in skip_steps,
470 namespace.steps or self._step_names)
471 default_step_runner = self._call_step487 default_step_runner = self._call_step
472 for step_name in step_names:488 for step_name, step, args in self.get_steps(namespace):
473 step, arg_names = self._steps[step_name]489 # Run the step using a dynamic dispatcher.
474 args = [getattr(namespace, i) for i in arg_names]
475 step_runner = getattr(490 step_runner = getattr(
476 self, 'call_' + step_name, default_step_runner)491 self, 'call_' + step_name, default_step_runner)
477 try:492 try:
478493
=== modified file 'lpsetup/subcommands/get.py'
--- lpsetup/subcommands/get.py 2012-06-25 19:24:56 +0000
+++ lpsetup/subcommands/get.py 2012-06-26 15:04:24 +0000
@@ -158,7 +158,6 @@
158 )158 )
159159
160 help = __doc__160 help = __doc__
161 needs_root = True
162 validators = (161 validators = (
163 handlers.handle_user,162 handlers.handle_user,
164 handlers.handle_lpuser,163 handlers.handle_lpuser,
165164
=== modified file 'lpsetup/subcommands/install.py'
--- lpsetup/subcommands/install.py 2012-06-22 20:50:41 +0000
+++ lpsetup/subcommands/install.py 2012-06-26 15:04:24 +0000
@@ -99,11 +99,13 @@
99 """Install the Launchpad environment."""99 """Install the Launchpad environment."""
100100
101 # The steps for "install" are a superset of the steps for "inithost".101 # The steps for "install" are a superset of the steps for "inithost".
102 setup_bzr_locations_step = (setup_bzr_locations,
103 'user', 'lpuser', 'directory')
104
102 steps = (105 steps = (
103 inithost.SubCommand.initialize_step,106 inithost.SubCommand.initialize_step,
104 get.SubCommand.fetch_step,107 get.SubCommand.fetch_step,
105 (setup_bzr_locations,108 setup_bzr_locations_step,
106 'user', 'lpuser', 'directory'),
107 inithost.SubCommand.setup_apt_step,109 inithost.SubCommand.setup_apt_step,
108 (setup_launchpad,110 (setup_launchpad,
109 'user', 'dependencies_dir', 'directory', 'valid_ssh_keys'),111 'user', 'dependencies_dir', 'directory', 'valid_ssh_keys'),
110112
=== modified file 'lpsetup/subcommands/lxcinstall.py'
--- lpsetup/subcommands/lxcinstall.py 2012-06-25 17:55:10 +0000
+++ lpsetup/subcommands/lxcinstall.py 2012-06-26 15:04:24 +0000
@@ -43,6 +43,7 @@
43 SCRIPTS,43 SCRIPTS,
44 )44 )
45from lpsetup.subcommands import (45from lpsetup.subcommands import (
46 get,
46 inithost,47 inithost,
47 install,48 install,
48 )49 )
@@ -201,16 +202,13 @@
201 subprocess.call(['lxc-stop', '-n', lxc_name])202 subprocess.call(['lxc-stop', '-n', lxc_name])
202203
203204
204class SubCommand(inithost.SubCommand):205class SubCommand(install.SubCommand):
205 """Install the Launchpad environment inside an LXC."""206 """Install the Launchpad environment inside an LXC."""
206207
207 steps = (208 steps = (
208 (inithost.initialize,209 inithost.SubCommand.initialize_step,
209 'user', 'full_name', 'email', 'lpuser',210 get.SubCommand.fetch_step,
210 'private_key', 'public_key', 'valid_ssh_keys', 'ssh_key_path',211 install.SubCommand.setup_bzr_locations_step,
211 'feed_random', 'dependencies_dir', 'directory'),
212 (install.setup_bzr_locations,
213 'user', 'lpuser', 'directory'),
214 (create_scripts,212 (create_scripts,
215 'user', 'lxc_name', 'ssh_key_path'),213 'user', 'lxc_name', 'ssh_key_path'),
216 (create_lxc,214 (create_lxc,
@@ -231,8 +229,7 @@
231229
232 def get_validators(self, namespace):230 def get_validators(self, namespace):
233 validators = super(SubCommand, self).get_validators(namespace)231 validators = super(SubCommand, self).get_validators(namespace)
234 return validators + (232 return validators + (handlers.handle_testing,)
235 handlers.handle_testing, handlers.handle_directories)
236233
237 def call_create_scripts(self, namespace, step, args):234 def call_create_scripts(self, namespace, step, args):
238 """Run the `create_scripts` step only if the related flag is set."""235 """Run the `create_scripts` step only if the related flag is set."""
@@ -257,7 +254,7 @@
257 parser.add_argument(254 parser.add_argument(
258 '-C', '--create-scripts', action='store_true',255 '-C', '--create-scripts', action='store_true',
259 help='Create the scripts used by buildbot for parallel testing.')256 help='Create the scripts used by buildbot for parallel testing.')
260 # The following flag is not present in the install sub command since257 # The following flag is not present in the inithost sub command since
261 # subunit is always installed there as a dependency of258 # subunit is always installed there as a dependency of
262 # launchpad-developer-dependencies.259 # launchpad-developer-dependencies.
263 parser.add_argument(260 parser.add_argument(
264261
=== added file 'lpsetup/tests/subcommands/test_get.py'
--- lpsetup/tests/subcommands/test_get.py 1970-01-01 00:00:00 +0000
+++ lpsetup/tests/subcommands/test_get.py 2012-06-26 15:04:24 +0000
@@ -0,0 +1,66 @@
1#!/usr/bin/env python
2# Copyright 2012 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).
4
5"""Tests for the get sub command."""
6
7import unittest
8
9from lpsetup import handlers
10from lpsetup.subcommands import get
11from lpsetup.tests.utils import (
12 get_random_string,
13 StepsBasedSubCommandTestMixin,
14 )
15
16
17fetch_step = (
18 get.fetch, ['user', 'directory', 'dependencies_dir', 'valid_ssh_keys'])
19update_launchpad_step = (
20 get.update_launchpad, ['user', 'dependencies_dir', 'directory',
21 'make_schema', 'apt'])
22link_sourcecode_in_branches_step = (
23 get.link_sourcecode_in_branches, ['user', 'dependencies_dir',
24 'directory'])
25
26
27def get_update_arguments():
28 user = get_random_string()
29 directory = '~/' + get_random_string()
30 dependencies_dir = '~/' + get_random_string()
31 ssh_key_name = get_random_string()
32 return (
33 '-u', user, '-c', directory, '--make-schema',
34 '-d', dependencies_dir, '-S', ssh_key_name,
35 )
36
37
38def get_arguments():
39 email = get_random_string()
40 full_name = get_random_string() + '@example.com'
41 lpuser = get_random_string()
42 private_key = get_random_string()
43 public_key = get_random_string()
44 return get_update_arguments() + (
45 '-e', email, '-f', full_name, '-l', lpuser,
46 '-v', private_key, '-b', public_key,
47 '--no-repositories', '--feed-random')
48
49
50class GetTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
51
52 sub_command_class = get.SubCommand
53 expected_arguments = get_arguments()
54 expected_handlers = (
55 handlers.handle_user,
56 handlers.handle_lpuser,
57 handlers.handle_userdata,
58 handlers.handle_ssh_keys,
59 handlers.handle_directories,
60 )
61 expected_steps = (
62 fetch_step,
63 update_launchpad_step,
64 link_sourcecode_in_branches_step,
65 )
66 needs_root = False
067
=== added file 'lpsetup/tests/subcommands/test_inithost.py'
--- lpsetup/tests/subcommands/test_inithost.py 1970-01-01 00:00:00 +0000
+++ lpsetup/tests/subcommands/test_inithost.py 2012-06-26 15:04:24 +0000
@@ -0,0 +1,50 @@
1#!/usr/bin/env python
2# Copyright 2012 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).
4
5"""Tests for the inithost sub command."""
6
7import unittest
8
9from lpsetup import handlers
10from lpsetup.subcommands import inithost
11from lpsetup.tests.utils import (
12 get_random_string,
13 StepsBasedSubCommandTestMixin,
14 )
15
16
17initialize_step = (
18 inithost.initialize, ['user', 'full_name', 'email', 'lpuser',
19 'private_key', 'public_key', 'valid_ssh_keys', 'ssh_key_path',
20 'feed_random'])
21setup_apt_step = (inithost.setup_apt, ['no_repositories'])
22
23
24def get_arguments():
25 user = get_random_string()
26 email = get_random_string()
27 full_name = get_random_string() + '@example.com'
28 lpuser = get_random_string()
29 private_key = get_random_string()
30 public_key = get_random_string()
31 ssh_key_name = get_random_string()
32 return (
33 '-u', user, '-e', email, '-f', full_name, '-l', lpuser,
34 '-v', private_key, '-b', public_key, '-S', ssh_key_name)
35
36
37class InithostTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
38
39 sub_command_class = inithost.SubCommand
40 expected_arguments = get_arguments()
41 expected_handlers = (
42 handlers.handle_user,
43 handlers.handle_lpuser,
44 handlers.handle_userdata,
45 handlers.handle_ssh_keys,
46 )
47 expected_steps = (initialize_step, setup_apt_step)
48 needs_root = True
49
50
051
=== added file 'lpsetup/tests/subcommands/test_install.py'
--- lpsetup/tests/subcommands/test_install.py 1970-01-01 00:00:00 +0000
+++ lpsetup/tests/subcommands/test_install.py 2012-06-26 15:04:24 +0000
@@ -0,0 +1,55 @@
1#!/usr/bin/env python
2# Copyright 2012 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).
4
5"""Tests for the install sub command."""
6
7import unittest
8
9from lpsetup import handlers
10from lpsetup.subcommands import install
11from lpsetup.tests.subcommands import (
12 test_get,
13 test_inithost,
14 )
15from lpsetup.tests.utils import (
16 get_random_string,
17 StepsBasedSubCommandTestMixin,
18 )
19
20
21setup_bzr_locations_step = (
22 install.setup_bzr_locations, ['user', 'lpuser', 'directory'])
23setup_launchpad_step = (
24 install.setup_launchpad, ['user', 'dependencies_dir', 'directory',
25 'valid_ssh_keys'])
26
27
28def get_arguments():
29 inithost_arguments = test_inithost.get_arguments()
30 dependencies_dir = '~/' + get_random_string()
31 directory = '~/' + get_random_string()
32 return inithost_arguments + ('-d', dependencies_dir, '-c', directory)
33
34
35class InstallTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
36
37 sub_command_class = install.SubCommand
38 expected_arguments = get_arguments()
39 expected_handlers = (
40 handlers.handle_user,
41 handlers.handle_lpuser,
42 handlers.handle_userdata,
43 handlers.handle_ssh_keys,
44 handlers.handle_directories,
45 )
46 expected_steps = (
47 test_inithost.initialize_step,
48 test_get.fetch_step,
49 setup_bzr_locations_step,
50 test_inithost.setup_apt_step,
51 setup_launchpad_step,
52 )
53 needs_root = True
54
55
056
=== added file 'lpsetup/tests/subcommands/test_lxcinstall.py'
--- lpsetup/tests/subcommands/test_lxcinstall.py 1970-01-01 00:00:00 +0000
+++ lpsetup/tests/subcommands/test_lxcinstall.py 2012-06-26 15:04:24 +0000
@@ -0,0 +1,75 @@
1#!/usr/bin/env python
2# Copyright 2012 Canonical Ltd. This software is licensed under the
3# GNU Affero General Public License version 3 (see the file LICENSE).
4
5"""Tests for the lxcinstall sub command."""
6
7import random
8import unittest
9
10from lpsetup import (
11 handlers,
12 settings,
13 )
14from lpsetup.subcommands import lxcinstall
15from lpsetup.tests.subcommands import (
16 test_get,
17 test_inithost,
18 test_install,
19 )
20from lpsetup.tests.utils import (
21 get_random_string,
22 StepsBasedSubCommandTestMixin,
23 )
24
25
26create_scripts_step = (
27 lxcinstall.create_scripts, ['user', 'lxc_name', 'ssh_key_path'])
28create_lxc_step = (
29 lxcinstall.create_lxc, ['user', 'lxc_name', 'lxc_arch', 'lxc_os',
30 'install_subunit'])
31start_lxc_step = (lxcinstall.start_lxc, ['lxc_name'])
32wait_for_lxc_step = (lxcinstall.wait_for_lxc, ['lxc_name', 'ssh_key_path'])
33initialize_lxc_step = (
34 lxcinstall.initialize_lxc, ['lxc_name', 'ssh_key_path', 'lxc_os'])
35setup_launchpad_lxc_step = (
36 lxcinstall.setup_launchpad_lxc, ['user', 'dependencies_dir', 'directory',
37 'valid_ssh_keys', 'ssh_key_path', 'lxc_name'])
38stop_lxc_step = (lxcinstall.stop_lxc, ['lxc_name', 'ssh_key_path'])
39
40
41def get_arguments():
42 lxc_name = get_random_string()
43 lxc_arch = random.choice(['i386', 'amd64'])
44 lxc_os = random.choice(settings.LXC_GUEST_CHOICES)
45 return test_inithost.get_arguments() + (
46 '-n', lxc_name, '-A', lxc_arch, '-r', lxc_os,
47 '--create-scripts', '--install-subunit', '--testing'
48 )
49
50
51class LxcInstallTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
52
53 sub_command_class = lxcinstall.SubCommand
54 expected_arguments = get_arguments()
55 expected_handlers = (
56 handlers.handle_user,
57 handlers.handle_lpuser,
58 handlers.handle_userdata,
59 handlers.handle_ssh_keys,
60 handlers.handle_directories,
61 handlers.handle_testing,
62 )
63 expected_steps = (
64 test_inithost.initialize_step,
65 test_get.fetch_step,
66 test_install.setup_bzr_locations_step,
67 create_scripts_step,
68 create_lxc_step,
69 start_lxc_step,
70 wait_for_lxc_step,
71 initialize_lxc_step,
72 setup_launchpad_lxc_step,
73 stop_lxc_step,
74 )
75 needs_root = True
076
=== modified file 'lpsetup/tests/subcommands/test_version.py'
--- lpsetup/tests/subcommands/test_version.py 2012-05-22 09:36:57 +0000
+++ lpsetup/tests/subcommands/test_version.py 2012-06-26 15:04:24 +0000
@@ -17,7 +17,6 @@
17class VersionTest(SubCommandTestMixin, unittest.TestCase):17class VersionTest(SubCommandTestMixin, unittest.TestCase):
1818
19 sub_command_class = version.SubCommand19 sub_command_class = version.SubCommand
20 sub_command_name = 'subcmd'
2120
22 def test_sub_command(self):21 def test_sub_command(self):
23 with capture_output() as output:22 with capture_output() as output:
2423
=== modified file 'lpsetup/tests/test_argparser.py'
--- lpsetup/tests/test_argparser.py 2012-05-22 09:36:57 +0000
+++ lpsetup/tests/test_argparser.py 2012-06-26 15:04:24 +0000
@@ -96,7 +96,7 @@
9696
97 def test_arguments(self):97 def test_arguments(self):
98 # Ensure the sub command arguments are correctly handled.98 # Ensure the sub command arguments are correctly handled.
99 namespace = self.parse_and_call_main('--foo eggs')99 namespace = self.parse_and_call_main('--foo', 'eggs')
100 self.assertEqual('eggs', namespace.foo)100 self.assertEqual('eggs', namespace.foo)
101101
102 def test_successful_validation(self):102 def test_successful_validation(self):
@@ -121,6 +121,12 @@
121 self.assertIn(self.sub_command.name, help)121 self.assertIn(self.sub_command.name, help)
122 self.assertIn(self.sub_command.help, help)122 self.assertIn(self.sub_command.help, help)
123123
124 def test_init_namespace(self):
125 # The namespace is initialized with current user info.
126 namespace = self.parse()
127 self.assertIsInstance(namespace.euid, int)
128 self.assertIsInstance(namespace.run_as_root, bool)
129
124130
125class StepsBasedSubCommandTest(SubCommandTestMixin, unittest.TestCase):131class StepsBasedSubCommandTest(SubCommandTestMixin, unittest.TestCase):
126132
@@ -129,23 +135,23 @@
129 def test_steps(self):135 def test_steps(self):
130 # Ensure steps are executed in the order they are provided.136 # Ensure steps are executed in the order they are provided.
131 with capture_output() as output:137 with capture_output() as output:
132 self.parse_and_call_main('--foo eggs --bar spam')138 self.parse_and_call_main('--foo', 'eggs', '--bar', 'spam')
133 self.check_output(139 self.check_output(
134 ['step1 received eggs', 'step2 received eggs and spam'],140 ['step1 received eggs', 'step2 received eggs and spam'],
135 output)141 output)
136142
137 def test_step_flag(self):143 def test_steps_flag(self):
138 # A special argument `-s` or `--steps` is automatically added to the144 # A special argument `-s` or `--steps` is automatically added to the
139 # parser. It can be used to execute only one or a subset of steps.145 # parser. It can be used to execute only one or a subset of steps.
140 with capture_output() as output:146 with capture_output() as output:
141 self.parse_and_call_main('--foo eggs -s step1')147 self.parse_and_call_main('--foo', 'eggs', '-s', 'step1')
142 self.check_output(['step1 received eggs'], output)148 self.check_output(['step1 received eggs'], output)
143149
144 def test_skip_steps(self):150 def test_skip_steps_flag(self):
145 # A special argument `--skip-steps` is automatically added to the151 # A special argument `--skip-steps` is automatically added to the
146 # parser. It can be used to skip one or more steps.152 # parser. It can be used to skip one or more steps.
147 with capture_output() as output:153 with capture_output() as output:
148 self.parse_and_call_main('--foo eggs --skip-steps step1')154 self.parse_and_call_main('--foo', 'eggs', '--skip-steps', 'step1')
149 self.check_output(['step2 received eggs and None'], output)155 self.check_output(['step2 received eggs and None'], output)
150156
151 def test_step_name(self):157 def test_step_name(self):
@@ -164,7 +170,7 @@
164 # Ensure the steps execution is stopped if a step raises170 # Ensure the steps execution is stopped if a step raises
165 # `subprocess.CalledProcessError`.171 # `subprocess.CalledProcessError`.
166 with capture_output() as output:172 with capture_output() as output:
167 error = self.parse_and_call_main('--foo eggs')173 error = self.parse_and_call_main('--foo', 'eggs')
168 self.assertEqual(1, error.returncode)174 self.assertEqual(1, error.returncode)
169 self.check_output(['step1 received eggs'], output)175 self.check_output(['step1 received eggs'], output)
170176
@@ -177,7 +183,7 @@
177 # The test runner calls a function named 'call_[step name]' if it is183 # The test runner calls a function named 'call_[step name]' if it is
178 # defined.184 # defined.
179 with capture_output() as output:185 with capture_output() as output:
180 self.parse_and_call_main('--foo eggs --bar spam')186 self.parse_and_call_main('--foo', 'eggs', '--bar', 'spam')
181 expected = [187 expected = [
182 'running step1 with eggs while bar is spam',188 'running step1 with eggs while bar is spam',
183 'step1 received eggs',189 'step1 received eggs',
184190
=== modified file 'lpsetup/tests/utils.py'
--- lpsetup/tests/utils.py 2012-05-22 09:36:57 +0000
+++ lpsetup/tests/utils.py 2012-06-26 15:04:24 +0000
@@ -8,10 +8,15 @@
8__all__ = [8__all__ = [
9 'capture_error',9 'capture_error',
10 'capture_output',10 'capture_output',
11 'get_random_string',
12 'StepsBasedSubCommandTestMixin',
13 'SubCommandTestMixin',
11 ]14 ]
1215
13from contextlib import contextmanager16from contextlib import contextmanager
14from functools import partial17from functools import partial
18import random
19import string
15from StringIO import StringIO20from StringIO import StringIO
16import sys21import sys
1722
@@ -34,23 +39,79 @@
34capture_error = partial(capture, 'stderr')39capture_error = partial(capture, 'stderr')
3540
3641
42def get_random_string(size=10):
43 """Return a random string to be used in tests."""
44 return ''.join(random.sample(string.ascii_letters, size))
45
46
37class SubCommandTestMixin(object):47class SubCommandTestMixin(object):
3848
39 sub_command_class = examples.SubCommand49 sub_command_class = examples.SubCommand
40 sub_command_name = 'subcmd'50 sub_command_name = 'subcmd'
4151
42 def setUp(self):52 def setUp(self):
53 """Set up an argument parser and instantiate *self.sub_command_class*.
54
55 The name used to create the sub command instance is
56 *self.sub_command_name*.
57 """
43 self.parser = argparser.ArgumentParser()58 self.parser = argparser.ArgumentParser()
44 self.sub_command = self.parser.register_subcommand(59 self.sub_command = self.parser.register_subcommand(
45 self.sub_command_name, self.sub_command_class)60 self.sub_command_name, self.sub_command_class)
4661
47 def parse_and_call_main(self, arguments=None):62 def parse(self, *args):
48 args = [self.sub_command_name]63 """Parse given *args* and return an initialized namespace object."""
49 if arguments is not None:64 namespace = self.parser.parse_args((self.sub_command_name,) + args)
50 args.extend(arguments.split())65 sub_command = self.sub_command
51 namespace = self.parser.parse_args(args)66 sub_command.init_namespace(namespace)
67 sub_command.validate(self.parser, namespace)
68 return namespace
69
70 def parse_and_call_main(self, *args):
71 """Create a namespace using the given *args* and invoke main."""
72 namespace = self.parse(*args)
52 return namespace.main(namespace)73 return namespace.main(namespace)
5374
54 def check_output(self, expected, output):75 def check_output(self, expected, output):
55 value = filter(None, output.getvalue().split('\n'))76 value = filter(None, output.getvalue().split('\n'))
56 self.assertSequenceEqual(expected, value)77 self.assertSequenceEqual(expected, value)
78
79
80class StepsBasedSubCommandTestMixin(SubCommandTestMixin):
81 """This mixin can be used to test sub commands steps and handlers.
82
83 Real TestCases subclassing this mixin must define:
84
85 - expected_arguments: a sequence of command line arguments
86 used by the current tested sub command
87 - expected_handlers: a sequence of expected handler callables
88 - expected_steps: a sequence of expected *(step_callable, arg_names)*
89 - needs_root: True if this sub command must be run as root
90
91 At this point steps and handlers are automatically tested, and the test
92 case also checks if root is required by the sub command.
93 """
94 def setUp(self):
95 """Set up a namespace using *self.expected_arguments*."""
96 super(StepsBasedSubCommandTestMixin, self).setUp()
97 self.namespace = self.parse(*self.expected_arguments)
98
99 def test_handlers(self):
100 # Ensure this sub command uses the expected handlers.
101 handlers = self.sub_command.get_validators(self.namespace)
102 self.assertSequenceEqual(self.expected_handlers, handlers)
103
104 def test_steps(self):
105 # Ensure this sub command wants to run the expected steps.
106 steps = self.sub_command.get_steps(self.namespace)
107 real_steps = [[step, list(args)] for _, step, args in steps]
108 expected_steps = []
109 for step, arg_names in self.expected_steps:
110 args = [getattr(self.namespace, name) for name in arg_names]
111 expected_steps.append([step, args])
112 self.assertListEqual(expected_steps, real_steps)
113
114 def test_needs_root(self):
115 # The root user may or may not be required to run this sub command.
116 needs_root = self.sub_command.get_needs_root(self.namespace)
117 self.assertEqual(self.needs_root, needs_root)

Subscribers

People subscribed via source and target branches

to all changes: