1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 from __future__
import print_function
, unicode_literals
14 from mozbuild
.base
import (
18 from mach
.decorators
import (
26 class MachCommands(MachCommandBase
):
28 Easily run Python and Python unit tests.
30 def __init__(self
, context
):
31 MachCommandBase
.__init
__(self
, context
)
32 self
._python
_executable
= None
35 def python_executable(self
):
37 Return path to Python executable, or print and sys.exit(1) if
38 executable does not exist.
40 if self
._python
_executable
:
41 return self
._python
_executable
42 if self
._is
_windows
():
43 executable
= '_virtualenv/Scripts/python.exe'
45 executable
= '_virtualenv/bin/python'
46 path
= mozpack
.path
.join(self
.topobjdir
, executable
)
47 if not os
.path
.exists(path
):
48 print("Could not find Python executable at %s." % path
,
49 "Run |mach configure| or |mach build| to install it.")
51 self
._python
_executable
= path
54 @Command('python', category
='devenv',
56 description
='Run Python.')
57 @CommandArgument('args', nargs
=argparse
.REMAINDER
)
58 def python(self
, args
):
59 # Avoid logging the command
60 self
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
61 return self
.run_process([self
.python_executable
] + args
,
62 pass_thru
=True, # Allow user to run Python interactively.
63 ensure_exit_code
=False, # Don't throw on non-zero exit code.
64 # Note: subprocess requires native strings in os.environ on Windows
65 append_env
={b
'PYTHONDONTWRITEBYTECODE': str('1')})
67 @Command('python-test', category
='testing',
68 description
='Run Python unit tests.')
69 @CommandArgument('--verbose',
72 help='Verbose output.')
73 @CommandArgument('--stop',
76 help='Stop running tests after the first error or failure.')
77 @CommandArgument('tests', nargs
='+',
79 help='Tests to run. Each test can be a single file or a directory.')
80 def python_test(self
, tests
, verbose
=False, stop
=False):
81 # Make sure we can find Python before doing anything else.
82 self
.python_executable
84 # Python's unittest, and in particular discover, has problems with
85 # clashing namespaces when importing multiple test modules. What follows
86 # is a simple way to keep environments separate, at the price of
87 # launching Python multiple times. This also runs tests via mozunit,
88 # which produces output in the format Mozilla infrastructure expects.
92 if test
.endswith('.py') and os
.path
.isfile(test
):
94 elif os
.path
.isfile(test
+ '.py'):
95 files
.append(test
+ '.py')
96 elif os
.path
.isdir(test
):
97 files
+= glob
.glob(mozpack
.path
.join(test
, 'test*.py'))
98 files
+= glob
.glob(mozpack
.path
.join(test
, 'unit*.py'))
100 self
.log(logging
.WARN
, 'python-test', {'test': test
},
101 'TEST-UNEXPECTED-FAIL | Invalid test: {test}')
106 file_displayed_test
= [] # Used as a boolean.
107 def _line_handler(line
):
108 if not file_displayed_test
and line
.startswith('TEST-'):
109 file_displayed_test
.append(True)
111 inner_return_code
= self
.run_process(
112 [self
.python_executable
, file],
113 ensure_exit_code
=False, # Don't throw on non-zero exit code.
114 log_name
='python-test',
115 # subprocess requires native strings in os.environ on Windows
116 append_env
={b
'PYTHONDONTWRITEBYTECODE': str('1')},
117 line_handler
=_line_handler
)
118 return_code
+= inner_return_code
120 if not file_displayed_test
:
121 self
.log(logging
.WARN
, 'python-test', {'file': file},
122 'TEST-UNEXPECTED-FAIL | No test output (missing mozunit.main() call?): {file}')
125 if inner_return_code
!= 0:
126 self
.log(logging
.INFO
, 'python-test', {'file': file},
127 'Test failed: {file}')
129 self
.log(logging
.INFO
, 'python-test', {'file': file},
130 'Test passed: {file}')
131 if stop
and return_code
> 0:
134 return 0 if return_code
== 0 else 1