https://fedorahosted.org/freeipa/ticket/3060

Here is a collection of smallish fixes to `ipa help` and `ipa <something> --help`.
This should address most of Nikolai's proposal.
Additionally, it's now possible to run `ipa <command> --help` without a Kerberos ticket. And there are some new tests.

I've not included the "Often used commands" in `ipa help`; I think that is material for a manual/tutorial, not a help command. Selecting a topic from `ipa topics` and then choosing a command from `ipa help <TOPIC>` is a better way to use the help than the verbose `ipa help commands` or proposed incomplete "Often used commands".

--
PetrĀ³
From 869fac2e8a6d402ab2f93dc0a14dcbfa0f40ad1c Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Tue, 6 Nov 2012 11:05:41 -0500
Subject: [PATCH] Improve `ipa --help` output

Fix the usage string to match actual usage.

Add command description.

Put information about `ipa help topics` etc. to the epilog,
instead of using empty option groups. Use a custom formatter
to preserve newlines.

Add the -h/--help option manually to ensure consistent case
(capital S).

Part of the effort for https://fedorahosted.org/freeipa/ticket/3060
---
 ipalib/plugable.py |   37 ++++++++++++++++++++++++++++++-------
 1 files changed, 30 insertions(+), 7 deletions(-)

diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index f3d185e14332f9f97937e2f1bc26069447eab855..46149795431174f8568994bb8db0d56d8510f5e5 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -34,6 +34,8 @@ from os import path
 import subprocess
 import optparse
 import errors
+import textwrap
+
 from config import Env
 import util
 import text
@@ -519,8 +521,23 @@ class API(DictProxy):
         Add global options to an optparse.OptionParser instance.
         """
         if parser is None:
-            parser = optparse.OptionParser()
+            parser = optparse.OptionParser(
+                add_help_option=False,
+                formatter=IPAHelpFormatter(),
+                usage='%prog [global-options] COMMAND [command-options]',
+                description='Manage an IPA domain',
+                epilog='\n'.join([
+                    'See "ipa help topics" for available help topics.',
+                    'See "ipa help <TOPIC>" for more information on a '
+                        'specific topic.',
+                    'See "ipa help commands" for the full list of commands.',
+                    'See "ipa help <COMMAND>" for more information on a '
+                        'specific command.',
+                ]))
             parser.disable_interspersed_args()
+            parser.add_option("-h", "--help", action="help",
+                help='Show this help message and exit')
+
         parser.add_option('-e', dest='env', metavar='KEY=VAL', action='append',
             help='Set environment variable KEY to VAL',
         )
@@ -548,12 +565,6 @@ class API(DictProxy):
                 dest='fallback',
                 help='Only use the server configured in /etc/ipa/default.conf'
             )
-        topics = optparse.OptionGroup(parser, "Available help topics",
-                    "ipa help topics")
-        cmds = optparse.OptionGroup(parser, "Available commands",
-                    "ipa help commands")
-        parser.add_option_group(topics)
-        parser.add_option_group(cmds)
 
         return parser
 
@@ -730,3 +741,15 @@ class API(DictProxy):
         object.__setattr__(self, 'plugins',
             tuple(PluginInfo(p) for p in plugins.itervalues())
         )
+
+
+class IPAHelpFormatter(optparse.IndentedHelpFormatter):
+    def format_epilog(self, text):
+        text_width = self.width - self.current_indent
+        indent = " " * self.current_indent
+        lines = text.splitlines()
+        result = '\n'.join(
+            textwrap.fill(line, text_width, initial_indent=indent,
+                subsequent_indent=indent)
+            for line in lines)
+        return '\n%s\n' % result
-- 
1.7.7.6

From ab861ae10d270523851a0f05ce0d45fedbff05a7 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Tue, 6 Nov 2012 11:45:50 -0500
Subject: [PATCH] Print help to stderr on error

Whenever a command is used incorrectly, it should output
an error message (and possibly additional help) to stderr.

This patch adds a parameter to a bunch of places to allow
selecting either stdout or stderr for help output, and makes
badly called commands output to stderr only.

Part of the effort for https://fedorahosted.org/freeipa/ticket/3060
---
 ipalib/cli.py |   76 ++++++++++++++++++++++++++++++++++----------------------
 1 files changed, 46 insertions(+), 30 deletions(-)

diff --git a/ipalib/cli.py b/ipalib/cli.py
index ac0eb058912246ef6f2c7aed34299dceae505990..704a75ca3510cfb0b26ffdb57d7f3cded250d8fd 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -50,7 +50,7 @@ from errors import (PublicError, CommandError, HelpError, InternalError,
         NoSuchNamespaceError, ValidationError, NotFound, NotConfiguredError,
         PromptFailed, ConversionError)
 from constants import CLI_TAB
-from parameters import Password, Bytes, File, Str, StrEnum
+from parameters import Password, Bytes, File, Str, StrEnum, Any
 from text import _
 from ipapython.version import API_VERSION
 
@@ -665,6 +665,9 @@ class help(frontend.Local):
     """
 
     takes_args = (Str('command?'),)
+    takes_options = (
+        Any('outfile?', flags=['no_option']),
+    )
 
     has_output = tuple()
 
@@ -749,56 +752,68 @@ class help(frontend.Local):
 
         super(help, self)._on_finalize()
 
-    def run(self, key):
+    def run(self, key, outfile=None):
+        if outfile is None:
+            outfile = sys.stdout
+        writer = self._writer(outfile)
         name = from_cli(key)
         mod_name = '%s.%s' % (self._PLUGIN_BASE_MODULE, name)
-        if key is None or name == "topics":
-            self.print_topics()
+        if key is None:
+            make_ipa_parser().print_help(outfile)
+            return
+        if name == "topics":
+            self.print_topics(outfile)
             return
         if name in self._topics:
-            self.print_commands(name)
+            self.print_commands(name, outfile)
         elif name in self.Command:
             cmd = self.Command[name]
             if cmd.NO_CLI:
                 raise HelpError(topic=name)
-            print unicode(_('Purpose: %s')) % unicode(_(cmd.doc)).strip()
+            writer(_('Purpose: %s') % unicode(_(cmd.doc)).strip())
             self.Backend.cli.build_parser(cmd).print_help()
         elif mod_name in sys.modules:
-            self.print_commands(name)
+            self.print_commands(name, outfile)
         elif name == "commands":
             mcl = max(len(s) for s in (self.Command))
             for cname in self.Command:
                 cmd = self.Command[cname]
                 if cmd.NO_CLI:
                     continue
-                print '%s  %s' % (to_cli(cmd.name).ljust(mcl), cmd.summary)
+                writer('%s  %s' % (to_cli(cmd.name).ljust(mcl), cmd.summary))
         else:
             raise HelpError(topic=name)
 
-    def print_topics(self):
+    def _writer(self, outfile):
+        def writer(string=''):
+            print >> outfile, unicode(string)
+        return writer
+
+    def print_topics(self, outfile):
+        writer = self._writer(outfile)
         topics = sorted(self._topics.keys())
 
-        print unicode(_('Usage: ipa [global-options] COMMAND ...'))
-        print ''
-        print unicode(_('Built-in commands:'))
+        writer(_('Usage: ipa [global-options] COMMAND [command-options]...'))
+        writer()
+        writer(_('Built-in commands:'))
         for c in self._builtins:
-            print unicode(_('Help subtopics:'))
-            print '  %s  %s' % (to_cli(c.name).ljust(self._mtl), c.summary)
-        print ''
-        print unicode(_('Help topics:'))
+            writer('  %s  %s' % (to_cli(c.name).ljust(self._mtl), c.summary))
+        writer()
+        writer(_('Help topics:'))
         for t in topics:
             topic = self._topics[t]
-            print '  %s  %s' % (to_cli(t).ljust(self._mtl), topic[0])
-        print ''
-        print unicode(_('Try `ipa --help` for a list of global options.'))
+            writer('  %s  %s' % (to_cli(t).ljust(self._mtl), topic[0]))
+        writer()
+        writer(_('Try `ipa --help` for a list of global options.'))
 
-    def print_commands(self, topic):
+    def print_commands(self, topic, outfile):
+        writer = self._writer(outfile)
         if topic in self._topics and type(self._topics[topic][2]) is dict:
             # we want to display topic which has subtopics
             for subtopic in self._topics[topic][2]:
                 doc = self._topics[topic][2][subtopic][0]
                 mcl = self._topics[topic][1]
-                print '  %s  %s' % (to_cli(subtopic).ljust(mcl), doc)
+                writer('  %s  %s' % (to_cli(subtopic).ljust(mcl), doc))
         else:
             # we want to display subtopic or a topic which has no subtopics
             if topic in self._topics:
@@ -821,13 +836,14 @@ class help(frontend.Local):
             if topic not in self.Command and len(commands) == 0:
                 raise HelpError(topic=topic)
 
-            print doc
+            writer(doc)
             if commands:
-                print ''
-                print unicode(_('Topic commands:'))
+                writer()
+                writer(_('Topic commands:'))
                 for c in commands:
-                    print '  %s  %s' % (to_cli(c.name).ljust(mcl), c.summary)
-            print "\n"
+                    writer(
+                        '  %s  %s' % (to_cli(c.name).ljust(mcl), c.summary))
+            writer()
 
 class show_mappings(frontend.Command):
     """
@@ -1013,14 +1029,14 @@ class cli(backend.Executioner):
         On incorrect invocation, prints out a help message and returns None
         """
         if len(argv) == 0:
+            print >>sys.stderr, 'Error: Command not specified'
             self.Command.help()
-            return
+            exit(2)
         (key, argv) = (argv[0], argv[1:])
         name = from_cli(key)
         if name not in self.Command and len(argv) == 0:
             try:
-                self.Command.help(unicode(key))
-                return
+                self.Command.help(unicode(key), outfile=sys.stderr)
             except HelpError:
                 pass
         if name not in self.Command or self.Command[name].NO_CLI:
@@ -1120,7 +1136,7 @@ class cli(backend.Executioner):
 
         for arg in cmd.args():
             name = self.__get_arg_name(arg, format_name=False)
-            if name is None:
+            if 'no_option' in arg.flags or name is None:
                 continue
             doc = unicode(arg.doc)
             parser.add_argument(name, doc)
-- 
1.7.7.6

From 2a8f2b5359676f247121e0b6b34438db2064d71b Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Thu, 8 Nov 2012 08:00:51 -0500
Subject: [PATCH] Store the OptionParser in the API, use it to print unified
 help messages

Make `ipa -h` and `ipa help` output the same message.

Since `ipa -h` output is generated by the OptionParser, we need to make
the parser available. Store it in `api.parser`.

Part of the effort for https://fedorahosted.org/freeipa/ticket/3060
---
 ipalib/cli.py      |   11 ++++-------
 ipalib/plugable.py |    1 +
 2 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/ipalib/cli.py b/ipalib/cli.py
index 704a75ca3510cfb0b26ffdb57d7f3cded250d8fd..a20c06e85d480a85d50b68c4f185e73ced7713c7 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -759,7 +759,7 @@ class help(frontend.Local):
         name = from_cli(key)
         mod_name = '%s.%s' % (self._PLUGIN_BASE_MODULE, name)
         if key is None:
-            make_ipa_parser().print_help(outfile)
+            self.api.parser.print_help(outfile)
             return
         if name == "topics":
             self.print_topics(outfile)
@@ -771,7 +771,7 @@ class help(frontend.Local):
             if cmd.NO_CLI:
                 raise HelpError(topic=name)
             writer(_('Purpose: %s') % unicode(_(cmd.doc)).strip())
-            self.Backend.cli.build_parser(cmd).print_help()
+            self.Backend.cli.build_parser(cmd).print_help(outfile)
         elif mod_name in sys.modules:
             self.print_commands(name, outfile)
         elif name == "commands":
@@ -795,10 +795,6 @@ class help(frontend.Local):
 
         writer(_('Usage: ipa [global-options] COMMAND [command-options]...'))
         writer()
-        writer(_('Built-in commands:'))
-        for c in self._builtins:
-            writer('  %s  %s' % (to_cli(c.name).ljust(self._mtl), c.summary))
-        writer()
         writer(_('Help topics:'))
         for t in topics:
             topic = self._topics[t]
@@ -1029,8 +1025,9 @@ class cli(backend.Executioner):
         On incorrect invocation, prints out a help message and returns None
         """
         if len(argv) == 0:
+            self.Command.help(outfile=sys.stderr)
+            print >>sys.stderr
             print >>sys.stderr, 'Error: Command not specified'
-            self.Command.help()
             exit(2)
         (key, argv) = (argv[0], argv[1:])
         name = from_cli(key)
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index 46149795431174f8568994bb8db0d56d8510f5e5..d2541e3b7fc190712fd6b08a6b8165fe6aaeca12 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -593,6 +593,7 @@ class API(DictProxy):
         if context is not None:
             overrides['context'] = context
         self.bootstrap(**overrides)
+        object.__setattr__(self, 'parser', parser)
         return (options, args)
 
     def load_plugins(self):
-- 
1.7.7.6

From 8715bb685f69cce407e005aa1e6f0641e3e82f50 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Thu, 8 Nov 2012 08:10:54 -0500
Subject: [PATCH] Simplify `ipa help topics` output

This brings the output closer to `ipa help commands` and removes
extraneous information.

Part of the effort for https://fedorahosted.org/freeipa/ticket/3060
---
 ipalib/cli.py |   11 ++---------
 1 files changed, 2 insertions(+), 9 deletions(-)

diff --git a/ipalib/cli.py b/ipalib/cli.py
index a20c06e85d480a85d50b68c4f185e73ced7713c7..fc6ac637879b9096efa86c5a02644b2e46e81f06 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -791,16 +791,9 @@ class help(frontend.Local):
 
     def print_topics(self, outfile):
         writer = self._writer(outfile)
-        topics = sorted(self._topics.keys())
 
-        writer(_('Usage: ipa [global-options] COMMAND [command-options]...'))
-        writer()
-        writer(_('Help topics:'))
-        for t in topics:
-            topic = self._topics[t]
-            writer('  %s  %s' % (to_cli(t).ljust(self._mtl), topic[0]))
-        writer()
-        writer(_('Try `ipa --help` for a list of global options.'))
+        for t, topic in sorted(self._topics.items()):
+            writer('%s  %s' % (to_cli(t).ljust(self._mtl), topic[0]))
 
     def print_commands(self, topic, outfile):
         writer = self._writer(outfile)
-- 
1.7.7.6

From 77b5a1fa24f6e164028488988fba20ae91336644 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Thu, 8 Nov 2012 08:56:52 -0500
Subject: [PATCH] Add command summary to `ipa COMMAND --help` output

This makes the output identical to `ipa help COMMAND`.

Part of the effort for https://fedorahosted.org/freeipa/ticket/3060
---
 ipalib/cli.py |    4 ++--
 1 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/ipalib/cli.py b/ipalib/cli.py
index fc6ac637879b9096efa86c5a02644b2e46e81f06..ab2918b53c63430b9cce9153b128c9071c419fec 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -770,7 +770,6 @@ class help(frontend.Local):
             cmd = self.Command[name]
             if cmd.NO_CLI:
                 raise HelpError(topic=name)
-            writer(_('Purpose: %s') % unicode(_(cmd.doc)).strip())
             self.Backend.cli.build_parser(cmd).print_help(outfile)
         elif mod_name in sys.modules:
             self.print_commands(name, outfile)
@@ -1085,7 +1084,8 @@ class cli(backend.Executioner):
 
     def build_parser(self, cmd):
         parser = CLIOptionParser(
-            usage=' '.join(self.usage_iter(cmd))
+            usage=' '.join(self.usage_iter(cmd)),
+            description=cmd.summary,
         )
         option_groups = {}
         for option in cmd.options():
-- 
1.7.7.6

From f9e1a88d7c3124cf3c4b7dbec85ee2f4aa191c28 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Thu, 8 Nov 2012 08:57:39 -0500
Subject: [PATCH] Mention `ipa COMMAND --help` as the preferred way to get
 command help

This avoids the problem with ambiguous command/topic names.

No functionality is changed; `ipa help <COMMAND>` still works as before
if there's no topic with the same name.

https://fedorahosted.org/freeipa/ticket/3247
---
 ipalib/cli.py      |    5 ++++-
 ipalib/plugable.py |    2 +-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/ipalib/cli.py b/ipalib/cli.py
index ab2918b53c63430b9cce9153b128c9071c419fec..59abc3b15e05a79496232f0bfbcd128644796d1a 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -664,7 +664,10 @@ class help(frontend.Local):
     Display help for a command or topic.
     """
 
-    takes_args = (Str('command?'),)
+    takes_args = (
+        Str('command?', cli_name='topic', label=_('Topic or Command'),
+            doc=_('The topic or command name.')),
+    )
     takes_options = (
         Any('outfile?', flags=['no_option']),
     )
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index d2541e3b7fc190712fd6b08a6b8165fe6aaeca12..8f42c630409e068abc4cb533885306c710c473e5 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -531,7 +531,7 @@ class API(DictProxy):
                     'See "ipa help <TOPIC>" for more information on a '
                         'specific topic.',
                     'See "ipa help commands" for the full list of commands.',
-                    'See "ipa help <COMMAND>" for more information on a '
+                    'See "ipa <COMMAND> --help" for more information on a '
                         'specific command.',
                 ]))
             parser.disable_interspersed_args()
-- 
1.7.7.6

From 8d3bb96eade8f9aa683dd1b66b53f20035bee522 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Thu, 8 Nov 2012 08:59:31 -0500
Subject: [PATCH] Parse command arguments before creating a context

This allows users to run `ipa COMMAND --help` even without
Kerberos credentials.
---
 ipalib/cli.py |    2 +-
 1 files changed, 1 insertions(+), 1 deletions(-)

diff --git a/ipalib/cli.py b/ipalib/cli.py
index 59abc3b15e05a79496232f0bfbcd128644796d1a..74c43b590c1272d8dbb94c7ae7501eaad79090c0 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -1051,10 +1051,10 @@ class cli(backend.Executioner):
         if cmd is None:
             return
         name = cmd.name
+        kw = self.argv_to_keyword_arguments(cmd, argv[1:])
         if not isinstance(cmd, frontend.Local):
             self.create_context()
         try:
-            kw = self.argv_to_keyword_arguments(cmd, argv[1:])
             result = self.execute(name, **kw)
             if callable(cmd.output_for_cli):
                 for param in cmd.params():
-- 
1.7.7.6

From 2e4bd02a04c97125ba9d7cdbe1513623e4aaef71 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Thu, 13 Dec 2012 09:23:32 -0500
Subject: [PATCH] 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 8f42c630409e068abc4cb533885306c710c473e5..fe09d3a6b489da1e8b3ce31a154c0aea239ddcda 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 0000000000000000000000000000000000000000..1ab3ddc497c40fac4636885d87f564d6de67c577
--- /dev/null
+++ b/tests/test_cmdline/test_help.py
@@ -0,0 +1,130 @@
+# Authors: Petr Viktorin <pvikt...@redhat.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+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
-- 
1.7.7.6

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to