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 absolute_import
, print_function
, unicode_literals
12 from multiprocessing
import cpu_count
16 from concurrent
.futures
import (
23 from mozfile
import which
24 from manifestparser
import TestManifest
25 from manifestparser
import filters
as mpf
27 from mozbuild
.base
import MachCommandBase
29 from mach
.decorators
import (
34 from mach
.util
import UserError
36 here
= os
.path
.abspath(os
.path
.dirname(__file__
))
40 class MachCommands(MachCommandBase
):
41 @Command("python", category
="devenv", description
="Run Python.")
43 "--no-virtualenv", action
="store_true", help="Do not set up a virtualenv"
46 "--no-activate", action
="store_true", help="Do not activate the virtualenv"
49 "--exec-file", default
=None, help="Execute this Python file using `exec`"
55 help="Use ipython instead of the default Python REPL.",
60 help="Install this requirements file before running Python",
62 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
64 self
, no_virtualenv
, no_activate
, exec_file
, ipython
, requirements
, args
66 # Avoid logging the command
67 self
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
69 # Note: subprocess requires native strings in os.environ on Windows.
71 "PYTHONDONTWRITEBYTECODE": str("1"),
74 if requirements
and no_virtualenv
:
75 raise UserError("Cannot pass both --requirements and --no-virtualenv.")
78 from mach_bootstrap
import mach_sys_path
80 python_path
= sys
.executable
81 append_env
["PYTHONPATH"] = os
.pathsep
.join(mach_sys_path(self
.topsrcdir
))
83 self
.virtualenv_manager
.ensure()
85 self
.virtualenv_manager
.activate()
86 python_path
= self
.virtualenv_manager
.python_path
88 self
.virtualenv_manager
.install_pip_requirements(
89 requirements
, require_hashes
=False
93 exec(open(exec_file
).read())
97 bindir
= os
.path
.dirname(python_path
)
98 python_path
= which("ipython", path
=bindir
)
100 if not no_virtualenv
:
101 # Use `_run_pip` directly rather than `install_pip_package` to bypass
102 # `req.check_if_exists()` which may detect a system installed ipython.
103 self
.virtualenv_manager
._run
_pip
(["install", "ipython"])
104 python_path
= which("ipython", path
=bindir
)
107 print("error: could not detect or install ipython")
110 return self
.run_process(
111 [python_path
] + args
,
112 pass_thru
=True, # Allow user to run Python interactively.
113 ensure_exit_code
=False, # Don't throw on non-zero exit code.
114 python_unbuffered
=False, # Leave input buffered.
115 append_env
=append_env
,
121 virtualenv_name
="python-test",
122 description
="Run Python unit tests with pytest.",
125 "-v", "--verbose", default
=False, action
="store_true", help="Verbose output."
132 help="Number of concurrent jobs to run. Default is the number of CPUs "
140 help="Runs all tests sequentially and breaks at the first failure.",
146 "Python subsuite to run. If not specified, all subsuites are run. "
147 "Use the string `default` to only run tests without a subsuite."
155 "Tests to run. Each test can be a single file or a directory. "
156 "Default test resolution relies on PYTHON_UNITTEST_MANIFESTS."
161 nargs
=argparse
.REMAINDER
,
162 metavar
="PYTEST ARGS",
164 "Arguments that aren't recognized by mach. These will be "
165 "passed as it is to pytest"
168 def python_test(self
, *args
, **kwargs
):
170 tempdir
= str(tempfile
.mkdtemp(suffix
="-python-test"))
172 os
.environ
[b
"PYTHON_TEST_TMP"] = tempdir
174 os
.environ
["PYTHON_TEST_TMP"] = tempdir
175 return self
.run_python_tests(*args
, **kwargs
)
179 mozfile
.remove(tempdir
)
181 def run_python_tests(
193 self
.activate_virtualenv()
194 if test_objects
is None:
195 from moztest
.resolve
import TestResolver
197 resolver
= self
._spawn
(TestResolver
)
198 # If we were given test paths, try to find tests matching them.
199 test_objects
= resolver
.resolve_tests(paths
=tests
, flavor
="python")
201 # We've received test_objects from |mach test|. We need to ignore
202 # the subsuite because python-tests don't use this key like other
203 # harnesses do and |mach test| doesn't realize this.
207 mp
.tests
.extend(test_objects
)
210 if subsuite
== "default":
211 filters
.append(mpf
.subsuite(None))
213 filters
.append(mpf
.subsuite(subsuite
))
215 tests
= mp
.active_tests(
218 python
=self
.virtualenv_manager
.version_info
[0],
223 submsg
= "for subsuite '{}' ".format(subsuite
) if subsuite
else ""
225 "TEST-UNEXPECTED-FAIL | No tests collected "
226 + "{}(Not in PYTHON_UNITTEST_MANIFESTS?)".format(submsg
)
228 self
.log(logging
.WARN
, "python-test", {}, message
)
233 os
.environ
.setdefault("PYTEST_ADDOPTS", "")
236 os
.environ
["PYTEST_ADDOPTS"] += " " + " ".join(extra
)
238 installed_requirements
= set()
241 test
.get("requirements")
242 and test
["requirements"] not in installed_requirements
244 self
.virtualenv_manager
.install_pip_requirements(
245 test
["requirements"],
248 installed_requirements
.add(test
["requirements"])
252 os
.environ
["PYTEST_ADDOPTS"] += " -x"
255 if test
.get("sequential"):
256 sequential
.append(test
)
258 parallel
.append(test
)
260 self
.jobs
= jobs
or cpu_count()
261 self
.terminate
= False
262 self
.verbose
= verbose
266 def on_test_finished(result
):
267 output
, ret
, test_path
= result
270 self
.log(logging
.INFO
, "python-test", {"line": line
.rstrip()}, "{line}")
272 if ret
and not return_code
:
276 {"test_path": test_path
, "ret": ret
},
277 "Setting retcode to {ret} from {test_path}",
279 return return_code
or ret
281 with
ThreadPoolExecutor(max_workers
=self
.jobs
) as executor
:
283 executor
.submit(self
._run
_python
_test
, test
) for test
in parallel
287 for future
in as_completed(futures
):
288 return_code
= on_test_finished(future
.result())
289 except KeyboardInterrupt:
290 # Hack to force stop currently running threads.
291 # https://gist.github.com/clchiou/f2608cbe54403edb0b13
292 executor
._threads
.clear()
293 thread
._threads
_queues
.clear()
296 for test
in sequential
:
297 return_code
= on_test_finished(self
._run
_python
_test
(test
))
298 if return_code
and exitfirst
:
304 {"return_code": return_code
},
305 "Return code from mach python-test: {return_code}",
309 def _run_python_test(self
, test
):
310 from mozprocess
import ProcessHandler
315 # Buffer messages if more than one worker to avoid interleaving
319 self
.log(logging
.INFO
, "python-test", {"line": line
.rstrip()}, "{line}")
321 file_displayed_test
= [] # used as boolean
323 def _line_handler(line
):
324 line
= six
.ensure_str(line
)
325 if not file_displayed_test
:
327 "Ran" in line
or "collected" in line
or line
.startswith("TEST-")
330 file_displayed_test
.append(True)
332 # Hack to make sure treeherder highlights pytest failures
333 if "FAILED" in line
.rsplit(" ", 1)[-1]:
334 line
= line
.replace("FAILED", "TEST-UNEXPECTED-FAIL")
339 python
= self
.virtualenv_manager
.python_path
340 cmd
= [python
, test
["path"]]
341 env
= os
.environ
.copy()
343 env
[b
"PYTHONDONTWRITEBYTECODE"] = b
"1"
345 env
["PYTHONDONTWRITEBYTECODE"] = "1"
347 # Homebrew on OS X will change Python's sys.executable to a custom value
348 # which messes with mach's virtualenv handling code. Override Homebrew's
349 # changes with the correct sys.executable value.
351 env
[b
"PYTHONEXECUTABLE"] = python
.encode("utf-8")
353 env
["PYTHONEXECUTABLE"] = python
355 proc
= ProcessHandler(
356 cmd
, env
=env
, processOutputLine
=_line_handler
, storeOutput
=False
360 return_code
= proc
.wait()
362 if not file_displayed_test
:
364 "TEST-UNEXPECTED-FAIL | No test output (missing mozunit.main() "
365 "call?): {}".format(test
["path"])
370 _log("Test failed: {}".format(test
["path"]))
372 _log("Test passed: {}".format(test
["path"]))
374 return output
, return_code
, test
["path"]