Bug 818744 - mach commands to run Python and Python unit tests. r=gps
authorNick Alexander <nalexander@mozilla.com>
Tue, 02 Jul 2013 17:33:48 -0700
changeset 149549 76b0a7f72e3553b956e964bfdd962efc762133cc
parent 149548 04c800fb45a6c4f5a86188cd2884559e94cd6de0
child 149550 b0f2330ed678b42c001afe86b85e8a9a63d6ef88
push id2859
push userakeybl@mozilla.com
push dateMon, 16 Sep 2013 19:14:59 +0000
treeherdermozilla-beta@87d3c51cd2bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs818744
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 818744 - mach commands to run Python and Python unit tests. r=gps DONTBUILD because NPOTB
build/mach_bootstrap.py
python/mach_commands.py
--- a/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -50,16 +50,17 @@ SEARCH_PATHS = [
     'testing/mozbase/mozrunner',
     'testing/mozbase/mozinfo',
 ]
 
 # Individual files providing mach commands.
 MACH_MODULES = [
     'addon-sdk/mach_commands.py',
     'layout/tools/reftest/mach_commands.py',
+    'python/mach_commands.py',
     'python/mach/mach/commands/commandinfo.py',
     'python/mozboot/mozboot/mach_commands.py',
     'python/mozbuild/mozbuild/config.py',
     'python/mozbuild/mozbuild/mach_commands.py',
     'python/mozbuild/mozbuild/frontend/mach_commands.py',
     'testing/marionette/mach_commands.py',
     'testing/mochitest/mach_commands.py',
     'testing/xpcshell/mach_commands.py',
new file mode 100644
--- /dev/null
+++ b/python/mach_commands.py
@@ -0,0 +1,130 @@
+# 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 print_function, unicode_literals
+
+import argparse
+import glob
+import logging
+import mozpack.path
+import os
+import sys
+
+from mozbuild.base import (
+    MachCommandBase,
+)
+
+from mach.decorators import (
+    CommandArgument,
+    CommandProvider,
+    Command,
+)
+
+
+@CommandProvider
+class MachCommands(MachCommandBase):
+    '''
+    Easily run Python and Python unit tests.
+    '''
+    def __init__(self, context):
+        MachCommandBase.__init__(self, context)
+        self._python_executable = None
+
+    @property
+    def python_executable(self):
+        '''
+        Return path to Python executable, or print and sys.exit(1) if
+        executable does not exist.
+        '''
+        if self._python_executable:
+            return self._python_executable
+        if self._is_windows():
+            executable = '_virtualenv/Scripts/python.exe'
+        else:
+            executable = '_virtualenv/bin/python'
+        path = mozpack.path.join(self.topobjdir, executable)
+        if not os.path.exists(path):
+            print("Could not find Python executable at %s." % path,
+                  "Run |mach configure| or |mach build| to install it.")
+            sys.exit(1)
+        self._python_executable = path
+        return path
+
+    @Command('python', category='devenv',
+        allow_all_args=True,
+        description='Run Python.')
+    @CommandArgument('args', nargs=argparse.REMAINDER)
+    def python(self, args):
+        return self.run_process([self.python_executable] + args,
+            pass_thru=True, # Allow user to run Python interactively.
+            ensure_exit_code=False, # Don't throw on non-zero exit code.
+            append_env={'PYTHONDONTWRITEBYTECODE': '1'})
+
+    @Command('python-test', category='testing',
+        description='Run Python unit tests.')
+    @CommandArgument('--verbose',
+        default=False,
+        action='store_true',
+        help='Verbose output.')
+    @CommandArgument('--stop',
+        default=False,
+        action='store_true',
+        help='Stop running tests after the first error or failure.')
+    @CommandArgument('tests', nargs='+',
+        metavar='TEST',
+        help='Tests to run. Each test can be a single file or a directory.')
+    def python_test(self, tests, verbose=False, stop=False):
+        # Make sure we can find Python before doing anything else.
+        self.python_executable
+
+        # Python's unittest, and in particular discover, has problems with
+        # clashing namespaces when importing multiple test modules. What follows
+        # is a simple way to keep environments separate, at the price of
+        # launching Python multiple times. This also runs tests via mozunit,
+        # which produces output in the format Mozilla infrastructure expects.
+        return_code = 0
+        files = []
+        for test in tests:
+            if test.endswith('.py') and os.path.isfile(test):
+                files.append(test)
+            elif os.path.isfile(test + '.py'):
+                files.append(test + '.py')
+            elif os.path.isdir(test):
+                files += glob.glob(mozpack.path.join(test, 'test*.py'))
+                files += glob.glob(mozpack.path.join(test, 'unit*.py'))
+            else:
+                self.log(logging.WARN, 'python-test', {'test': test},
+                         'TEST-UNEXPECTED-FAIL | Invalid test: {test}')
+                if stop:
+                    return 1
+
+        for file in files:
+            file_displayed_test = [] # Used as a boolean.
+            def _line_handler(line):
+                if not file_displayed_test and line.startswith('TEST-'):
+                    file_displayed_test.append(True)
+
+            inner_return_code = self.run_process(
+                [self.python_executable, file],
+                ensure_exit_code=False, # Don't throw on non-zero exit code.
+                log_name='python-test',
+                append_env={'PYTHONDONTWRITEBYTECODE': '1'},
+                line_handler=_line_handler)
+            return_code += inner_return_code
+
+            if not file_displayed_test:
+                self.log(logging.WARN, 'python-test', {'file': file},
+                         'TEST-UNEXPECTED-FAIL | No test output (missing mozunit.main() call?): {file}')
+
+            if verbose:
+                if inner_return_code != 0:
+                    self.log(logging.INFO, 'python-test', {'file': file},
+                             'Test failed: {file}')
+                else:
+                    self.log(logging.INFO, 'python-test', {'file': file},
+                             'Test passed: {file}')
+            if stop and return_code > 0:
+                return 1
+
+        return 0 if return_code == 0 else 1