yellow team mailing list archive
-
yellow team
-
Mailing list archive
-
Message #00950
[Merge] lp:~frankban/lpsetup/commands-unittests into lp:lpsetup
Francesco Banconi has proposed merging lp:~frankban/lpsetup/commands-unittests into lp:lpsetup.
Requested reviews:
Yellow Squad (yellow)
Related bugs:
Bug #1017426 in lpsetup: "lpsetup: subcommands unit tests (steps and handlers)"
https://bugs.launchpad.net/lpsetup/+bug/1017426
For more details, see:
https://code.launchpad.net/~frankban/lpsetup/commands-unittests/+merge/112094
== 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.
--
https://code.launchpad.net/~frankban/lpsetup/commands-unittests/+merge/112094
Your team Yellow Squad is requested to review the proposed merge of lp:~frankban/lpsetup/commands-unittests into lp:lpsetup.
=== modified file 'lpsetup/argparser.py'
--- lpsetup/argparser.py 2012-03-30 11:02:11 +0000
+++ lpsetup/argparser.py 2012-06-26 12:40:25 +0000
@@ -416,15 +416,15 @@
steps = ()
- def __init__(self, *args, **kwargs):
- super(StepsBasedSubCommand, self).__init__(*args, **kwargs)
- self._step_names = []
- self._steps = {}
- for step_args in self.steps:
- step, args = step_args[0], step_args[1:]
- step_name = self._get_step_name(step)
- self._step_names.append(step_name)
- self._steps[step_name] = (step, args)
+ def add_arguments(self, parser):
+ super(StepsBasedSubCommand, self).add_arguments(parser)
+ step_names = [self._get_step_name(i[0]) for i in self.steps]
+ parser.add_argument(
+ '-s', '--steps', nargs='+', choices=step_names,
+ help='Call one or more internal functions.')
+ parser.add_argument(
+ '--skip-steps', nargs='+', choices=step_names,
+ help='Skip one or more internal functions.')
def _get_step_name(self, step):
"""Return the string representation of a step callable.
@@ -450,28 +450,37 @@
except AttributeError:
return step.__name__
- def add_arguments(self, parser):
- super(StepsBasedSubCommand, self).add_arguments(parser)
- parser.add_argument(
- '-s', '--steps', nargs='+', choices=self._step_names,
- help='Call one or more internal functions.')
- parser.add_argument(
- '--skip-steps', nargs='+', choices=self._step_names,
- help='Skip one or more internal functions.')
+ 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 is not
+ skipped using either `--skip-steps` or `--steps`.
+ """
+ steps_to_skip = namespace.skip_steps or []
+ steps_to_run = namespace.steps
+ if step_name not in steps_to_skip:
+ return step_name in steps_to_run if steps_to_run else True
+ return False
+
+ def get_steps(self, namespace):
+ """Return a list of *(step_name, step_callable, step_args)* tuples."""
+ steps = []
+ for step_arg_names in self.steps:
+ step, arg_names = step_arg_names[0], step_arg_names[1:]
+ step_name = self._get_step_name(step)
+ if self._include_step(step_name, namespace):
+ args = [getattr(namespace, i) for i in arg_names]
+ steps.append((step_name, step, args))
+ return steps
def _call_step(self, namespace, step, args):
"""Default callable used to run a `step`, using given `args`."""
return step(*args)
def handle(self, namespace):
- skip_steps = namespace.skip_steps or []
- step_names = filter(
- lambda step_name: step_name not in skip_steps,
- namespace.steps or self._step_names)
default_step_runner = self._call_step
- for step_name in step_names:
- step, arg_names = self._steps[step_name]
- args = [getattr(namespace, i) for i in arg_names]
+ for step_name, step, args in self.get_steps(namespace):
+ # Run the step using a dynamic dispatcher.
step_runner = getattr(
self, 'call_' + step_name, default_step_runner)
try:
=== modified file 'lpsetup/subcommands/get.py'
--- lpsetup/subcommands/get.py 2012-06-25 19:24:56 +0000
+++ lpsetup/subcommands/get.py 2012-06-26 12:40:25 +0000
@@ -158,7 +158,6 @@
)
help = __doc__
- needs_root = True
validators = (
handlers.handle_user,
handlers.handle_lpuser,
=== modified file 'lpsetup/subcommands/install.py'
--- lpsetup/subcommands/install.py 2012-06-22 20:50:41 +0000
+++ lpsetup/subcommands/install.py 2012-06-26 12:40:25 +0000
@@ -99,11 +99,13 @@
"""Install the Launchpad environment."""
# The steps for "install" are a superset of the steps for "inithost".
+ setup_bzr_locations_step = (setup_bzr_locations,
+ 'user', 'lpuser', 'directory')
+
steps = (
inithost.SubCommand.initialize_step,
get.SubCommand.fetch_step,
- (setup_bzr_locations,
- 'user', 'lpuser', 'directory'),
+ setup_bzr_locations_step,
inithost.SubCommand.setup_apt_step,
(setup_launchpad,
'user', 'dependencies_dir', 'directory', 'valid_ssh_keys'),
=== modified file 'lpsetup/subcommands/lxcinstall.py'
--- lpsetup/subcommands/lxcinstall.py 2012-06-25 17:55:10 +0000
+++ lpsetup/subcommands/lxcinstall.py 2012-06-26 12:40:25 +0000
@@ -43,6 +43,7 @@
SCRIPTS,
)
from lpsetup.subcommands import (
+ get,
inithost,
install,
)
@@ -201,16 +202,13 @@
subprocess.call(['lxc-stop', '-n', lxc_name])
-class SubCommand(inithost.SubCommand):
+class SubCommand(install.SubCommand):
"""Install the Launchpad environment inside an LXC."""
steps = (
- (inithost.initialize,
- 'user', 'full_name', 'email', 'lpuser',
- 'private_key', 'public_key', 'valid_ssh_keys', 'ssh_key_path',
- 'feed_random', 'dependencies_dir', 'directory'),
- (install.setup_bzr_locations,
- 'user', 'lpuser', 'directory'),
+ inithost.SubCommand.initialize_step,
+ get.SubCommand.fetch_step,
+ install.SubCommand.setup_bzr_locations_step,
(create_scripts,
'user', 'lxc_name', 'ssh_key_path'),
(create_lxc,
@@ -231,8 +229,7 @@
def get_validators(self, namespace):
validators = super(SubCommand, self).get_validators(namespace)
- return validators + (
- handlers.handle_testing, handlers.handle_directories)
+ return validators + (handlers.handle_testing,)
def call_create_scripts(self, namespace, step, args):
"""Run the `create_scripts` step only if the related flag is set."""
@@ -257,7 +254,7 @@
parser.add_argument(
'-C', '--create-scripts', action='store_true',
help='Create the scripts used by buildbot for parallel testing.')
- # The following flag is not present in the install sub command since
+ # The following flag is not present in the inithost sub command since
# subunit is always installed there as a dependency of
# launchpad-developer-dependencies.
parser.add_argument(
=== 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 12:40:25 +0000
@@ -0,0 +1,66 @@
+#!/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 get sub command."""
+
+import unittest
+
+from lpsetup import handlers
+from lpsetup.subcommands import get
+from lpsetup.tests.utils import (
+ get_random_string,
+ StepsBasedSubCommandTestMixin,
+ )
+
+
+fetch_step = (
+ get.fetch, ['user', 'directory', 'dependencies_dir', 'valid_ssh_keys'])
+update_launchpad_step = (
+ get.update_launchpad, ['user', 'dependencies_dir', 'directory',
+ 'make_schema', 'apt'])
+link_sourcecode_in_branches_step = (
+ get.link_sourcecode_in_branches, ['user', 'dependencies_dir',
+ 'directory'])
+
+
+def get_update_arguments():
+ user = get_random_string()
+ directory = '~/' + get_random_string()
+ dependencies_dir = '~/' + get_random_string()
+ ssh_key_name = get_random_string()
+ return (
+ '-u', user, '-c', directory, '--make-schema',
+ '-d', dependencies_dir, '-S', ssh_key_name,
+ )
+
+
+def get_arguments():
+ email = get_random_string()
+ full_name = get_random_string() + '@example.com'
+ lpuser = get_random_string()
+ private_key = get_random_string()
+ public_key = get_random_string()
+ return get_update_arguments() + (
+ '-e', email, '-f', full_name, '-l', lpuser,
+ '-v', private_key, '-b', public_key,
+ '--no-repositories', '--feed-random')
+
+
+class GetTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
+
+ sub_command_class = get.SubCommand
+ expected_arguments = get_arguments()
+ expected_handlers = (
+ handlers.handle_user,
+ handlers.handle_lpuser,
+ handlers.handle_userdata,
+ handlers.handle_ssh_keys,
+ handlers.handle_directories,
+ )
+ expected_steps = (
+ fetch_step,
+ update_launchpad_step,
+ link_sourcecode_in_branches_step,
+ )
+ needs_root = False
=== 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 12:40:25 +0000
@@ -0,0 +1,50 @@
+#!/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 inithost sub command."""
+
+import unittest
+
+from lpsetup import handlers
+from lpsetup.subcommands import inithost
+from lpsetup.tests.utils import (
+ get_random_string,
+ StepsBasedSubCommandTestMixin,
+ )
+
+
+initialize_step = (
+ inithost.initialize, ['user', 'full_name', 'email', 'lpuser',
+ 'private_key', 'public_key', 'valid_ssh_keys', 'ssh_key_path',
+ 'feed_random'])
+setup_apt_step = (inithost.setup_apt, ['no_repositories'])
+
+
+def get_arguments():
+ user = get_random_string()
+ email = get_random_string()
+ full_name = get_random_string() + '@example.com'
+ lpuser = get_random_string()
+ private_key = get_random_string()
+ public_key = get_random_string()
+ ssh_key_name = get_random_string()
+ return (
+ '-u', user, '-e', email, '-f', full_name, '-l', lpuser,
+ '-v', private_key, '-b', public_key, '-S', ssh_key_name)
+
+
+class InithostTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
+
+ sub_command_class = inithost.SubCommand
+ expected_arguments = get_arguments()
+ expected_handlers = (
+ handlers.handle_user,
+ handlers.handle_lpuser,
+ handlers.handle_userdata,
+ handlers.handle_ssh_keys,
+ )
+ expected_steps = (initialize_step, setup_apt_step)
+ needs_root = True
+
+
=== 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 12:40:25 +0000
@@ -0,0 +1,55 @@
+#!/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 install sub command."""
+
+import unittest
+
+from lpsetup import handlers
+from lpsetup.subcommands import install
+from lpsetup.tests.subcommands import (
+ test_get,
+ test_inithost,
+ )
+from lpsetup.tests.utils import (
+ get_random_string,
+ StepsBasedSubCommandTestMixin,
+ )
+
+
+setup_bzr_locations_step = (
+ install.setup_bzr_locations, ['user', 'lpuser', 'directory'])
+setup_launchpad_step = (
+ install.setup_launchpad, ['user', 'dependencies_dir', 'directory',
+ 'valid_ssh_keys'])
+
+
+def get_arguments():
+ inithost_arguments = test_inithost.get_arguments()
+ dependencies_dir = '~/' + get_random_string()
+ directory = '~/' + get_random_string()
+ return inithost_arguments + ('-d', dependencies_dir, '-c', directory)
+
+
+class InstallTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
+
+ sub_command_class = install.SubCommand
+ expected_arguments = get_arguments()
+ expected_handlers = (
+ handlers.handle_user,
+ handlers.handle_lpuser,
+ handlers.handle_userdata,
+ handlers.handle_ssh_keys,
+ handlers.handle_directories,
+ )
+ expected_steps = (
+ test_inithost.initialize_step,
+ test_get.fetch_step,
+ setup_bzr_locations_step,
+ test_inithost.setup_apt_step,
+ setup_launchpad_step,
+ )
+ needs_root = True
+
+
=== 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 12:40:25 +0000
@@ -0,0 +1,75 @@
+#!/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 lxcinstall sub command."""
+
+import random
+import unittest
+
+from lpsetup import (
+ handlers,
+ settings,
+ )
+from lpsetup.subcommands import lxcinstall
+from lpsetup.tests.subcommands import (
+ test_get,
+ test_inithost,
+ test_install,
+ )
+from lpsetup.tests.utils import (
+ get_random_string,
+ StepsBasedSubCommandTestMixin,
+ )
+
+
+create_scripts_step = (
+ lxcinstall.create_scripts, ['user', 'lxc_name', 'ssh_key_path'])
+create_lxc_step = (
+ lxcinstall.create_lxc, ['user', 'lxc_name', 'lxc_arch', 'lxc_os',
+ 'install_subunit'])
+start_lxc_step = (lxcinstall.start_lxc, ['lxc_name'])
+wait_for_lxc_step = (lxcinstall.wait_for_lxc, ['lxc_name', 'ssh_key_path'])
+initialize_lxc_step = (
+ lxcinstall.initialize_lxc, ['lxc_name', 'ssh_key_path', 'lxc_os'])
+setup_launchpad_lxc_step = (
+ lxcinstall.setup_launchpad_lxc, ['user', 'dependencies_dir', 'directory',
+ 'valid_ssh_keys', 'ssh_key_path', 'lxc_name'])
+stop_lxc_step = (lxcinstall.stop_lxc, ['lxc_name', 'ssh_key_path'])
+
+
+def get_arguments():
+ lxc_name = get_random_string()
+ lxc_arch = random.choice(['i386', 'amd64'])
+ lxc_os = random.choice(settings.LXC_GUEST_CHOICES)
+ return test_inithost.get_arguments() + (
+ '-n', lxc_name, '-A', lxc_arch, '-r', lxc_os,
+ '--create-scripts', '--install-subunit', '--testing'
+ )
+
+
+class LxcInstallTest(StepsBasedSubCommandTestMixin, unittest.TestCase):
+
+ sub_command_class = lxcinstall.SubCommand
+ expected_arguments = get_arguments()
+ expected_handlers = (
+ handlers.handle_user,
+ handlers.handle_lpuser,
+ handlers.handle_userdata,
+ handlers.handle_ssh_keys,
+ handlers.handle_directories,
+ handlers.handle_testing,
+ )
+ expected_steps = (
+ test_inithost.initialize_step,
+ test_get.fetch_step,
+ test_install.setup_bzr_locations_step,
+ create_scripts_step,
+ create_lxc_step,
+ start_lxc_step,
+ wait_for_lxc_step,
+ initialize_lxc_step,
+ setup_launchpad_lxc_step,
+ stop_lxc_step,
+ )
+ needs_root = True
=== 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 12:40:25 +0000
@@ -17,7 +17,6 @@
class VersionTest(SubCommandTestMixin, unittest.TestCase):
sub_command_class = version.SubCommand
- sub_command_name = 'subcmd'
def test_sub_command(self):
with capture_output() as output:
=== 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 12:40:25 +0000
@@ -96,7 +96,7 @@
def test_arguments(self):
# Ensure the sub command arguments are correctly handled.
- namespace = self.parse_and_call_main('--foo eggs')
+ namespace = self.parse_and_call_main('--foo', 'eggs')
self.assertEqual('eggs', namespace.foo)
def test_successful_validation(self):
@@ -121,6 +121,12 @@
self.assertIn(self.sub_command.name, help)
self.assertIn(self.sub_command.help, help)
+ def test_init_namespace(self):
+ # The namespace is initialized with current user info.
+ namespace = self.parse()
+ self.assertIsInstance(namespace.euid, int)
+ self.assertIsInstance(namespace.run_as_root, bool)
+
class StepsBasedSubCommandTest(SubCommandTestMixin, unittest.TestCase):
@@ -129,23 +135,23 @@
def test_steps(self):
# Ensure steps are executed in the order they are provided.
with capture_output() as output:
- self.parse_and_call_main('--foo eggs --bar spam')
+ self.parse_and_call_main('--foo', 'eggs', '--bar', 'spam')
self.check_output(
['step1 received eggs', 'step2 received eggs and spam'],
output)
- def test_step_flag(self):
+ def test_steps_flag(self):
# A special argument `-s` or `--steps` is automatically added to the
# parser. It can be used to execute only one or a subset of steps.
with capture_output() as output:
- self.parse_and_call_main('--foo eggs -s step1')
+ self.parse_and_call_main('--foo', 'eggs', '-s', 'step1')
self.check_output(['step1 received eggs'], output)
- def test_skip_steps(self):
+ def test_skip_steps_flag(self):
# A special argument `--skip-steps` is automatically added to the
# parser. It can be used to skip one or more steps.
with capture_output() as output:
- self.parse_and_call_main('--foo eggs --skip-steps step1')
+ self.parse_and_call_main('--foo', 'eggs', '--skip-steps', 'step1')
self.check_output(['step2 received eggs and None'], output)
def test_step_name(self):
@@ -164,7 +170,7 @@
# Ensure the steps execution is stopped if a step raises
# `subprocess.CalledProcessError`.
with capture_output() as output:
- error = self.parse_and_call_main('--foo eggs')
+ error = self.parse_and_call_main('--foo', 'eggs')
self.assertEqual(1, error.returncode)
self.check_output(['step1 received eggs'], output)
@@ -177,7 +183,7 @@
# The test runner calls a function named 'call_[step name]' if it is
# defined.
with capture_output() as output:
- self.parse_and_call_main('--foo eggs --bar spam')
+ self.parse_and_call_main('--foo', 'eggs', '--bar', 'spam')
expected = [
'running step1 with eggs while bar is spam',
'step1 received eggs',
=== modified file 'lpsetup/tests/utils.py'
--- lpsetup/tests/utils.py 2012-05-22 09:36:57 +0000
+++ lpsetup/tests/utils.py 2012-06-26 12:40:25 +0000
@@ -8,10 +8,20 @@
__all__ = [
'capture_error',
'capture_output',
+ 'get_random_string',
+ 'StepsBasedSubCommandTestMixin',
+ 'SubCommandTestMixin',
]
from contextlib import contextmanager
from functools import partial
+from itertools import (
+ imap,
+ islice,
+ repeat,
+ )
+import random
+import string
from StringIO import StringIO
import sys
@@ -34,23 +44,87 @@
capture_error = partial(capture, 'stderr')
+_random_letters = imap(random.choice, repeat(string.letters + string.digits))
+
+
+def get_random_string(size=10):
+ """Return a random string to be used in tests."""
+ return "".join(islice(_random_letters, size))
+
+
class SubCommandTestMixin(object):
sub_command_class = examples.SubCommand
sub_command_name = 'subcmd'
def setUp(self):
+ """Set up an argument parser and instantiate *self.sub_command_class*.
+
+ The name used to create the sub command instance is
+ *self.sub_command_name*.
+ """
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)
+ def parse(self, *args):
+ """Parse given *args* and return an initialized namespace object."""
+ namespace = self.parser.parse_args((self.sub_command_name,) + args)
+ sub_command = self.sub_command
+ sub_command.init_namespace(namespace)
+ sub_command.validate(self.parser, namespace)
+ return namespace
+
+ def parse_and_call_main(self, *args):
+ """Create a namespace using the given *args* and invoke main."""
+ namespace = self.parse(*args)
return namespace.main(namespace)
def check_output(self, expected, output):
value = filter(None, output.getvalue().split('\n'))
self.assertSequenceEqual(expected, value)
+
+
+class StepsBasedSubCommandTestMixin(SubCommandTestMixin):
+ """This mixin can be used to test sub commands steps and handlers.
+
+ Real TestCases subclassing this mixin must define:
+
+ - expected_arguments: a sequence of command line arguments
+ used by the current tested sub command
+ - expected_handlers: a sequence of expected handler callables
+ - expected_steps: a sequence of expected *(step_callable, arg_names)*
+ - needs_root: True if this sub command must be run as root
+
+ At this point steps and handlers are automatically tested, and the test
+ case also checks if root is required by the sub command.
+ """
+ expected_arguments = ()
+ expected_handlers = ()
+ expected_steps = ()
+ needs_root = False
+
+ def setUp(self):
+ """Set up a namespace using *self.expected_arguments*."""
+ super(StepsBasedSubCommandTestMixin, self).setUp()
+ self.namespace = self.parse(*self.expected_arguments)
+
+ def test_handlers(self):
+ # Ensure this sub command uses the expected handlers.
+ handlers = self.sub_command.get_validators(self.namespace)
+ self.assertSequenceEqual(self.expected_handlers, handlers)
+
+ def test_steps(self):
+ # Ensure this sub command wants to run the expected steps.
+ steps = self.sub_command.get_steps(self.namespace)
+ real_steps = [[step, list(args)] for _, step, args in steps]
+ expected_steps = []
+ for step, arg_names in self.expected_steps:
+ args = [getattr(self.namespace, name) for name in arg_names]
+ expected_steps.append([step, args])
+ self.assertListEqual(expected_steps, real_steps)
+
+ def test_needs_root(self):
+ # The root user may or may not be required to run this sub command.
+ needs_root = self.sub_command.get_needs_root(self.namespace)
+ self.assertEqual(self.needs_root, needs_root)