Adds help for subcommands

Adds help subcommand which takes other subcommands as param.
Adds -h/--help option for subcommands.

This contains integration testing code for tuskar CLI.
Following tests are implemented:
o bare help command
o help command with bad parameter
o help command with -h / --help parameter
o set of tests for "help <command>", "<command> -h / --help" for following
commands:
  - flavor-create
  - flavor-delete
  - flavor-list
  - flavor-show
  - flavor-update
  - rack-create
  - rack-delete
  - rack-list
  - rack-show
  - rack-update

Fixes bug in tests/test_shell.py - {} -> []

Fixes bug: 1213050

Change-Id: Ic554198f4fb4cdbaeefb9831c15f969301a7be87
This commit is contained in:
Petr Blaho 2013-11-13 18:22:01 +01:00
parent b389de3544
commit 6c06cbcf01
5 changed files with 556 additions and 14 deletions

View File

@ -31,24 +31,42 @@ logger = logging.getLogger(__name__)
class TuskarShell(object):
def __init__(self, raw_args):
def __init__(self, raw_args,
argument_parser_class=argparse.ArgumentParser):
self.raw_args = raw_args
self.argument_parser_class = argument_parser_class
self.partial_args = None
self.parser = None
self.subparsers = None
self._prepare_parsers()
def _prepare_parsers(self):
nonversioned_parser = self._nonversioned_parser()
self.partial_args =\
nonversioned_parser.parse_known_args(self.raw_args)[0]
self.parser, self.subparsers =\
self._parser(self.partial_args.tuskar_api_version)
def run(self):
'''Run the CLI. Parse arguments and do the respective action.'''
nonversioned_parser = self._nonversioned_parser()
partial_args = nonversioned_parser.parse_known_args(self.raw_args)[0]
parser = self._parser(partial_args.tuskar_api_version)
if partial_args.help or not self.raw_args:
parser.print_help()
# run self.do_help() if we have no raw_args at all or just -h/--help
if not self.raw_args\
or self.raw_args in (['-h'], ['--help']):
self.do_help(self.partial_args)
return 0
args = self.parser.parse_args(self.raw_args)
# run self.do_help() if we have help subcommand or -h/--help option
if args.func == self.do_help or args.help:
self.do_help(args)
return 0
args = parser.parse_args(self.raw_args)
self._ensure_auth_info(args)
tuskar_client = client.get_client(partial_args.tuskar_api_version,
tuskar_client = client.get_client(self.partial_args.tuskar_api_version,
**args.__dict__)
args.func(tuskar_client, args)
@ -87,12 +105,15 @@ class TuskarShell(object):
:param version: version of Tuskar API (and corresponding CLI
commands) to use
:return: main parser and subparsers
:rtype: (Parser, Subparsers)
'''
parser = self._nonversioned_parser()
subparsers = parser.add_subparsers(metavar='<subcommand>')
versioned_shell = utils.import_versioned_module(version, 'shell')
versioned_shell.enhance_parser(parser, subparsers)
return parser
utils.define_commands_from_module(subparsers, self)
return parser, subparsers
def _nonversioned_parser(self):
'''Create a basic parser that doesn't contain version-specific
@ -100,10 +121,10 @@ class TuskarShell(object):
version should be used for the versioned full blown parser and
defining common version-agnostic options.
'''
parser = argparse.ArgumentParser(
parser = self.argument_parser_class(
prog='tuskar',
description='OpenStack Management CLI',
add_help=False
add_help=False,
)
parser.add_argument('-h', '--help',
@ -181,6 +202,22 @@ class TuskarShell(object):
return parser
@utils.arg(
'command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>')
def do_help(self, args):
"""Display help about this program or one of its subcommands."""
if getattr(args, 'command', None):
if args.command in self.subparsers.choices:
# print help for subcommand
self.subparsers.choices[args.command].print_help()
else:
raise exc.CommandError("'%s' is not a valid subcommand" %
args.command)
else:
# print general help
self.parser.print_help()
def main():
try:

View File

@ -0,0 +1,311 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import tuskarclient.tests.utils as tutils
class HelpCommandTest(tutils.CommandTestCase):
pass
tests = [
# help
{
'commands': ['help'], # commands to test "tuskar help"
# helps find failed tests in code - needs "test_" prefix
'test_identifiers': ['test_help'],
'out_includes': [ # what should be in output
'usage:',
'positional arguments:',
'optional arguments:',
],
'out_excludes': [ # what should not be in output
'foo bar baz',
],
'err_string': '', # how error output should look like
'return_code': 0,
},
{
'commands': ['help -h', 'help --help', 'help help'],
'test_identifiers': ['test_help_dash_h',
'test_help_dashdash_help',
'test_help_help'],
'out_includes': [
'usage:',
'positional arguments:',
'optional arguments:',
'Display help for <subcommand>',
],
'out_excludes': [
'flavor-list',
'--os-username OS_USERNAME',
],
'err_string': '',
'return_code': 0,
},
{
'commands': ['help -r'],
'test_identifiers': ['test_help_dash_r'],
'out_string': '',
'err_includes': [
'error: unrecognized arguments: -r',
],
'return_code': 2,
},
# rack
{
'commands': ['rack-delete -h',
'rack-delete --help',
'help rack-delete'],
'test_identifiers': ['test_rack_delete_dash_h',
'test_rack_delete_dashdash_help',
'test_help_rack_delete'],
'out_includes': [
'usage: tuskar rack-delete [-h] <NAME or ID>',
'positional arguments:',
'optional arguments:',
'<NAME or ID>',
],
'out_excludes': [
'rack-list',
'--os-username OS_USERNAME',
'Display help for <subcommand>',
],
'err_string': '',
'return_code': 0,
},
{
'commands': ['rack-update -h',
'rack-update --help',
'help rack-update'],
'test_identifiers': ['test_rack_update_dash_h',
'test_rack_update_dashdash_help',
'test_help_rack_update'],
'out_includes': [
'usage: tuskar rack-update [-h] [--name NAME] [--subnet SUBNET]',
'positional arguments:',
'<NAME or ID>',
'optional arguments:',
'--name NAME',
'--subnet SUBNET',
'--slots SLOTS',
'--capacities CAPACITIES',
'--resource-class RESOURCE_CLASS',
],
'out_excludes': [
'rack-list',
'--os-username OS_USERNAME',
'Display help for <subcommand>',
],
'err_string': '',
'return_code': 0,
},
{
'commands': ['rack-create -h',
'rack-create --help',
'help rack-create'],
'test_identifiers': ['test_rack_create_dash_h',
'test_rack_create_dashdash_help',
'test_help_rack_create'],
'out_includes': [
'usage: tuskar rack-create [-h] --subnet SUBNET --slots SLOTS',
'positional arguments:',
'optional arguments:',
'--subnet SUBNET',
'--slots SLOTS',
'--capacities CAPACITIES',
'--resource-class RESOURCE_CLASS',
],
'out_excludes': [
'rack-list',
'--os-username OS_USERNAME',
'Display help for <subcommand>',
],
'err_string': '',
'return_code': 0,
},
{
'commands': ['rack-show -h', 'rack-show --help', 'help rack-show'],
'test_identifiers': ['test_rack_show_dash_h',
'test_rack_show_dashdash_help',
'test_help_rack_show'],
'out_includes': [
'usage: tuskar rack-show [-h] <NAME or ID>',
'positional arguments:',
'optional arguments:',
'<NAME or ID>',
],
'out_excludes': [
'rack-list',
'--os-username OS_USERNAME',
'Display help for <subcommand>',
],
'err_string': '',
'return_code': 0,
},
{
'commands': ['rack-list -h', 'rack-list --help', 'help rack-list'],
'test_identifiers': ['test_rack_list_dash_h',
'test_rack_list_dashdash_help',
'test_help_rack_list'],
'out_includes': [
'usage: tuskar rack-list [-h]',
'optional arguments:',
],
'out_excludes': [
'rack-show',
'--os-username OS_USERNAME',
'Display help for <subcommand>',
'positional arguments:',
],
'err_string': '',
'return_code': 0,
},
# flavor
{
'commands': ['flavor-delete -h',
'flavor-delete --help',
'help flavor-delete'],
'test_identifiers': ['test_flavor_delete_dash_h',
'test_flavor_delete_dashdash_help',
'test_help_flavor_delete'],
'out_includes': [
'usage: tuskar flavor-delete [-h]',
'optional arguments:',
'positional arguments:',
],
'out_excludes': [
'flavor-list',
'--os-username OS_USERNAME',
'Display help for <subcommand>',
'--capacities CAPACITIES',
],
'err_string': '',
'return_code': 0,
},
{
'commands': ['flavor-update -h',
'flavor-update --help',
'help flavor-update'],
'test_identifiers': ['test_flavor_update_dash_h',
'test_flavor_update_dashdash_help',
'test_help_flavor_update'],
'out_includes': [
'usage: tuskar flavor-update [-h] [--name NAME]'
+ ' [--capacities CAPACITIES]',
'positional arguments:',
'optional arguments:',
'--capacities CAPACITIES',
'--name NAME',
'--max-vms MAX_VMS',
],
'out_excludes': [
'flavor-list',
'--os-username OS_USERNAME',
'Display help for <subcommand>',
],
'err_string': '',
'return_code': 0,
},
{
'commands': ['flavor-create -h',
'flavor-create --help',
'help flavor-create'],
'test_identifiers': ['test_flavor_create_dash_h',
'test_flavor_create_dashdash_help',
'test_help_flavor_create'],
'out_includes': [
'usage:',
'positional arguments:',
'optional arguments:',
'--capacities CAPACITIES',
],
'out_excludes': [
'flavor-list',
'--os-username OS_USERNAME',
'Display help for <subcommand>',
],
'err_string': '',
'return_code': 0,
},
{
'commands': ['flavor-show -h',
'flavor-show --help',
'help flavor-show'],
'test_identifiers': ['test_flavor_show_dash_h',
'test_flavor_show_dashdash_help',
'test_help_flavor_show'],
'out_includes': [
'usage: tuskar flavor-show [-h] <RESOURCE CLASS NAME or ID>'
+ ' <FLAVOR NAME or ID>',
'positional arguments:',
'optional arguments:',
'Name or ID of resource class associated to.',
],
'out_excludes': [
'flavor-list',
'--os-username OS_USERNAME',
'Display help for <subcommand>',
'--capacities CAPACITIES',
],
'err_string': '',
'return_code': 0,
},
{
'commands': ['flavor-list -h',
'flavor-list --help',
'help flavor-list'],
'test_identifiers': ['test_flavor_list_dash_h',
'test_flavor_list_dashdash_help',
'test_help_flavor_list'],
'out_includes': [
'usage: tuskar flavor-list [-h] <RESOURCE CLASS NAME or ID>',
'positional arguments:',
'optional arguments:',
],
'out_excludes': [
'--os-username OS_USERNAME',
'Display help for <subcommand>',
'--capacities CAPACITIES',
],
'err_string': '',
'return_code': 0,
},
]
def create_test_method(command, expected_values):
def test_command_method(self):
self.assertThat(
self.run_tuskar(command),
tutils.CommandOutputMatches(
out_str=expected_values.get('out_string'),
out_inc=expected_values.get('out_includes'),
out_exc=expected_values.get('out_excludes'),
err_str=expected_values.get('err_string'),
err_inc=expected_values.get('err_includes'),
err_exc=expected_values.get('err_excludes'),
))
return test_command_method
# creates a method for each command found in tests
# to let developer see what test is failing in test results,
# ie: ... HelpCommandTest.test_help_flavor_list
# this way dev can "just search" for "test_help_flavor_list"
# and he will find actual data used in failing test
for test in tests:
commands = test.get('commands')
for index, command in enumerate(commands):
test_command_method = create_test_method(command, test)
test_command_method.__name__ = test.get('test_identifiers')[index]
setattr(HelpCommandTest,
test_command_method.__name__,
test_command_method)

View File

@ -24,7 +24,7 @@ class ShellTest(tutils.TestCase):
def setUp(self):
super(ShellTest, self).setUp()
self.s = shell.TuskarShell({})
self.s = shell.TuskarShell([])
def empty_args(self):
args = lambda: None # i'd use object(), but it can't have attributes
@ -61,7 +61,7 @@ class ShellTest(tutils.TestCase):
v1_commands = [
'rack-list', 'rack-show',
]
parser = self.s._parser(1)
parser, subparsers = self.s._parser(1)
tuskar_help = parser.format_help()
for arg in map(lambda a: a.replace('_', '-'), self.args_attributes):

View File

@ -10,14 +10,18 @@
# License for the specific language governing permissions and limitations
# under the License.
import argparse
import copy
import fixtures
from gettext import gettext as _
import os
import sys
from six import StringIO
import testtools
from tuskarclient.common import http
from tuskarclient import shell
class TestCase(testtools.TestCase):
@ -34,6 +38,196 @@ class TestCase(testtools.TestCase):
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
class CommandTestCase(TestCase):
def setUp(self):
super(CommandTestCase, self).setUp()
self.tuskar_bin = os.path.join(
os.path.dirname(os.path.realpath(sys.executable)),
'tuskar')
def run_tuskar(self, params=''):
args = params.split()
out = StringIO()
err = StringIO()
ArgumentParserForTests.OUT = out
ArgumentParserForTests.ERR = err
try:
shell.TuskarShell(
args, argument_parser_class=ArgumentParserForTests).run()
except TestExit:
pass
outvalue = out.getvalue()
errvalue = err.getvalue()
return [outvalue, errvalue]
class CommandOutputMatches(object):
def __init__(self,
out_str=None, out_inc=None, out_exc=None,
err_str=None, err_inc=None, err_exc=None,
return_code=None):
self.out_str = out_str
self.out_inc = out_inc or []
self.out_exc = out_exc or []
self.err_str = err_str
self.err_inc = err_inc or []
self.err_exc = err_exc or []
self.return_code = return_code
def match(self, outputs):
out, err = outputs[0], outputs[1]
errors = []
# tests for exact output and error output match
errors.append(self.match_output(out, self.out_str, type='output'))
errors.append(self.match_output(err, self.err_str, type='error'))
# tests for what output should include and what it should not
errors.append(self.match_includes(out, self.out_inc, type='output'))
errors.append(self.match_excludes(out, self.out_exc, type='output'))
# tests for what error output should include and what it should not
errors.append(self.match_includes(err, self.err_inc, type='error'))
errors.append(self.match_excludes(err, self.err_exc, type='error'))
# get first non None item or None if none is found and return it
return next((item for item in errors if item is not None), None)
def match_return_code(self, return_code, expected_return_code):
if expected_return_code is not None:
if expected_return_code != return_code:
return CommandOutputReturnCodeMismatch(
return_code, expected_return_code)
def match_output(self, output, expected_output, type='output'):
if expected_output is not None:
if expected_output != output:
return CommandOutputMismatch(
output, expected_output, type=type)
def match_includes(self, output, includes, type='output'):
for part in includes:
if part not in output:
return CommandOutputMissingMismatch(output, part, type=type)
def match_excludes(self, output, excludes, type='error'):
for part in excludes:
if part in output:
return CommandOutputExtraMismatch(output, part, type=type)
class CommandOutputMismatch(object):
def __init__(self, out, out_str, type='output'):
if type == 'error':
self.type = 'Error output'
else:
self.type = 'Output'
self.out = out
self.out_str = out_str
def describe(self):
return "%s '%s' should be '%s'" % (self.type, self.out, self.out_str)
def get_details(self):
return {}
class CommandOutputMissingMismatch(object):
def __init__(self, out, out_inc, type='output'):
if type == 'error':
self.type = 'Error output'
else:
self.type = 'Output'
self.out = out
self.out_inc = out_inc
def describe(self):
return "%s '%s' should contain '%s'"\
% (self.type, self.out, self.out_inc)
def get_details(self):
return {}
class CommandOutputExtraMismatch(object):
def __init__(self, out, out_exc, type='output'):
if type == 'error':
self.type = 'Error output'
else:
self.type = 'Output'
self.out = out
self.out_exc = out_exc
def describe(self):
return "%s '%s' should not contain '%s'"\
% (self.type, self.out, self.out_exc)
def get_details(self):
return {}
class CommandOutputReturnCodeMismatch(object):
def __init__(self, ret, ret_exp):
self.ret = ret
self.ret_exp = ret_exp
def describe(self):
return "Return code is '%s' but expected '%s'"\
% (self.ret, self.ret_exp)
def get_details(self):
return {}
class TestExit(Exception):
pass
class ArgumentParserForTests(argparse.ArgumentParser):
OUT = sys.stdout
ERR = sys.stderr
def __init__(self, **kwargs):
self.out = ArgumentParserForTests.OUT
self.err = ArgumentParserForTests.ERR
super(ArgumentParserForTests, self).__init__(**kwargs)
def error(self, message):
self.print_usage(self.err)
self.exit(2, _('%(prog)s: error: %(message)s\n') %
{'prog': self.prog, 'message': message})
def exit(self, status=0, message=None):
if message:
self._print_message(message, self.err)
raise TestExit
def print_usage(self, file=None):
if file is None:
file = self.out
self._print_message(self.format_usage(), file)
def print_help(self, file=None):
if file is None:
file = self.out
self._print_message(self.format_help(), file)
def print_version(self, file=None):
import warnings
warnings.warn(
'The print_version method is deprecated -- the "version" '
'argument to ArgumentParser is no longer supported.',
DeprecationWarning)
self._print_message(self.format_version(), file)
def _print_message(self, message, file=None):
if message:
if file is None:
file = self.err
file.write(message)
class FakeAPI(object):
def __init__(self, fixtures):
self.fixtures = fixtures