diff options
-rw-r--r-- | cliff/app.py | 19 | ||||
-rw-r--r-- | cliff/help.py | 12 | ||||
-rw-r--r-- | cliff/interactive.py | 1 | ||||
-rw-r--r-- | tests/test_app.py | 170 | ||||
-rw-r--r-- | tests/test_command.py | 22 | ||||
-rw-r--r-- | tests/test_commandmanager.py | 46 | ||||
-rw-r--r-- | tests/test_help.py | 75 | ||||
-rw-r--r-- | tox.ini | 7 |
8 files changed, 338 insertions, 14 deletions
diff --git a/cliff/app.py b/cliff/app.py index 6b1a851..eec2d5c 100644 --- a/cliff/app.py +++ b/cliff/app.py @@ -90,7 +90,7 @@ class App(object): '-h', '--help', action=HelpAction, nargs=0, - default=self.command_manager, # tricky + default=self, # tricky help="show this help message and exit", ) parser.add_argument( @@ -118,7 +118,7 @@ class App(object): root_logger.addHandler(file_handler) # Send higher-level messages to the console via stderr - console = logging.StreamHandler() + console = logging.StreamHandler(self.stderr) console_level = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG, @@ -177,7 +177,6 @@ class App(object): def interact(self): self.interactive_mode = True interpreter = self.interactive_app_factory(self, self.command_manager, self.stdin, self.stdout) - interpreter.prompt = '(%s) ' % self.NAME interpreter.cmdloop() return 0 @@ -193,11 +192,9 @@ class App(object): parsed_args = cmd_parser.parse_args(sub_argv) result = cmd.run(parsed_args) except Exception as err: + LOG.error('ERROR: %s', err) if self.options.debug: LOG.exception(err) - raise - LOG.error('ERROR: %s', err) - finally: try: self.clean_up(cmd, result, err) except Exception as err2: @@ -205,4 +202,14 @@ class App(object): LOG.exception(err2) else: LOG.error('Could not clean up: %s', err2) + if self.options.debug: + raise + else: + try: + self.clean_up(cmd, result, None) + except Exception as err3: + if self.options.debug: + LOG.exception(err3) + else: + LOG.error('Could not clean up: %s', err3) return result diff --git a/cliff/help.py b/cliff/help.py index 391d8cc..2ac8bc4 100644 --- a/cliff/help.py +++ b/cliff/help.py @@ -12,15 +12,15 @@ class HelpAction(argparse.Action): instance, passed in as the "default" value for the action. """ def __call__(self, parser, namespace, values, option_string=None): - parser.print_help() - print('') - print('Commands:') - command_manager = self.default + app = self.default + parser.print_help(app.stdout) + app.stdout.write('\nCommands:\n') + command_manager = app.command_manager for name, ep in sorted(command_manager): factory = ep.load() cmd = factory(self, None) one_liner = cmd.get_description().split('\n')[0] - print(' %-13s %s' % (name, one_liner)) + app.stdout.write(' %-13s %s\n' % (name, one_liner)) sys.exit(0) @@ -47,5 +47,5 @@ class HelpCommand(Command): cmd_parser = cmd.get_parser(full_name) else: cmd_parser = self.get_parser(' '.join([self.app.NAME, 'help'])) - cmd_parser.parse_args(['--help']) + cmd_parser.print_help(self.app.stdout) return 0 diff --git a/cliff/interactive.py b/cliff/interactive.py index 493235c..2543b20 100644 --- a/cliff/interactive.py +++ b/cliff/interactive.py @@ -33,6 +33,7 @@ class InteractiveApp(cmd2.Cmd): def __init__(self, parent_app, command_manager, stdin, stdout): self.parent_app = parent_app + self.prompt = '(%s) ' % parent_app.NAME self.command_manager = command_manager cmd2.Cmd.__init__(self, 'tab', stdin=stdin, stdout=stdout) diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..cae2c32 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,170 @@ +from cliff.app import App +from cliff.command import Command +from cliff.commandmanager import CommandManager + +import mock + + +def make_app(): + cmd_mgr = CommandManager('cliff.tests') + + # Register a command that succeeds + command = mock.MagicMock(spec=Command) + command_inst = mock.MagicMock(spec=Command) + command_inst.run.return_value = 0 + command.return_value = command_inst + cmd_mgr.add_command('mock', command) + + # Register a command that fails + err_command = mock.Mock(name='err_command', spec=Command) + err_command_inst = mock.Mock(spec=Command) + err_command_inst.run = mock.Mock(side_effect=RuntimeError('test exception')) + err_command.return_value = err_command_inst + cmd_mgr.add_command('error', err_command) + + app = App('testing interactive mode', + '1', + cmd_mgr, + stderr=mock.Mock(), # suppress warning messages + ) + return app, command + + +def test_no_args_triggers_interactive_mode(): + app, command = make_app() + app.interact = mock.MagicMock(name='inspect') + app.run([]) + app.interact.assert_called_once_with() + + +def test_interactive_mode_cmdloop(): + app, command = make_app() + app.interactive_app_factory = mock.MagicMock(name='interactive_app_factory') + app.run([]) + app.interactive_app_factory.return_value.cmdloop.assert_called_once_with() + + +def test_initialize_app(): + app, command = make_app() + app.initialize_app = mock.MagicMock(name='initialize_app') + app.run(['mock']) + app.initialize_app.assert_called_once_with() + + +def test_prepare_to_run_command(): + app, command = make_app() + app.prepare_to_run_command = mock.MagicMock(name='prepare_to_run_command') + app.run(['mock']) + app.prepare_to_run_command.assert_called_once_with(command()) + + +def test_clean_up_success(): + app, command = make_app() + app.clean_up = mock.MagicMock(name='clean_up') + app.run(['mock']) + app.clean_up.assert_called_once_with(command.return_value, 0, None) + + +def test_clean_up_error(): + app, command = make_app() + + app.clean_up = mock.MagicMock(name='clean_up') + app.run(['error']) + + app.clean_up.assert_called_once() + call_args = app.clean_up.call_args_list[0] + assert call_args == mock.call(mock.ANY, 1, mock.ANY) + args, kwargs = call_args + assert isinstance(args[2], RuntimeError) + assert args[2].args == ('test exception',) + + +def test_clean_up_error_debug(): + app, command = make_app() + + app.clean_up = mock.MagicMock(name='clean_up') + try: + app.run(['--debug', 'error']) + except RuntimeError as err: + assert app.clean_up.call_args_list[0][0][2] is err + else: + assert False, 'Should have had an exception' + + app.clean_up.assert_called_once() + call_args = app.clean_up.call_args_list[0] + assert call_args == mock.call(mock.ANY, 1, mock.ANY) + args, kwargs = call_args + assert isinstance(args[2], RuntimeError) + assert args[2].args == ('test exception',) + + +def test_error_handling_clean_up_raises_exception(): + app, command = make_app() + + app.clean_up = mock.MagicMock( + name='clean_up', + side_effect=RuntimeError('within clean_up'), + ) + app.run(['error']) + + app.clean_up.assert_called_once() + call_args = app.clean_up.call_args_list[0] + assert call_args == mock.call(mock.ANY, 1, mock.ANY) + args, kwargs = call_args + assert isinstance(args[2], RuntimeError) + assert args[2].args == ('test exception',) + + +def test_error_handling_clean_up_raises_exception_debug(): + app, command = make_app() + + app.clean_up = mock.MagicMock( + name='clean_up', + side_effect=RuntimeError('within clean_up'), + ) + try: + app.run(['--debug', 'error']) + except RuntimeError as err: + if not hasattr(err, '__context__'): + # The exception passed to clean_up is not the exception + # caused *by* clean_up. This test is only valid in python + # 2 because under v3 the original exception is re-raised + # with the new one as a __context__ attribute. + assert app.clean_up.call_args_list[0][0][2] is not err + else: + assert False, 'Should have had an exception' + + app.clean_up.assert_called_once() + call_args = app.clean_up.call_args_list[0] + assert call_args == mock.call(mock.ANY, 1, mock.ANY) + args, kwargs = call_args + assert isinstance(args[2], RuntimeError) + assert args[2].args == ('test exception',) + + +def test_normal_clean_up_raises_exception(): + app, command = make_app() + + app.clean_up = mock.MagicMock( + name='clean_up', + side_effect=RuntimeError('within clean_up'), + ) + app.run(['mock']) + + app.clean_up.assert_called_once() + call_args = app.clean_up.call_args_list[0] + assert call_args == mock.call(mock.ANY, 0, None) + + +def test_normal_clean_up_raises_exception_debug(): + app, command = make_app() + + app.clean_up = mock.MagicMock( + name='clean_up', + side_effect=RuntimeError('within clean_up'), + ) + app.run(['--debug', 'mock']) + + app.clean_up.assert_called_once() + call_args = app.clean_up.call_args_list[0] + assert call_args == mock.call(mock.ANY, 0, None) diff --git a/tests/test_command.py b/tests/test_command.py new file mode 100644 index 0000000..8b0d217 --- /dev/null +++ b/tests/test_command.py @@ -0,0 +1,22 @@ + +from cliff.command import Command + + +class TestCommand(Command): + """Description of command. + """ + + def run(self, parsed_args): + return + + +def test_get_description(): + cmd = TestCommand(None, None) + desc = cmd.get_description() + assert desc == "Description of command.\n " + + +def test_get_parser(): + cmd = TestCommand(None, None) + parser = cmd.get_parser('NAME') + assert parser.prog == 'NAME' diff --git a/tests/test_commandmanager.py b/tests/test_commandmanager.py index 6be244e..1945f2e 100644 --- a/tests/test_commandmanager.py +++ b/tests/test_commandmanager.py @@ -1,4 +1,6 @@ +import mock + from cliff.commandmanager import CommandManager @@ -47,3 +49,47 @@ def test_lookup_with_remainder(): ]: yield check, mgr, expected return + + +def test_find_invalid_command(): + mgr = TestCommandManager('test') + def check_one(argv): + try: + mgr.find_command(argv) + except ValueError as err: + assert '-b' in ('%s' % err) + else: + assert False, 'expected a failure' + for argv in [['a', '-b'], + ['-b'], + ]: + yield check_one, argv + + +def test_find_unknown_command(): + mgr = TestCommandManager('test') + try: + mgr.find_command(['a', 'b']) + except ValueError as err: + assert "['a', 'b']" in ('%s' % err) + else: + assert False, 'expected a failure' + + +def test_add_command(): + mgr = TestCommandManager('test') + mock_cmd = mock.Mock() + mgr.add_command('mock', mock_cmd) + found_cmd, name, args = mgr.find_command(['mock']) + assert found_cmd is mock_cmd + + +def test_load_commands(): + testcmd = mock.Mock(name='testcmd') + testcmd.name.replace.return_value = 'test' + mock_pkg_resources = mock.Mock(return_value=[testcmd]) + with mock.patch('pkg_resources.iter_entry_points', mock_pkg_resources) as iter_entry_points: + mgr = CommandManager('test') + assert iter_entry_points.called_once_with('test') + names = [n for n, v in mgr] + assert names == ['test'] diff --git a/tests/test_help.py b/tests/test_help.py new file mode 100644 index 0000000..4a2fd1c --- /dev/null +++ b/tests/test_help.py @@ -0,0 +1,75 @@ +try: + from StringIO import StringIO +except: + from io import StringIO + +import mock + +from cliff.app import App +from cliff.command import Command +from cliff.commandmanager import CommandManager +from cliff.help import HelpCommand + + +class TestParser(object): + + def print_help(self, stdout): + stdout.write('TestParser') + + +class TestCommand(Command): + + @classmethod + def load(cls): + return cls + + def get_parser(self, ignore): + # Make it look like this class is the parser + # so parse_args() is called. + return TestParser() + + def run(self, args): + return + + +class TestCommandManager(CommandManager): + def _load_commands(self): + self.commands = { + 'one': TestCommand, + 'two words': TestCommand, + 'three word command': TestCommand, + } + + +def test_show_help_for_command(): + # FIXME(dhellmann): Are commands tied too closely to the app? Or + # do commands know too much about apps by using them to get to the + # command manager? + stdout = StringIO() + app = App('testing', '1', TestCommandManager('cliff.test'), stdout=stdout) + app.NAME = 'test' + help_cmd = HelpCommand(app, mock.Mock()) + parser = help_cmd.get_parser('test') + parsed_args = parser.parse_args(['one']) + try: + help_cmd.run(parsed_args) + except SystemExit: + pass + assert stdout.getvalue() == 'TestParser' + +def test_show_help_for_help(): + # FIXME(dhellmann): Are commands tied too closely to the app? Or + # do commands know too much about apps by using them to get to the + # command manager? + stdout = StringIO() + app = App('testing', '1', TestCommandManager('cliff.test'), stdout=stdout) + app.NAME = 'test' + help_cmd = HelpCommand(app, mock.Mock()) + parser = help_cmd.get_parser('test') + parsed_args = parser.parse_args([]) + try: + help_cmd.run(parsed_args) + except SystemExit: + pass + help_text = stdout.getvalue() + assert 'usage: test help [-h]' in help_text @@ -2,5 +2,8 @@ envlist = py27,py32 [testenv] -commands = nosetests -d [] -deps = nose +commands = nosetests -d --with-coverage --cover-package=cliff [] +deps = + nose + mock + coverage |