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 ThreadPoolExecutor
, as_completed
, thread
19 from mozfile
import which
20 from manifestparser
import TestManifest
21 from manifestparser
import filters
as mpf
24 from mach
.decorators
import CommandArgument
, Command
25 from mach
.requirements
import MachEnvRequirements
26 from mach
.util
import UserError
28 here
= os
.path
.abspath(os
.path
.dirname(__file__
))
31 @Command("python", category
="devenv", description
="Run Python.")
33 "--no-virtualenv", action
="store_true", help="Do not set up a virtualenv"
36 "--no-activate", action
="store_true", help="Do not activate the virtualenv"
39 "--exec-file", default
=None, help="Execute this Python file using `exec`"
45 help="Use ipython instead of the default Python REPL.",
50 help="Install this requirements file before running Python",
52 @CommandArgument("args", nargs
=argparse
.REMAINDER
)
62 # Avoid logging the command
63 command_context
.log_manager
.terminal_handler
.setLevel(logging
.CRITICAL
)
65 # Note: subprocess requires native strings in os.environ on Windows.
66 append_env
= {"PYTHONDONTWRITEBYTECODE": str("1")}
68 if requirements
and no_virtualenv
:
69 raise UserError("Cannot pass both --requirements and --no-virtualenv.")
72 python_path
= sys
.executable
73 requirements
= MachEnvRequirements
.from_requirements_definition(
74 command_context
.topsrcdir
,
78 command_context
.topsrcdir
, "build", "mach_virtualenv_packages.txt"
82 append_env
["PYTHONPATH"] = os
.pathsep
.join(
83 os
.path
.join(command_context
.topsrcdir
, pth
.path
)
84 for pth
in requirements
.pth_requirements
85 + requirements
.vendored_requirements
88 command_context
.virtualenv_manager
.ensure()
90 command_context
.virtualenv_manager
.activate()
91 python_path
= command_context
.virtualenv_manager
.python_path
93 command_context
.virtualenv_manager
.install_pip_requirements(
94 requirements
, require_hashes
=False
98 exec(open(exec_file
).read())
102 bindir
= os
.path
.dirname(python_path
)
103 python_path
= which("ipython", path
=bindir
)
105 if not no_virtualenv
:
106 # Use `_run_pip` directly rather than `install_pip_package` to bypass
107 # `req.check_if_exists()` which may detect a system installed ipython.
108 command_context
.virtualenv_manager
._run
_pip
(["install", "ipython"])
109 python_path
= which("ipython", path
=bindir
)
112 print("error: could not detect or install ipython")
115 return command_context
.run_process(
116 [python_path
] + args
,
117 pass_thru
=True, # Allow user to run Python interactively.
118 ensure_exit_code
=False, # Don't throw on non-zero exit code.
119 python_unbuffered
=False, # Leave input buffered.
120 append_env
=append_env
,
127 virtualenv_name
="python-test",
128 description
="Run Python unit tests with pytest.",
131 "-v", "--verbose", default
=False, action
="store_true", help="Verbose output."
138 help="Number of concurrent jobs to run. Default is the number of CPUs "
146 help="Runs all tests sequentially and breaks at the first failure.",
152 "Python subsuite to run. If not specified, all subsuites are run. "
153 "Use the string `default` to only run tests without a subsuite."
161 "Tests to run. Each test can be a single file or a directory. "
162 "Default test resolution relies on PYTHON_UNITTEST_MANIFESTS."
167 nargs
=argparse
.REMAINDER
,
168 metavar
="PYTEST ARGS",
170 "Arguments that aren't recognized by mach. These will be "
171 "passed as it is to pytest"
174 def python_test(command_context
, *args
, **kwargs
):
176 tempdir
= str(tempfile
.mkdtemp(suffix
="-python-test"))
178 os
.environ
[b
"PYTHON_TEST_TMP"] = tempdir
180 os
.environ
["PYTHON_TEST_TMP"] = tempdir
181 return run_python_tests(command_context
, *args
, **kwargs
)
185 mozfile
.remove(tempdir
)
188 def run_python_tests(
200 command_context
.activate_virtualenv()
201 if test_objects
is None:
202 from moztest
.resolve
import TestResolver
204 resolver
= command_context
._spawn
(TestResolver
)
205 # If we were given test paths, try to find tests matching them.
206 test_objects
= resolver
.resolve_tests(paths
=tests
, flavor
="python")
208 # We've received test_objects from |mach test|. We need to ignore
209 # the subsuite because python-tests don't use this key like other
210 # harnesses do and |mach test| doesn't realize this.
214 mp
.tests
.extend(test_objects
)
217 if subsuite
== "default":
218 filters
.append(mpf
.subsuite(None))
220 filters
.append(mpf
.subsuite(subsuite
))
222 tests
= mp
.active_tests(
225 python
=command_context
.virtualenv_manager
.version_info()[0],
230 submsg
= "for subsuite '{}' ".format(subsuite
) if subsuite
else ""
232 "TEST-UNEXPECTED-FAIL | No tests collected "
233 + "{}(Not in PYTHON_UNITTEST_MANIFESTS?)".format(submsg
)
235 command_context
.log(logging
.WARN
, "python-test", {}, message
)
240 os
.environ
.setdefault("PYTEST_ADDOPTS", "")
243 os
.environ
["PYTEST_ADDOPTS"] += " " + " ".join(extra
)
245 installed_requirements
= set()
248 test
.get("requirements")
249 and test
["requirements"] not in installed_requirements
251 command_context
.virtualenv_manager
.install_pip_requirements(
252 test
["requirements"], quiet
=True
254 installed_requirements
.add(test
["requirements"])
258 os
.environ
["PYTEST_ADDOPTS"] += " -x"
261 if test
.get("sequential"):
262 sequential
.append(test
)
264 parallel
.append(test
)
266 jobs
= jobs
or cpu_count()
270 def on_test_finished(result
):
271 output
, ret
, test_path
= result
275 logging
.INFO
, "python-test", {"line": line
.rstrip()}, "{line}"
278 if ret
and not return_code
:
282 {"test_path": test_path
, "ret": ret
},
283 "Setting retcode to {ret} from {test_path}",
285 return return_code
or ret
287 with
ThreadPoolExecutor(max_workers
=jobs
) as executor
:
289 executor
.submit(_run_python_test
, command_context
, test
, jobs
, verbose
)
294 for future
in as_completed(futures
):
295 return_code
= on_test_finished(future
.result())
296 except KeyboardInterrupt:
297 # Hack to force stop currently running threads.
298 # https://gist.github.com/clchiou/f2608cbe54403edb0b13
299 executor
._threads
.clear()
300 thread
._threads
_queues
.clear()
303 for test
in sequential
:
304 return_code
= on_test_finished(
305 _run_python_test(command_context
, test
, jobs
, verbose
)
307 if return_code
and exitfirst
:
313 {"return_code": return_code
},
314 "Return code from mach python-test: {return_code}",
319 def _run_python_test(command_context
, test
, jobs
, verbose
):
320 from mozprocess
import ProcessHandler
325 # Buffer messages if more than one worker to avoid interleaving
330 logging
.INFO
, "python-test", {"line": line
.rstrip()}, "{line}"
333 file_displayed_test
= [] # used as boolean
335 def _line_handler(line
):
336 line
= six
.ensure_str(line
)
337 if not file_displayed_test
:
338 output
= "Ran" in line
or "collected" in line
or line
.startswith("TEST-")
340 file_displayed_test
.append(True)
342 # Hack to make sure treeherder highlights pytest failures
343 if "FAILED" in line
.rsplit(" ", 1)[-1]:
344 line
= line
.replace("FAILED", "TEST-UNEXPECTED-FAIL")
349 python
= command_context
.virtualenv_manager
.python_path
350 cmd
= [python
, test
["path"]]
351 env
= os
.environ
.copy()
353 env
[b
"PYTHONDONTWRITEBYTECODE"] = b
"1"
355 env
["PYTHONDONTWRITEBYTECODE"] = "1"
357 proc
= ProcessHandler(
358 cmd
, env
=env
, processOutputLine
=_line_handler
, storeOutput
=False
362 return_code
= proc
.wait()
364 if not file_displayed_test
:
366 "TEST-UNEXPECTED-FAIL | No test output (missing mozunit.main() "
367 "call?): {}".format(test
["path"])
372 _log("Test failed: {}".format(test
["path"]))
374 _log("Test passed: {}".format(test
["path"]))
376 return output
, return_code
, test
["path"]