Improve help text for base arguments
Add more unit tests for the cratonclient.shell.main module and refactor some of the logic in that module to be more future-proof. Change-Id: Iddfc5a170d15de77e1e400c2fa54b4a91cd94b7a
This commit is contained in:
parent
e89a10eb24
commit
637a25c1a0
@ -14,23 +14,23 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import six
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from oslo_utils import encodeutils
|
from oslo_utils import encodeutils
|
||||||
|
from oslo_utils import importutils
|
||||||
|
|
||||||
from cratonclient import __version__
|
from cratonclient import __version__
|
||||||
from cratonclient import session as craton
|
from cratonclient import session as craton
|
||||||
|
|
||||||
from cratonclient.common import cliutils
|
from cratonclient.common import cliutils
|
||||||
from cratonclient.shell.v1 import shell
|
|
||||||
from cratonclient.v1 import client
|
from cratonclient.v1 import client
|
||||||
|
|
||||||
|
|
||||||
class CratonShell(object):
|
class CratonShell(object):
|
||||||
"""Class used to handle shell definition and parsing."""
|
"""Class used to handle shell definition and parsing."""
|
||||||
|
|
||||||
def get_base_parser(self):
|
@staticmethod
|
||||||
|
def get_base_parser():
|
||||||
"""Configure base craton arguments and parsing."""
|
"""Configure base craton arguments and parsing."""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog='craton',
|
prog='craton',
|
||||||
@ -51,32 +51,47 @@ class CratonShell(object):
|
|||||||
)
|
)
|
||||||
parser.add_argument('--craton-url',
|
parser.add_argument('--craton-url',
|
||||||
default=cliutils.env('CRATON_URL'),
|
default=cliutils.env('CRATON_URL'),
|
||||||
help='Defaults to env[CRATON_URL]',
|
help='The base URL of the running Craton service.'
|
||||||
|
' Defaults to env[CRATON_URL].',
|
||||||
)
|
)
|
||||||
parser.add_argument('--craton-project-id',
|
parser.add_argument('--craton-version',
|
||||||
type=int,
|
type=int,
|
||||||
default=1,
|
default=cliutils.env('CRATON_VERSION',
|
||||||
help='Defaults to 1',
|
default=1),
|
||||||
|
help='The version of the Craton API to use. '
|
||||||
|
'Defaults to env[CRATON_VERSION].'
|
||||||
|
)
|
||||||
|
parser.add_argument('--os-project-id',
|
||||||
|
default=cliutils.env('OS_PROJECT_ID'),
|
||||||
|
help='The project ID used to authenticate to '
|
||||||
|
'Craton. Defaults to env[OS_PROJECT_ID].',
|
||||||
)
|
)
|
||||||
parser.add_argument('--os-username',
|
parser.add_argument('--os-username',
|
||||||
default=cliutils.env('OS_USERNAME'),
|
default=cliutils.env('OS_USERNAME'),
|
||||||
help='Defaults to env[OS_USERNAME]',
|
help='The username used to authenticate to '
|
||||||
|
'Craton. Defaults to env[OS_USERNAME].',
|
||||||
)
|
)
|
||||||
parser.add_argument('--os-password',
|
parser.add_argument('--os-password',
|
||||||
default=cliutils.env('OS_PASSWORD'),
|
default=cliutils.env('OS_PASSWORD'),
|
||||||
help='Defaults to env[OS_PASSWORD]',
|
help='The password used to authenticate to '
|
||||||
|
'Craton. Defaults to env[OS_PASSWORD].',
|
||||||
)
|
)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
# NOTE(cmspence): Credit for this get_subcommand_parser function
|
# NOTE(cmspence): Credit for this get_subcommand_parser function
|
||||||
# goes to the magnumclient developers and contributors.
|
# goes to the magnumclient developers and contributors.
|
||||||
def get_subcommand_parser(self):
|
def get_subcommand_parser(self, api_version):
|
||||||
"""Get subcommands by parsing COMMAND_MODULES."""
|
"""Get subcommands by parsing COMMAND_MODULES."""
|
||||||
parser = self.get_base_parser()
|
parser = self.get_base_parser()
|
||||||
|
|
||||||
self.subcommands = {}
|
self.subcommands = {}
|
||||||
subparsers = parser.add_subparsers(metavar='<subcommand>',
|
subparsers = parser.add_subparsers(metavar='<subcommand>',
|
||||||
dest='subparser_name')
|
dest='subparser_name')
|
||||||
|
shell = importutils.import_versioned_module(
|
||||||
|
'cratonclient.shell',
|
||||||
|
api_version,
|
||||||
|
'shell',
|
||||||
|
)
|
||||||
command_modules = shell.COMMAND_MODULES
|
command_modules = shell.COMMAND_MODULES
|
||||||
for command_module in command_modules:
|
for command_module in command_modules:
|
||||||
self._find_subparsers(subparsers, command_module)
|
self._find_subparsers(subparsers, command_module)
|
||||||
@ -113,7 +128,7 @@ class CratonShell(object):
|
|||||||
parser = self.get_base_parser()
|
parser = self.get_base_parser()
|
||||||
(options, args) = parser.parse_known_args(argv)
|
(options, args) = parser.parse_known_args(argv)
|
||||||
subcommand_parser = (
|
subcommand_parser = (
|
||||||
self.get_subcommand_parser()
|
self.get_subcommand_parser(options.craton_version)
|
||||||
)
|
)
|
||||||
self.parser = subcommand_parser
|
self.parser = subcommand_parser
|
||||||
|
|
||||||
@ -125,7 +140,7 @@ class CratonShell(object):
|
|||||||
session = craton.Session(
|
session = craton.Session(
|
||||||
username=args.os_username,
|
username=args.os_username,
|
||||||
token=args.os_password,
|
token=args.os_password,
|
||||||
project_id=args.craton_project_id,
|
project_id=args.os_project_id,
|
||||||
)
|
)
|
||||||
self.cc = client.Client(session, args.craton_url)
|
self.cc = client.Client(session, args.craton_url)
|
||||||
args.func(self.cc, args)
|
args.func(self.cc, args)
|
||||||
@ -136,10 +151,9 @@ def main():
|
|||||||
try:
|
try:
|
||||||
CratonShell().main([encodeutils.safe_decode(a) for a in sys.argv[1:]])
|
CratonShell().main([encodeutils.safe_decode(a) for a in sys.argv[1:]])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("ERROR: %s" % encodeutils.safe_encode(six.text_type(e)),
|
print("ERROR: {}".format(encodeutils.exception_to_unicode(e)),
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,11 +67,11 @@ class TestMainShell(base.ShellTestCase):
|
|||||||
@mock.patch('cratonclient.session.Session')
|
@mock.patch('cratonclient.session.Session')
|
||||||
@mock.patch('cratonclient.v1.client.Client')
|
@mock.patch('cratonclient.v1.client.Client')
|
||||||
def test_main_craton_project_id(self, mock_client, mock_session):
|
def test_main_craton_project_id(self, mock_client, mock_session):
|
||||||
"""Verify --craton-project-id command is used for client connection."""
|
"""Verify --os-project-id command is used for client connection."""
|
||||||
self.shell('--craton-project-id 99 host-list -r 1')
|
self.shell('--os-project-id 99 host-list -r 1')
|
||||||
mock_session.assert_called_with(username=mock.ANY,
|
mock_session.assert_called_with(username=mock.ANY,
|
||||||
token=mock.ANY,
|
token=mock.ANY,
|
||||||
project_id=99)
|
project_id='99')
|
||||||
mock_client.assert_called_with(mock.ANY, mock.ANY)
|
mock_client.assert_called_with(mock.ANY, mock.ANY)
|
||||||
|
|
||||||
@mock.patch('cratonclient.session.Session')
|
@mock.patch('cratonclient.session.Session')
|
||||||
@ -108,7 +108,7 @@ class TestMainShell(base.ShellTestCase):
|
|||||||
url = '--craton-url test_url'
|
url = '--craton-url test_url'
|
||||||
username = '--os-username test_name'
|
username = '--os-username test_name'
|
||||||
pw = '--os-password test_pw'
|
pw = '--os-password test_pw'
|
||||||
proj_id = '--craton-project-id 1'
|
proj_id = '--os-project-id 1'
|
||||||
self.shell('{} {} {} {} host-create'.format(url,
|
self.shell('{} {} {} {} host-create'.format(url,
|
||||||
username,
|
username,
|
||||||
pw,
|
pw,
|
||||||
|
167
cratonclient/tests/unit/shell/test_main.py
Normal file
167
cratonclient/tests/unit/shell/test_main.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# 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.
|
||||||
|
"""Tests for the cratonclient.shell.main module."""
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import six
|
||||||
|
|
||||||
|
import cratonclient
|
||||||
|
from cratonclient.shell import main
|
||||||
|
from cratonclient.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestEntryPoint(base.TestCase):
|
||||||
|
"""Tests for the craton shell entry-point."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Patch out the CratonShell class."""
|
||||||
|
super(TestEntryPoint, self).setUp()
|
||||||
|
self.class_mock = mock.patch('cratonclient.shell.main.CratonShell')
|
||||||
|
self.craton_shell = self.class_mock.start()
|
||||||
|
self.addCleanup(self.class_mock.stop)
|
||||||
|
self.print_mock = mock.patch('cratonclient.shell.main.print')
|
||||||
|
self.print_func = self.print_mock.start()
|
||||||
|
self.addCleanup(self.print_mock.stop)
|
||||||
|
self.sys_exit_mock = mock.patch('sys.exit')
|
||||||
|
self.sys_exit = self.sys_exit_mock.start()
|
||||||
|
self.addCleanup(self.sys_exit_mock.stop)
|
||||||
|
|
||||||
|
def test_entry_point_creates_a_shell_instance(self):
|
||||||
|
"""Verify that our main entry-point uses CratonShell."""
|
||||||
|
CratonShell = self.craton_shell
|
||||||
|
|
||||||
|
main.main()
|
||||||
|
|
||||||
|
CratonShell.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_entry_point_calls_shell_main_method(self):
|
||||||
|
"""Verify we call the main method on our CratonShell instance."""
|
||||||
|
shell_instance = mock.Mock()
|
||||||
|
self.craton_shell.return_value = shell_instance
|
||||||
|
|
||||||
|
main.main()
|
||||||
|
|
||||||
|
self.assertTrue(shell_instance.main.called)
|
||||||
|
|
||||||
|
def test_entry_point_converts_args_to_text(self):
|
||||||
|
"""Verify we call the main method with a list of text objects."""
|
||||||
|
shell_instance = mock.Mock()
|
||||||
|
self.craton_shell.return_value = shell_instance
|
||||||
|
|
||||||
|
main.main()
|
||||||
|
|
||||||
|
# NOTE(sigmavirus24): call_args is a tuple of positional arguments and
|
||||||
|
# keyword arguments, so since we pass a list positionally, we want the
|
||||||
|
# first of the positional arguments.
|
||||||
|
arglist = shell_instance.main.call_args[0][0]
|
||||||
|
self.assertTrue(
|
||||||
|
all(isinstance(arg, six.text_type) for arg in arglist)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_entry_point_handles_all_exceptions(self):
|
||||||
|
"""Verify that we handle unexpected exceptions and print a message."""
|
||||||
|
shell_instance = mock.Mock()
|
||||||
|
shell_instance.main.side_effect = ValueError
|
||||||
|
self.craton_shell.return_value = shell_instance
|
||||||
|
|
||||||
|
main.main()
|
||||||
|
|
||||||
|
self.print_func.assert_called_once_with(
|
||||||
|
"ERROR: ",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCratonShell(base.TestCase):
|
||||||
|
"""Tests for the CratonShell class."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Create an instance of CratonShell for each test."""
|
||||||
|
super(TestCratonShell, self).setUp()
|
||||||
|
self.shell = main.CratonShell()
|
||||||
|
|
||||||
|
def test_get_base_parser(self):
|
||||||
|
"""Verify how we construct our basic Argument Parser."""
|
||||||
|
with mock.patch('argparse.ArgumentParser') as ArgumentParser:
|
||||||
|
parser = self.shell.get_base_parser()
|
||||||
|
|
||||||
|
self.assertEqual(ArgumentParser.return_value, parser)
|
||||||
|
ArgumentParser.assert_called_once_with(
|
||||||
|
prog='craton',
|
||||||
|
description=('Main shell for parsing arguments directed toward '
|
||||||
|
'Craton.'),
|
||||||
|
epilog='See "craton help COMMAND" for help on a specific command.',
|
||||||
|
add_help=False,
|
||||||
|
formatter_class=argparse.HelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_base_parser_sets_default_options(self):
|
||||||
|
"""Verify how we construct our basic Argument Parser."""
|
||||||
|
with mock.patch('cratonclient.common.cliutils.env') as env:
|
||||||
|
env.return_value = ''
|
||||||
|
with mock.patch('argparse.ArgumentParser'):
|
||||||
|
parser = self.shell.get_base_parser()
|
||||||
|
|
||||||
|
self.assertEqual([
|
||||||
|
mock.call(
|
||||||
|
'-h', '--help', action='store_true', help=argparse.SUPPRESS,
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
'--version', action='version',
|
||||||
|
version=cratonclient.__version__,
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
'--craton-url', default='',
|
||||||
|
help='The base URL of the running Craton service. '
|
||||||
|
'Defaults to env[CRATON_URL].',
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
'--craton-version', default='',
|
||||||
|
type=int,
|
||||||
|
help='The version of the Craton API to use. '
|
||||||
|
'Defaults to env[CRATON_VERSION].'
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
'--os-project-id', default='',
|
||||||
|
help='The project ID used to authenticate to Craton. '
|
||||||
|
'Defaults to env[OS_PROJECT_ID].',
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
'--os-username', default='',
|
||||||
|
help='The username used to authenticate to Craton. '
|
||||||
|
'Defaults to env[OS_USERNAME].',
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
'--os-password', default='',
|
||||||
|
help='The password used to authenticate to Craton. '
|
||||||
|
'Defaults to env[OS_PASSWORD].',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
parser.add_argument.call_args_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_base_parser_retrieves_environment_values(self):
|
||||||
|
"""Verify the environment variables that are requested."""
|
||||||
|
with mock.patch('cratonclient.common.cliutils.env') as env:
|
||||||
|
self.shell.get_base_parser()
|
||||||
|
|
||||||
|
self.assertEqual([
|
||||||
|
mock.call('CRATON_URL'),
|
||||||
|
mock.call('CRATON_VERSION', default=1),
|
||||||
|
mock.call('OS_PROJECT_ID'),
|
||||||
|
mock.call('OS_USERNAME'),
|
||||||
|
mock.call('OS_PASSWORD'),
|
||||||
|
],
|
||||||
|
env.call_args_list,
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user