From 991f4719e06ef00409a0270e171c04f5b0e4e41b Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 13 Dec 2012 09:23:32 -0500 Subject: Add tests for the help command & --help options Move the parser setup from bootstrap_with_global_options to bootstrap, so all API objects have access to it. Add some CLI tests for the help system. Part of the effort for https://fedorahosted.org/freeipa/ticket/3060 --- ipalib/plugable.py | 9 ++- tests/test_cmdline/test_help.py | 130 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 tests/test_cmdline/test_help.py diff --git a/ipalib/plugable.py b/ipalib/plugable.py index 8f42c630..fe09d3a6 100644 --- a/ipalib/plugable.py +++ b/ipalib/plugable.py @@ -456,7 +456,7 @@ class API(DictProxy): def isdone(self, name): return name in self.__done - def bootstrap(self, **overrides): + def bootstrap(self, parser=None, **overrides): """ Initialize environment variables and logging. """ @@ -516,6 +516,10 @@ class API(DictProxy): log.error('Cannot open log file %r: %s', self.env.log, e) return + if not parser: + parser = self.build_global_parser() + object.__setattr__(self, 'parser', parser) + def build_global_parser(self, parser=None, context=None): """ Add global options to an optparse.OptionParser instance. @@ -592,8 +596,7 @@ class API(DictProxy): overrides['webui_prod'] = options.prod if context is not None: overrides['context'] = context - self.bootstrap(**overrides) - object.__setattr__(self, 'parser', parser) + self.bootstrap(parser, **overrides) return (options, args) def load_plugins(self): diff --git a/tests/test_cmdline/test_help.py b/tests/test_cmdline/test_help.py new file mode 100644 index 00000000..1ab3ddc4 --- /dev/null +++ b/tests/test_cmdline/test_help.py @@ -0,0 +1,130 @@ +# Authors: Petr Viktorin +# +# Copyright (C) 2012 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import sys +import contextlib +import StringIO + +from nose.tools import assert_raises # pylint: disable=E0611 + +from ipalib import api, errors +from ipalib.plugins.user import user_add + + +class CLITestContext(object): + """Context manager that replaces stdout & stderr, and catches SystemExit + + Whatever was printed to the streams is available in ``stdout`` and + ``stderr`` attrributes once the with statement finishes. + + When exception is given, asserts that exception is raised. The exception + will be available in the ``exception`` attribute. + """ + def __init__(self, exception=None): + self.exception = exception + + def __enter__(self): + self.old_streams = sys.stdout, sys.stderr + self.stdout_fileobj = sys.stdout = StringIO.StringIO() + self.stderr_fileobj = sys.stderr = StringIO.StringIO() + return self + + def __exit__(self, exc_type, exc_value, traceback): + sys.stdout, sys.stderr = self.old_streams + self.stdout = self.stdout_fileobj.getvalue() + self.stderr = self.stderr_fileobj.getvalue() + self.stdout_fileobj.close() + self.stderr_fileobj.close() + if self.exception: + assert isinstance(exc_value, self.exception), exc_value + self.exception = exc_value + return True + + +def test_ipa_help(): + """Test that `ipa help` only writes to stdout""" + with CLITestContext() as ctx: + return_value = api.Backend.cli.run(['help']) + assert return_value == 0 + assert ctx.stderr == '' + + +def test_ipa_without_arguments(): + """Test that `ipa` errors out, and prints the help to stderr""" + with CLITestContext(exception=SystemExit) as ctx: + api.Backend.cli.run([]) + assert ctx.exception.code == 2 + assert ctx.stdout == '' + assert 'Error: Command not specified' in ctx.stderr + + with CLITestContext() as help_ctx: + api.Backend.cli.run(['help']) + assert help_ctx.stdout in ctx.stderr + + +def test_bare_topic(): + """Test that `ipa user` errors out, and prints the help to stderr + + This is because `user` is a topic, not a command, so `ipa user` doesn't + match our usage string. The help should be accessed using `ipa help user`. + """ + with CLITestContext(exception=errors.CommandError) as ctx: + api.Backend.cli.run(['user']) + assert ctx.exception.name == 'user' + assert ctx.stdout == '' + + with CLITestContext() as help_ctx: + return_value = api.Backend.cli.run(['help', 'user']) + assert return_value == 0 + assert help_ctx.stdout in ctx.stderr + + +def test_command_help(): + """Test that `help user-add` & `user-add -h` are equivalent and contain doc + """ + with CLITestContext() as help_ctx: + return_value = api.Backend.cli.run(['help', 'user-add']) + assert return_value == 0 + assert help_ctx.stderr == '' + + with CLITestContext(exception=SystemExit) as h_ctx: + api.Backend.cli.run(['user-add', '-h']) + assert h_ctx.exception.code == 0 + assert h_ctx.stderr == '' + + assert h_ctx.stdout == help_ctx.stdout + assert unicode(user_add.__doc__) in help_ctx.stdout + + +def test_ambiguous_command_or_topic(): + """Test that `help ping` & `ping -h` are NOT equivalent + + One is a topic, the other is a command + """ + with CLITestContext() as help_ctx: + return_value = api.Backend.cli.run(['help', 'ping']) + assert return_value == 0 + assert help_ctx.stderr == '' + + with CLITestContext(exception=SystemExit) as h_ctx: + api.Backend.cli.run(['ping', '-h']) + assert h_ctx.exception.code == 0 + assert h_ctx.stderr == '' + + assert h_ctx.stdout != help_ctx.stdout -- cgit