Backed out 2 changesets (bug 1865633) for causing xpc timeout failures CLOSED TREE
[gecko.git] / testing / mochitest / runtests.py
blob983f9f84c0861148e36aafba7486a866962a4864
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 """
6 Runs the Mochitest test harness.
7 """
9 import os
10 import sys
12 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
13 sys.path.insert(0, SCRIPT_DIR)
15 import ctypes
16 import glob
17 import json
18 import numbers
19 import platform
20 import re
21 import shlex
22 import shutil
23 import signal
24 import socket
25 import subprocess
26 import sys
27 import tempfile
28 import time
29 import traceback
30 import uuid
31 import zipfile
32 from argparse import Namespace
33 from collections import defaultdict
34 from contextlib import closing
35 from ctypes.util import find_library
36 from datetime import datetime, timedelta
37 from distutils import spawn
39 import bisection
40 import mozcrash
41 import mozdebug
42 import mozinfo
43 import mozprocess
44 import mozrunner
45 from manifestparser import TestManifest
46 from manifestparser.filters import (
47 chunk_by_dir,
48 chunk_by_runtime,
49 chunk_by_slice,
50 failures,
51 pathprefix,
52 subsuite,
53 tags,
55 from manifestparser.util import normsep
56 from mozgeckoprofiler import symbolicate_profile_json, view_gecko_profile
57 from mozserve import DoHServer, Http2Server, Http3Server
59 try:
60 from marionette_driver.addons import Addons
61 from marionette_driver.marionette import Marionette
62 except ImportError as e: # noqa
63 # Defer ImportError until attempt to use Marionette
64 def reraise(*args, **kwargs):
65 raise (e) # noqa
67 Marionette = reraise
69 import mozleak
70 from leaks import LSANLeaks, ShutdownLeaks
71 from mochitest_options import (
72 MochitestArgumentParser,
73 build_obj,
74 get_default_valgrind_suppression_files,
76 from mozlog import commandline, get_proxy_logger
77 from mozprofile import Profile
78 from mozprofile.cli import KeyValueParseError, parse_key_value, parse_preferences
79 from mozprofile.permissions import ServerLocations
80 from mozrunner.utils import get_stack_fixer_function, test_environment
81 from mozscreenshot import dump_screen
83 HAVE_PSUTIL = False
84 try:
85 import psutil
87 HAVE_PSUTIL = True
88 except ImportError:
89 pass
91 import six
92 from six.moves.urllib.parse import quote_plus as encodeURIComponent
93 from six.moves.urllib_request import urlopen
95 try:
96 from mozbuild.base import MozbuildObject
98 build = MozbuildObject.from_environment(cwd=SCRIPT_DIR)
99 except ImportError:
100 build = None
102 here = os.path.abspath(os.path.dirname(__file__))
104 NO_TESTS_FOUND = """
105 No tests were found for flavor '{}' and the following manifest filters:
108 Make sure the test paths (if any) are spelt correctly and the corresponding
109 --flavor and --subsuite are being used. See `mach mochitest --help` for a
110 list of valid flavors.
111 """.lstrip()
114 ########################################
115 # Option for MOZ (former NSPR) logging #
116 ########################################
118 # Set the desired log modules you want a log be produced
119 # by a try run for, or leave blank to disable the feature.
120 # This will be passed to MOZ_LOG environment variable.
121 # Try run will then put a download link for a zip archive
122 # of all the log files on treeherder.
123 MOZ_LOG = ""
125 ########################################
126 # Option for web server log #
127 ########################################
129 # If True, debug logging from the web server will be
130 # written to mochitest-server-%d.txt artifacts on
131 # treeherder.
132 MOCHITEST_SERVER_LOGGING = False
134 #####################
135 # Test log handling #
136 #####################
138 # output processing
139 TBPL_RETRY = 4 # Defined in mozharness
142 class MessageLogger(object):
144 """File-like object for logging messages (structured logs)"""
146 BUFFERING_THRESHOLD = 100
147 # This is a delimiter used by the JS side to avoid logs interleaving
148 DELIMITER = "\ue175\uee31\u2c32\uacbf"
149 BUFFERED_ACTIONS = set(["test_status", "log"])
150 VALID_ACTIONS = set(
152 "suite_start",
153 "suite_end",
154 "group_start",
155 "group_end",
156 "test_start",
157 "test_end",
158 "test_status",
159 "log",
160 "assertion_count",
161 "buffering_on",
162 "buffering_off",
165 # Regexes that will be replaced with an empty string if found in a test
166 # name. We do this to normalize test names which may contain URLs and test
167 # package prefixes.
168 TEST_PATH_PREFIXES = [
169 r"^/tests/",
170 r"^\w+://[\w\.]+(:\d+)?(/\w+)?/(tests?|a11y|chrome)/",
171 r"^\w+://[\w\.]+(:\d+)?(/\w+)?/(tests?|browser)/",
174 def __init__(self, logger, buffering=True, structured=True):
175 self.logger = logger
176 self.structured = structured
177 self.gecko_id = "GECKO"
178 self.is_test_running = False
180 # Even if buffering is enabled, we only want to buffer messages between
181 # TEST-START/TEST-END. So it is off to begin, but will be enabled after
182 # a TEST-START comes in.
183 self._buffering = False
184 self.restore_buffering = buffering
186 # Guard to ensure we never buffer if this value was initially `False`
187 self._buffering_initially_enabled = buffering
189 # Message buffering
190 self.buffered_messages = []
192 def validate(self, obj):
193 """Tests whether the given object is a valid structured message
194 (only does a superficial validation)"""
195 if not (
196 isinstance(obj, dict)
197 and "action" in obj
198 and obj["action"] in MessageLogger.VALID_ACTIONS
200 raise ValueError
202 def _fix_subtest_name(self, message):
203 """Make sure subtest name is a string"""
204 if "subtest" in message and not isinstance(
205 message["subtest"], six.string_types
207 message["subtest"] = str(message["subtest"])
209 def _fix_test_name(self, message):
210 """Normalize a logged test path to match the relative path from the sourcedir."""
211 if message.get("test") is not None:
212 test = message["test"]
213 for pattern in MessageLogger.TEST_PATH_PREFIXES:
214 test = re.sub(pattern, "", test)
215 if test != message["test"]:
216 message["test"] = test
217 break
219 def _fix_message_format(self, message):
220 if "message" in message:
221 if isinstance(message["message"], bytes):
222 message["message"] = message["message"].decode("utf-8", "replace")
223 elif not isinstance(message["message"], six.text_type):
224 message["message"] = six.text_type(message["message"])
226 def parse_line(self, line):
227 """Takes a given line of input (structured or not) and
228 returns a list of structured messages"""
229 if isinstance(line, six.binary_type):
230 # if line is a sequence of bytes, let's decode it
231 line = line.rstrip().decode("UTF-8", "replace")
232 else:
233 # line is in unicode - so let's use it as it is
234 line = line.rstrip()
236 messages = []
237 for fragment in line.split(MessageLogger.DELIMITER):
238 if not fragment:
239 continue
240 try:
241 message = json.loads(fragment)
242 self.validate(message)
243 except ValueError:
244 if self.structured:
245 message = dict(
246 action="process_output",
247 process=self.gecko_id,
248 data=fragment,
250 else:
251 message = dict(
252 action="log",
253 level="info",
254 message=fragment,
257 self._fix_subtest_name(message)
258 self._fix_test_name(message)
259 self._fix_message_format(message)
260 messages.append(message)
262 return messages
264 @property
265 def buffering(self):
266 if not self._buffering_initially_enabled:
267 return False
268 return self._buffering
270 @buffering.setter
271 def buffering(self, val):
272 self._buffering = val
274 def process_message(self, message):
275 """Processes a structured message. Takes into account buffering, errors, ..."""
276 # Activation/deactivating message buffering from the JS side
277 if message["action"] == "buffering_on":
278 if self.is_test_running:
279 self.buffering = True
280 return
281 if message["action"] == "buffering_off":
282 self.buffering = False
283 return
285 # Error detection also supports "raw" errors (in log messages) because some tests
286 # manually dump 'TEST-UNEXPECTED-FAIL'.
287 if "expected" in message or (
288 message["action"] == "log"
289 and message.get("message", "").startswith("TEST-UNEXPECTED")
291 self.restore_buffering = self.restore_buffering or self.buffering
292 self.buffering = False
293 if self.buffered_messages:
294 snipped = len(self.buffered_messages) - self.BUFFERING_THRESHOLD
295 if snipped > 0:
296 self.logger.info(
297 "<snipped {0} output lines - "
298 "if you need more context, please use "
299 "SimpleTest.requestCompleteLog() in your test>".format(snipped)
301 # Dumping previously buffered messages
302 self.dump_buffered(limit=True)
304 # Logging the error message
305 self.logger.log_raw(message)
306 # Determine if message should be buffered
307 elif (
308 self.buffering
309 and self.structured
310 and message["action"] in self.BUFFERED_ACTIONS
312 self.buffered_messages.append(message)
313 # Otherwise log the message directly
314 else:
315 self.logger.log_raw(message)
317 # If a test ended, we clean the buffer
318 if message["action"] == "test_end":
319 self.is_test_running = False
320 self.buffered_messages = []
321 self.restore_buffering = self.restore_buffering or self.buffering
322 self.buffering = False
324 if message["action"] == "test_start":
325 self.is_test_running = True
326 if self.restore_buffering:
327 self.restore_buffering = False
328 self.buffering = True
330 def write(self, line):
331 messages = self.parse_line(line)
332 for message in messages:
333 self.process_message(message)
334 return messages
336 def flush(self):
337 sys.stdout.flush()
339 def dump_buffered(self, limit=False):
340 if limit:
341 dumped_messages = self.buffered_messages[-self.BUFFERING_THRESHOLD :]
342 else:
343 dumped_messages = self.buffered_messages
345 last_timestamp = None
346 for buf in dumped_messages:
347 # pylint --py3k W1619
348 timestamp = datetime.fromtimestamp(buf["time"] / 1000).strftime("%H:%M:%S")
349 if timestamp != last_timestamp:
350 self.logger.info("Buffered messages logged at {}".format(timestamp))
351 last_timestamp = timestamp
353 self.logger.log_raw(buf)
354 self.logger.info("Buffered messages finished")
355 # Cleaning the list of buffered messages
356 self.buffered_messages = []
358 def finish(self):
359 self.dump_buffered()
360 self.buffering = False
361 self.logger.suite_end()
364 ####################
365 # PROCESS HANDLING #
366 ####################
369 def call(*args, **kwargs):
370 """wraps mozprocess.run_and_wait with process output logging"""
371 log = get_proxy_logger("mochitest")
373 def on_output(proc, line):
374 cmdline = subprocess.list2cmdline(proc.args)
375 log.process_output(
376 process=proc.pid,
377 data=line,
378 command=cmdline,
381 process = mozprocess.run_and_wait(*args, output_line_handler=on_output, **kwargs)
382 return process.returncode
385 def killPid(pid, log):
386 # see also https://bugzilla.mozilla.org/show_bug.cgi?id=911249#c58
388 if HAVE_PSUTIL:
389 # Kill a process tree (including grandchildren) with signal.SIGTERM
390 if pid == os.getpid():
391 raise RuntimeError("Error: trying to kill ourselves, not another process")
392 try:
393 parent = psutil.Process(pid)
394 children = parent.children(recursive=True)
395 children.append(parent)
396 for p in children:
397 p.send_signal(signal.SIGTERM)
398 gone, alive = psutil.wait_procs(children, timeout=30)
399 for p in gone:
400 log.info("psutil found pid %s dead" % p.pid)
401 for p in alive:
402 log.info("failed to kill pid %d after 30s" % p.pid)
403 except Exception as e:
404 log.info("Error: Failed to kill process %d: %s" % (pid, str(e)))
405 else:
406 try:
407 os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
408 except Exception as e:
409 log.info("Failed to kill process %d: %s" % (pid, str(e)))
412 if mozinfo.isWin:
413 import ctypes.wintypes
415 def isPidAlive(pid):
416 STILL_ACTIVE = 259
417 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
418 pHandle = ctypes.windll.kernel32.OpenProcess(
419 PROCESS_QUERY_LIMITED_INFORMATION, 0, pid
421 if not pHandle:
422 return False
424 try:
425 pExitCode = ctypes.wintypes.DWORD()
426 ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
428 if pExitCode.value != STILL_ACTIVE:
429 return False
431 # We have a live process handle. But Windows aggressively
432 # re-uses pids, so let's attempt to verify that this is
433 # actually Firefox.
434 namesize = 1024
435 pName = ctypes.create_string_buffer(namesize)
436 namelen = ctypes.windll.psapi.GetProcessImageFileNameA(
437 pHandle, pName, namesize
439 if namelen == 0:
440 # Still an active process, so conservatively assume it's Firefox.
441 return True
443 return pName.value.endswith((b"firefox.exe", b"plugin-container.exe"))
444 finally:
445 ctypes.windll.kernel32.CloseHandle(pHandle)
447 else:
448 import errno
450 def isPidAlive(pid):
451 try:
452 # kill(pid, 0) checks for a valid PID without actually sending a signal
453 # The method throws OSError if the PID is invalid, which we catch
454 # below.
455 os.kill(pid, 0)
457 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
458 # the process terminates before we get to this point.
459 wpid, wstatus = os.waitpid(pid, os.WNOHANG)
460 return wpid == 0
461 except OSError as err:
462 # Catch the errors we might expect from os.kill/os.waitpid,
463 # and re-raise any others
464 if err.errno in (errno.ESRCH, errno.ECHILD, errno.EPERM):
465 return False
466 raise
469 # TODO: ^ upstream isPidAlive to mozprocess
471 #######################
472 # HTTP SERVER SUPPORT #
473 #######################
476 class MochitestServer(object):
477 "Web server used to serve Mochitests, for closer fidelity to the real web."
479 instance_count = 0
481 def __init__(self, options, logger):
482 if isinstance(options, Namespace):
483 options = vars(options)
484 self._log = logger
485 self._keep_open = bool(options["keep_open"])
486 self._utilityPath = options["utilityPath"]
487 self._xrePath = options["xrePath"]
488 self._profileDir = options["profilePath"]
489 self.webServer = options["webServer"]
490 self.httpPort = options["httpPort"]
491 if options.get("remoteWebServer") == "10.0.2.2":
492 # probably running an Android emulator and 10.0.2.2 will
493 # not be visible from host
494 shutdownServer = "127.0.0.1"
495 else:
496 shutdownServer = self.webServer
497 self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % {
498 "server": shutdownServer,
499 "port": self.httpPort,
501 self.debugURL = "http://%(server)s:%(port)s/server/debug?2" % {
502 "server": shutdownServer,
503 "port": self.httpPort,
505 self.testPrefix = "undefined"
507 if options.get("httpdPath"):
508 self._httpdPath = options["httpdPath"]
509 else:
510 self._httpdPath = SCRIPT_DIR
511 self._httpdPath = os.path.abspath(self._httpdPath)
513 MochitestServer.instance_count += 1
515 def start(self):
516 "Run the Mochitest server, returning the process ID of the server."
518 # get testing environment
519 env = test_environment(xrePath=self._xrePath, log=self._log)
520 env["XPCOM_DEBUG_BREAK"] = "warn"
521 if "LD_LIBRARY_PATH" not in env or env["LD_LIBRARY_PATH"] is None:
522 env["LD_LIBRARY_PATH"] = self._xrePath
523 else:
524 env["LD_LIBRARY_PATH"] = ":".join([self._xrePath, env["LD_LIBRARY_PATH"]])
526 # When running with an ASan build, our xpcshell server will also be ASan-enabled,
527 # thus consuming too much resources when running together with the browser on
528 # the test machines. Try to limit the amount of resources by disabling certain
529 # features.
530 env["ASAN_OPTIONS"] = "quarantine_size=1:redzone=32:malloc_context_size=5"
532 # Likewise, when running with a TSan build, our xpcshell server will
533 # also be TSan-enabled. Except that in this case, we don't really
534 # care about races in xpcshell. So disable TSan for the server.
535 env["TSAN_OPTIONS"] = "report_bugs=0"
537 # Don't use socket process for the xpcshell server.
538 env["MOZ_DISABLE_SOCKET_PROCESS"] = "1"
540 if mozinfo.isWin:
541 env["PATH"] = env["PATH"] + ";" + str(self._xrePath)
543 args = [
544 "-g",
545 self._xrePath,
546 "-e",
547 "const _PROFILE_PATH = '%(profile)s'; const _SERVER_PORT = '%(port)s'; "
548 "const _SERVER_ADDR = '%(server)s'; const _TEST_PREFIX = %(testPrefix)s; "
549 "const _DISPLAY_RESULTS = %(displayResults)s; "
550 "const _HTTPD_PATH = '%(httpdPath)s';"
552 "httpdPath": self._httpdPath.replace("\\", "\\\\"),
553 "profile": self._profileDir.replace("\\", "\\\\"),
554 "port": self.httpPort,
555 "server": self.webServer,
556 "testPrefix": self.testPrefix,
557 "displayResults": str(self._keep_open).lower(),
559 "-f",
560 os.path.join(SCRIPT_DIR, "server.js"),
563 xpcshell = os.path.join(
564 self._utilityPath, "xpcshell" + mozinfo.info["bin_suffix"]
566 command = [xpcshell] + args
567 if MOCHITEST_SERVER_LOGGING and "MOZ_UPLOAD_DIR" in os.environ:
568 server_logfile_path = os.path.join(
569 os.environ["MOZ_UPLOAD_DIR"],
570 "mochitest-server-%d.txt" % MochitestServer.instance_count,
572 self.server_logfile = open(server_logfile_path, "w")
573 self._process = subprocess.Popen(
574 command,
575 cwd=SCRIPT_DIR,
576 env=env,
577 stdout=self.server_logfile,
578 stderr=subprocess.STDOUT,
580 else:
581 self.server_logfile = None
582 self._process = subprocess.Popen(
583 command,
584 cwd=SCRIPT_DIR,
585 env=env,
587 self._log.info("%s : launching %s" % (self.__class__.__name__, command))
588 pid = self._process.pid
589 self._log.info("runtests.py | Server pid: %d" % pid)
590 if MOCHITEST_SERVER_LOGGING and "MOZ_UPLOAD_DIR" in os.environ:
591 self._log.info("runtests.py enabling server debugging...")
592 i = 0
593 while i < 5:
594 try:
595 with closing(urlopen(self.debugURL)) as c:
596 self._log.info(six.ensure_text(c.read()))
597 break
598 except Exception as e:
599 self._log.info("exception when enabling debugging: %s" % str(e))
600 time.sleep(1)
601 i += 1
603 def ensureReady(self, timeout):
604 assert timeout >= 0
606 aliveFile = os.path.join(self._profileDir, "server_alive.txt")
607 i = 0
608 while i < timeout:
609 if os.path.exists(aliveFile):
610 break
611 time.sleep(0.05)
612 i += 0.05
613 else:
614 self._log.error(
615 "TEST-UNEXPECTED-FAIL | runtests.py | Timed out while waiting for server startup."
617 self.stop()
618 sys.exit(1)
620 def stop(self):
621 try:
622 with closing(urlopen(self.shutdownURL)) as c:
623 self._log.info(six.ensure_text(c.read()))
624 except Exception:
625 self._log.info("Failed to stop web server on %s" % self.shutdownURL)
626 traceback.print_exc()
627 finally:
628 if self.server_logfile is not None:
629 self.server_logfile.close()
630 if self._process is not None:
631 # Kill the server immediately to avoid logging intermittent
632 # shutdown crashes, sometimes observed on Windows 10.
633 self._process.kill()
634 self._log.info("Web server killed.")
637 class WebSocketServer(object):
638 "Class which encapsulates the mod_pywebsocket server"
640 def __init__(self, options, scriptdir, logger, debuggerInfo=None):
641 self.port = options.webSocketPort
642 self.debuggerInfo = debuggerInfo
643 self._log = logger
644 self._scriptdir = scriptdir
646 def start(self):
647 # Invoke pywebsocket through a wrapper which adds special SIGINT handling.
649 # If we're in an interactive debugger, the wrapper causes the server to
650 # ignore SIGINT so the server doesn't capture a ctrl+c meant for the
651 # debugger.
653 # If we're not in an interactive debugger, the wrapper causes the server to
654 # die silently upon receiving a SIGINT.
655 scriptPath = "pywebsocket_wrapper.py"
656 script = os.path.join(self._scriptdir, scriptPath)
658 cmd = [sys.executable, script]
659 if self.debuggerInfo and self.debuggerInfo.interactive:
660 cmd += ["--interactive"]
661 cmd += [
662 "-H",
663 "127.0.0.1",
664 "-p",
665 str(self.port),
666 "-w",
667 self._scriptdir,
668 "-l",
669 os.path.join(self._scriptdir, "websock.log"),
670 "--log-level=debug",
671 "--allow-handlers-outside-root-dir",
673 env = dict(os.environ)
674 env["PYTHONPATH"] = os.pathsep.join(sys.path)
675 # Start the process. Ignore stderr so that exceptions from the server
676 # are not treated as failures when parsing the test log.
677 self._process = subprocess.Popen(
678 cmd, cwd=SCRIPT_DIR, env=env, stderr=subprocess.DEVNULL
680 pid = self._process.pid
681 self._log.info("runtests.py | Websocket server pid: %d" % pid)
683 def stop(self):
684 if self._process is not None:
685 self._process.kill()
688 class SSLTunnel:
689 def __init__(self, options, logger):
690 self.log = logger
691 self.process = None
692 self.utilityPath = options.utilityPath
693 self.xrePath = options.xrePath
694 self.certPath = options.certPath
695 self.sslPort = options.sslPort
696 self.httpPort = options.httpPort
697 self.webServer = options.webServer
698 self.webSocketPort = options.webSocketPort
700 self.customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
701 self.clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
702 self.redirRE = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
704 def writeLocation(self, config, loc):
705 for option in loc.options:
706 match = self.customCertRE.match(option)
707 if match:
708 customcert = match.group("nickname")
709 config.write(
710 "listen:%s:%s:%s:%s\n"
711 % (loc.host, loc.port, self.sslPort, customcert)
714 match = self.clientAuthRE.match(option)
715 if match:
716 clientauth = match.group("clientauth")
717 config.write(
718 "clientauth:%s:%s:%s:%s\n"
719 % (loc.host, loc.port, self.sslPort, clientauth)
722 match = self.redirRE.match(option)
723 if match:
724 redirhost = match.group("redirhost")
725 config.write(
726 "redirhost:%s:%s:%s:%s\n"
727 % (loc.host, loc.port, self.sslPort, redirhost)
730 if option in (
731 "tls1",
732 "tls1_1",
733 "tls1_2",
734 "tls1_3",
735 "ssl3",
736 "3des",
737 "failHandshake",
739 config.write(
740 "%s:%s:%s:%s\n" % (option, loc.host, loc.port, self.sslPort)
743 def buildConfig(self, locations, public=None):
744 """Create the ssltunnel configuration file"""
745 configFd, self.configFile = tempfile.mkstemp(prefix="ssltunnel", suffix=".cfg")
746 with os.fdopen(configFd, "w") as config:
747 config.write("httpproxy:1\n")
748 config.write("certdbdir:%s\n" % self.certPath)
749 config.write("forward:127.0.0.1:%s\n" % self.httpPort)
750 config.write(
751 "websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort)
753 # Use "*" to tell ssltunnel to listen on the public ip
754 # address instead of the loopback address 127.0.0.1. This
755 # may have the side-effect of causing firewall warnings on
756 # macOS and Windows. Use "127.0.0.1" to listen on the
757 # loopback address. Remote tests using physical or
758 # emulated Android devices must use the public ip address
759 # in order for the sslproxy to work but Desktop tests
760 # which run on the same host as ssltunnel may use the
761 # loopback address.
762 listen_address = "*" if public else "127.0.0.1"
763 config.write("listen:%s:%s:pgoserver\n" % (listen_address, self.sslPort))
765 for loc in locations:
766 if loc.scheme == "https" and "nocert" not in loc.options:
767 self.writeLocation(config, loc)
769 def start(self):
770 """Starts the SSL Tunnel"""
772 # start ssltunnel to provide https:// URLs capability
773 ssltunnel = os.path.join(self.utilityPath, "ssltunnel")
774 if os.name == "nt":
775 ssltunnel += ".exe"
776 if not os.path.exists(ssltunnel):
777 self.log.error(
778 "INFO | runtests.py | expected to find ssltunnel at %s" % ssltunnel
780 sys.exit(1)
782 env = test_environment(xrePath=self.xrePath, log=self.log)
783 env["LD_LIBRARY_PATH"] = self.xrePath
784 self.process = subprocess.Popen([ssltunnel, self.configFile], env=env)
785 self.log.info("runtests.py | SSL tunnel pid: %d" % self.process.pid)
787 def stop(self):
788 """Stops the SSL Tunnel and cleans up"""
789 if self.process is not None:
790 self.process.kill()
791 if os.path.exists(self.configFile):
792 os.remove(self.configFile)
795 def checkAndConfigureV4l2loopback(device):
797 Determine if a given device path is a v4l2loopback device, and if so
798 toggle a few settings on it via fcntl. Very linux-specific.
800 Returns (status, device name) where status is a boolean.
802 if not mozinfo.isLinux:
803 return False, ""
805 libc = ctypes.cdll.LoadLibrary(find_library("c"))
806 O_RDWR = 2
807 # These are from linux/videodev2.h
809 class v4l2_capability(ctypes.Structure):
810 _fields_ = [
811 ("driver", ctypes.c_char * 16),
812 ("card", ctypes.c_char * 32),
813 ("bus_info", ctypes.c_char * 32),
814 ("version", ctypes.c_uint32),
815 ("capabilities", ctypes.c_uint32),
816 ("device_caps", ctypes.c_uint32),
817 ("reserved", ctypes.c_uint32 * 3),
820 VIDIOC_QUERYCAP = 0x80685600
822 fd = libc.open(six.ensure_binary(device), O_RDWR)
823 if fd < 0:
824 return False, ""
826 vcap = v4l2_capability()
827 if libc.ioctl(fd, VIDIOC_QUERYCAP, ctypes.byref(vcap)) != 0:
828 return False, ""
830 if six.ensure_text(vcap.driver) != "v4l2 loopback":
831 return False, ""
833 class v4l2_control(ctypes.Structure):
834 _fields_ = [("id", ctypes.c_uint32), ("value", ctypes.c_int32)]
836 # These are private v4l2 control IDs, see:
837 # https://github.com/umlaeute/v4l2loopback/blob/fd822cf0faaccdf5f548cddd9a5a3dcebb6d584d/v4l2loopback.c#L131
838 KEEP_FORMAT = 0x8000000
839 SUSTAIN_FRAMERATE = 0x8000001
840 VIDIOC_S_CTRL = 0xC008561C
842 control = v4l2_control()
843 control.id = KEEP_FORMAT
844 control.value = 1
845 libc.ioctl(fd, VIDIOC_S_CTRL, ctypes.byref(control))
847 control.id = SUSTAIN_FRAMERATE
848 control.value = 1
849 libc.ioctl(fd, VIDIOC_S_CTRL, ctypes.byref(control))
850 libc.close(fd)
852 return True, six.ensure_text(vcap.card)
855 def findTestMediaDevices(log):
857 Find the test media devices configured on this system, and return a dict
858 containing information about them. The dict will have keys for 'audio'
859 and 'video', each containing the name of the media device to use.
861 If audio and video devices could not be found, return None.
863 This method is only currently implemented for Linux.
865 if not mozinfo.isLinux:
866 return None
868 info = {}
869 # Look for a v4l2loopback device.
870 name = None
871 device = None
872 for dev in sorted(glob.glob("/dev/video*")):
873 result, name_ = checkAndConfigureV4l2loopback(dev)
874 if result:
875 name = name_
876 device = dev
877 break
879 if not (name and device):
880 log.error("Couldn't find a v4l2loopback video device")
881 return None
883 # Feed it a frame of output so it has something to display
884 gst01 = spawn.find_executable("gst-launch-0.1")
885 gst010 = spawn.find_executable("gst-launch-0.10")
886 gst10 = spawn.find_executable("gst-launch-1.0")
887 if gst01:
888 gst = gst01
889 if gst010:
890 gst = gst010
891 else:
892 gst = gst10
893 process = subprocess.Popen(
895 gst,
896 "--no-fault",
897 "videotestsrc",
898 "pattern=green",
899 "num-buffers=1",
900 "!",
901 "v4l2sink",
902 "device=%s" % device,
905 info["video"] = {"name": name, "process": process}
907 # check if PulseAudio module-null-sink is loaded
908 pactl = spawn.find_executable("pactl")
910 if not pactl:
911 log.error("Could not find pactl on system")
912 return None
914 try:
915 o = subprocess.check_output([pactl, "list", "short", "modules"])
916 except subprocess.CalledProcessError:
917 log.error("Could not list currently loaded modules")
918 return None
920 null_sink = [x for x in o.splitlines() if b"module-null-sink" in x]
922 if not null_sink:
923 try:
924 subprocess.check_call([pactl, "load-module", "module-null-sink"])
925 except subprocess.CalledProcessError:
926 log.error("Could not load module-null-sink")
927 return None
929 # Hardcode the name since it's always the same.
930 info["audio"] = {"name": "Monitor of Null Output"}
931 return info
934 def create_zip(path):
936 Takes a `path` on disk and creates a zipfile with its contents. Returns a
937 path to the location of the temporary zip file.
939 with tempfile.NamedTemporaryFile() as f:
940 # `shutil.make_archive` writes to "{f.name}.zip", so we're really just
941 # using `NamedTemporaryFile` as a way to get a random path.
942 return shutil.make_archive(f.name, "zip", path)
945 def update_mozinfo():
946 """walk up directories to find mozinfo.json update the info"""
947 # TODO: This should go in a more generic place, e.g. mozinfo
949 path = SCRIPT_DIR
950 dirs = set()
951 while path != os.path.expanduser("~"):
952 if path in dirs:
953 break
954 dirs.add(path)
955 path = os.path.split(path)[0]
957 mozinfo.find_and_update_from_json(*dirs)
960 class MochitestDesktop(object):
962 Mochitest class for desktop firefox.
965 oldcwd = os.getcwd()
967 # Path to the test script on the server
968 TEST_PATH = "tests"
969 CHROME_PATH = "redirect.html"
971 certdbNew = False
972 sslTunnel = None
973 DEFAULT_TIMEOUT = 60.0
974 mediaDevices = None
975 mozinfo_variables_shown = False
977 patternFiles = {}
979 # XXX use automation.py for test name to avoid breaking legacy
980 # TODO: replace this with 'runtests.py' or 'mochitest' or the like
981 test_name = "automation.py"
983 def __init__(self, flavor, logger_options, staged_addons=None, quiet=False):
984 update_mozinfo()
985 self.flavor = flavor
986 self.staged_addons = staged_addons
987 self.server = None
988 self.wsserver = None
989 self.websocketProcessBridge = None
990 self.sslTunnel = None
991 self.manifest = None
992 self.tests_by_manifest = defaultdict(list)
993 self.args_by_manifest = defaultdict(set)
994 self.prefs_by_manifest = defaultdict(set)
995 self.env_vars_by_manifest = defaultdict(set)
996 self.tests_dirs_by_manifest = defaultdict(set)
997 self._active_tests = None
998 self.currentTests = None
999 self._locations = None
1000 self.browserEnv = None
1002 self.marionette = None
1003 self.start_script = None
1004 self.mozLogs = None
1005 self.start_script_kwargs = {}
1006 self.extraArgs = []
1007 self.extraPrefs = {}
1008 self.extraEnv = {}
1009 self.extraTestsDirs = []
1010 self.conditioned_profile_dir = None
1012 if logger_options.get("log"):
1013 self.log = logger_options["log"]
1014 else:
1015 self.log = commandline.setup_logging(
1016 "mochitest", logger_options, {"tbpl": sys.stdout}
1019 self.message_logger = MessageLogger(
1020 logger=self.log, buffering=quiet, structured=True
1023 # Max time in seconds to wait for server startup before tests will fail -- if
1024 # this seems big, it's mostly for debug machines where cold startup
1025 # (particularly after a build) takes forever.
1026 self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get("debug") else 90
1028 # metro browser sub process id
1029 self.browserProcessId = None
1031 self.haveDumpedScreen = False
1032 # Create variables to count the number of passes, fails, todos.
1033 self.countpass = 0
1034 self.countfail = 0
1035 self.counttodo = 0
1037 self.expectedError = {}
1038 self.result = {}
1040 self.start_script = os.path.join(here, "start_desktop.js")
1042 # Used to temporarily serve a performance profile
1043 self.profiler_tempdir = None
1045 def environment(self, **kwargs):
1046 kwargs["log"] = self.log
1047 return test_environment(**kwargs)
1049 def getFullPath(self, path):
1050 "Get an absolute path relative to self.oldcwd."
1051 return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
1053 def getLogFilePath(self, logFile):
1054 """return the log file path relative to the device we are testing on, in most cases
1055 it will be the full path on the local system
1057 return self.getFullPath(logFile)
1059 @property
1060 def locations(self):
1061 if self._locations is not None:
1062 return self._locations
1063 locations_file = os.path.join(SCRIPT_DIR, "server-locations.txt")
1064 self._locations = ServerLocations(locations_file)
1065 return self._locations
1067 def buildURLOptions(self, options, env):
1068 """Add test control options from the command line to the url
1070 URL parameters to test URL:
1072 autorun -- kick off tests automatically
1073 closeWhenDone -- closes the browser after the tests
1074 hideResultsTable -- hides the table of individual test results
1075 logFile -- logs test run to an absolute path
1076 startAt -- name of test to start at
1077 endAt -- name of test to end at
1078 timeout -- per-test timeout in seconds
1079 repeat -- How many times to repeat the test, ie: repeat=1 will run the test twice.
1081 self.urlOpts = []
1083 if not hasattr(options, "logFile"):
1084 options.logFile = ""
1085 if not hasattr(options, "fileLevel"):
1086 options.fileLevel = "INFO"
1088 # allow relative paths for logFile
1089 if options.logFile:
1090 options.logFile = self.getLogFilePath(options.logFile)
1092 if options.flavor in ("a11y", "browser", "chrome"):
1093 self.makeTestConfig(options)
1094 else:
1095 if options.autorun:
1096 self.urlOpts.append("autorun=1")
1097 if options.timeout:
1098 self.urlOpts.append("timeout=%d" % options.timeout)
1099 if options.maxTimeouts:
1100 self.urlOpts.append("maxTimeouts=%d" % options.maxTimeouts)
1101 if not options.keep_open:
1102 self.urlOpts.append("closeWhenDone=1")
1103 if options.logFile:
1104 self.urlOpts.append("logFile=" + encodeURIComponent(options.logFile))
1105 self.urlOpts.append(
1106 "fileLevel=" + encodeURIComponent(options.fileLevel)
1108 if options.consoleLevel:
1109 self.urlOpts.append(
1110 "consoleLevel=" + encodeURIComponent(options.consoleLevel)
1112 if options.startAt:
1113 self.urlOpts.append("startAt=%s" % options.startAt)
1114 if options.endAt:
1115 self.urlOpts.append("endAt=%s" % options.endAt)
1116 if options.shuffle:
1117 self.urlOpts.append("shuffle=1")
1118 if "MOZ_HIDE_RESULTS_TABLE" in env and env["MOZ_HIDE_RESULTS_TABLE"] == "1":
1119 self.urlOpts.append("hideResultsTable=1")
1120 if options.runUntilFailure:
1121 self.urlOpts.append("runUntilFailure=1")
1122 if options.repeat:
1123 self.urlOpts.append("repeat=%d" % options.repeat)
1124 if len(options.test_paths) == 1 and os.path.isfile(
1125 os.path.join(
1126 self.oldcwd,
1127 os.path.dirname(__file__),
1128 self.TEST_PATH,
1129 options.test_paths[0],
1132 self.urlOpts.append(
1133 "testname=%s" % "/".join([self.TEST_PATH, options.test_paths[0]])
1135 if options.manifestFile:
1136 self.urlOpts.append("manifestFile=%s" % options.manifestFile)
1137 if options.failureFile:
1138 self.urlOpts.append(
1139 "failureFile=%s" % self.getFullPath(options.failureFile)
1141 if options.runSlower:
1142 self.urlOpts.append("runSlower=true")
1143 if options.debugOnFailure:
1144 self.urlOpts.append("debugOnFailure=true")
1145 if options.dumpOutputDirectory:
1146 self.urlOpts.append(
1147 "dumpOutputDirectory=%s"
1148 % encodeURIComponent(options.dumpOutputDirectory)
1150 if options.dumpAboutMemoryAfterTest:
1151 self.urlOpts.append("dumpAboutMemoryAfterTest=true")
1152 if options.dumpDMDAfterTest:
1153 self.urlOpts.append("dumpDMDAfterTest=true")
1154 if options.debugger or options.jsdebugger:
1155 self.urlOpts.append("interactiveDebugger=true")
1156 if options.jscov_dir_prefix:
1157 self.urlOpts.append("jscovDirPrefix=%s" % options.jscov_dir_prefix)
1158 if options.cleanupCrashes:
1159 self.urlOpts.append("cleanupCrashes=true")
1160 if "MOZ_XORIGIN_MOCHITEST" in env and env["MOZ_XORIGIN_MOCHITEST"] == "1":
1161 options.xOriginTests = True
1162 if options.xOriginTests:
1163 self.urlOpts.append("xOriginTests=true")
1164 if options.comparePrefs:
1165 self.urlOpts.append("comparePrefs=true")
1166 self.urlOpts.append("ignorePrefsFile=ignorePrefs.json")
1168 def normflavor(self, flavor):
1170 In some places the string 'browser-chrome' is expected instead of
1171 'browser' and 'mochitest' instead of 'plain'. Normalize the flavor
1172 strings for those instances.
1174 # TODO Use consistent flavor strings everywhere and remove this
1175 if flavor == "browser":
1176 return "browser-chrome"
1177 elif flavor == "plain":
1178 return "mochitest"
1179 return flavor
1181 # This check can be removed when bug 983867 is fixed.
1182 def isTest(self, options, filename):
1183 allow_js_css = False
1184 if options.flavor == "browser":
1185 allow_js_css = True
1186 testPattern = re.compile(r"browser_.+\.js")
1187 elif options.flavor in ("a11y", "chrome"):
1188 testPattern = re.compile(r"(browser|test)_.+\.(xul|html|js|xhtml)")
1189 else:
1190 testPattern = re.compile(r"test_")
1192 if not allow_js_css and (".js" in filename or ".css" in filename):
1193 return False
1195 pathPieces = filename.split("/")
1197 return testPattern.match(pathPieces[-1]) and not re.search(
1198 r"\^headers\^$", filename
1201 def setTestRoot(self, options):
1202 if options.flavor != "plain":
1203 self.testRoot = options.flavor
1204 else:
1205 self.testRoot = self.TEST_PATH
1206 self.testRootAbs = os.path.join(SCRIPT_DIR, self.testRoot)
1208 def buildTestURL(self, options, scheme="http"):
1209 if scheme == "https":
1210 testHost = "https://example.com:443"
1211 elif options.xOriginTests:
1212 testHost = "http://mochi.xorigin-test:8888"
1213 else:
1214 testHost = "http://mochi.test:8888"
1215 testURL = "/".join([testHost, self.TEST_PATH])
1217 if len(options.test_paths) == 1:
1218 if os.path.isfile(
1219 os.path.join(
1220 self.oldcwd,
1221 os.path.dirname(__file__),
1222 self.TEST_PATH,
1223 options.test_paths[0],
1226 testURL = "/".join([testURL, os.path.dirname(options.test_paths[0])])
1227 else:
1228 testURL = "/".join([testURL, options.test_paths[0]])
1230 if options.flavor in ("a11y", "chrome"):
1231 testURL = "/".join([testHost, self.CHROME_PATH])
1232 elif options.flavor == "browser":
1233 testURL = "about:blank"
1234 return testURL
1236 def parseAndCreateTestsDirs(self, m):
1237 testsDirs = list(self.tests_dirs_by_manifest[m])[0]
1238 self.extraTestsDirs = []
1239 if testsDirs:
1240 self.extraTestsDirs = testsDirs.strip().split()
1241 self.log.info(
1242 "The following extra test directories will be created:\n {}".format(
1243 "\n ".join(self.extraTestsDirs)
1246 self.createExtraTestsDirs(self.extraTestsDirs, m)
1248 def createExtraTestsDirs(self, extraTestsDirs=None, manifest=None):
1249 """Take a list of directories that might be needed to exist by the test
1250 prior to even the main process be executed, and:
1251 - verify it does not already exists
1252 - create it if it does
1253 Removal of those directories is handled in cleanup()
1255 if type(extraTestsDirs) != list:
1256 return
1258 for d in extraTestsDirs:
1259 if os.path.exists(d):
1260 raise FileExistsError(
1261 "Directory '{}' already exists. This is a member of "
1262 "test-directories in manifest {}.".format(d, manifest)
1265 created = []
1266 for d in extraTestsDirs:
1267 os.makedirs(d)
1268 created += [d]
1270 if created != extraTestsDirs:
1271 raise EnvironmentError(
1272 "Not all directories were created: extraTestsDirs={} -- created={}".format(
1273 extraTestsDirs, created
1277 def getTestsByScheme(
1278 self, options, testsToFilter=None, disabled=True, manifestToFilter=None
1280 """Build the url path to the specific test harness and test file or directory
1281 Build a manifest of tests to run and write out a json file for the harness to read
1282 testsToFilter option is used to filter/keep the tests provided in the list
1284 disabled -- This allows to add all disabled tests on the build side
1285 and then on the run side to only run the enabled ones
1288 tests = self.getActiveTests(options, disabled)
1289 paths = []
1290 for test in tests:
1291 if testsToFilter and (test["path"] not in testsToFilter):
1292 continue
1293 # If we are running a specific manifest, the previously computed set of active
1294 # tests should be filtered out based on the manifest that contains that entry.
1296 # This is especially important when a test file is listed in multiple
1297 # manifests (e.g. because the same test runs under a different configuration,
1298 # and so it is being included in multiple manifests), without filtering the
1299 # active tests based on the current manifest (configuration) that we are
1300 # running for each of the N manifests we would be executing the active tests
1301 # exactly N times (and so NxN runs instead of the expected N runs, one for each
1302 # manifest).
1303 if manifestToFilter and (test["manifest"] not in manifestToFilter):
1304 continue
1305 paths.append(test)
1307 # Generate test by schemes
1308 for scheme, grouped_tests in self.groupTestsByScheme(paths).items():
1309 # Bug 883865 - add this functionality into manifestparser
1310 with open(
1311 os.path.join(SCRIPT_DIR, options.testRunManifestFile), "w"
1312 ) as manifestFile:
1313 manifestFile.write(json.dumps({"tests": grouped_tests}))
1314 options.manifestFile = options.testRunManifestFile
1315 yield (scheme, grouped_tests)
1317 def startWebSocketServer(self, options, debuggerInfo):
1318 """Launch the websocket server"""
1319 self.wsserver = WebSocketServer(options, SCRIPT_DIR, self.log, debuggerInfo)
1320 self.wsserver.start()
1322 def startWebServer(self, options):
1323 """Create the webserver and start it up"""
1325 self.server = MochitestServer(options, self.log)
1326 self.server.start()
1328 if options.pidFile != "":
1329 with open(options.pidFile + ".xpcshell.pid", "w") as f:
1330 f.write("%s" % self.server._process.pid)
1332 def startWebsocketProcessBridge(self, options):
1333 """Create a websocket server that can launch various processes that
1334 JS needs (eg; ICE server for webrtc testing)
1337 command = [
1338 sys.executable,
1339 os.path.join("websocketprocessbridge", "websocketprocessbridge.py"),
1340 "--port",
1341 options.websocket_process_bridge_port,
1343 self.websocketProcessBridge = subprocess.Popen(command, cwd=SCRIPT_DIR)
1344 self.log.info(
1345 "runtests.py | websocket/process bridge pid: %d"
1346 % self.websocketProcessBridge.pid
1349 # ensure the server is up, wait for at most ten seconds
1350 for i in range(1, 100):
1351 if self.websocketProcessBridge.poll() is not None:
1352 self.log.error(
1353 "runtests.py | websocket/process bridge failed "
1354 "to launch. Are all the dependencies installed?"
1356 return
1358 try:
1359 sock = socket.create_connection(("127.0.0.1", 8191))
1360 sock.close()
1361 break
1362 except Exception:
1363 time.sleep(0.1)
1364 else:
1365 self.log.error(
1366 "runtests.py | Timed out while waiting for "
1367 "websocket/process bridge startup."
1370 def needsWebsocketProcessBridge(self, options):
1372 Returns a bool indicating if the current test configuration needs
1373 to start the websocket process bridge or not. The boils down to if
1374 WebRTC tests that need the bridge are present.
1376 tests = self.getActiveTests(options)
1377 is_webrtc_tag_present = False
1378 for test in tests:
1379 if "webrtc" in test.get("tags", ""):
1380 is_webrtc_tag_present = True
1381 break
1382 return is_webrtc_tag_present and options.subsuite in ["media"]
1384 def startHttp3Server(self, options):
1386 Start a Http3 test server.
1388 http3ServerPath = os.path.join(
1389 options.utilityPath, "http3server" + mozinfo.info["bin_suffix"]
1391 serverOptions = {}
1392 serverOptions["http3ServerPath"] = http3ServerPath
1393 serverOptions["profilePath"] = options.profilePath
1394 serverOptions["isMochitest"] = True
1395 serverOptions["isWin"] = mozinfo.isWin
1396 serverOptions["proxyPort"] = options.http3ServerPort
1397 env = test_environment(xrePath=options.xrePath, log=self.log)
1398 self.http3Server = Http3Server(serverOptions, env, self.log)
1399 self.http3Server.start()
1401 port = self.http3Server.ports().get("MOZHTTP3_PORT_PROXY")
1402 if int(port) != options.http3ServerPort:
1403 self.http3Server = None
1404 raise RuntimeError("Error: Unable to start Http/3 server")
1406 def findNodeBin(self):
1407 # We try to find the node executable in the path given to us by the user in
1408 # the MOZ_NODE_PATH environment variable
1409 nodeBin = os.getenv("MOZ_NODE_PATH", None)
1410 self.log.info("Use MOZ_NODE_PATH at %s" % (nodeBin))
1411 if not nodeBin and build:
1412 nodeBin = build.substs.get("NODEJS")
1413 self.log.info("Use build node at %s" % (nodeBin))
1414 return nodeBin
1416 def startHttp2Server(self, options):
1418 Start a Http2 test server.
1420 serverOptions = {}
1421 serverOptions["serverPath"] = os.path.join(
1422 SCRIPT_DIR, "Http2Server", "http2_server.js"
1424 serverOptions["nodeBin"] = self.findNodeBin()
1425 serverOptions["isWin"] = mozinfo.isWin
1426 serverOptions["port"] = options.http2ServerPort
1427 env = test_environment(xrePath=options.xrePath, log=self.log)
1428 self.http2Server = Http2Server(serverOptions, env, self.log)
1429 self.http2Server.start()
1431 port = self.http2Server.port()
1432 if port != options.http2ServerPort:
1433 raise RuntimeError("Error: Unable to start Http2 server")
1435 def startDoHServer(self, options, dstServerPort, alpn):
1436 serverOptions = {}
1437 serverOptions["serverPath"] = os.path.join(
1438 SCRIPT_DIR, "DoHServer", "doh_server.js"
1440 serverOptions["nodeBin"] = self.findNodeBin()
1441 serverOptions["dstServerPort"] = dstServerPort
1442 serverOptions["isWin"] = mozinfo.isWin
1443 serverOptions["port"] = options.dohServerPort
1444 serverOptions["alpn"] = alpn
1445 env = test_environment(xrePath=options.xrePath, log=self.log)
1446 self.dohServer = DoHServer(serverOptions, env, self.log)
1447 self.dohServer.start()
1449 port = self.dohServer.port()
1450 if port != options.dohServerPort:
1451 raise RuntimeError("Error: Unable to start DoH server")
1453 def startServers(self, options, debuggerInfo, public=None):
1454 # start servers and set ports
1455 # TODO: pass these values, don't set on `self`
1456 self.webServer = options.webServer
1457 self.httpPort = options.httpPort
1458 self.sslPort = options.sslPort
1459 self.webSocketPort = options.webSocketPort
1461 # httpd-path is specified by standard makefile targets and may be specified
1462 # on the command line to select a particular version of httpd.js. If not
1463 # specified, try to select the one from hostutils.zip, as required in
1464 # bug 882932.
1465 if not options.httpdPath:
1466 options.httpdPath = os.path.join(options.utilityPath, "components")
1468 self.startWebServer(options)
1469 self.startWebSocketServer(options, debuggerInfo)
1471 # Only webrtc mochitests in the media suite need the websocketprocessbridge.
1472 if self.needsWebsocketProcessBridge(options):
1473 self.startWebsocketProcessBridge(options)
1475 # start SSL pipe
1476 self.sslTunnel = SSLTunnel(options, logger=self.log)
1477 self.sslTunnel.buildConfig(self.locations, public=public)
1478 self.sslTunnel.start()
1480 # If we're lucky, the server has fully started by now, and all paths are
1481 # ready, etc. However, xpcshell cold start times suck, at least for debug
1482 # builds. We'll try to connect to the server for awhile, and if we fail,
1483 # we'll try to kill the server and exit with an error.
1484 if self.server is not None:
1485 self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
1487 self.log.info("use http3 server: %d" % options.useHttp3Server)
1488 self.http3Server = None
1489 self.http2Server = None
1490 self.dohServer = None
1491 if options.useHttp3Server:
1492 self.startHttp3Server(options)
1493 self.startDoHServer(options, options.http3ServerPort, "h3")
1494 elif options.useHttp2Server:
1495 self.startHttp2Server(options)
1496 self.startDoHServer(options, options.http2ServerPort, "h2")
1498 def stopServers(self):
1499 """Servers are no longer needed, and perhaps more importantly, anything they
1500 might spew to console might confuse things."""
1501 if self.server is not None:
1502 try:
1503 self.log.info("Stopping web server")
1504 self.server.stop()
1505 except Exception:
1506 self.log.critical("Exception when stopping web server")
1508 if self.wsserver is not None:
1509 try:
1510 self.log.info("Stopping web socket server")
1511 self.wsserver.stop()
1512 except Exception:
1513 self.log.critical("Exception when stopping web socket server")
1515 if self.sslTunnel is not None:
1516 try:
1517 self.log.info("Stopping ssltunnel")
1518 self.sslTunnel.stop()
1519 except Exception:
1520 self.log.critical("Exception stopping ssltunnel")
1522 if self.websocketProcessBridge is not None:
1523 try:
1524 self.websocketProcessBridge.kill()
1525 self.websocketProcessBridge.wait()
1526 self.log.info("Stopping websocket/process bridge")
1527 except Exception:
1528 self.log.critical("Exception stopping websocket/process bridge")
1529 if self.http3Server is not None:
1530 try:
1531 self.http3Server.stop()
1532 except Exception:
1533 self.log.critical("Exception stopping http3 server")
1534 if self.http2Server is not None:
1535 try:
1536 self.http2Server.stop()
1537 except Exception:
1538 self.log.critical("Exception stopping http2 server")
1539 if self.dohServer is not None:
1540 try:
1541 self.dohServer.stop()
1542 except Exception:
1543 self.log.critical("Exception stopping doh server")
1545 if hasattr(self, "gstForV4l2loopbackProcess"):
1546 try:
1547 self.gstForV4l2loopbackProcess.kill()
1548 self.gstForV4l2loopbackProcess.wait()
1549 self.log.info("Stopping gst for v4l2loopback")
1550 except Exception:
1551 self.log.critical("Exception stopping gst for v4l2loopback")
1553 def copyExtraFilesToProfile(self, options):
1554 "Copy extra files or dirs specified on the command line to the testing profile."
1555 for f in options.extraProfileFiles:
1556 abspath = self.getFullPath(f)
1557 if os.path.isfile(abspath):
1558 shutil.copy2(abspath, options.profilePath)
1559 elif os.path.isdir(abspath):
1560 dest = os.path.join(options.profilePath, os.path.basename(abspath))
1561 shutil.copytree(abspath, dest)
1562 else:
1563 self.log.warning("runtests.py | Failed to copy %s to profile" % abspath)
1565 def getChromeTestDir(self, options):
1566 dir = os.path.join(os.path.abspath("."), SCRIPT_DIR) + "/"
1567 if mozinfo.isWin:
1568 dir = "file:///" + dir.replace("\\", "/")
1569 return dir
1571 def writeChromeManifest(self, options):
1572 manifest = os.path.join(options.profilePath, "tests.manifest")
1573 with open(manifest, "w") as manifestFile:
1574 # Register chrome directory.
1575 chrometestDir = self.getChromeTestDir(options)
1576 manifestFile.write(
1577 "content mochitests %s contentaccessible=yes\n" % chrometestDir
1579 manifestFile.write(
1580 "content mochitests-any %s contentaccessible=yes remoteenabled=yes\n"
1581 % chrometestDir
1583 manifestFile.write(
1584 "content mochitests-content %s contentaccessible=yes remoterequired=yes\n"
1585 % chrometestDir
1588 if options.testingModulesDir is not None:
1589 manifestFile.write(
1590 "resource testing-common file:///%s\n" % options.testingModulesDir
1592 if options.store_chrome_manifest:
1593 shutil.copyfile(manifest, options.store_chrome_manifest)
1594 return manifest
1596 def addChromeToProfile(self, options):
1597 "Adds MochiKit chrome tests to the profile."
1599 # Create (empty) chrome directory.
1600 chromedir = os.path.join(options.profilePath, "chrome")
1601 os.mkdir(chromedir)
1603 # Write userChrome.css.
1604 chrome = """
1605 /* set default namespace to XUL */
1606 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
1607 toolbar,
1608 toolbarpalette {
1609 background-color: rgb(235, 235, 235) !important;
1611 toolbar#nav-bar {
1612 background-image: none !important;
1615 with open(
1616 os.path.join(options.profilePath, "userChrome.css"), "a"
1617 ) as chromeFile:
1618 chromeFile.write(chrome)
1620 manifest = self.writeChromeManifest(options)
1622 return manifest
1624 def getExtensionsToInstall(self, options):
1625 "Return a list of extensions to install in the profile"
1626 extensions = []
1627 appDir = (
1628 options.app[: options.app.rfind(os.sep)]
1629 if options.app
1630 else options.utilityPath
1633 extensionDirs = [
1634 # Extensions distributed with the test harness.
1635 os.path.normpath(os.path.join(SCRIPT_DIR, "extensions")),
1637 if appDir:
1638 # Extensions distributed with the application.
1639 extensionDirs.append(os.path.join(appDir, "distribution", "extensions"))
1641 for extensionDir in extensionDirs:
1642 if os.path.isdir(extensionDir):
1643 for dirEntry in os.listdir(extensionDir):
1644 if dirEntry not in options.extensionsToExclude:
1645 path = os.path.join(extensionDir, dirEntry)
1646 if os.path.isdir(path) or (
1647 os.path.isfile(path) and path.endswith(".xpi")
1649 extensions.append(path)
1650 extensions.extend(options.extensionsToInstall)
1651 return extensions
1653 def logPreamble(self, tests):
1654 """Logs a suite_start message and test_start/test_end at the beginning of a run."""
1655 self.log.suite_start(
1656 self.tests_by_manifest, name="mochitest-{}".format(self.flavor)
1658 for test in tests:
1659 if "disabled" in test:
1660 self.log.test_start(test["path"])
1661 self.log.test_end(test["path"], "SKIP", message=test["disabled"])
1663 def loadFailurePatternFile(self, pat_file):
1664 if pat_file in self.patternFiles:
1665 return self.patternFiles[pat_file]
1666 if not os.path.isfile(pat_file):
1667 self.log.warning(
1668 "runtests.py | Cannot find failure pattern file " + pat_file
1670 return None
1672 # Using ":error" to ensure it shows up in the failure summary.
1673 self.log.warning(
1674 "[runtests.py:error] Using {} to filter failures. If there "
1675 "is any number mismatch below, you could have fixed "
1676 "something documented in that file. Please reduce the "
1677 "failure count appropriately.".format(pat_file)
1679 patternRE = re.compile(
1680 r"""
1681 ^\s*\*\s* # list bullet
1682 (test_\S+|\.{3}) # test name
1683 (?:\s*(`.+?`|asserts))? # failure pattern
1684 (?::.+)? # optional description
1685 \s*\[(\d+|\*)\] # expected count
1686 \s*$
1687 """,
1688 re.X,
1690 patterns = {}
1691 with open(pat_file) as f:
1692 last_name = None
1693 for line in f:
1694 match = patternRE.match(line)
1695 if not match:
1696 continue
1697 name = match.group(1)
1698 name = last_name if name == "..." else name
1699 last_name = name
1700 pat = match.group(2)
1701 if pat is not None:
1702 pat = "ASSERTION" if pat == "asserts" else pat[1:-1]
1703 count = match.group(3)
1704 count = None if count == "*" else int(count)
1705 if name not in patterns:
1706 patterns[name] = []
1707 patterns[name].append((pat, count))
1708 self.patternFiles[pat_file] = patterns
1709 return patterns
1711 def getFailurePatterns(self, pat_file, test_name):
1712 patterns = self.loadFailurePatternFile(pat_file)
1713 if patterns:
1714 return patterns.get(test_name, None)
1716 def getActiveTests(self, options, disabled=True):
1718 This method is used to parse the manifest and return active filtered tests.
1720 if self._active_tests:
1721 return self._active_tests
1723 tests = []
1724 manifest = self.getTestManifest(options)
1725 if manifest:
1726 if options.extra_mozinfo_json:
1727 mozinfo.update(options.extra_mozinfo_json)
1729 info = mozinfo.info
1731 filters = [
1732 subsuite(options.subsuite),
1735 # Allow for only running tests/manifests which match this tag
1736 if options.conditionedProfile:
1737 if not options.test_tags:
1738 options.test_tags = []
1739 options.test_tags.append("condprof")
1741 if options.test_tags:
1742 filters.append(tags(options.test_tags))
1744 if options.test_paths:
1745 options.test_paths = self.normalize_paths(options.test_paths)
1746 filters.append(pathprefix(options.test_paths))
1748 # Add chunking filters if specified
1749 if options.totalChunks:
1750 if options.chunkByDir:
1751 filters.append(
1752 chunk_by_dir(
1753 options.thisChunk, options.totalChunks, options.chunkByDir
1756 elif options.chunkByRuntime:
1757 if mozinfo.info["os"] == "android":
1758 platkey = "android"
1759 elif mozinfo.isWin:
1760 platkey = "windows"
1761 else:
1762 platkey = "unix"
1764 runtime_file = os.path.join(
1765 SCRIPT_DIR,
1766 "runtimes",
1767 "manifest-runtimes-{}.json".format(platkey),
1769 if not os.path.exists(runtime_file):
1770 self.log.error("runtime file %s not found!" % runtime_file)
1771 sys.exit(1)
1773 # Given the mochitest flavor, load the runtimes information
1774 # for only that flavor due to manifest runtime format change in Bug 1637463.
1775 with open(runtime_file, "r") as f:
1776 if "suite_name" in options:
1777 runtimes = json.load(f).get(options.suite_name, {})
1778 else:
1779 runtimes = {}
1781 filters.append(
1782 chunk_by_runtime(
1783 options.thisChunk, options.totalChunks, runtimes
1786 else:
1787 filters.append(
1788 chunk_by_slice(options.thisChunk, options.totalChunks)
1791 noDefaultFilters = False
1792 if options.runFailures:
1793 filters.append(failures(options.runFailures))
1794 noDefaultFilters = True
1796 tests = manifest.active_tests(
1797 exists=False,
1798 disabled=disabled,
1799 filters=filters,
1800 noDefaultFilters=noDefaultFilters,
1801 **info
1804 if len(tests) == 0:
1805 self.log.error(
1806 NO_TESTS_FOUND.format(options.flavor, manifest.fmt_filters())
1809 paths = []
1810 for test in tests:
1811 if len(tests) == 1 and "disabled" in test:
1812 del test["disabled"]
1814 pathAbs = os.path.abspath(test["path"])
1815 assert os.path.normcase(pathAbs).startswith(
1816 os.path.normcase(self.testRootAbs)
1818 tp = pathAbs[len(self.testRootAbs) :].replace("\\", "/").strip("/")
1820 if not self.isTest(options, tp):
1821 self.log.warning(
1822 "Warning: %s from manifest %s is not a valid test"
1823 % (test["name"], test["manifest"])
1825 continue
1827 manifest_key = test["manifest_relpath"]
1828 # Ignore ancestor_manifests that live at the root (e.g, don't have a
1829 # path separator).
1830 if "ancestor_manifest" in test and "/" in normsep(
1831 test["ancestor_manifest"]
1833 manifest_key = "{}:{}".format(test["ancestor_manifest"], manifest_key)
1835 manifest_key = manifest_key.replace("\\", "/")
1836 self.tests_by_manifest[manifest_key].append(tp)
1837 self.args_by_manifest[manifest_key].add(test.get("args"))
1838 self.prefs_by_manifest[manifest_key].add(test.get("prefs"))
1839 self.env_vars_by_manifest[manifest_key].add(test.get("environment"))
1840 self.tests_dirs_by_manifest[manifest_key].add(test.get("test-directories"))
1842 for key in ["args", "prefs", "environment", "test-directories"]:
1843 if key in test and not options.runByManifest and "disabled" not in test:
1844 self.log.error(
1845 "parsing {}: runByManifest mode must be enabled to "
1846 "set the `{}` key".format(test["manifest_relpath"], key)
1848 sys.exit(1)
1850 testob = {"path": tp, "manifest": manifest_key}
1851 if "disabled" in test:
1852 testob["disabled"] = test["disabled"]
1853 if "expected" in test:
1854 testob["expected"] = test["expected"]
1855 if "https_first_disabled" in test:
1856 testob["https_first_disabled"] = test["https_first_disabled"] == "true"
1857 if "allow_xul_xbl" in test:
1858 testob["allow_xul_xbl"] = test["allow_xul_xbl"] == "true"
1859 if "scheme" in test:
1860 testob["scheme"] = test["scheme"]
1861 if "tags" in test:
1862 testob["tags"] = test["tags"]
1863 if options.failure_pattern_file:
1864 pat_file = os.path.join(
1865 os.path.dirname(test["manifest"]), options.failure_pattern_file
1867 patterns = self.getFailurePatterns(pat_file, test["name"])
1868 if patterns:
1869 testob["expected"] = patterns
1870 paths.append(testob)
1872 # The 'args' key needs to be set in the DEFAULT section, unfortunately
1873 # we can't tell what comes from DEFAULT or not. So to validate this, we
1874 # stash all args from tests in the same manifest into a set. If the
1875 # length of the set > 1, then we know 'args' didn't come from DEFAULT.
1876 args_not_default = [
1877 m for m, p in six.iteritems(self.args_by_manifest) if len(p) > 1
1879 if args_not_default:
1880 self.log.error(
1881 "The 'args' key must be set in the DEFAULT section of a "
1882 "manifest. Fix the following manifests: {}".format(
1883 "\n".join(args_not_default)
1886 sys.exit(1)
1888 # The 'prefs' key needs to be set in the DEFAULT section too.
1889 pref_not_default = [
1890 m for m, p in six.iteritems(self.prefs_by_manifest) if len(p) > 1
1892 if pref_not_default:
1893 self.log.error(
1894 "The 'prefs' key must be set in the DEFAULT section of a "
1895 "manifest. Fix the following manifests: {}".format(
1896 "\n".join(pref_not_default)
1899 sys.exit(1)
1900 # The 'environment' key needs to be set in the DEFAULT section too.
1901 env_not_default = [
1902 m for m, p in six.iteritems(self.env_vars_by_manifest) if len(p) > 1
1904 if env_not_default:
1905 self.log.error(
1906 "The 'environment' key must be set in the DEFAULT section of a "
1907 "manifest. Fix the following manifests: {}".format(
1908 "\n".join(env_not_default)
1911 sys.exit(1)
1913 paths.sort(key=lambda p: p["path"].split("/"))
1914 if options.dump_tests:
1915 options.dump_tests = os.path.expanduser(options.dump_tests)
1916 assert os.path.exists(os.path.dirname(options.dump_tests))
1917 with open(options.dump_tests, "w") as dumpFile:
1918 dumpFile.write(json.dumps({"active_tests": paths}))
1920 self.log.info("Dumping active_tests to %s file." % options.dump_tests)
1921 sys.exit()
1923 # Upload a list of test manifests that were executed in this run.
1924 if "MOZ_UPLOAD_DIR" in os.environ:
1925 artifact = os.path.join(os.environ["MOZ_UPLOAD_DIR"], "manifests.list")
1926 with open(artifact, "a") as fh:
1927 fh.write("\n".join(sorted(self.tests_by_manifest.keys())))
1929 self._active_tests = paths
1930 return self._active_tests
1932 def getTestManifest(self, options):
1933 if isinstance(options.manifestFile, TestManifest):
1934 manifest = options.manifestFile
1935 elif options.manifestFile and os.path.isfile(options.manifestFile):
1936 manifestFileAbs = os.path.abspath(options.manifestFile)
1937 assert manifestFileAbs.startswith(SCRIPT_DIR)
1938 manifest = TestManifest([options.manifestFile], strict=False)
1939 elif options.manifestFile and os.path.isfile(
1940 os.path.join(SCRIPT_DIR, options.manifestFile)
1942 manifestFileAbs = os.path.abspath(
1943 os.path.join(SCRIPT_DIR, options.manifestFile)
1945 assert manifestFileAbs.startswith(SCRIPT_DIR)
1946 manifest = TestManifest([manifestFileAbs], strict=False)
1947 else:
1948 masterName = self.normflavor(options.flavor) + ".toml"
1949 masterPath = os.path.join(SCRIPT_DIR, self.testRoot, masterName)
1951 if not os.path.exists(masterPath):
1952 masterName = self.normflavor(options.flavor) + ".ini"
1953 masterPath = os.path.join(SCRIPT_DIR, self.testRoot, masterName)
1955 if os.path.exists(masterPath):
1956 manifest = TestManifest([masterPath], strict=False)
1957 else:
1958 manifest = None
1959 self.log.warning(
1960 "TestManifest masterPath %s does not exist" % masterPath
1963 return manifest
1965 def makeTestConfig(self, options):
1966 "Creates a test configuration file for customizing test execution."
1967 options.logFile = options.logFile.replace("\\", "\\\\")
1969 if (
1970 "MOZ_HIDE_RESULTS_TABLE" in os.environ
1971 and os.environ["MOZ_HIDE_RESULTS_TABLE"] == "1"
1973 options.hideResultsTable = True
1975 # strip certain unnecessary items to avoid serialization errors in json.dumps()
1976 d = dict(
1977 (k, v)
1978 for k, v in options.__dict__.items()
1979 if (v is None) or isinstance(v, (six.string_types, numbers.Number))
1981 d["testRoot"] = self.testRoot
1982 if options.jscov_dir_prefix:
1983 d["jscovDirPrefix"] = options.jscov_dir_prefix
1984 if not options.keep_open:
1985 d["closeWhenDone"] = "1"
1987 d["runFailures"] = False
1988 if options.runFailures:
1989 d["runFailures"] = True
1991 shutil.copy(
1992 os.path.join(SCRIPT_DIR, "ignorePrefs.json"),
1993 os.path.join(options.profilePath, "ignorePrefs.json"),
1995 d["ignorePrefsFile"] = "ignorePrefs.json"
1996 content = json.dumps(d)
1998 with open(os.path.join(options.profilePath, "testConfig.js"), "w") as config:
1999 config.write(content)
2001 def buildBrowserEnv(self, options, debugger=False, env=None):
2002 """build the environment variables for the specific test and operating system"""
2003 if mozinfo.info["asan"] and mozinfo.isLinux and mozinfo.bits == 64:
2004 useLSan = True
2005 else:
2006 useLSan = False
2008 browserEnv = self.environment(
2009 xrePath=options.xrePath, env=env, debugger=debugger, useLSan=useLSan
2012 if options.headless:
2013 browserEnv["MOZ_HEADLESS"] = "1"
2015 if options.dmd:
2016 browserEnv["DMD"] = os.environ.get("DMD", "1")
2018 # bug 1443327: do not set MOZ_CRASHREPORTER_SHUTDOWN during browser-chrome
2019 # tests, since some browser-chrome tests test content process crashes;
2020 # also exclude non-e10s since at least one non-e10s mochitest is problematic
2021 if (
2022 options.flavor == "browser" or not options.e10s
2023 ) and "MOZ_CRASHREPORTER_SHUTDOWN" in browserEnv:
2024 del browserEnv["MOZ_CRASHREPORTER_SHUTDOWN"]
2026 try:
2027 browserEnv.update(
2028 dict(
2029 parse_key_value(
2030 self.extraEnv, context="environment variable in manifest"
2034 except KeyValueParseError as e:
2035 self.log.error(str(e))
2036 return None
2038 # These variables are necessary for correct application startup; change
2039 # via the commandline at your own risk.
2040 browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
2042 # interpolate environment passed with options
2043 try:
2044 browserEnv.update(
2045 dict(parse_key_value(options.environment, context="--setenv"))
2047 except KeyValueParseError as e:
2048 self.log.error(str(e))
2049 return None
2051 if (
2052 "MOZ_PROFILER_STARTUP_FEATURES" not in browserEnv
2053 or "nativeallocations"
2054 not in browserEnv["MOZ_PROFILER_STARTUP_FEATURES"].split(",")
2056 # Only turn on the bloat log if the profiler's native allocation feature is
2057 # not enabled. The two are not compatible.
2058 browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
2060 # If profiling options are enabled, turn on the gecko profiler by using the
2061 # profiler environmental variables.
2062 if options.profiler:
2063 # The user wants to capture a profile, and automatically view it. The
2064 # profile will be saved to a temporary folder, then deleted after
2065 # opening in profiler.firefox.com.
2066 self.profiler_tempdir = tempfile.mkdtemp()
2067 browserEnv["MOZ_PROFILER_SHUTDOWN"] = os.path.join(
2068 self.profiler_tempdir, "mochitest-profile.json"
2070 browserEnv["MOZ_PROFILER_STARTUP"] = "1"
2072 if options.profilerSaveOnly:
2073 # The user wants to capture a profile, but only to save it. This defaults
2074 # to the MOZ_UPLOAD_DIR.
2075 browserEnv["MOZ_PROFILER_STARTUP"] = "1"
2076 if "MOZ_UPLOAD_DIR" in browserEnv:
2077 browserEnv["MOZ_PROFILER_SHUTDOWN"] = os.path.join(
2078 browserEnv["MOZ_UPLOAD_DIR"], "mochitest-profile.json"
2080 else:
2081 self.log.error(
2082 "--profiler-save-only was specified, but no MOZ_UPLOAD_DIR "
2083 "environment variable was provided. Please set this "
2084 "environment variable to a directory path in order to save "
2085 "a performance profile."
2087 return None
2089 try:
2090 gmp_path = self.getGMPPluginPath(options)
2091 if gmp_path is not None:
2092 browserEnv["MOZ_GMP_PATH"] = gmp_path
2093 except EnvironmentError:
2094 self.log.error("Could not find path to gmp-fake plugin!")
2095 return None
2097 if options.fatalAssertions:
2098 browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
2100 # Produce a mozlog, if setup (see MOZ_LOG global at the top of
2101 # this script).
2102 self.mozLogs = MOZ_LOG and "MOZ_UPLOAD_DIR" in os.environ
2103 if self.mozLogs:
2104 browserEnv["MOZ_LOG"] = MOZ_LOG
2106 return browserEnv
2108 def killNamedProc(self, pname, orphans=True):
2109 """Kill processes matching the given command name"""
2110 self.log.info("Checking for %s processes..." % pname)
2112 if HAVE_PSUTIL:
2113 for proc in psutil.process_iter():
2114 try:
2115 if proc.name() == pname:
2116 procd = proc.as_dict(attrs=["pid", "ppid", "name", "username"])
2117 if proc.ppid() == 1 or not orphans:
2118 self.log.info("killing %s" % procd)
2119 killPid(proc.pid, self.log)
2120 else:
2121 self.log.info("NOT killing %s (not an orphan?)" % procd)
2122 except Exception as e:
2123 self.log.info(
2124 "Warning: Unable to kill process %s: %s" % (pname, str(e))
2126 # may not be able to access process info for all processes
2127 continue
2128 else:
2130 def _psInfo(_, line):
2131 if pname in line:
2132 self.log.info(line)
2134 mozprocess.run_and_wait(
2135 ["ps", "-f"],
2136 output_line_handler=_psInfo,
2139 def _psKill(_, line):
2140 parts = line.split()
2141 if len(parts) == 3 and parts[0].isdigit():
2142 pid = int(parts[0])
2143 ppid = int(parts[1])
2144 if parts[2] == pname:
2145 if ppid == 1 or not orphans:
2146 self.log.info("killing %s (pid %d)" % (pname, pid))
2147 killPid(pid, self.log)
2148 else:
2149 self.log.info(
2150 "NOT killing %s (pid %d) (not an orphan?)"
2151 % (pname, pid)
2154 mozprocess.run_and_wait(
2155 ["ps", "-o", "pid,ppid,comm"],
2156 output_line_handler=_psKill,
2159 def execute_start_script(self):
2160 if not self.start_script or not self.marionette:
2161 return
2163 if os.path.isfile(self.start_script):
2164 with open(self.start_script, "r") as fh:
2165 script = fh.read()
2166 else:
2167 script = self.start_script
2169 with self.marionette.using_context("chrome"):
2170 return self.marionette.execute_script(
2171 script, script_args=(self.start_script_kwargs,)
2174 def fillCertificateDB(self, options):
2175 # TODO: move -> mozprofile:
2176 # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c35
2178 pwfilePath = os.path.join(options.profilePath, ".crtdbpw")
2179 with open(pwfilePath, "w") as pwfile:
2180 pwfile.write("\n")
2182 # Pre-create the certification database for the profile
2183 env = self.environment(xrePath=options.xrePath)
2184 env["LD_LIBRARY_PATH"] = options.xrePath
2185 bin_suffix = mozinfo.info.get("bin_suffix", "")
2186 certutil = os.path.join(options.utilityPath, "certutil" + bin_suffix)
2187 pk12util = os.path.join(options.utilityPath, "pk12util" + bin_suffix)
2188 toolsEnv = env
2189 if mozinfo.info["asan"]:
2190 # Disable leak checking when running these tools
2191 toolsEnv["ASAN_OPTIONS"] = "detect_leaks=0"
2192 if mozinfo.info["tsan"]:
2193 # Disable race checking when running these tools
2194 toolsEnv["TSAN_OPTIONS"] = "report_bugs=0"
2196 if self.certdbNew:
2197 # android uses the new DB formats exclusively
2198 certdbPath = "sql:" + options.profilePath
2199 else:
2200 # desktop seems to use the old
2201 certdbPath = options.profilePath
2203 # certutil.exe depends on some DLLs in the app directory
2204 # When running tests against an MSIX-installed Firefox, these DLLs
2205 # cannot be used out of the install directory, they must be copied
2206 # elsewhere first.
2207 if "WindowsApps" in options.app:
2208 install_dir = os.path.dirname(options.app)
2209 for f in os.listdir(install_dir):
2210 if f.endswith(".dll"):
2211 shutil.copy(os.path.join(install_dir, f), options.utilityPath)
2213 status = call(
2214 [certutil, "-N", "-d", certdbPath, "-f", pwfilePath], env=toolsEnv
2216 if status:
2217 return status
2219 # Walk the cert directory and add custom CAs and client certs
2220 files = os.listdir(options.certPath)
2221 for item in files:
2222 root, ext = os.path.splitext(item)
2223 if ext == ".ca":
2224 trustBits = "CT,,"
2225 if root.endswith("-object"):
2226 trustBits = "CT,,CT"
2227 call(
2229 certutil,
2230 "-A",
2231 "-i",
2232 os.path.join(options.certPath, item),
2233 "-d",
2234 certdbPath,
2235 "-f",
2236 pwfilePath,
2237 "-n",
2238 root,
2239 "-t",
2240 trustBits,
2242 env=toolsEnv,
2244 elif ext == ".client":
2245 call(
2247 pk12util,
2248 "-i",
2249 os.path.join(options.certPath, item),
2250 "-w",
2251 pwfilePath,
2252 "-d",
2253 certdbPath,
2255 env=toolsEnv,
2258 os.unlink(pwfilePath)
2259 return 0
2261 def findFreePort(self, type):
2262 with closing(socket.socket(socket.AF_INET, type)) as s:
2263 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
2264 s.bind(("127.0.0.1", 0))
2265 return s.getsockname()[1]
2267 def proxy(self, options):
2268 # proxy
2269 # use SSL port for legacy compatibility; see
2270 # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
2271 # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
2272 # - https://github.com/mozilla/mozbase/commit/43f9510e3d58bfed32790c82a57edac5f928474d
2273 # 'ws': str(self.webSocketPort)
2274 proxyOptions = {
2275 "remote": options.webServer,
2276 "http": options.httpPort,
2277 "https": options.sslPort,
2278 "ws": options.sslPort,
2281 if options.useHttp3Server:
2282 options.dohServerPort = self.findFreePort(socket.SOCK_STREAM)
2283 options.http3ServerPort = self.findFreePort(socket.SOCK_DGRAM)
2284 proxyOptions["dohServerPort"] = options.dohServerPort
2285 self.log.info("use doh server at port: %d" % options.dohServerPort)
2286 self.log.info("use http3 server at port: %d" % options.http3ServerPort)
2287 elif options.useHttp2Server:
2288 options.dohServerPort = self.findFreePort(socket.SOCK_STREAM)
2289 options.http2ServerPort = self.findFreePort(socket.SOCK_STREAM)
2290 proxyOptions["dohServerPort"] = options.dohServerPort
2291 self.log.info("use doh server at port: %d" % options.dohServerPort)
2292 self.log.info("use http2 server at port: %d" % options.http2ServerPort)
2293 return proxyOptions
2295 def merge_base_profiles(self, options, category):
2296 """Merge extra profile data from testing/profiles."""
2298 # In test packages used in CI, the profile_data directory is installed
2299 # in the SCRIPT_DIR.
2300 profile_data_dir = os.path.join(SCRIPT_DIR, "profile_data")
2301 # If possible, read profile data from topsrcdir. This prevents us from
2302 # requiring a re-build to pick up newly added extensions in the
2303 # <profile>/extensions directory.
2304 if build_obj:
2305 path = os.path.join(build_obj.topsrcdir, "testing", "profiles")
2306 if os.path.isdir(path):
2307 profile_data_dir = path
2308 # Still not found? Look for testing/profiles relative to testing/mochitest.
2309 if not os.path.isdir(profile_data_dir):
2310 path = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "profiles"))
2311 if os.path.isdir(path):
2312 profile_data_dir = path
2314 with open(os.path.join(profile_data_dir, "profiles.json"), "r") as fh:
2315 base_profiles = json.load(fh)[category]
2317 # values to use when interpolating preferences
2318 interpolation = {
2319 "server": "%s:%s" % (options.webServer, options.httpPort),
2322 for profile in base_profiles:
2323 path = os.path.join(profile_data_dir, profile)
2324 self.profile.merge(path, interpolation=interpolation)
2326 @property
2327 def conditioned_profile_copy(self):
2328 """Returns a copy of the original conditioned profile that was created."""
2330 condprof_copy = os.path.join(tempfile.mkdtemp(), "profile")
2331 shutil.copytree(
2332 self.conditioned_profile_dir,
2333 condprof_copy,
2334 ignore=shutil.ignore_patterns("lock"),
2336 self.log.info("Created a conditioned-profile copy: %s" % condprof_copy)
2337 return condprof_copy
2339 def downloadConditionedProfile(self, profile_scenario, app):
2340 from condprof.client import get_profile
2341 from condprof.util import get_current_platform, get_version
2343 if self.conditioned_profile_dir:
2344 # We already have a directory, so provide a copy that
2345 # will get deleted after it's done with
2346 return self.conditioned_profile_copy
2348 temp_download_dir = tempfile.mkdtemp()
2350 # Call condprof's client API to yield our platform-specific
2351 # conditioned-profile binary
2352 platform = get_current_platform()
2354 if not profile_scenario:
2355 profile_scenario = "settled"
2357 version = get_version(app)
2358 try:
2359 cond_prof_target_dir = get_profile(
2360 temp_download_dir,
2361 platform,
2362 profile_scenario,
2363 repo="mozilla-central",
2364 version=version,
2365 retries=2, # quicker failure
2367 except Exception:
2368 if version is None:
2369 # any other error is a showstopper
2370 self.log.critical("Could not get the conditioned profile")
2371 traceback.print_exc()
2372 raise
2373 version = None
2374 try:
2375 self.log.info("retrying a profile with no version specified")
2376 cond_prof_target_dir = get_profile(
2377 temp_download_dir,
2378 platform,
2379 profile_scenario,
2380 repo="mozilla-central",
2381 version=version,
2383 except Exception:
2384 self.log.critical("Could not get the conditioned profile")
2385 traceback.print_exc()
2386 raise
2388 # Now get the full directory path to our fetched conditioned profile
2389 self.conditioned_profile_dir = os.path.join(
2390 temp_download_dir, cond_prof_target_dir
2392 if not os.path.exists(cond_prof_target_dir):
2393 self.log.critical(
2394 "Can't find target_dir {}, from get_profile()"
2395 "temp_download_dir {}, platform {}, scenario {}".format(
2396 cond_prof_target_dir, temp_download_dir, platform, profile_scenario
2399 raise OSError
2401 self.log.info(
2402 "Original self.conditioned_profile_dir is now set: {}".format(
2403 self.conditioned_profile_dir
2406 return self.conditioned_profile_copy
2408 def buildProfile(self, options):
2409 """create the profile and add optional chrome bits and files if requested"""
2410 # get extensions to install
2411 extensions = self.getExtensionsToInstall(options)
2413 # Whitelist the _tests directory (../..) so that TESTING_JS_MODULES work
2414 tests_dir = os.path.dirname(os.path.dirname(SCRIPT_DIR))
2415 sandbox_whitelist_paths = [tests_dir] + options.sandboxReadWhitelist
2416 if platform.system() == "Linux" or platform.system() in (
2417 "Windows",
2418 "Microsoft",
2420 # Trailing slashes are needed to indicate directories on Linux and Windows
2421 sandbox_whitelist_paths = [
2422 os.path.join(p, "") for p in sandbox_whitelist_paths
2425 if options.conditionedProfile:
2426 if options.profilePath and os.path.exists(options.profilePath):
2427 shutil.rmtree(options.profilePath, ignore_errors=True)
2428 options.profilePath = self.downloadConditionedProfile("full", options.app)
2430 # This is causing `certutil -N -d -f`` to not use -f (pwd file)
2431 try:
2432 os.remove(os.path.join(options.profilePath, "key4.db"))
2433 except Exception as e:
2434 self.log.info(
2435 "Caught exception while removing key4.db"
2436 "during setup of conditioned profile: %s" % e
2439 # Create the profile
2440 self.profile = Profile(
2441 profile=options.profilePath,
2442 addons=extensions,
2443 locations=self.locations,
2444 proxy=self.proxy(options),
2445 whitelistpaths=sandbox_whitelist_paths,
2448 # Fix options.profilePath for legacy consumers.
2449 options.profilePath = self.profile.profile
2451 manifest = self.addChromeToProfile(options)
2452 self.copyExtraFilesToProfile(options)
2454 # create certificate database for the profile
2455 # TODO: this should really be upstreamed somewhere, maybe mozprofile
2456 certificateStatus = self.fillCertificateDB(options)
2457 if certificateStatus:
2458 self.log.error(
2459 "TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed"
2461 return None
2463 # Set preferences in the following order (latter overrides former):
2464 # 1) Preferences from base profile (e.g from testing/profiles)
2465 # 2) Prefs hardcoded in this function
2466 # 3) Prefs from --setpref
2468 # Prefs from base profiles
2469 self.merge_base_profiles(options, "mochitest")
2471 # Hardcoded prefs (TODO move these into a base profile)
2472 prefs = {
2473 "browser.tabs.remote.autostart": options.e10s,
2474 # Enable tracing output for detailed failures in case of
2475 # failing connection attempts, and hangs (bug 1397201)
2476 "remote.log.level": "Trace",
2477 # Disable async font fallback, because the unpredictable
2478 # extra reflow it can trigger (potentially affecting a later
2479 # test) results in spurious intermittent failures.
2480 "gfx.font_rendering.fallback.async": False,
2483 test_timeout = None
2484 if options.flavor == "browser" and options.timeout:
2485 test_timeout = options.timeout
2487 # browser-chrome tests use a fairly short default timeout of 45 seconds;
2488 # this is sometimes too short on asan and debug, where we expect reduced
2489 # performance.
2490 if (
2491 (mozinfo.info["asan"] or mozinfo.info["debug"])
2492 and options.flavor == "browser"
2493 and options.timeout is None
2495 self.log.info("Increasing default timeout to 90 seconds (asan or debug)")
2496 test_timeout = 90
2498 # tsan builds need even more time
2499 if (
2500 mozinfo.info["tsan"]
2501 and options.flavor == "browser"
2502 and options.timeout is None
2504 self.log.info("Increasing default timeout to 120 seconds (tsan)")
2505 test_timeout = 120
2507 if mozinfo.info["os"] == "win" and mozinfo.info["processor"] == "aarch64":
2508 test_timeout = self.DEFAULT_TIMEOUT * 4
2509 self.log.info(
2510 "Increasing default timeout to {} seconds (win aarch64)".format(
2511 test_timeout
2515 if "MOZ_CHAOSMODE=0xfb" in options.environment and test_timeout:
2516 test_timeout *= 2
2517 self.log.info(
2518 "Increasing default timeout to {} seconds (MOZ_CHAOSMODE)".format(
2519 test_timeout
2523 if test_timeout:
2524 prefs["testing.browserTestHarness.timeout"] = test_timeout
2526 if getattr(self, "testRootAbs", None):
2527 prefs["mochitest.testRoot"] = self.testRootAbs
2529 # See if we should use fake media devices.
2530 if options.useTestMediaDevices:
2531 prefs["media.audio_loopback_dev"] = self.mediaDevices["audio"]["name"]
2532 prefs["media.video_loopback_dev"] = self.mediaDevices["video"]["name"]
2533 prefs["media.cubeb.output_device"] = "Null Output"
2534 prefs["media.volume_scale"] = "1.0"
2535 self.gstForV4l2loopbackProcess = self.mediaDevices["video"]["process"]
2537 self.profile.set_preferences(prefs)
2539 # Extra prefs from --setpref
2540 self.profile.set_preferences(self.extraPrefs)
2541 return manifest
2543 def getGMPPluginPath(self, options):
2544 if options.gmp_path:
2545 return options.gmp_path
2547 gmp_parentdirs = [
2548 # For local builds, GMP plugins will be under dist/bin.
2549 options.xrePath,
2550 # For packaged builds, GMP plugins will get copied under
2551 # $profile/plugins.
2552 os.path.join(self.profile.profile, "plugins"),
2555 gmp_subdirs = [
2556 os.path.join("gmp-fake", "1.0"),
2557 os.path.join("gmp-fakeopenh264", "1.0"),
2558 os.path.join("gmp-clearkey", "0.1"),
2561 gmp_paths = [
2562 os.path.join(parent, sub)
2563 for parent in gmp_parentdirs
2564 for sub in gmp_subdirs
2565 if os.path.isdir(os.path.join(parent, sub))
2568 if not gmp_paths:
2569 # This is fatal for desktop environments.
2570 raise EnvironmentError("Could not find test gmp plugins")
2572 return os.pathsep.join(gmp_paths)
2574 def cleanup(self, options, final=False):
2575 """remove temporary files, profile and virtual audio input device"""
2576 if hasattr(self, "manifest") and self.manifest is not None:
2577 if os.path.exists(self.manifest):
2578 os.remove(self.manifest)
2579 if hasattr(self, "profile"):
2580 del self.profile
2581 if hasattr(self, "extraTestsDirs"):
2582 for d in self.extraTestsDirs:
2583 if os.path.exists(d):
2584 shutil.rmtree(d)
2585 if options.pidFile != "" and os.path.exists(options.pidFile):
2586 try:
2587 os.remove(options.pidFile)
2588 if os.path.exists(options.pidFile + ".xpcshell.pid"):
2589 os.remove(options.pidFile + ".xpcshell.pid")
2590 except Exception:
2591 self.log.warning(
2592 "cleaning up pidfile '%s' was unsuccessful from the test harness"
2593 % options.pidFile
2595 options.manifestFile = None
2597 if hasattr(self, "virtualInputDeviceIdList"):
2598 pactl = spawn.find_executable("pactl")
2600 if not pactl:
2601 self.log.error("Could not find pactl on system")
2602 return None
2604 for id in self.virtualInputDeviceIdList:
2605 try:
2606 subprocess.check_call([pactl, "unload-module", str(id)])
2607 except subprocess.CalledProcessError:
2608 self.log.error(
2609 "Could not remove pulse module with id {}".format(id)
2611 return None
2613 self.virtualInputDeviceIdList = []
2615 def dumpScreen(self, utilityPath):
2616 if self.haveDumpedScreen:
2617 self.log.info(
2618 "Not taking screenshot here: see the one that was previously logged"
2620 return
2621 self.haveDumpedScreen = True
2622 dump_screen(utilityPath, self.log)
2624 def killAndGetStack(self, processPID, utilityPath, debuggerInfo, dump_screen=False):
2626 Kill the process, preferrably in a way that gets us a stack trace.
2627 Also attempts to obtain a screenshot before killing the process
2628 if specified.
2630 self.log.info("Killing process: %s" % processPID)
2631 if dump_screen:
2632 self.dumpScreen(utilityPath)
2634 if mozinfo.info.get("crashreporter", True) and not debuggerInfo:
2635 try:
2636 minidump_path = os.path.join(self.profile.profile, "minidumps")
2637 mozcrash.kill_and_get_minidump(processPID, minidump_path, utilityPath)
2638 except OSError:
2639 # https://bugzilla.mozilla.org/show_bug.cgi?id=921509
2640 self.log.info("Can't trigger Breakpad, process no longer exists")
2641 return
2642 self.log.info("Can't trigger Breakpad, just killing process")
2643 killPid(processPID, self.log)
2645 def extract_child_pids(self, process_log, parent_pid=None):
2646 """Parses the given log file for the pids of any processes launched by
2647 the main process and returns them as a list.
2648 If parent_pid is provided, and psutil is available, returns children of
2649 parent_pid according to psutil.
2651 rv = []
2652 if parent_pid and HAVE_PSUTIL:
2653 self.log.info("Determining child pids from psutil...")
2654 try:
2655 rv = [p.pid for p in psutil.Process(parent_pid).children()]
2656 self.log.info(str(rv))
2657 except psutil.NoSuchProcess:
2658 self.log.warning("Failed to lookup children of pid %d" % parent_pid)
2660 rv = set(rv)
2661 pid_re = re.compile(r"==> process \d+ launched child process (\d+)")
2662 with open(process_log) as fd:
2663 for line in fd:
2664 self.log.info(line.rstrip())
2665 m = pid_re.search(line)
2666 if m:
2667 rv.add(int(m.group(1)))
2668 return rv
2670 def checkForZombies(self, processLog, utilityPath, debuggerInfo):
2671 """Look for hung processes"""
2673 if not os.path.exists(processLog):
2674 self.log.info("Automation Error: PID log not found: %s" % processLog)
2675 # Whilst no hung process was found, the run should still display as
2676 # a failure
2677 return True
2679 # scan processLog for zombies
2680 self.log.info("zombiecheck | Reading PID log: %s" % processLog)
2681 processList = self.extract_child_pids(processLog)
2682 # kill zombies
2683 foundZombie = False
2684 for processPID in processList:
2685 self.log.info(
2686 "zombiecheck | Checking for orphan process with PID: %d" % processPID
2688 if isPidAlive(processPID):
2689 foundZombie = True
2690 self.log.error(
2691 "TEST-UNEXPECTED-FAIL | zombiecheck | child process "
2692 "%d still alive after shutdown" % processPID
2694 self.killAndGetStack(
2695 processPID, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
2698 return foundZombie
2700 def checkForRunningBrowsers(self):
2701 firefoxes = ""
2702 if HAVE_PSUTIL:
2703 attrs = ["pid", "ppid", "name", "cmdline", "username"]
2704 for proc in psutil.process_iter():
2705 try:
2706 if "firefox" in proc.name():
2707 firefoxes = "%s%s\n" % (firefoxes, proc.as_dict(attrs=attrs))
2708 except Exception:
2709 # may not be able to access process info for all processes
2710 continue
2711 if len(firefoxes) > 0:
2712 # In automation, this warning is unexpected and should be investigated.
2713 # In local testing, this is probably okay, as long as the browser is not
2714 # running a marionette server.
2715 self.log.warning("Found 'firefox' running before starting test browser!")
2716 self.log.warning(firefoxes)
2718 def runApp(
2719 self,
2720 testUrl,
2721 env,
2722 app,
2723 profile,
2724 extraArgs,
2725 utilityPath,
2726 debuggerInfo=None,
2727 valgrindPath=None,
2728 valgrindArgs=None,
2729 valgrindSuppFiles=None,
2730 symbolsPath=None,
2731 timeout=-1,
2732 detectShutdownLeaks=False,
2733 screenshotOnFail=False,
2734 bisectChunk=None,
2735 marionette_args=None,
2736 e10s=True,
2737 runFailures=False,
2738 crashAsPass=False,
2739 currentManifest=None,
2742 Run the app, log the duration it took to execute, return the status code.
2743 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing
2744 for |timeout| seconds.
2746 # It can't be the case that both a with-debugger and an
2747 # on-Valgrind run have been requested. doTests() should have
2748 # already excluded this possibility.
2749 assert not (valgrindPath and debuggerInfo)
2751 # debugger information
2752 interactive = False
2753 debug_args = None
2754 if debuggerInfo:
2755 interactive = debuggerInfo.interactive
2756 debug_args = [debuggerInfo.path] + debuggerInfo.args
2758 # Set up Valgrind arguments.
2759 if valgrindPath:
2760 interactive = False
2761 valgrindArgs_split = (
2762 [] if valgrindArgs is None else shlex.split(valgrindArgs)
2765 valgrindSuppFiles_final = []
2766 if valgrindSuppFiles is not None:
2767 valgrindSuppFiles_final = [
2768 "--suppressions=" + path for path in valgrindSuppFiles.split(",")
2771 debug_args = (
2772 [valgrindPath]
2773 + mozdebug.get_default_valgrind_args()
2774 + valgrindArgs_split
2775 + valgrindSuppFiles_final
2778 # fix default timeout
2779 if timeout == -1:
2780 timeout = self.DEFAULT_TIMEOUT
2782 # Note in the log if running on Valgrind
2783 if valgrindPath:
2784 self.log.info(
2785 "runtests.py | Running on Valgrind. "
2786 + "Using timeout of %d seconds." % timeout
2789 # copy env so we don't munge the caller's environment
2790 env = env.copy()
2792 # Used to defer a possible IOError exception from Marionette
2793 marionette_exception = None
2795 temp_file_paths = []
2797 # make sure we clean up after ourselves.
2798 try:
2799 # set process log environment variable
2800 tmpfd, processLog = tempfile.mkstemp(suffix="pidlog")
2801 os.close(tmpfd)
2802 env["MOZ_PROCESS_LOG"] = processLog
2804 if debuggerInfo:
2805 # If a debugger is attached, don't use timeouts, and don't
2806 # capture ctrl-c.
2807 timeout = None
2808 signal.signal(signal.SIGINT, lambda sigid, frame: None)
2810 # build command line
2811 cmd = os.path.abspath(app)
2812 args = list(extraArgs)
2813 args.append("-marionette")
2814 # TODO: mozrunner should use -foreground at least for mac
2815 # https://bugzilla.mozilla.org/show_bug.cgi?id=916512
2816 args.append("-foreground")
2817 self.start_script_kwargs["testUrl"] = testUrl or "about:blank"
2819 if detectShutdownLeaks:
2820 env["MOZ_LOG"] = (
2821 env["MOZ_LOG"] + "," if env["MOZ_LOG"] else ""
2822 ) + "DocShellAndDOMWindowLeak:3"
2823 shutdownLeaks = ShutdownLeaks(self.log)
2824 else:
2825 shutdownLeaks = None
2827 if mozinfo.info["asan"] and mozinfo.isLinux and mozinfo.bits == 64:
2828 lsanLeaks = LSANLeaks(self.log)
2829 else:
2830 lsanLeaks = None
2832 # create an instance to process the output
2833 outputHandler = self.OutputHandler(
2834 harness=self,
2835 utilityPath=utilityPath,
2836 symbolsPath=symbolsPath,
2837 dump_screen_on_timeout=not debuggerInfo,
2838 dump_screen_on_fail=screenshotOnFail,
2839 shutdownLeaks=shutdownLeaks,
2840 lsanLeaks=lsanLeaks,
2841 bisectChunk=bisectChunk,
2844 def timeoutHandler():
2845 browserProcessId = outputHandler.browserProcessId
2846 self.handleTimeout(
2847 timeout,
2848 proc,
2849 utilityPath,
2850 debuggerInfo,
2851 browserProcessId,
2852 processLog,
2855 kp_kwargs = {
2856 "kill_on_timeout": False,
2857 "cwd": SCRIPT_DIR,
2858 "onTimeout": [timeoutHandler],
2860 kp_kwargs["processOutputLine"] = [outputHandler]
2862 self.checkForRunningBrowsers()
2864 # create mozrunner instance and start the system under test process
2865 self.lastTestSeen = self.test_name
2866 self.lastManifest = currentManifest
2867 startTime = datetime.now()
2869 runner_cls = mozrunner.runners.get(
2870 mozinfo.info.get("appname", "firefox"), mozrunner.Runner
2872 runner = runner_cls(
2873 profile=self.profile,
2874 binary=cmd,
2875 cmdargs=args,
2876 env=env,
2877 process_class=mozprocess.ProcessHandlerMixin,
2878 process_args=kp_kwargs,
2881 # start the runner
2882 try:
2883 runner.start(
2884 debug_args=debug_args,
2885 interactive=interactive,
2886 outputTimeout=timeout,
2888 proc = runner.process_handler
2889 self.log.info("runtests.py | Application pid: %d" % proc.pid)
2891 gecko_id = "GECKO(%d)" % proc.pid
2892 self.log.process_start(gecko_id)
2893 self.message_logger.gecko_id = gecko_id
2894 except PermissionError:
2895 # treat machine as bad, return
2896 return TBPL_RETRY, "Failure to launch browser"
2897 except Exception as e:
2898 raise e # unknown error
2900 try:
2901 # start marionette and kick off the tests
2902 marionette_args = marionette_args or {}
2903 self.marionette = Marionette(**marionette_args)
2904 self.marionette.start_session()
2906 # install specialpowers and mochikit addons
2907 addons = Addons(self.marionette)
2909 if self.staged_addons:
2910 for addon_path in self.staged_addons:
2911 if not os.path.isdir(addon_path):
2912 self.log.error(
2913 "TEST-UNEXPECTED-FAIL | invalid setup: missing extension at %s"
2914 % addon_path
2916 return 1, self.lastTestSeen
2917 temp_addon_path = create_zip(addon_path)
2918 temp_file_paths.append(temp_addon_path)
2919 addons.install(temp_addon_path)
2921 self.execute_start_script()
2923 # an open marionette session interacts badly with mochitest,
2924 # delete it until we figure out why.
2925 self.marionette.delete_session()
2926 del self.marionette
2928 except IOError:
2929 # Any IOError as thrown by Marionette means that something is
2930 # wrong with the process, like a crash or the socket is no
2931 # longer open. We defer raising this specific error so that
2932 # post-test checks for leaks and crashes are performed and
2933 # reported first.
2934 marionette_exception = sys.exc_info()
2936 # wait until app is finished
2937 # XXX copy functionality from
2938 # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py#L61
2939 # until bug 913970 is fixed regarding mozrunner `wait` not returning status
2940 # see https://bugzilla.mozilla.org/show_bug.cgi?id=913970
2941 self.log.info("runtests.py | Waiting for browser...")
2942 status = proc.wait()
2943 if status is None:
2944 self.log.warning(
2945 "runtests.py | Failed to get app exit code - running/crashed?"
2947 # must report an integer to process_exit()
2948 status = 0
2949 self.log.process_exit("Main app process", status)
2950 runner.process_handler = None
2952 # finalize output handler
2953 outputHandler.finish()
2955 # record post-test information
2956 if status:
2957 # no need to keep return code 137, 245, etc.
2958 status = 1
2959 self.message_logger.dump_buffered()
2960 msg = "application terminated with exit code %s" % status
2961 # self.message_logger.is_test_running indicates we need to send a test_end
2962 if crashAsPass and self.message_logger.is_test_running:
2963 # this works for browser-chrome, mochitest-plain has status=0
2964 message = {
2965 "action": "test_end",
2966 "status": "CRASH",
2967 "expected": "CRASH",
2968 "thread": None,
2969 "pid": None,
2970 "source": "mochitest",
2971 "time": int(time.time()) * 1000,
2972 "test": self.lastTestSeen,
2973 "message": msg,
2975 # need to send a test_end in order to have mozharness process messages properly
2976 # this requires a custom message vs log.error/log.warning/etc.
2977 self.message_logger.process_message(message)
2978 else:
2979 self.lastTestSeen = (
2980 currentManifest or "Main app process exited normally"
2983 self.log.info(
2984 "runtests.py | Application ran for: %s"
2985 % str(datetime.now() - startTime)
2988 # Do a final check for zombie child processes.
2989 zombieProcesses = self.checkForZombies(
2990 processLog, utilityPath, debuggerInfo
2993 # check for crashes
2994 quiet = False
2995 if crashAsPass:
2996 quiet = True
2998 minidump_path = os.path.join(self.profile.profile, "minidumps")
2999 crash_count = mozcrash.log_crashes(
3000 self.log,
3001 minidump_path,
3002 symbolsPath,
3003 test=self.lastTestSeen,
3004 quiet=quiet,
3007 expected = None
3008 if crashAsPass or crash_count > 0:
3009 # self.message_logger.is_test_running indicates we need a test_end message
3010 if self.message_logger.is_test_running:
3011 # this works for browser-chrome, mochitest-plain has status=0
3012 expected = "CRASH"
3013 if crashAsPass:
3014 status = 0
3015 elif crash_count or zombieProcesses:
3016 if self.message_logger.is_test_running:
3017 expected = "PASS"
3018 status = 1
3020 if expected:
3021 # send this out so we always wrap up the test-end message
3022 message = {
3023 "action": "test_end",
3024 "status": "CRASH",
3025 "expected": expected,
3026 "thread": None,
3027 "pid": None,
3028 "source": "mochitest",
3029 "time": int(time.time()) * 1000,
3030 "test": self.lastTestSeen,
3031 "message": "application terminated with exit code %s" % status,
3033 # need to send a test_end in order to have mozharness process messages properly
3034 # this requires a custom message vs log.error/log.warning/etc.
3035 self.message_logger.process_message(message)
3036 finally:
3037 # cleanup
3038 if os.path.exists(processLog):
3039 os.remove(processLog)
3040 for p in temp_file_paths:
3041 os.remove(p)
3043 if marionette_exception is not None:
3044 exc, value, tb = marionette_exception
3045 six.reraise(exc, value, tb)
3047 return status, self.lastTestSeen
3049 def initializeLooping(self, options):
3051 This method is used to clear the contents before each run of for loop.
3052 This method is used for --run-by-dir and --bisect-chunk.
3054 if options.conditionedProfile:
3055 if options.profilePath and os.path.exists(options.profilePath):
3056 shutil.rmtree(options.profilePath, ignore_errors=True)
3057 if options.manifestFile and os.path.exists(options.manifestFile):
3058 os.remove(options.manifestFile)
3060 self.expectedError.clear()
3061 self.result.clear()
3062 options.manifestFile = None
3063 options.profilePath = None
3065 def initializeVirtualInputDevices(self):
3067 Configure the system to have a number of virtual audio input devices, that
3068 each produce a tone at a particular frequency.
3070 This method is only currently implemented for Linux.
3072 if not mozinfo.isLinux:
3073 return
3075 pactl = spawn.find_executable("pactl")
3077 if not pactl:
3078 self.log.error("Could not find pactl on system")
3079 return
3081 DEVICES_COUNT = 4
3082 DEVICES_BASE_FREQUENCY = 110 # Hz
3083 self.virtualInputDeviceIdList = []
3084 # If the device are already present, find their id and return early
3085 o = subprocess.check_output([pactl, "list", "modules", "short"])
3086 found_devices = 0
3087 for input in o.splitlines():
3088 device = input.decode().split("\t")
3089 if device[1] == "module-sine-source":
3090 self.virtualInputDeviceIdList.append(int(device[0]))
3091 found_devices += 1
3093 if found_devices == DEVICES_COUNT:
3094 return
3095 elif found_devices != 0:
3096 # Remove all devices and reinitialize them properly
3097 for id in self.virtualInputDeviceIdList:
3098 try:
3099 subprocess.check_call([pactl, "unload-module", str(id)])
3100 except subprocess.CalledProcessError:
3101 log.error("Could not remove pulse module with id {}".format(id))
3102 return None
3104 # We want quite a number of input devices, each with a different tone
3105 # frequency and device name so that we can recognize them easily during
3106 # testing.
3107 command = [pactl, "load-module", "module-sine-source", "rate=44100"]
3108 for i in range(1, DEVICES_COUNT + 1):
3109 freq = i * DEVICES_BASE_FREQUENCY
3110 complete_command = command + [
3111 "source_name=sine-{}".format(freq),
3112 "frequency={}".format(freq),
3114 try:
3115 o = subprocess.check_output(complete_command)
3116 self.virtualInputDeviceIdList.append(o)
3118 except subprocess.CalledProcessError:
3119 self.log.error(
3120 "Could not create device with module-sine-source"
3121 " (freq={})".format(freq)
3124 def normalize_paths(self, paths):
3125 # Normalize test paths so they are relative to test root
3126 norm_paths = []
3127 for p in paths:
3128 abspath = os.path.abspath(os.path.join(self.oldcwd, p))
3129 if abspath.startswith(self.testRootAbs):
3130 norm_paths.append(os.path.relpath(abspath, self.testRootAbs))
3131 else:
3132 norm_paths.append(p)
3133 return norm_paths
3135 def runMochitests(self, options, testsToRun, manifestToFilter=None):
3136 "This is a base method for calling other methods in this class for --bisect-chunk."
3137 # Making an instance of bisect class for --bisect-chunk option.
3138 bisect = bisection.Bisect(self)
3139 finished = False
3140 status = 0
3141 bisection_log = 0
3142 while not finished:
3143 if options.bisectChunk:
3144 testsToRun = bisect.pre_test(options, testsToRun, status)
3145 # To inform that we are in the process of bisection, and to
3146 # look for bleedthrough
3147 if options.bisectChunk != "default" and not bisection_log:
3148 self.log.error(
3149 "TEST-UNEXPECTED-FAIL | Bisection | Please ignore repeats "
3150 "and look for 'Bleedthrough' (if any) at the end of "
3151 "the failure list"
3153 bisection_log = 1
3155 result = self.doTests(options, testsToRun, manifestToFilter)
3156 if result == TBPL_RETRY: # terminate task
3157 return result
3159 if options.bisectChunk:
3160 status = bisect.post_test(options, self.expectedError, self.result)
3161 else:
3162 status = -1
3164 if status == -1:
3165 finished = True
3167 # We need to print the summary only if options.bisectChunk has a value.
3168 # Also we need to make sure that we do not print the summary in between
3169 # running tests via --run-by-dir.
3170 if options.bisectChunk and options.bisectChunk in self.result:
3171 bisect.print_summary()
3173 return result
3175 def groupTestsByScheme(self, tests):
3177 split tests into groups by schemes. test is classified as http if
3178 no scheme specified
3180 httpTests = []
3181 httpsTests = []
3182 for test in tests:
3183 if not test.get("scheme") or test.get("scheme") == "http":
3184 httpTests.append(test)
3185 elif test.get("scheme") == "https":
3186 httpsTests.append(test)
3187 return {"http": httpTests, "https": httpsTests}
3189 def verifyTests(self, options):
3191 Support --verify mode: Run test(s) many times in a variety of
3192 configurations/environments in an effort to find intermittent
3193 failures.
3196 # Number of times to repeat test(s) when running with --repeat
3197 VERIFY_REPEAT = 10
3198 # Number of times to repeat test(s) when running test in
3199 VERIFY_REPEAT_SINGLE_BROWSER = 5
3201 def step1():
3202 options.repeat = VERIFY_REPEAT
3203 options.keep_open = False
3204 options.runUntilFailure = True
3205 options.profilePath = None
3206 options.comparePrefs = True
3207 result = self.runTests(options)
3208 result = result or (-2 if self.countfail > 0 else 0)
3209 self.message_logger.finish()
3210 return result
3212 def step2():
3213 options.repeat = 0
3214 options.keep_open = False
3215 options.runUntilFailure = False
3216 for i in range(VERIFY_REPEAT_SINGLE_BROWSER):
3217 options.profilePath = None
3218 result = self.runTests(options)
3219 result = result or (-2 if self.countfail > 0 else 0)
3220 self.message_logger.finish()
3221 if result != 0:
3222 break
3223 return result
3225 def step3():
3226 options.repeat = VERIFY_REPEAT
3227 options.keep_open = False
3228 options.runUntilFailure = True
3229 options.environment.append("MOZ_CHAOSMODE=0xfb")
3230 options.profilePath = None
3231 result = self.runTests(options)
3232 options.environment.remove("MOZ_CHAOSMODE=0xfb")
3233 result = result or (-2 if self.countfail > 0 else 0)
3234 self.message_logger.finish()
3235 return result
3237 def step4():
3238 options.repeat = 0
3239 options.keep_open = False
3240 options.runUntilFailure = False
3241 options.environment.append("MOZ_CHAOSMODE=0xfb")
3242 for i in range(VERIFY_REPEAT_SINGLE_BROWSER):
3243 options.profilePath = None
3244 result = self.runTests(options)
3245 result = result or (-2 if self.countfail > 0 else 0)
3246 self.message_logger.finish()
3247 if result != 0:
3248 break
3249 options.environment.remove("MOZ_CHAOSMODE=0xfb")
3250 return result
3252 def fission_step(fission_pref):
3253 if fission_pref not in options.extraPrefs:
3254 options.extraPrefs.append(fission_pref)
3255 options.keep_open = False
3256 options.runUntilFailure = True
3257 options.profilePath = None
3258 result = self.runTests(options)
3259 result = result or (-2 if self.countfail > 0 else 0)
3260 self.message_logger.finish()
3261 return result
3263 def fission_step1():
3264 return fission_step("fission.autostart=false")
3266 def fission_step2():
3267 return fission_step("fission.autostart=true")
3269 if options.verify_fission:
3270 steps = [
3271 ("1. Run each test without fission.", fission_step1),
3272 ("2. Run each test with fission.", fission_step2),
3274 else:
3275 steps = [
3276 ("1. Run each test %d times in one browser." % VERIFY_REPEAT, step1),
3278 "2. Run each test %d times in a new browser each time."
3279 % VERIFY_REPEAT_SINGLE_BROWSER,
3280 step2,
3283 "3. Run each test %d times in one browser, in chaos mode."
3284 % VERIFY_REPEAT,
3285 step3,
3288 "4. Run each test %d times in a new browser each time, "
3289 "in chaos mode." % VERIFY_REPEAT_SINGLE_BROWSER,
3290 step4,
3294 stepResults = {}
3295 for descr, step in steps:
3296 stepResults[descr] = "not run / incomplete"
3298 startTime = datetime.now()
3299 maxTime = timedelta(seconds=options.verify_max_time)
3300 finalResult = "PASSED"
3301 for descr, step in steps:
3302 if (datetime.now() - startTime) > maxTime:
3303 self.log.info("::: Test verification is taking too long: Giving up!")
3304 self.log.info(
3305 "::: So far, all checks passed, but not all checks were run."
3307 break
3308 self.log.info(":::")
3309 self.log.info('::: Running test verification step "%s"...' % descr)
3310 self.log.info(":::")
3311 result = step()
3312 if result != 0:
3313 stepResults[descr] = "FAIL"
3314 finalResult = "FAILED!"
3315 break
3316 stepResults[descr] = "Pass"
3318 self.logPreamble([])
3320 self.log.info(":::")
3321 self.log.info("::: Test verification summary for:")
3322 self.log.info(":::")
3323 tests = self.getActiveTests(options)
3324 for test in tests:
3325 self.log.info("::: " + test["path"])
3326 self.log.info(":::")
3327 for descr in sorted(stepResults.keys()):
3328 self.log.info("::: %s : %s" % (descr, stepResults[descr]))
3329 self.log.info(":::")
3330 self.log.info("::: Test verification %s" % finalResult)
3331 self.log.info(":::")
3333 return 0
3335 def runTests(self, options):
3336 """Prepare, configure, run tests and cleanup"""
3337 self.extraPrefs = parse_preferences(options.extraPrefs)
3338 self.extraPrefs["fission.autostart"] = not options.disable_fission
3340 # for test manifest parsing.
3341 mozinfo.update(
3343 "a11y_checks": options.a11y_checks,
3344 "e10s": options.e10s,
3345 "fission": not options.disable_fission,
3346 "headless": options.headless,
3347 "http3": options.useHttp3Server,
3348 "http2": options.useHttp2Server,
3349 # Until the test harness can understand default pref values,
3350 # (https://bugzilla.mozilla.org/show_bug.cgi?id=1577912) this value
3351 # should by synchronized with the default pref value indicated in
3352 # StaticPrefList.yaml.
3354 # Currently for automation, the pref defaults to true (but can be
3355 # overridden with --setpref).
3356 "serviceworker_e10s": True,
3357 "sessionHistoryInParent": not options.disable_fission
3358 or not self.extraPrefs.get(
3359 "fission.disableSessionHistoryInParent",
3360 mozinfo.info["os"] == "android",
3362 "socketprocess_e10s": self.extraPrefs.get(
3363 "network.process.enabled", False
3365 "socketprocess_networking": self.extraPrefs.get(
3366 "network.http.network_access_on_socket_process.enabled", False
3368 "swgl": self.extraPrefs.get("gfx.webrender.software", False),
3369 "verify": options.verify,
3370 "verify_fission": options.verify_fission,
3371 "webgl_ipc": self.extraPrefs.get("webgl.out-of-process", False),
3372 "wmfme": (
3373 self.extraPrefs.get("media.wmf.media-engine.enabled", 0)
3374 and self.extraPrefs.get(
3375 "media.wmf.media-engine.channel-decoder.enabled", False
3378 "mda_gpu": self.extraPrefs.get(
3379 "media.hardware-video-decoding.force-enabled", False
3381 "xorigin": options.xOriginTests,
3382 "condprof": options.conditionedProfile,
3383 "msix": "WindowsApps" in options.app,
3387 if not self.mozinfo_variables_shown:
3388 self.mozinfo_variables_shown = True
3389 self.log.info(
3390 "These variables are available in the mozinfo environment and "
3391 "can be used to skip tests conditionally:"
3393 for info in sorted(mozinfo.info.items(), key=lambda item: item[0]):
3394 self.log.info(" {key}: {value}".format(key=info[0], value=info[1]))
3395 self.setTestRoot(options)
3397 # Despite our efforts to clean up servers started by this script, in practice
3398 # we still see infrequent cases where a process is orphaned and interferes
3399 # with future tests, typically because the old server is keeping the port in use.
3400 # Try to avoid those failures by checking for and killing servers before
3401 # trying to start new ones.
3402 self.killNamedProc("ssltunnel")
3403 self.killNamedProc("xpcshell")
3405 if options.cleanupCrashes:
3406 mozcrash.cleanup_pending_crash_reports()
3408 tests = self.getActiveTests(options)
3409 self.logPreamble(tests)
3411 if mozinfo.info["fission"] and not mozinfo.info["e10s"]:
3412 # Make sure this is logged *after* suite_start so it gets associated with the
3413 # current suite in the summary formatters.
3414 self.log.error("Fission is not supported without e10s.")
3415 return 1
3417 tests = [t for t in tests if "disabled" not in t]
3419 # Until we have all green, this does not run on a11y (for perf reasons)
3420 if not options.runByManifest:
3421 result = self.runMochitests(options, [t["path"] for t in tests])
3422 self.handleShutdownProfile(options)
3423 return result
3425 # code for --run-by-manifest
3426 manifests = set(t["manifest"].replace("\\", "/") for t in tests)
3427 result = 0
3429 origPrefs = self.extraPrefs.copy()
3430 for m in sorted(manifests):
3431 self.log.group_start(name=m)
3432 self.log.info("Running manifest: {}".format(m))
3434 args = list(self.args_by_manifest[m])[0]
3435 self.extraArgs = []
3436 if args:
3437 for arg in args.strip().split():
3438 # Split off the argument value if available so that both
3439 # name and value will be set individually
3440 self.extraArgs.extend(arg.split("="))
3442 self.log.info(
3443 "The following arguments will be set:\n {}".format(
3444 "\n ".join(self.extraArgs)
3448 prefs = list(self.prefs_by_manifest[m])[0]
3449 self.extraPrefs = origPrefs.copy()
3450 if prefs:
3451 prefs = prefs.strip().split()
3452 self.log.info(
3453 "The following extra prefs will be set:\n {}".format(
3454 "\n ".join(prefs)
3457 self.extraPrefs.update(parse_preferences(prefs))
3459 envVars = list(self.env_vars_by_manifest[m])[0]
3460 self.extraEnv = {}
3461 if envVars:
3462 self.extraEnv = envVars.strip().split()
3463 self.log.info(
3464 "The following extra environment variables will be set:\n {}".format(
3465 "\n ".join(self.extraEnv)
3469 self.parseAndCreateTestsDirs(m)
3471 # If we are using --run-by-manifest, we should not use the profile path (if) provided
3472 # by the user, since we need to create a new directory for each run. We would face
3473 # problems if we use the directory provided by the user.
3474 tests_in_manifest = [t["path"] for t in tests if t["manifest"] == m]
3475 res = self.runMochitests(options, tests_in_manifest, manifestToFilter=m)
3476 if res == TBPL_RETRY: # terminate task
3477 return res
3478 result = result or res
3480 # Dump the logging buffer
3481 self.message_logger.dump_buffered()
3482 self.log.group_end(name=m)
3484 if res == -1:
3485 break
3487 if self.manifest is not None:
3488 self.cleanup(options, True)
3490 e10s_mode = "e10s" if options.e10s else "non-e10s"
3492 # for failure mode: where browser window has crashed and we have no reported results
3493 if (
3494 self.countpass == self.countfail == self.counttodo == 0
3495 and options.crashAsPass
3497 self.countpass = 1
3498 self.result = 0
3500 # printing total number of tests
3501 if options.flavor == "browser":
3502 print("TEST-INFO | checking window state")
3503 print("Browser Chrome Test Summary")
3504 print("\tPassed: %s" % self.countpass)
3505 print("\tFailed: %s" % self.countfail)
3506 print("\tTodo: %s" % self.counttodo)
3507 print("\tMode: %s" % e10s_mode)
3508 print("*** End BrowserChrome Test Results ***")
3509 else:
3510 print("0 INFO TEST-START | Shutdown")
3511 print("1 INFO Passed: %s" % self.countpass)
3512 print("2 INFO Failed: %s" % self.countfail)
3513 print("3 INFO Todo: %s" % self.counttodo)
3514 print("4 INFO Mode: %s" % e10s_mode)
3515 print("5 INFO SimpleTest FINISHED")
3517 self.handleShutdownProfile(options)
3519 if not result:
3520 if self.countfail or not (self.countpass or self.counttodo):
3521 # at least one test failed, or
3522 # no tests passed, and no tests failed (possibly a crash)
3523 result = 1
3525 return result
3527 def handleShutdownProfile(self, options):
3528 # If shutdown profiling was enabled, then the user will want to access the
3529 # performance profile. The following code will display helpful log messages
3530 # and automatically open the profile if it is requested.
3531 if self.browserEnv and "MOZ_PROFILER_SHUTDOWN" in self.browserEnv:
3532 profile_path = self.browserEnv["MOZ_PROFILER_SHUTDOWN"]
3534 profiler_logger = get_proxy_logger("profiler")
3535 profiler_logger.info("Shutdown performance profiling was enabled")
3536 profiler_logger.info("Profile saved locally to: %s" % profile_path)
3538 if options.profilerSaveOnly or options.profiler:
3539 # Only do the extra work of symbolicating and viewing the profile if
3540 # officially requested through a command line flag. The MOZ_PROFILER_*
3541 # flags can be set by a user.
3542 symbolicate_profile_json(profile_path, options.topobjdir)
3543 view_gecko_profile_from_mochitest(
3544 profile_path, options, profiler_logger
3546 else:
3547 profiler_logger.info(
3548 "The profiler was enabled outside of the mochitests. "
3549 "Use --profiler instead of MOZ_PROFILER_SHUTDOWN to "
3550 "symbolicate and open the profile automatically."
3553 # Clean up the temporary file if it exists.
3554 if self.profiler_tempdir:
3555 shutil.rmtree(self.profiler_tempdir)
3557 def doTests(self, options, testsToFilter=None, manifestToFilter=None):
3558 # A call to initializeLooping method is required in case of --run-by-dir or --bisect-chunk
3559 # since we need to initialize variables for each loop.
3560 if options.bisectChunk or options.runByManifest:
3561 self.initializeLooping(options)
3563 # get debugger info, a dict of:
3564 # {'path': path to the debugger (string),
3565 # 'interactive': whether the debugger is interactive or not (bool)
3566 # 'args': arguments to the debugger (list)
3567 # TODO: use mozrunner.local.debugger_arguments:
3568 # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42
3570 debuggerInfo = None
3571 if options.debugger:
3572 debuggerInfo = mozdebug.get_debugger_info(
3573 options.debugger, options.debuggerArgs, options.debuggerInteractive
3576 if options.useTestMediaDevices:
3577 devices = findTestMediaDevices(self.log)
3578 if not devices:
3579 self.log.error("Could not find test media devices to use")
3580 return 1
3581 self.mediaDevices = devices
3582 self.initializeVirtualInputDevices()
3584 # See if we were asked to run on Valgrind
3585 valgrindPath = None
3586 valgrindArgs = None
3587 valgrindSuppFiles = None
3588 if options.valgrind:
3589 valgrindPath = options.valgrind
3590 if options.valgrindArgs:
3591 valgrindArgs = options.valgrindArgs
3592 if options.valgrindSuppFiles:
3593 valgrindSuppFiles = options.valgrindSuppFiles
3595 if (valgrindArgs or valgrindSuppFiles) and not valgrindPath:
3596 self.log.error(
3597 "Specified --valgrind-args or --valgrind-supp-files,"
3598 " but not --valgrind"
3600 return 1
3602 if valgrindPath and debuggerInfo:
3603 self.log.error("Can't use both --debugger and --valgrind together")
3604 return 1
3606 if valgrindPath and not valgrindSuppFiles:
3607 valgrindSuppFiles = ",".join(get_default_valgrind_suppression_files())
3609 # buildProfile sets self.profile .
3610 # This relies on sideeffects and isn't very stateful:
3611 # https://bugzilla.mozilla.org/show_bug.cgi?id=919300
3612 self.manifest = self.buildProfile(options)
3613 if self.manifest is None:
3614 return 1
3616 self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log")
3618 self.browserEnv = self.buildBrowserEnv(options, debuggerInfo is not None)
3620 if self.browserEnv is None:
3621 return 1
3623 if self.mozLogs:
3624 self.browserEnv["MOZ_LOG_FILE"] = "{}/moz-pid=%PID-uid={}.log".format(
3625 self.browserEnv["MOZ_UPLOAD_DIR"], str(uuid.uuid4())
3628 status = 0
3629 try:
3630 self.startServers(options, debuggerInfo)
3632 if options.jsconsole:
3633 options.browserArgs.extend(["--jsconsole"])
3635 if options.jsdebugger:
3636 options.browserArgs.extend(["-wait-for-jsdebugger", "-jsdebugger"])
3638 # -jsdebugger takes a binary path as an optional argument.
3639 # Append jsdebuggerPath right after `-jsdebugger`.
3640 if options.jsdebuggerPath:
3641 options.browserArgs.extend([options.jsdebuggerPath])
3643 # Remove the leak detection file so it can't "leak" to the tests run.
3644 # The file is not there if leak logging was not enabled in the
3645 # application build.
3646 if os.path.exists(self.leak_report_file):
3647 os.remove(self.leak_report_file)
3649 # then again to actually run mochitest
3650 if options.timeout:
3651 timeout = options.timeout + 30
3652 elif options.debugger or options.jsdebugger or not options.autorun:
3653 timeout = None
3654 else:
3655 # We generally want the JS harness or marionette to handle
3656 # timeouts if they can.
3657 # The default JS harness timeout is currently 300 seconds.
3658 # The default Marionette socket timeout is currently 360 seconds.
3659 # Wait a little (10 seconds) more before timing out here.
3660 # See bug 479518 and bug 1414063.
3661 timeout = 370.0
3663 if "MOZ_CHAOSMODE=0xfb" in options.environment and timeout:
3664 timeout *= 2
3666 # Detect shutdown leaks for m-bc runs if
3667 # code coverage is not enabled.
3668 detectShutdownLeaks = False
3669 if options.jscov_dir_prefix is None:
3670 detectShutdownLeaks = (
3671 mozinfo.info["debug"]
3672 and options.flavor == "browser"
3673 and options.subsuite != "thunderbird"
3674 and not options.crashAsPass
3677 self.start_script_kwargs["flavor"] = self.normflavor(options.flavor)
3678 marionette_args = {
3679 "symbols_path": options.symbolsPath,
3680 "socket_timeout": options.marionette_socket_timeout,
3681 "startup_timeout": options.marionette_startup_timeout,
3684 if options.marionette:
3685 host, port = options.marionette.split(":")
3686 marionette_args["host"] = host
3687 marionette_args["port"] = int(port)
3689 # testsToFilter parameter is used to filter out the test list that
3690 # is sent to getTestsByScheme
3691 for scheme, tests in self.getTestsByScheme(
3692 options, testsToFilter, True, manifestToFilter
3694 # read the number of tests here, if we are not going to run any,
3695 # terminate early
3696 if not tests:
3697 continue
3699 self.currentTests = [t["path"] for t in tests]
3700 testURL = self.buildTestURL(options, scheme=scheme)
3702 self.buildURLOptions(options, self.browserEnv)
3703 if self.urlOpts:
3704 testURL += "?" + "&".join(self.urlOpts)
3706 if options.runFailures:
3707 testURL += "&runFailures=true"
3709 if options.timeoutAsPass:
3710 testURL += "&timeoutAsPass=true"
3712 if options.conditionedProfile:
3713 testURL += "&conditionedProfile=true"
3715 self.log.info("runtests.py | Running with scheme: {}".format(scheme))
3716 self.log.info(
3717 "runtests.py | Running with e10s: {}".format(options.e10s)
3719 self.log.info(
3720 "runtests.py | Running with fission: {}".format(
3721 mozinfo.info.get("fission", True)
3724 self.log.info(
3725 "runtests.py | Running with cross-origin iframes: {}".format(
3726 mozinfo.info.get("xorigin", False)
3729 self.log.info(
3730 "runtests.py | Running with serviceworker_e10s: {}".format(
3731 mozinfo.info.get("serviceworker_e10s", False)
3734 self.log.info(
3735 "runtests.py | Running with socketprocess_e10s: {}".format(
3736 mozinfo.info.get("socketprocess_e10s", False)
3739 self.log.info("runtests.py | Running tests: start.\n")
3740 ret, _ = self.runApp(
3741 testURL,
3742 self.browserEnv,
3743 options.app,
3744 profile=self.profile,
3745 extraArgs=options.browserArgs + self.extraArgs,
3746 utilityPath=options.utilityPath,
3747 debuggerInfo=debuggerInfo,
3748 valgrindPath=valgrindPath,
3749 valgrindArgs=valgrindArgs,
3750 valgrindSuppFiles=valgrindSuppFiles,
3751 symbolsPath=options.symbolsPath,
3752 timeout=timeout,
3753 detectShutdownLeaks=detectShutdownLeaks,
3754 screenshotOnFail=options.screenshotOnFail,
3755 bisectChunk=options.bisectChunk,
3756 marionette_args=marionette_args,
3757 e10s=options.e10s,
3758 runFailures=options.runFailures,
3759 crashAsPass=options.crashAsPass,
3760 currentManifest=manifestToFilter,
3762 status = ret or status
3763 except KeyboardInterrupt:
3764 self.log.info("runtests.py | Received keyboard interrupt.\n")
3765 status = -1
3766 except Exception as e:
3767 traceback.print_exc()
3768 self.log.error(
3769 "Automation Error: Received unexpected exception while running application\n"
3771 if "ADBTimeoutError" in repr(e):
3772 self.log.info("runtests.py | Device disconnected. Aborting test.\n")
3773 raise
3774 status = 1
3775 finally:
3776 self.stopServers()
3778 ignoreMissingLeaks = options.ignoreMissingLeaks
3779 leakThresholds = options.leakThresholds
3781 if options.crashAsPass:
3782 ignoreMissingLeaks.append("tab")
3783 ignoreMissingLeaks.append("socket")
3785 # Provide a floor for Windows chrome leak detection, because we know
3786 # we have some Windows-specific shutdown hangs that we avoid by timing
3787 # out and leaking memory.
3788 if options.flavor == "chrome" and mozinfo.isWin:
3789 leakThresholds["default"] += 1296
3791 # Stop leak detection if m-bc code coverage is enabled
3792 # by maxing out the leak threshold for all processes.
3793 if options.jscov_dir_prefix:
3794 for processType in leakThresholds:
3795 ignoreMissingLeaks.append(processType)
3796 leakThresholds[processType] = sys.maxsize
3798 utilityPath = options.utilityPath or options.xrePath
3799 if status == 0:
3800 # ignore leak checks for crashes
3801 mozleak.process_leak_log(
3802 self.leak_report_file,
3803 leak_thresholds=leakThresholds,
3804 ignore_missing_leaks=ignoreMissingLeaks,
3805 log=self.log,
3806 stack_fixer=get_stack_fixer_function(utilityPath, options.symbolsPath),
3807 scope=manifestToFilter,
3810 self.log.info("runtests.py | Running tests: end.")
3812 if self.manifest is not None:
3813 self.cleanup(options, False)
3815 return status
3817 def handleTimeout(
3818 self, timeout, proc, utilityPath, debuggerInfo, browser_pid, processLog
3820 """handle process output timeout"""
3821 # TODO: bug 913975 : _processOutput should call self.processOutputLine
3822 # one more time one timeout (I think)
3823 message = {
3824 "action": "test_end",
3825 "status": "TIMEOUT",
3826 "expected": "PASS",
3827 "thread": None,
3828 "pid": None,
3829 "source": "mochitest",
3830 "time": int(time.time()) * 1000,
3831 "test": self.lastTestSeen,
3832 "message": "application timed out after %d seconds with no output"
3833 % int(timeout),
3835 # need to send a test_end in order to have mozharness process messages properly
3836 # this requires a custom message vs log.error/log.warning/etc.
3837 self.message_logger.process_message(message)
3838 self.message_logger.dump_buffered()
3839 self.message_logger.buffering = False
3840 self.log.warning("Force-terminating active process(es).")
3842 browser_pid = browser_pid or proc.pid
3843 child_pids = self.extract_child_pids(processLog, browser_pid)
3844 self.log.info("Found child pids: %s" % child_pids)
3846 if HAVE_PSUTIL:
3847 try:
3848 browser_proc = [psutil.Process(browser_pid)]
3849 except Exception:
3850 self.log.info("Failed to get proc for pid %d" % browser_pid)
3851 browser_proc = []
3852 try:
3853 child_procs = [psutil.Process(pid) for pid in child_pids]
3854 except Exception:
3855 self.log.info("Failed to get child procs")
3856 child_procs = []
3857 for pid in child_pids:
3858 self.killAndGetStack(
3859 pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
3861 gone, alive = psutil.wait_procs(child_procs, timeout=30)
3862 for p in gone:
3863 self.log.info("psutil found pid %s dead" % p.pid)
3864 for p in alive:
3865 self.log.warning("failed to kill pid %d after 30s" % p.pid)
3866 self.killAndGetStack(
3867 browser_pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
3869 gone, alive = psutil.wait_procs(browser_proc, timeout=30)
3870 for p in gone:
3871 self.log.info("psutil found pid %s dead" % p.pid)
3872 for p in alive:
3873 self.log.warning("failed to kill pid %d after 30s" % p.pid)
3874 else:
3875 self.log.error(
3876 "psutil not available! Will wait 30s before "
3877 "attempting to kill parent process. This should "
3878 "not occur in mozilla automation. See bug 1143547."
3880 for pid in child_pids:
3881 self.killAndGetStack(
3882 pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
3884 if child_pids:
3885 time.sleep(30)
3887 self.killAndGetStack(
3888 browser_pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
3891 def archiveMozLogs(self):
3892 if self.mozLogs:
3893 with zipfile.ZipFile(
3894 "{}/mozLogs.zip".format(os.environ["MOZ_UPLOAD_DIR"]),
3895 "w",
3896 zipfile.ZIP_DEFLATED,
3897 ) as logzip:
3898 for logfile in glob.glob(
3899 "{}/moz*.log*".format(os.environ["MOZ_UPLOAD_DIR"])
3901 logzip.write(logfile, os.path.basename(logfile))
3902 os.remove(logfile)
3903 logzip.close()
3905 class OutputHandler(object):
3907 """line output handler for mozrunner"""
3909 def __init__(
3910 self,
3911 harness,
3912 utilityPath,
3913 symbolsPath=None,
3914 dump_screen_on_timeout=True,
3915 dump_screen_on_fail=False,
3916 shutdownLeaks=None,
3917 lsanLeaks=None,
3918 bisectChunk=None,
3921 harness -- harness instance
3922 dump_screen_on_timeout -- whether to dump the screen on timeout
3924 self.harness = harness
3925 self.utilityPath = utilityPath
3926 self.symbolsPath = symbolsPath
3927 self.dump_screen_on_timeout = dump_screen_on_timeout
3928 self.dump_screen_on_fail = dump_screen_on_fail
3929 self.shutdownLeaks = shutdownLeaks
3930 self.lsanLeaks = lsanLeaks
3931 self.bisectChunk = bisectChunk
3932 self.browserProcessId = None
3933 self.stackFixerFunction = self.stackFixer()
3935 def processOutputLine(self, line):
3936 """per line handler of output for mozprocess"""
3937 # Parsing the line (by the structured messages logger).
3938 messages = self.harness.message_logger.parse_line(line)
3940 for message in messages:
3941 # Passing the message to the handlers
3942 msg = message
3943 for handler in self.outputHandlers():
3944 msg = handler(msg)
3946 # Processing the message by the logger
3947 self.harness.message_logger.process_message(msg)
3949 __call__ = processOutputLine
3951 def outputHandlers(self):
3952 """returns ordered list of output handlers"""
3953 handlers = [
3954 self.fix_stack,
3955 self.record_last_test,
3956 self.dumpScreenOnTimeout,
3957 self.dumpScreenOnFail,
3958 self.trackShutdownLeaks,
3959 self.trackLSANLeaks,
3960 self.countline,
3962 if self.bisectChunk:
3963 handlers.append(self.record_result)
3964 handlers.append(self.first_error)
3966 return handlers
3968 def stackFixer(self):
3970 return get_stack_fixer_function, if any, to use on the output lines
3972 return get_stack_fixer_function(self.utilityPath, self.symbolsPath)
3974 def finish(self):
3975 if self.shutdownLeaks:
3976 numFailures, errorMessages = self.shutdownLeaks.process()
3977 self.harness.countfail += numFailures
3978 for message in errorMessages:
3979 msg = {
3980 "action": "test_end",
3981 "status": "FAIL",
3982 "expected": "PASS",
3983 "thread": None,
3984 "pid": None,
3985 "source": "mochitest",
3986 "time": int(time.time()) * 1000,
3987 "test": message["test"],
3988 "message": message["msg"],
3990 self.harness.message_logger.process_message(msg)
3992 if self.lsanLeaks:
3993 self.harness.countfail += self.lsanLeaks.process()
3995 # output message handlers:
3996 # these take a message and return a message
3998 def record_result(self, message):
3999 # by default make the result key equal to pass.
4000 if message["action"] == "test_start":
4001 key = message["test"].split("/")[-1].strip()
4002 self.harness.result[key] = "PASS"
4003 elif message["action"] == "test_status":
4004 if "expected" in message:
4005 key = message["test"].split("/")[-1].strip()
4006 self.harness.result[key] = "FAIL"
4007 elif message["status"] == "FAIL":
4008 key = message["test"].split("/")[-1].strip()
4009 self.harness.result[key] = "TODO"
4010 return message
4012 def first_error(self, message):
4013 if (
4014 message["action"] == "test_status"
4015 and "expected" in message
4016 and message["status"] == "FAIL"
4018 key = message["test"].split("/")[-1].strip()
4019 if key not in self.harness.expectedError:
4020 self.harness.expectedError[key] = message.get(
4021 "message", message["subtest"]
4022 ).strip()
4023 return message
4025 def countline(self, message):
4026 if message["action"] == "log":
4027 line = message.get("message", "")
4028 elif message["action"] == "process_output":
4029 line = message.get("data", "")
4030 else:
4031 return message
4032 val = 0
4033 try:
4034 val = int(line.split(":")[-1].strip())
4035 except (AttributeError, ValueError):
4036 return message
4038 if "Passed:" in line:
4039 self.harness.countpass += val
4040 elif "Failed:" in line:
4041 self.harness.countfail += val
4042 elif "Todo:" in line:
4043 self.harness.counttodo += val
4044 return message
4046 def fix_stack(self, message):
4047 if self.stackFixerFunction:
4048 if message["action"] == "log":
4049 message["message"] = self.stackFixerFunction(message["message"])
4050 elif message["action"] == "process_output":
4051 message["data"] = self.stackFixerFunction(message["data"])
4052 return message
4054 def record_last_test(self, message):
4055 """record last test on harness"""
4056 if message["action"] == "test_start":
4057 self.harness.lastTestSeen = message["test"]
4058 elif message["action"] == "test_end":
4059 self.harness.lastTestSeen = "{} (finished)".format(message["test"])
4060 return message
4062 def dumpScreenOnTimeout(self, message):
4063 if (
4064 not self.dump_screen_on_fail
4065 and self.dump_screen_on_timeout
4066 and message["action"] == "test_status"
4067 and "expected" in message
4068 and "Test timed out" in message["subtest"]
4070 self.harness.dumpScreen(self.utilityPath)
4071 return message
4073 def dumpScreenOnFail(self, message):
4074 if (
4075 self.dump_screen_on_fail
4076 and "expected" in message
4077 and message["status"] == "FAIL"
4079 self.harness.dumpScreen(self.utilityPath)
4080 return message
4082 def trackLSANLeaks(self, message):
4083 if self.lsanLeaks and message["action"] in ("log", "process_output"):
4084 line = (
4085 message.get("message", "")
4086 if message["action"] == "log"
4087 else message["data"]
4089 if "(finished)" in self.harness.lastTestSeen:
4090 self.lsanLeaks.log(line, self.harness.lastManifest)
4091 else:
4092 self.lsanLeaks.log(line, self.harness.lastTestSeen)
4093 return message
4095 def trackShutdownLeaks(self, message):
4096 if self.shutdownLeaks:
4097 self.shutdownLeaks.log(message)
4098 return message
4101 def view_gecko_profile_from_mochitest(profile_path, options, profiler_logger):
4102 """Getting shutdown performance profiles from just the command line arguments is
4103 difficult. This function makes the developer ergonomics a bit easier by taking the
4104 generated Gecko profile, and automatically serving it to profiler.firefox.com. The
4105 Gecko profile during shutdown is dumped to disk at:
4107 {objdir}/_tests/testing/mochitest/{profilename}
4109 This function takes that file, and launches a local webserver, and then points
4110 a browser to profiler.firefox.com to view it. From there it's easy to publish
4111 or save the profile.
4114 if options.profilerSaveOnly:
4115 # The user did not want this to automatically open, only share the location.
4116 return
4118 if not os.path.exists(profile_path):
4119 profiler_logger.error(
4120 "No profile was found at the profile path, cannot "
4121 "launch profiler.firefox.com."
4123 return
4125 profiler_logger.info("Loading this profile in the Firefox Profiler")
4127 view_gecko_profile(profile_path)
4130 def run_test_harness(parser, options):
4131 parser.validate(options)
4133 logger_options = {
4134 key: value
4135 for key, value in six.iteritems(vars(options))
4136 if key.startswith("log") or key == "valgrind"
4139 runner = MochitestDesktop(
4140 options.flavor, logger_options, options.stagedAddons, quiet=options.quiet
4143 options.runByManifest = False
4144 if options.flavor in ("plain", "a11y", "browser", "chrome"):
4145 options.runByManifest = True
4147 if options.verify or options.verify_fission:
4148 result = runner.verifyTests(options)
4149 else:
4150 result = runner.runTests(options)
4152 runner.archiveMozLogs()
4153 runner.message_logger.finish()
4154 return result
4157 def cli(args=sys.argv[1:]):
4158 # parse command line options
4159 parser = MochitestArgumentParser(app="generic")
4160 options = parser.parse_args(args)
4161 if options is None:
4162 # parsing error
4163 sys.exit(1)
4165 return run_test_harness(parser, options)
4168 if __name__ == "__main__":
4169 sys.exit(cli())