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
1=== modified file 'lpsetup/argparser.py'
2--- lpsetup/argparser.py 2012-03-30 11:02:11 +0000
3+++ lpsetup/argparser.py 2012-06-26 15:04:24 +0000
4@@ -416,15 +416,15 @@
5
6 steps = ()
7
8- def __init__(self, *args, **kwargs):
9- super(StepsBasedSubCommand, self).__init__(*args, **kwargs)
10- self._step_names = []
11- self._steps = {}
12- for step_args in self.steps:
13- step, args = step_args[0], step_args[1:]
14- step_name = self._get_step_name(step)
15- self._step_names.append(step_name)
16- self._steps[step_name] = (step, args)
17+ def add_arguments(self, parser):
18+ super(StepsBasedSubCommand, self).add_arguments(parser)
19+ step_names = [self._get_step_name(i[0]) for i in self.steps]
20+ parser.add_argument(
21+ '-s', '--steps', nargs='+', choices=step_names,
22+ help='Call one or more internal functions.')
23+ parser.add_argument(
24+ '--skip-steps', nargs='+', choices=step_names,
25+ help='Skip one or more internal functions.')
26
27 def _get_step_name(self, step):
28 """Return the string representation of a step callable.
29@@ -450,28 +450,43 @@
30 except AttributeError:
31 return step.__name__
32
33- def add_arguments(self, parser):
34- super(StepsBasedSubCommand, self).add_arguments(parser)
35- parser.add_argument(
36- '-s', '--steps', nargs='+', choices=self._step_names,
37- help='Call one or more internal functions.')
38- parser.add_argument(
39- '--skip-steps', nargs='+', choices=self._step_names,
40- help='Skip one or more internal functions.')
41+ def _include_step(self, step_name, namespace):
42+ """Return True if the given *step_name* must be run, False otherwise.
43+
44+ A step is included in the command execution if the step included in
45+ `--steps` (or steps is None) and is not included in `--skip-steps`.
46+ """
47+ steps_to_skip = namespace.skip_steps or []
48+ steps_to_run = namespace.steps
49+ # If explicitly told to skip a step, then skip it.
50+ if step_name in steps_to_skip:
51+ return False
52+ # If no list of steps was provided then any non-skipped are to be run.
53+ if steps_to_run is None:
54+ return True
55+ # A list of steps to run was provided, so the step has to be included
56+ # in order to be run.
57+ return step_name in steps_to_run
58+
59+ def get_steps(self, namespace):
60+ """Return a list of *(step_name, step_callable, step_args)* tuples."""
61+ steps = []
62+ for step_arg_names in self.steps:
63+ step, arg_names = step_arg_names[0], step_arg_names[1:]
64+ step_name = self._get_step_name(step)
65+ if self._include_step(step_name, namespace):
66+ args = [getattr(namespace, i) for i in arg_names]
67+ steps.append((step_name, step, args))
68+ return steps
69
70 def _call_step(self, namespace, step, args):
71 """Default callable used to run a `step`, using given `args`."""
72 return step(*args)
73
74 def handle(self, namespace):
75- skip_steps = namespace.skip_steps or []
76- step_names = filter(
77- lambda step_name: step_name not in skip_steps,
78- namespace.steps or self._step_names)
79 default_step_runner = self._call_step
80- for step_name in step_names:
81- step, arg_names = self._steps[step_name]
82- args = [getattr(namespace, i) for i in arg_names]
83+ for step_name, step, args in self.get_steps(namespace):
84+ # Run the step using a dynamic dispatcher.
85 step_runner = getattr(
86 self, 'call_' + step_name, default_step_runner)
87 try:
88
89=== modified file 'lpsetup/subcommands/get.py'
90--- lpsetup/subcommands/get.py 2012-06-25 19:24:56 +0000
91+++ lpsetup/subcommands/get.py 2012-06-26 15:04:24 +0000
92@@ -158,7 +158,6 @@
93 )
94
95 help = __doc__
96- needs_root = True
97 validators = (
98 handlers.handle_user,
99 handlers.handle_lpuser,
100
101=== modified file 'lpsetup/subcommands/install.py'
102--- lpsetup/subcommands/install.py 2012-06-22 20:50:41 +0000
103+++ lpsetup/subcommands/install.py 2012-06-26 15:04:24 +0000
104@@ -99,11 +99,13 @@
105 """Install the Launchpad environment."""
106
107 # The steps for "install" are a superset of the steps for "inithost".
108+ setup_bzr_locations_step = (setup_bzr_locations,
109+ 'user', 'lpuser', 'directory')
110+
111 steps = (
112 inithost.SubCommand.initialize_step,
113 get.SubCommand.fetch_step,
114- (setup_bzr_locations,
115- 'user', 'lpuser', 'directory'),
116+ setup_bzr_locations_step,
117 inithost.SubCommand.setup_apt_step,
118 (setup_launchpad,
119 'user', 'dependencies_dir', 'directory', 'valid_ssh_keys'),
120
121=== modified file 'lpsetup/subcommands/lxcinstall.py'
122--- lpsetup/subcommands/lxcinstall.py 2012-06-25 17:55:10 +0000
123+++ lpsetup/subcommands/lxcinstall.py 2012-06-26 15:04:24 +0000
124@@ -43,6 +43,7 @@
125 SCRIPTS,
126 )
127 from lpsetup.subcommands import (
128+ get,
129 inithost,
130 install,
131 )
132@@ -201,16 +202,13 @@
133 subprocess.call(['lxc-stop', '-n', lxc_name])
134
135
136-class SubCommand(inithost.SubCommand):
137+class SubCommand(install.SubCommand):
138 """Install the Launchpad environment inside an LXC."""
139
140 steps = (
141- (inithost.initialize,
142- 'user', 'full_name', 'email', 'lpuser',
143- 'private_key', 'public_key', 'valid_ssh_keys', 'ssh_key_path',
144- 'feed_random', 'dependencies_dir', 'directory'),
145- (install.setup_bzr_locations,
146- 'user', 'lpuser', 'directory'),
147+ inithost.SubCommand.initialize_step,
148+ get.SubCommand.fetch_step,
149+ install.SubCommand.setup_bzr_locations_step,
150 (create_scripts,
151 'user', 'lxc_name', 'ssh_key_path'),
152 (create_lxc,
153@@ -231,8 +229,7 @@
154
155 def get_validators(self, namespace):
156 validators = super(SubCommand, self).get_validators(namespace)
157- return validators + (
158- handlers.handle_testing, handlers.handle_directories)
159+ return validators + (handlers.handle_testing,)
160
161 def call_create_scripts(self, namespace, step, args):
162 """Run the `create_scripts` step only if the related flag is set."""
163@@ -257,7 +254,7 @@
164 parser.add_argument(
165 '-C', '--create-scripts', action='store_true',
166 help='Create the scripts used by buildbot for parallel testing.')
167- # The following flag is not present in the install sub command since
168+ # The following flag is not present in the inithost sub command since
169 # subunit is always installed there as a dependency of
170 # launchpad-developer-dependencies.
171 parser.add_argument(
172
173=== added file 'lpsetup/tests/subcommands/test_get.py'
174--- lpsetup/tests/subcommands/test_get.py 1970-01-01 00:00:00 +0000
175+++ lpsetup/tests/subcommands/test_get.py 2012-06-26 15:04:24 +0000
176@@ -0,0 +1,66 @@
177+#!/usr/bin/env python
178+# Copyright 2012 Canonical Ltd. This software is licensed under the
179+# GNU Affero General Public License version 3 (see the file LICENSE).
180+
181+"""Tests for the get sub command."""
182+
183+import unittest
184+
185+from lpsetup import handlers
186+from lpsetup.subcommands import get
187+from lpsetup.tests.utils import (
188+ get_random_string,
189+ StepsBasedSubCommandTestMixin,
190+ )
191+
192+
193+fetch_step = (
194+ get.fetch, ['user', 'directory', 'dependencies_dir', 'valid_ssh_keys'])
195+update_launchpad_step = (
196+ get.update_launchpad, ['user', 'dependencies_dir', 'directory',
197+ 'make_schema', 'apt'])
198+link_sourcecode_in_branches_step = (
199+ get.link_sourcecode_in_branches, ['user', 'dependencies_dir',
200+ 'directory'])
201+
202+
203+def get_update_arguments():
204+ user = get_random_string()
205+ directory = '~/' + get_random_string()
206+ dependencies_dir = '~/' + get_random_string()
207+ ssh_key_name = get_random_string()
208+ return (
209+ '-u', user, '-c', directory, '--make-schema',
210+ '-d', dependencies_dir, '-S', ssh_key_name,
211+ )
212+
213+
214+def get_arguments():
215+ email = get_random_string()
216+ full_name = get_random_string() + '@example.com'
217+ lpuser = get_random_string()
218+ private_key = get_random_string()
219+ public_key = get_random_string()
220+ return get_update_arguments() + (
221+ '-e', email, '-f', full_name, '-l', lpuser,
222+ '-v', private_key, '-b', public_key,
223+ '--no-repositories', '--feed-random')
224+
225+
226+class GetTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
227+
228+ sub_command_class = get.SubCommand
229+ expected_arguments = get_arguments()
230+ expected_handlers = (
231+ handlers.handle_user,
232+ handlers.handle_lpuser,
233+ handlers.handle_userdata,
234+ handlers.handle_ssh_keys,
235+ handlers.handle_directories,
236+ )
237+ expected_steps = (
238+ fetch_step,
239+ update_launchpad_step,
240+ link_sourcecode_in_branches_step,
241+ )
242+ needs_root = False
243
244=== added file 'lpsetup/tests/subcommands/test_inithost.py'
245--- lpsetup/tests/subcommands/test_inithost.py 1970-01-01 00:00:00 +0000
246+++ lpsetup/tests/subcommands/test_inithost.py 2012-06-26 15:04:24 +0000
247@@ -0,0 +1,50 @@
248+#!/usr/bin/env python
249+# Copyright 2012 Canonical Ltd. This software is licensed under the
250+# GNU Affero General Public License version 3 (see the file LICENSE).
251+
252+"""Tests for the inithost sub command."""
253+
254+import unittest
255+
256+from lpsetup import handlers
257+from lpsetup.subcommands import inithost
258+from lpsetup.tests.utils import (
259+ get_random_string,
260+ StepsBasedSubCommandTestMixin,
261+ )
262+
263+
264+initialize_step = (
265+ inithost.initialize, ['user', 'full_name', 'email', 'lpuser',
266+ 'private_key', 'public_key', 'valid_ssh_keys', 'ssh_key_path',
267+ 'feed_random'])
268+setup_apt_step = (inithost.setup_apt, ['no_repositories'])
269+
270+
271+def get_arguments():
272+ user = get_random_string()
273+ email = get_random_string()
274+ full_name = get_random_string() + '@example.com'
275+ lpuser = get_random_string()
276+ private_key = get_random_string()
277+ public_key = get_random_string()
278+ ssh_key_name = get_random_string()
279+ return (
280+ '-u', user, '-e', email, '-f', full_name, '-l', lpuser,
281+ '-v', private_key, '-b', public_key, '-S', ssh_key_name)
282+
283+
284+class InithostTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
285+
286+ sub_command_class = inithost.SubCommand
287+ expected_arguments = get_arguments()
288+ expected_handlers = (
289+ handlers.handle_user,
290+ handlers.handle_lpuser,
291+ handlers.handle_userdata,
292+ handlers.handle_ssh_keys,
293+ )
294+ expected_steps = (initialize_step, setup_apt_step)
295+ needs_root = True
296+
297+
298
299=== added file 'lpsetup/tests/subcommands/test_install.py'
300--- lpsetup/tests/subcommands/test_install.py 1970-01-01 00:00:00 +0000
301+++ lpsetup/tests/subcommands/test_install.py 2012-06-26 15:04:24 +0000
302@@ -0,0 +1,55 @@
303+#!/usr/bin/env python
304+# Copyright 2012 Canonical Ltd. This software is licensed under the
305+# GNU Affero General Public License version 3 (see the file LICENSE).
306+
307+"""Tests for the install sub command."""
308+
309+import unittest
310+
311+from lpsetup import handlers
312+from lpsetup.subcommands import install
313+from lpsetup.tests.subcommands import (
314+ test_get,
315+ test_inithost,
316+ )
317+from lpsetup.tests.utils import (
318+ get_random_string,
319+ StepsBasedSubCommandTestMixin,
320+ )
321+
322+
323+setup_bzr_locations_step = (
324+ install.setup_bzr_locations, ['user', 'lpuser', 'directory'])
325+setup_launchpad_step = (
326+ install.setup_launchpad, ['user', 'dependencies_dir', 'directory',
327+ 'valid_ssh_keys'])
328+
329+
330+def get_arguments():
331+ inithost_arguments = test_inithost.get_arguments()
332+ dependencies_dir = '~/' + get_random_string()
333+ directory = '~/' + get_random_string()
334+ return inithost_arguments + ('-d', dependencies_dir, '-c', directory)
335+
336+
337+class InstallTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
338+
339+ sub_command_class = install.SubCommand
340+ expected_arguments = get_arguments()
341+ expected_handlers = (
342+ handlers.handle_user,
343+ handlers.handle_lpuser,
344+ handlers.handle_userdata,
345+ handlers.handle_ssh_keys,
346+ handlers.handle_directories,
347+ )
348+ expected_steps = (
349+ test_inithost.initialize_step,
350+ test_get.fetch_step,
351+ setup_bzr_locations_step,
352+ test_inithost.setup_apt_step,
353+ setup_launchpad_step,
354+ )
355+ needs_root = True
356+
357+
358
359=== added file 'lpsetup/tests/subcommands/test_lxcinstall.py'
360--- lpsetup/tests/subcommands/test_lxcinstall.py 1970-01-01 00:00:00 +0000
361+++ lpsetup/tests/subcommands/test_lxcinstall.py 2012-06-26 15:04:24 +0000
362@@ -0,0 +1,75 @@
363+#!/usr/bin/env python
364+# Copyright 2012 Canonical Ltd. This software is licensed under the
365+# GNU Affero General Public License version 3 (see the file LICENSE).
366+
367+"""Tests for the lxcinstall sub command."""
368+
369+import random
370+import unittest
371+
372+from lpsetup import (
373+ handlers,
374+ settings,
375+ )
376+from lpsetup.subcommands import lxcinstall
377+from lpsetup.tests.subcommands import (
378+ test_get,
379+ test_inithost,
380+ test_install,
381+ )
382+from lpsetup.tests.utils import (
383+ get_random_string,
384+ StepsBasedSubCommandTestMixin,
385+ )
386+
387+
388+create_scripts_step = (
389+ lxcinstall.create_scripts, ['user', 'lxc_name', 'ssh_key_path'])
390+create_lxc_step = (
391+ lxcinstall.create_lxc, ['user', 'lxc_name', 'lxc_arch', 'lxc_os',
392+ 'install_subunit'])
393+start_lxc_step = (lxcinstall.start_lxc, ['lxc_name'])
394+wait_for_lxc_step = (lxcinstall.wait_for_lxc, ['lxc_name', 'ssh_key_path'])
395+initialize_lxc_step = (
396+ lxcinstall.initialize_lxc, ['lxc_name', 'ssh_key_path', 'lxc_os'])
397+setup_launchpad_lxc_step = (
398+ lxcinstall.setup_launchpad_lxc, ['user', 'dependencies_dir', 'directory',
399+ 'valid_ssh_keys', 'ssh_key_path', 'lxc_name'])
400+stop_lxc_step = (lxcinstall.stop_lxc, ['lxc_name', 'ssh_key_path'])
401+
402+
403+def get_arguments():
404+ lxc_name = get_random_string()
405+ lxc_arch = random.choice(['i386', 'amd64'])
406+ lxc_os = random.choice(settings.LXC_GUEST_CHOICES)
407+ return test_inithost.get_arguments() + (
408+ '-n', lxc_name, '-A', lxc_arch, '-r', lxc_os,
409+ '--create-scripts', '--install-subunit', '--testing'
410+ )
411+
412+
413+class LxcInstallTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
414+
415+ sub_command_class = lxcinstall.SubCommand
416+ expected_arguments = get_arguments()
417+ expected_handlers = (
418+ handlers.handle_user,
419+ handlers.handle_lpuser,
420+ handlers.handle_userdata,
421+ handlers.handle_ssh_keys,
422+ handlers.handle_directories,
423+ handlers.handle_testing,
424+ )
425+ expected_steps = (
426+ test_inithost.initialize_step,
427+ test_get.fetch_step,
428+ test_install.setup_bzr_locations_step,
429+ create_scripts_step,
430+ create_lxc_step,
431+ start_lxc_step,
432+ wait_for_lxc_step,
433+ initialize_lxc_step,
434+ setup_launchpad_lxc_step,
435+ stop_lxc_step,
436+ )
437+ needs_root = True
438
439=== modified file 'lpsetup/tests/subcommands/test_version.py'
440--- lpsetup/tests/subcommands/test_version.py 2012-05-22 09:36:57 +0000
441+++ lpsetup/tests/subcommands/test_version.py 2012-06-26 15:04:24 +0000
442@@ -17,7 +17,6 @@
443 class VersionTest(SubCommandTestMixin, unittest.TestCase):
444
445 sub_command_class = version.SubCommand
446- sub_command_name = 'subcmd'
447
448 def test_sub_command(self):
449 with capture_output() as output:
450
451=== modified file 'lpsetup/tests/test_argparser.py'
452--- lpsetup/tests/test_argparser.py 2012-05-22 09:36:57 +0000
453+++ lpsetup/tests/test_argparser.py 2012-06-26 15:04:24 +0000
454@@ -96,7 +96,7 @@
455
456 def test_arguments(self):
457 # Ensure the sub command arguments are correctly handled.
458- namespace = self.parse_and_call_main('--foo eggs')
459+ namespace = self.parse_and_call_main('--foo', 'eggs')
460 self.assertEqual('eggs', namespace.foo)
461
462 def test_successful_validation(self):
463@@ -121,6 +121,12 @@
464 self.assertIn(self.sub_command.name, help)
465 self.assertIn(self.sub_command.help, help)
466
467+ def test_init_namespace(self):
468+ # The namespace is initialized with current user info.
469+ namespace = self.parse()
470+ self.assertIsInstance(namespace.euid, int)
471+ self.assertIsInstance(namespace.run_as_root, bool)
472+
473
474 class StepsBasedSubCommandTest(SubCommandTestMixin, unittest.TestCase):
475
476@@ -129,23 +135,23 @@
477 def test_steps(self):
478 # Ensure steps are executed in the order they are provided.
479 with capture_output() as output:
480- self.parse_and_call_main('--foo eggs --bar spam')
481+ self.parse_and_call_main('--foo', 'eggs', '--bar', 'spam')
482 self.check_output(
483 ['step1 received eggs', 'step2 received eggs and spam'],
484 output)
485
486- def test_step_flag(self):
487+ def test_steps_flag(self):
488 # A special argument `-s` or `--steps` is automatically added to the
489 # parser. It can be used to execute only one or a subset of steps.
490 with capture_output() as output:
491- self.parse_and_call_main('--foo eggs -s step1')
492+ self.parse_and_call_main('--foo', 'eggs', '-s', 'step1')
493 self.check_output(['step1 received eggs'], output)
494
495- def test_skip_steps(self):
496+ def test_skip_steps_flag(self):
497 # A special argument `--skip-steps` is automatically added to the
498 # parser. It can be used to skip one or more steps.
499 with capture_output() as output:
500- self.parse_and_call_main('--foo eggs --skip-steps step1')
501+ self.parse_and_call_main('--foo', 'eggs', '--skip-steps', 'step1')
502 self.check_output(['step2 received eggs and None'], output)
503
504 def test_step_name(self):
505@@ -164,7 +170,7 @@
506 # Ensure the steps execution is stopped if a step raises
507 # `subprocess.CalledProcessError`.
508 with capture_output() as output:
509- error = self.parse_and_call_main('--foo eggs')
510+ error = self.parse_and_call_main('--foo', 'eggs')
511 self.assertEqual(1, error.returncode)
512 self.check_output(['step1 received eggs'], output)
513
514@@ -177,7 +183,7 @@
515 # The test runner calls a function named 'call_[step name]' if it is
516 # defined.
517 with capture_output() as output:
518- self.parse_and_call_main('--foo eggs --bar spam')
519+ self.parse_and_call_main('--foo', 'eggs', '--bar', 'spam')
520 expected = [
521 'running step1 with eggs while bar is spam',
522 'step1 received eggs',
523
524=== modified file 'lpsetup/tests/utils.py'
525--- lpsetup/tests/utils.py 2012-05-22 09:36:57 +0000
526+++ lpsetup/tests/utils.py 2012-06-26 15:04:24 +0000
527@@ -8,10 +8,15 @@
528 __all__ = [
529 'capture_error',
530 'capture_output',
531+ 'get_random_string',
532+ 'StepsBasedSubCommandTestMixin',
533+ 'SubCommandTestMixin',
534 ]
535
536 from contextlib import contextmanager
537 from functools import partial
538+import random
539+import string
540 from StringIO import StringIO
541 import sys
542
543@@ -34,23 +39,79 @@
544 capture_error = partial(capture, 'stderr')
545
546
547+def get_random_string(size=10):
548+ """Return a random string to be used in tests."""
549+ return ''.join(random.sample(string.ascii_letters, size))
550+
551+
552 class SubCommandTestMixin(object):
553
554 sub_command_class = examples.SubCommand
555 sub_command_name = 'subcmd'
556
557 def setUp(self):
558+ """Set up an argument parser and instantiate *self.sub_command_class*.
559+
560+ The name used to create the sub command instance is
561+ *self.sub_command_name*.
562+ """
563 self.parser = argparser.ArgumentParser()
564 self.sub_command = self.parser.register_subcommand(
565 self.sub_command_name, self.sub_command_class)
566
567- def parse_and_call_main(self, arguments=None):
568- args = [self.sub_command_name]
569- if arguments is not None:
570- args.extend(arguments.split())
571- namespace = self.parser.parse_args(args)
572+ def parse(self, *args):
573+ """Parse given *args* and return an initialized namespace object."""
574+ namespace = self.parser.parse_args((self.sub_command_name,) + args)
575+ sub_command = self.sub_command
576+ sub_command.init_namespace(namespace)
577+ sub_command.validate(self.parser, namespace)
578+ return namespace
579+
580+ def parse_and_call_main(self, *args):
581+ """Create a namespace using the given *args* and invoke main."""
582+ namespace = self.parse(*args)
583 return namespace.main(namespace)
584
585 def check_output(self, expected, output):
586 value = filter(None, output.getvalue().split('\n'))
587 self.assertSequenceEqual(expected, value)
588+
589+
590+class StepsBasedSubCommandTestMixin(SubCommandTestMixin):
591+ """This mixin can be used to test sub commands steps and handlers.
592+
593+ Real TestCases subclassing this mixin must define:
594+
595+ - expected_arguments: a sequence of command line arguments
596+ used by the current tested sub command
597+ - expected_handlers: a sequence of expected handler callables
598+ - expected_steps: a sequence of expected *(step_callable, arg_names)*
599+ - needs_root: True if this sub command must be run as root
600+
601+ At this point steps and handlers are automatically tested, and the test
602+ case also checks if root is required by the sub command.
603+ """
604+ def setUp(self):
605+ """Set up a namespace using *self.expected_arguments*."""
606+ super(StepsBasedSubCommandTestMixin, self).setUp()
607+ self.namespace = self.parse(*self.expected_arguments)
608+
609+ def test_handlers(self):
610+ # Ensure this sub command uses the expected handlers.
611+ handlers = self.sub_command.get_validators(self.namespace)
612+ self.assertSequenceEqual(self.expected_handlers, handlers)
613+
614+ def test_steps(self):
615+ # Ensure this sub command wants to run the expected steps.
616+ steps = self.sub_command.get_steps(self.namespace)
617+ real_steps = [[step, list(args)] for _, step, args in steps]
618+ expected_steps = []
619+ for step, arg_names in self.expected_steps:
620+ args = [getattr(self.namespace, name) for name in arg_names]
621+ expected_steps.append([step, args])
622+ self.assertListEqual(expected_steps, real_steps)
623+
624+ def test_needs_root(self):
625+ # The root user may or may not be required to run this sub command.
626+ needs_root = self.sub_command.get_needs_root(self.namespace)
627+ self.assertEqual(self.needs_root, needs_root)

Subscribers

People subscribed via source and target branches

to all changes: