diff --git a/build/mach_bootstrap.py b/build/mach_bootstrap.py index 4671b18b048..dd81a7ded69 100644 --- a/build/mach_bootstrap.py +++ b/build/mach_bootstrap.py @@ -55,6 +55,7 @@ SEARCH_PATHS = [ 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', diff --git a/python/mach_commands.py b/python/mach_commands.py new file mode 100644 index 00000000000..50286ba232f --- /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