Bug 1255450 - [mach] Create setting for defining command aliases, r=gps
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Wed, 23 Mar 2016 17:34:35 -0400
changeset 292852 355e8bb48aeeef736df7ca21351f7e87caf90668
parent 292851 84ba3a5bc33cc4af94f5a46465a6f921d794041b
child 292853 b5600b3a7deb840b40307ac6cbfac96ea71f50ae
push id18663
push userkwierso@gmail.com
push dateTue, 12 Apr 2016 22:37:08 +0000
treeherderfx-team@c62d9a911c27 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1255450
milestone48.0a1
Bug 1255450 - [mach] Create setting for defining command aliases, r=gps These config options can be defined in ~/.mozbuild/machrc or topsrcdir/machrc. Aliases work similar to the identically named option in an hgrc. For example: [alias] browser-test = mochitest -f browser mochitest = mochitest -f plain MozReview-Commit-ID: CnOocEslRUI
python/mach/docs/settings.rst
python/mach/mach/dispatcher.py
python/mach/mach/locale/en_US/LC_MESSAGES/alias.mo
python/mach/mach/locale/en_US/LC_MESSAGES/alias.po
python/mach/mach/test/common.py
python/mach/mach/test/providers/basic.py
python/mach/mach/test/test_dispatcher.py
python/moz.build
--- a/python/mach/docs/settings.rst
+++ b/python/mach/docs/settings.rst
@@ -8,16 +8,30 @@ Mach can read settings in from a set of 
 configuration files are either named ``mach.ini`` or ``.machrc`` and
 are specified by the bootstrap script. In mozilla-central, these files
 can live in ``~/.mozbuild`` and/or ``topsrcdir``.
 
 Settings can be specified anywhere, and used both by mach core or
 individual commands.
 
 
+Core Settings
+=============
+
+These settings are implemented by mach core.
+
+* alias - Create a command alias. This is useful if you want to alias a command to something else, optionally including some defaults. It can either be used to create an entire new command, or provide defaults for an existing one. For example:
+
+.. parsed-literal::
+
+    [alias]
+    mochitest = mochitest -f browser
+    browser-test = mochitest -f browser
+
+
 Defining Settings
 =================
 
 Settings need to be explicitly defined, along with their type,
 otherwise mach will throw when loading the configuration files.
 
 To define settings, use the :func:`~decorators.SettingsProvider`
 decorator in an existing mach command module. E.g:
--- a/python/mach/mach/dispatcher.py
+++ b/python/mach/mach/dispatcher.py
@@ -1,26 +1,34 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, unicode_literals
 
 import argparse
 import difflib
+import shlex
 import sys
 
 from operator import itemgetter
 
 from .base import (
-    MachError,
     NoCommandError,
     UnknownCommandError,
     UnrecognizedArgumentError,
 )
+from .decorators import SettingsProvider
+
+
+@SettingsProvider
+class DispatchSettings():
+    config_settings = [
+        ('alias.*', 'string'),
+    ]
 
 
 class CommandFormatter(argparse.HelpFormatter):
     """Custom formatter to format just a subcommand."""
 
     def add_usage(self, *args):
         pass
 
@@ -105,37 +113,29 @@ class CommandAction(argparse.Action):
                     # -- is in command arguments
                     if '-h' in args[:args.index('--')] or '--help' in args[:args.index('--')]:
                         # Honor -h or --help only if it appears before --
                         self._handle_command_help(parser, command)
                         sys.exit(0)
                 else:
                     self._handle_command_help(parser, command)
                     sys.exit(0)
-
-
         else:
             raise NoCommandError()
 
-        # Command suggestion
+        # First see if the this is a user-defined alias
+        if command in self._context.settings.alias:
+            alias = self._context.settings.alias[command]
+            defaults = shlex.split(alias)
+            command = defaults.pop(0)
+            args = defaults + args
+
         if command not in self._mach_registrar.command_handlers:
-            # Make sure we don't suggest any deprecated commands.
-            names = [h.name for h in self._mach_registrar.command_handlers.values()
-                        if h.cls.__name__ != 'DeprecatedCommands']
-            # We first try to look for a valid command that is very similar to the given command.
-            suggested_commands = difflib.get_close_matches(command, names, cutoff=0.8)
-            # If we find more than one matching command, or no command at all, we give command suggestions instead
-            # (with a lower matching threshold). All commands that start with the given command (for instance: 'mochitest-plain',
-            # 'mochitest-chrome', etc. for 'mochitest-') are also included.
-            if len(suggested_commands) != 1:
-                suggested_commands = set(difflib.get_close_matches(command, names, cutoff=0.5))
-                suggested_commands |= {cmd for cmd in names if cmd.startswith(command)}
-                raise UnknownCommandError(command, 'run', suggested_commands)
-            sys.stderr.write("We're assuming the '%s' command is '%s' and we're executing it for you.\n\n" % (command, suggested_commands[0]))
-            command = suggested_commands[0]
+            # Try to find similar commands, may raise UnknownCommandError.
+            command = self._suggest_command(command)
 
         handler = self._mach_registrar.command_handlers.get(command)
 
         usage = '%(prog)s [global arguments] ' + command + \
             ' [command arguments]'
 
         subcommand = None
 
@@ -277,17 +277,17 @@ class CommandAction(argparse.Action):
 
                 description = handler.description
                 group.add_argument(command, help=description,
                     action='store_true')
 
         if disabled_commands and 'disabled' in r.categories:
             title, description, _priority = r.categories['disabled']
             group = parser.add_argument_group(title, description)
-            if verbose == True:
+            if verbose:
                 for c in disabled_commands:
                     group.add_argument(c['command'], help=c['description'],
                                        action='store_true')
 
         parser.print_help()
 
     def _populate_command_group(self, parser, handler, group):
         extra_groups = {}
@@ -399,16 +399,35 @@ class CommandAction(argparse.Action):
             parser.description = format_docstring(handler.docstring)
 
         parser.formatter_class = argparse.RawDescriptionHelpFormatter
 
         parser.print_help()
         print('')
         c_parser.print_help()
 
+    def _suggest_command(self, command):
+        # Make sure we don't suggest any deprecated commands.
+        names = [h.name for h in self._mach_registrar.command_handlers.values()
+                    if h.cls.__name__ != 'DeprecatedCommands']
+        # We first try to look for a valid command that is very similar to the given command.
+        suggested_commands = difflib.get_close_matches(command, names, cutoff=0.8)
+        # If we find more than one matching command, or no command at all,
+        # we give command suggestions instead (with a lower matching threshold).
+        # All commands that start with the given command (for instance:
+        # 'mochitest-plain', 'mochitest-chrome', etc. for 'mochitest-')
+        # are also included.
+        if len(suggested_commands) != 1:
+            suggested_commands = set(difflib.get_close_matches(command, names, cutoff=0.5))
+            suggested_commands |= {cmd for cmd in names if cmd.startswith(command)}
+            raise UnknownCommandError(command, 'run', suggested_commands)
+        sys.stderr.write("We're assuming the '%s' command is '%s' and we're "
+                         "executing it for you.\n\n" % (command, suggested_commands[0]))
+        return suggested_commands[0]
+
 
 class NoUsageFormatter(argparse.HelpFormatter):
     def _format_usage(self, *args, **kwargs):
         return ""
 
 
 def format_docstring(docstring):
     """Format a raw docstring into something suitable for presentation.
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6631808417c8d7e8fc1c79aa04e422d383e2960c
GIT binary patch
literal 193
zc$~W@#4?ou2$+Fb28c}<AixKS<^f_~Am#<)D5!V_5IX>ICJ>7O@hXsDVoqjav7VM*
zT4_!WoKu{UUsS@t;9QiNSdyxcsF0kWo12)Iq5zgx$WK!!$w*a5%P-1RNU#Bm*(um6
V*g#d;DcB?yr5D>J=;1Jc0RRCjE8YMA
new file mode 100644
--- /dev/null
+++ b/python/mach/mach/locale/en_US/LC_MESSAGES/alias.po
@@ -0,0 +1,9 @@
+#
+msgid ""
+msgstr ""
+
+msgid "alias.*.short"
+msgstr "Create a command alias"
+
+msgid "alias.*.full"
+msgstr "Create a command alias of the form `<alias> = <command> <args>`."
--- a/python/mach/mach/test/common.py
+++ b/python/mach/mach/test/common.py
@@ -11,30 +11,35 @@ import unittest
 from mach.main import Mach
 from mach.base import CommandContext
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 class TestBase(unittest.TestCase):
     provider_dir = os.path.join(here, 'providers')
 
-    def _run_mach(self, args, provider_file=None, entry_point=None, context_handler=None):
+    def get_mach(self, provider_file=None, entry_point=None, context_handler=None):
         m = Mach(os.getcwd())
         m.define_category('testing', 'Mach unittest', 'Testing for mach core', 10)
         m.populate_context_handler = context_handler
 
         if provider_file:
             m.load_commands_from_file(os.path.join(self.provider_dir, provider_file))
 
         if entry_point:
             m.load_commands_from_entry_point(entry_point)
 
+        return m
+
+    def _run_mach(self, argv, *args, **kwargs):
+        m = self.get_mach(*args, **kwargs)
+
         stdout = StringIO()
         stderr = StringIO()
         stdout.encoding = 'UTF-8'
         stderr.encoding = 'UTF-8'
 
         try:
-            result = m.run(args, stdout=stdout, stderr=stderr)
+            result = m.run(argv, stdout=stdout, stderr=stderr)
         except SystemExit:
             result = None
 
         return (result, stdout.getvalue(), stderr.getvalue())
--- a/python/mach/mach/test/providers/basic.py
+++ b/python/mach/mach/test/providers/basic.py
@@ -1,15 +1,23 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 from __future__ import unicode_literals
 
 from mach.decorators import (
+    CommandArgument,
     CommandProvider,
     Command,
 )
 
+
 @CommandProvider
 class ConditionsProvider(object):
     @Command('cmd_foo', category='testing')
     def run_foo(self):
         pass
+
+    @Command('cmd_bar', category='testing')
+    @CommandArgument('--baz', action="store_true",
+                     help='Run with baz')
+    def run_bar(self, baz=None):
+        pass
new file mode 100644
--- /dev/null
+++ b/python/mach/mach/test/test_dispatcher.py
@@ -0,0 +1,61 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import unicode_literals
+
+import os
+from cStringIO import StringIO
+
+from mach.base import CommandContext
+from mach.registrar import Registrar
+from mach.test.common import TestBase
+
+from mozunit import main
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+class TestDispatcher(TestBase):
+    """Tests dispatch related code"""
+
+    def get_parser(self, config=None):
+        mach = self.get_mach('basic.py')
+
+        for provider in Registrar.settings_providers:
+            mach.settings.register_provider(provider)
+
+        if config:
+            if isinstance(config, basestring):
+                config = StringIO(config)
+            mach.settings.load_fps([config])
+
+        context = CommandContext(settings=mach.settings)
+        return mach.get_argument_parser(context)
+
+    def test_command_aliases(self):
+        config = """
+[alias]
+foo = cmd_foo
+bar = cmd_bar
+baz = cmd_bar --baz
+cmd_bar = cmd_bar --baz
+"""
+        parser = self.get_parser(config=config)
+
+        args = parser.parse_args(['foo'])
+        self.assertEquals(args.command, 'cmd_foo')
+
+        def assert_bar_baz(argv):
+            args = parser.parse_args(argv)
+            self.assertEquals(args.command, 'cmd_bar')
+            self.assertTrue(args.command_args.baz)
+
+        # The following should all result in |cmd_bar --baz|
+        assert_bar_baz(['bar', '--baz'])
+        assert_bar_baz(['baz'])
+        assert_bar_baz(['cmd_bar'])
+
+
+if __name__ == '__main__':
+    main()
--- a/python/moz.build
+++ b/python/moz.build
@@ -17,16 +17,17 @@ SPHINX_PYTHON_PACKAGE_DIRS += [
     'mozversioncontrol/mozversioncontrol',
 ]
 
 SPHINX_TREES['mach'] = 'mach/docs'
 
 PYTHON_UNIT_TESTS += [
     'mach/mach/test/test_conditions.py',
     'mach/mach/test/test_config.py',
+    'mach/mach/test/test_dispatcher.py',
     'mach/mach/test/test_entry_point.py',
     'mach/mach/test/test_error_output.py',
     'mach/mach/test/test_logger.py',
     'mozbuild/dumbmake/test/test_dumbmake.py',
     'mozbuild/mozbuild/test/action/test_buildlist.py',
     'mozbuild/mozbuild/test/action/test_generate_browsersearch.py',
     'mozbuild/mozbuild/test/action/test_package_fennec_apk.py',
     'mozbuild/mozbuild/test/backend/test_android_eclipse.py',