Update configs. IGNORE BROKEN CHANGESETS CLOSED TREE NO BUG a=release ba=release
[gecko.git] / testing / mochitest / runtests.py
blob912ed01af66a43b902d80ce3ba64dd7abe119b43
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
179 self._manifest = None
181 # Even if buffering is enabled, we only want to buffer messages between
182 # TEST-START/TEST-END. So it is off to begin, but will be enabled after
183 # a TEST-START comes in.
184 self._buffering = False
185 self.restore_buffering = buffering
187 # Guard to ensure we never buffer if this value was initially `False`
188 self._buffering_initially_enabled = buffering
190 # Message buffering
191 self.buffered_messages = []
193 def setManifest(self, name):
194 self._manifest = name
196 def validate(self, obj):
197 """Tests whether the given object is a valid structured message
198 (only does a superficial validation)"""
199 if not (
200 isinstance(obj, dict)
201 and "action" in obj
202 and obj["action"] in MessageLogger.VALID_ACTIONS
204 raise ValueError
206 def _fix_subtest_name(self, message):
207 """Make sure subtest name is a string"""
208 if "subtest" in message and not isinstance(
209 message["subtest"], six.string_types
211 message["subtest"] = str(message["subtest"])
213 def _fix_test_name(self, message):
214 """Normalize a logged test path to match the relative path from the sourcedir."""
215 if message.get("test") is not None:
216 test = message["test"]
217 for pattern in MessageLogger.TEST_PATH_PREFIXES:
218 test = re.sub(pattern, "", test)
219 if test != message["test"]:
220 message["test"] = test
221 break
223 def _fix_message_format(self, message):
224 if "message" in message:
225 if isinstance(message["message"], bytes):
226 message["message"] = message["message"].decode("utf-8", "replace")
227 elif not isinstance(message["message"], six.text_type):
228 message["message"] = six.text_type(message["message"])
230 def parse_line(self, line):
231 """Takes a given line of input (structured or not) and
232 returns a list of structured messages"""
233 if isinstance(line, six.binary_type):
234 # if line is a sequence of bytes, let's decode it
235 line = line.rstrip().decode("UTF-8", "replace")
236 else:
237 # line is in unicode - so let's use it as it is
238 line = line.rstrip()
240 messages = []
241 for fragment in line.split(MessageLogger.DELIMITER):
242 if not fragment:
243 continue
244 try:
245 message = json.loads(fragment)
246 self.validate(message)
247 except ValueError:
248 if self.structured:
249 message = dict(
250 action="process_output",
251 process=self.gecko_id,
252 data=fragment,
254 else:
255 message = dict(
256 action="log",
257 level="info",
258 message=fragment,
261 self._fix_subtest_name(message)
262 self._fix_test_name(message)
263 self._fix_message_format(message)
264 message["group"] = self._manifest
265 messages.append(message)
267 return messages
269 @property
270 def buffering(self):
271 if not self._buffering_initially_enabled:
272 return False
273 return self._buffering
275 @buffering.setter
276 def buffering(self, val):
277 self._buffering = val
279 def process_message(self, message):
280 """Processes a structured message. Takes into account buffering, errors, ..."""
281 # Activation/deactivating message buffering from the JS side
282 if message["action"] == "buffering_on":
283 if self.is_test_running:
284 self.buffering = True
285 return
286 if message["action"] == "buffering_off":
287 self.buffering = False
288 return
290 # Error detection also supports "raw" errors (in log messages) because some tests
291 # manually dump 'TEST-UNEXPECTED-FAIL'.
292 if "expected" in message or (
293 message["action"] == "log"
294 and message.get("message", "").startswith("TEST-UNEXPECTED")
296 self.restore_buffering = self.restore_buffering or self.buffering
297 self.buffering = False
298 if self.buffered_messages:
299 snipped = len(self.buffered_messages) - self.BUFFERING_THRESHOLD
300 if snipped > 0:
301 self.logger.info(
302 "<snipped {0} output lines - "
303 "if you need more context, please use "
304 "SimpleTest.requestCompleteLog() in your test>".format(snipped)
306 # Dumping previously buffered messages
307 self.dump_buffered(limit=True)
309 # Logging the error message
310 self.logger.log_raw(message)
311 # Determine if message should be buffered
312 elif (
313 self.buffering
314 and self.structured
315 and message["action"] in self.BUFFERED_ACTIONS
317 self.buffered_messages.append(message)
318 # Otherwise log the message directly
319 else:
320 self.logger.log_raw(message)
322 # If a test ended, we clean the buffer
323 if message["action"] == "test_end":
324 self.is_test_running = False
325 self.buffered_messages = []
326 self.restore_buffering = self.restore_buffering or self.buffering
327 self.buffering = False
329 if message["action"] == "test_start":
330 self.is_test_running = True
331 if self.restore_buffering:
332 self.restore_buffering = False
333 self.buffering = True
335 def write(self, line):
336 messages = self.parse_line(line)
337 for message in messages:
338 self.process_message(message)
339 return messages
341 def flush(self):
342 sys.stdout.flush()
344 def dump_buffered(self, limit=False):
345 if limit:
346 dumped_messages = self.buffered_messages[-self.BUFFERING_THRESHOLD :]
347 else:
348 dumped_messages = self.buffered_messages
350 last_timestamp = None
351 for buf in dumped_messages:
352 # pylint --py3k W1619
353 timestamp = datetime.fromtimestamp(buf["time"] / 1000).strftime("%H:%M:%S")
354 if timestamp != last_timestamp:
355 self.logger.info("Buffered messages logged at {}".format(timestamp))
356 last_timestamp = timestamp
358 self.logger.log_raw(buf)
359 self.logger.info("Buffered messages finished")
360 # Cleaning the list of buffered messages
361 self.buffered_messages = []
363 def finish(self):
364 self.dump_buffered()
365 self.buffering = False
366 self.logger.suite_end()
369 ####################
370 # PROCESS HANDLING #
371 ####################
374 def call(*args, **kwargs):
375 """wraps mozprocess.run_and_wait with process output logging"""
376 log = get_proxy_logger("mochitest")
378 def on_output(proc, line):
379 cmdline = subprocess.list2cmdline(proc.args)
380 log.process_output(
381 process=proc.pid,
382 data=line,
383 command=cmdline,
386 process = mozprocess.run_and_wait(*args, output_line_handler=on_output, **kwargs)
387 return process.returncode
390 def killPid(pid, log):
391 # see also https://bugzilla.mozilla.org/show_bug.cgi?id=911249#c58
393 if HAVE_PSUTIL:
394 # Kill a process tree (including grandchildren) with signal.SIGTERM
395 if pid == os.getpid():
396 raise RuntimeError("Error: trying to kill ourselves, not another process")
397 try:
398 parent = psutil.Process(pid)
399 children = parent.children(recursive=True)
400 children.append(parent)
401 for p in children:
402 p.send_signal(signal.SIGTERM)
403 gone, alive = psutil.wait_procs(children, timeout=30)
404 for p in gone:
405 log.info("psutil found pid %s dead" % p.pid)
406 for p in alive:
407 log.info("failed to kill pid %d after 30s" % p.pid)
408 except Exception as e:
409 log.info("Error: Failed to kill process %d: %s" % (pid, str(e)))
410 else:
411 try:
412 os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
413 except Exception as e:
414 log.info("Failed to kill process %d: %s" % (pid, str(e)))
417 if mozinfo.isWin:
418 import ctypes.wintypes
420 def isPidAlive(pid):
421 STILL_ACTIVE = 259
422 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
423 pHandle = ctypes.windll.kernel32.OpenProcess(
424 PROCESS_QUERY_LIMITED_INFORMATION, 0, pid
426 if not pHandle:
427 return False
429 try:
430 pExitCode = ctypes.wintypes.DWORD()
431 ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
433 if pExitCode.value != STILL_ACTIVE:
434 return False
436 # We have a live process handle. But Windows aggressively
437 # re-uses pids, so let's attempt to verify that this is
438 # actually Firefox.
439 namesize = 1024
440 pName = ctypes.create_string_buffer(namesize)
441 namelen = ctypes.windll.psapi.GetProcessImageFileNameA(
442 pHandle, pName, namesize
444 if namelen == 0:
445 # Still an active process, so conservatively assume it's Firefox.
446 return True
448 return pName.value.endswith((b"firefox.exe", b"plugin-container.exe"))
449 finally:
450 ctypes.windll.kernel32.CloseHandle(pHandle)
452 else:
453 import errno
455 def isPidAlive(pid):
456 try:
457 # kill(pid, 0) checks for a valid PID without actually sending a signal
458 # The method throws OSError if the PID is invalid, which we catch
459 # below.
460 os.kill(pid, 0)
462 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
463 # the process terminates before we get to this point.
464 wpid, wstatus = os.waitpid(pid, os.WNOHANG)
465 return wpid == 0
466 except OSError as err:
467 # Catch the errors we might expect from os.kill/os.waitpid,
468 # and re-raise any others
469 if err.errno in (errno.ESRCH, errno.ECHILD, errno.EPERM):
470 return False
471 raise
474 # TODO: ^ upstream isPidAlive to mozprocess
476 #######################
477 # HTTP SERVER SUPPORT #
478 #######################
481 class MochitestServer(object):
482 "Web server used to serve Mochitests, for closer fidelity to the real web."
484 instance_count = 0
486 def __init__(self, options, logger):
487 if isinstance(options, Namespace):
488 options = vars(options)
489 self._log = logger
490 self._keep_open = bool(options["keep_open"])
491 self._utilityPath = options["utilityPath"]
492 self._xrePath = options["xrePath"]
493 self._profileDir = options["profilePath"]
494 self.webServer = options["webServer"]
495 self.httpPort = options["httpPort"]
496 if options.get("remoteWebServer") == "10.0.2.2":
497 # probably running an Android emulator and 10.0.2.2 will
498 # not be visible from host
499 shutdownServer = "127.0.0.1"
500 else:
501 shutdownServer = self.webServer
502 self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % {
503 "server": shutdownServer,
504 "port": self.httpPort,
506 self.debugURL = "http://%(server)s:%(port)s/server/debug?2" % {
507 "server": shutdownServer,
508 "port": self.httpPort,
510 self.testPrefix = "undefined"
512 if options.get("httpdPath"):
513 self._httpdPath = options["httpdPath"]
514 else:
515 self._httpdPath = SCRIPT_DIR
516 self._httpdPath = os.path.abspath(self._httpdPath)
518 MochitestServer.instance_count += 1
520 def start(self):
521 "Run the Mochitest server, returning the process ID of the server."
523 # get testing environment
524 env = test_environment(xrePath=self._xrePath, log=self._log)
525 env["XPCOM_DEBUG_BREAK"] = "warn"
526 if "LD_LIBRARY_PATH" not in env or env["LD_LIBRARY_PATH"] is None:
527 env["LD_LIBRARY_PATH"] = self._xrePath
528 else:
529 env["LD_LIBRARY_PATH"] = ":".join([self._xrePath, env["LD_LIBRARY_PATH"]])
531 # When running with an ASan build, our xpcshell server will also be ASan-enabled,
532 # thus consuming too much resources when running together with the browser on
533 # the test machines. Try to limit the amount of resources by disabling certain
534 # features.
535 env["ASAN_OPTIONS"] = "quarantine_size=1:redzone=32:malloc_context_size=5"
537 # Likewise, when running with a TSan build, our xpcshell server will
538 # also be TSan-enabled. Except that in this case, we don't really
539 # care about races in xpcshell. So disable TSan for the server.
540 env["TSAN_OPTIONS"] = "report_bugs=0"
542 # Don't use socket process for the xpcshell server.
543 env["MOZ_DISABLE_SOCKET_PROCESS"] = "1"
545 if mozinfo.isWin:
546 env["PATH"] = env["PATH"] + ";" + str(self._xrePath)
548 args = [
549 "-g",
550 self._xrePath,
551 "-e",
552 "const _PROFILE_PATH = '%(profile)s'; const _SERVER_PORT = '%(port)s'; "
553 "const _SERVER_ADDR = '%(server)s'; const _TEST_PREFIX = %(testPrefix)s; "
554 "const _DISPLAY_RESULTS = %(displayResults)s; "
555 "const _HTTPD_PATH = '%(httpdPath)s';"
557 "httpdPath": self._httpdPath.replace("\\", "\\\\"),
558 "profile": self._profileDir.replace("\\", "\\\\"),
559 "port": self.httpPort,
560 "server": self.webServer,
561 "testPrefix": self.testPrefix,
562 "displayResults": str(self._keep_open).lower(),
564 "-f",
565 os.path.join(SCRIPT_DIR, "server.js"),
568 xpcshell = os.path.join(
569 self._utilityPath, "xpcshell" + mozinfo.info["bin_suffix"]
571 command = [xpcshell] + args
572 if MOCHITEST_SERVER_LOGGING and "MOZ_UPLOAD_DIR" in os.environ:
573 server_logfile_path = os.path.join(
574 os.environ["MOZ_UPLOAD_DIR"],
575 "mochitest-server-%d.txt" % MochitestServer.instance_count,
577 self.server_logfile = open(server_logfile_path, "w")
578 self._process = subprocess.Popen(
579 command,
580 cwd=SCRIPT_DIR,
581 env=env,
582 stdout=self.server_logfile,
583 stderr=subprocess.STDOUT,
585 else:
586 self.server_logfile = None
587 self._process = subprocess.Popen(
588 command,
589 cwd=SCRIPT_DIR,
590 env=env,
592 self._log.info("%s : launching %s" % (self.__class__.__name__, command))
593 pid = self._process.pid
594 self._log.info("runtests.py | Server pid: %d" % pid)
595 if MOCHITEST_SERVER_LOGGING and "MOZ_UPLOAD_DIR" in os.environ:
596 self._log.info("runtests.py enabling server debugging...")
597 i = 0
598 while i < 5:
599 try:
600 with closing(urlopen(self.debugURL)) as c:
601 self._log.info(six.ensure_text(c.read()))
602 break
603 except Exception as e:
604 self._log.info("exception when enabling debugging: %s" % str(e))
605 time.sleep(1)
606 i += 1
608 def ensureReady(self, timeout):
609 assert timeout >= 0
611 aliveFile = os.path.join(self._profileDir, "server_alive.txt")
612 i = 0
613 while i < timeout:
614 if os.path.exists(aliveFile):
615 break
616 time.sleep(0.05)
617 i += 0.05
618 else:
619 self._log.error(
620 "TEST-UNEXPECTED-FAIL | runtests.py | Timed out while waiting for server startup."
622 self.stop()
623 sys.exit(1)
625 def stop(self):
626 try:
627 with closing(urlopen(self.shutdownURL)) as c:
628 self._log.info(six.ensure_text(c.read()))
629 except Exception:
630 self._log.info("Failed to stop web server on %s" % self.shutdownURL)
631 traceback.print_exc()
632 finally:
633 if self.server_logfile is not None:
634 self.server_logfile.close()
635 if self._process is not None:
636 # Kill the server immediately to avoid logging intermittent
637 # shutdown crashes, sometimes observed on Windows 10.
638 self._process.kill()
639 self._log.info("Web server killed.")
642 class WebSocketServer(object):
643 "Class which encapsulates the mod_pywebsocket server"
645 def __init__(self, options, scriptdir, logger, debuggerInfo=None):
646 self.port = options.webSocketPort
647 self.debuggerInfo = debuggerInfo
648 self._log = logger
649 self._scriptdir = scriptdir
651 def start(self):
652 # Invoke pywebsocket through a wrapper which adds special SIGINT handling.
654 # If we're in an interactive debugger, the wrapper causes the server to
655 # ignore SIGINT so the server doesn't capture a ctrl+c meant for the
656 # debugger.
658 # If we're not in an interactive debugger, the wrapper causes the server to
659 # die silently upon receiving a SIGINT.
660 scriptPath = "pywebsocket_wrapper.py"
661 script = os.path.join(self._scriptdir, scriptPath)
663 cmd = [sys.executable, script]
664 if self.debuggerInfo and self.debuggerInfo.interactive:
665 cmd += ["--interactive"]
666 # We need to use 0.0.0.0 to listen on all interfaces because
667 # Android tests connect from a different hosts
668 cmd += [
669 "-H",
670 "0.0.0.0",
671 "-p",
672 str(self.port),
673 "-w",
674 self._scriptdir,
675 "-l",
676 os.path.join(self._scriptdir, "websock.log"),
677 "--log-level=debug",
678 "--allow-handlers-outside-root-dir",
680 env = dict(os.environ)
681 env["PYTHONPATH"] = os.pathsep.join(sys.path)
682 # Start the process. Ignore stderr so that exceptions from the server
683 # are not treated as failures when parsing the test log.
684 self._process = subprocess.Popen(
685 cmd, cwd=SCRIPT_DIR, env=env, stderr=subprocess.DEVNULL
687 pid = self._process.pid
688 self._log.info("runtests.py | Websocket server pid: %d" % pid)
690 def stop(self):
691 if self._process is not None:
692 self._process.kill()
695 class SSLTunnel:
696 def __init__(self, options, logger):
697 self.log = logger
698 self.process = None
699 self.utilityPath = options.utilityPath
700 self.xrePath = options.xrePath
701 self.certPath = options.certPath
702 self.sslPort = options.sslPort
703 self.httpPort = options.httpPort
704 self.webServer = options.webServer
705 self.webSocketPort = options.webSocketPort
707 self.customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
708 self.clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
709 self.redirRE = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
711 def writeLocation(self, config, loc):
712 for option in loc.options:
713 match = self.customCertRE.match(option)
714 if match:
715 customcert = match.group("nickname")
716 config.write(
717 "listen:%s:%s:%s:%s\n"
718 % (loc.host, loc.port, self.sslPort, customcert)
721 match = self.clientAuthRE.match(option)
722 if match:
723 clientauth = match.group("clientauth")
724 config.write(
725 "clientauth:%s:%s:%s:%s\n"
726 % (loc.host, loc.port, self.sslPort, clientauth)
729 match = self.redirRE.match(option)
730 if match:
731 redirhost = match.group("redirhost")
732 config.write(
733 "redirhost:%s:%s:%s:%s\n"
734 % (loc.host, loc.port, self.sslPort, redirhost)
737 if option in (
738 "tls1",
739 "tls1_1",
740 "tls1_2",
741 "tls1_3",
742 "ssl3",
743 "3des",
744 "failHandshake",
746 config.write(
747 "%s:%s:%s:%s\n" % (option, loc.host, loc.port, self.sslPort)
750 def buildConfig(self, locations, public=None):
751 """Create the ssltunnel configuration file"""
752 configFd, self.configFile = tempfile.mkstemp(prefix="ssltunnel", suffix=".cfg")
753 with os.fdopen(configFd, "w") as config:
754 config.write("httpproxy:1\n")
755 config.write("certdbdir:%s\n" % self.certPath)
756 config.write("forward:127.0.0.1:%s\n" % self.httpPort)
758 wsserver = self.webServer
759 if self.webServer == "10.0.2.2":
760 wsserver = "127.0.0.1"
762 config.write("websocketserver:%s:%s\n" % (wsserver, self.webSocketPort))
763 # Use "*" to tell ssltunnel to listen on the public ip
764 # address instead of the loopback address 127.0.0.1. This
765 # may have the side-effect of causing firewall warnings on
766 # macOS and Windows. Use "127.0.0.1" to listen on the
767 # loopback address. Remote tests using physical or
768 # emulated Android devices must use the public ip address
769 # in order for the sslproxy to work but Desktop tests
770 # which run on the same host as ssltunnel may use the
771 # loopback address.
772 listen_address = "*" if public else "127.0.0.1"
773 config.write("listen:%s:%s:pgoserver\n" % (listen_address, self.sslPort))
775 for loc in locations:
776 if loc.scheme == "https" and "nocert" not in loc.options:
777 self.writeLocation(config, loc)
779 def start(self):
780 """Starts the SSL Tunnel"""
782 # start ssltunnel to provide https:// URLs capability
783 ssltunnel = os.path.join(self.utilityPath, "ssltunnel")
784 if os.name == "nt":
785 ssltunnel += ".exe"
786 if not os.path.exists(ssltunnel):
787 self.log.error(
788 "INFO | runtests.py | expected to find ssltunnel at %s" % ssltunnel
790 sys.exit(1)
792 env = test_environment(xrePath=self.xrePath, log=self.log)
793 env["LD_LIBRARY_PATH"] = self.xrePath
794 self.process = subprocess.Popen([ssltunnel, self.configFile], env=env)
795 self.log.info("runtests.py | SSL tunnel pid: %d" % self.process.pid)
797 def stop(self):
798 """Stops the SSL Tunnel and cleans up"""
799 if self.process is not None:
800 self.process.kill()
801 if os.path.exists(self.configFile):
802 os.remove(self.configFile)
805 def checkAndConfigureV4l2loopback(device):
807 Determine if a given device path is a v4l2loopback device, and if so
808 toggle a few settings on it via fcntl. Very linux-specific.
810 Returns (status, device name) where status is a boolean.
812 if not mozinfo.isLinux:
813 return False, ""
815 libc = ctypes.cdll.LoadLibrary(find_library("c"))
816 O_RDWR = 2
817 # These are from linux/videodev2.h
819 class v4l2_capability(ctypes.Structure):
820 _fields_ = [
821 ("driver", ctypes.c_char * 16),
822 ("card", ctypes.c_char * 32),
823 ("bus_info", ctypes.c_char * 32),
824 ("version", ctypes.c_uint32),
825 ("capabilities", ctypes.c_uint32),
826 ("device_caps", ctypes.c_uint32),
827 ("reserved", ctypes.c_uint32 * 3),
830 VIDIOC_QUERYCAP = 0x80685600
832 fd = libc.open(six.ensure_binary(device), O_RDWR)
833 if fd < 0:
834 return False, ""
836 vcap = v4l2_capability()
837 if libc.ioctl(fd, VIDIOC_QUERYCAP, ctypes.byref(vcap)) != 0:
838 return False, ""
840 if six.ensure_text(vcap.driver) != "v4l2 loopback":
841 return False, ""
843 class v4l2_control(ctypes.Structure):
844 _fields_ = [("id", ctypes.c_uint32), ("value", ctypes.c_int32)]
846 # These are private v4l2 control IDs, see:
847 # https://github.com/umlaeute/v4l2loopback/blob/fd822cf0faaccdf5f548cddd9a5a3dcebb6d584d/v4l2loopback.c#L131
848 KEEP_FORMAT = 0x8000000
849 SUSTAIN_FRAMERATE = 0x8000001
850 VIDIOC_S_CTRL = 0xC008561C
852 control = v4l2_control()
853 control.id = KEEP_FORMAT
854 control.value = 1
855 libc.ioctl(fd, VIDIOC_S_CTRL, ctypes.byref(control))
857 control.id = SUSTAIN_FRAMERATE
858 control.value = 1
859 libc.ioctl(fd, VIDIOC_S_CTRL, ctypes.byref(control))
860 libc.close(fd)
862 return True, six.ensure_text(vcap.card)
865 def findTestMediaDevices(log):
867 Find the test media devices configured on this system, and return a dict
868 containing information about them. The dict will have keys for 'audio'
869 and 'video', each containing the name of the media device to use.
871 If audio and video devices could not be found, return None.
873 This method is only currently implemented for Linux.
875 if not mozinfo.isLinux:
876 return None
878 info = {}
879 # Look for a v4l2loopback device.
880 name = None
881 device = None
882 for dev in sorted(glob.glob("/dev/video*")):
883 result, name_ = checkAndConfigureV4l2loopback(dev)
884 if result:
885 name = name_
886 device = dev
887 break
889 if not (name and device):
890 log.error("Couldn't find a v4l2loopback video device")
891 return None
893 # Feed it a frame of output so it has something to display
894 gst01 = spawn.find_executable("gst-launch-0.1")
895 gst010 = spawn.find_executable("gst-launch-0.10")
896 gst10 = spawn.find_executable("gst-launch-1.0")
897 if gst01:
898 gst = gst01
899 if gst010:
900 gst = gst010
901 else:
902 gst = gst10
903 process = subprocess.Popen(
905 gst,
906 "--no-fault",
907 "videotestsrc",
908 "pattern=green",
909 "num-buffers=1",
910 "!",
911 "v4l2sink",
912 "device=%s" % device,
915 info["video"] = {"name": name, "process": process}
917 # Hardcode the PulseAudio module-null-sink name since it's always the same.
918 info["audio"] = {"name": "Monitor of Null Output"}
919 return info
922 def create_zip(path):
924 Takes a `path` on disk and creates a zipfile with its contents. Returns a
925 path to the location of the temporary zip file.
927 with tempfile.NamedTemporaryFile() as f:
928 # `shutil.make_archive` writes to "{f.name}.zip", so we're really just
929 # using `NamedTemporaryFile` as a way to get a random path.
930 return shutil.make_archive(f.name, "zip", path)
933 def update_mozinfo():
934 """walk up directories to find mozinfo.json update the info"""
935 # TODO: This should go in a more generic place, e.g. mozinfo
937 path = SCRIPT_DIR
938 dirs = set()
939 while path != os.path.expanduser("~"):
940 if path in dirs:
941 break
942 dirs.add(path)
943 path = os.path.split(path)[0]
945 mozinfo.find_and_update_from_json(*dirs)
948 class MochitestDesktop(object):
950 Mochitest class for desktop firefox.
953 oldcwd = os.getcwd()
955 # Path to the test script on the server
956 TEST_PATH = "tests"
957 CHROME_PATH = "redirect.html"
959 certdbNew = False
960 sslTunnel = None
961 DEFAULT_TIMEOUT = 60.0
962 mediaDevices = None
963 mozinfo_variables_shown = False
965 patternFiles = {}
967 # XXX use automation.py for test name to avoid breaking legacy
968 # TODO: replace this with 'runtests.py' or 'mochitest' or the like
969 test_name = "automation.py"
971 def __init__(self, flavor, logger_options, staged_addons=None, quiet=False):
972 update_mozinfo()
973 self.flavor = flavor
974 self.staged_addons = staged_addons
975 self.server = None
976 self.wsserver = None
977 self.websocketProcessBridge = None
978 self.sslTunnel = None
979 self.manifest = None
980 self.tests_by_manifest = defaultdict(list)
981 self.args_by_manifest = defaultdict(set)
982 self.prefs_by_manifest = defaultdict(set)
983 self.env_vars_by_manifest = defaultdict(set)
984 self.tests_dirs_by_manifest = defaultdict(set)
985 self._active_tests = None
986 self.currentTests = None
987 self._locations = None
988 self.browserEnv = None
990 self.marionette = None
991 self.start_script = None
992 self.mozLogs = None
993 self.start_script_kwargs = {}
994 self.extraArgs = []
995 self.extraPrefs = {}
996 self.extraEnv = {}
997 self.extraTestsDirs = []
998 self.conditioned_profile_dir = None
1000 if logger_options.get("log"):
1001 self.log = logger_options["log"]
1002 else:
1003 self.log = commandline.setup_logging(
1004 "mochitest", logger_options, {"tbpl": sys.stdout}
1007 self.message_logger = MessageLogger(
1008 logger=self.log, buffering=quiet, structured=True
1011 # Max time in seconds to wait for server startup before tests will fail -- if
1012 # this seems big, it's mostly for debug machines where cold startup
1013 # (particularly after a build) takes forever.
1014 self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get("debug") else 90
1016 # metro browser sub process id
1017 self.browserProcessId = None
1019 self.haveDumpedScreen = False
1020 # Create variables to count the number of passes, fails, todos.
1021 self.countpass = 0
1022 self.countfail = 0
1023 self.counttodo = 0
1025 self.expectedError = {}
1026 self.result = {}
1028 self.start_script = os.path.join(here, "start_desktop.js")
1030 # Used to temporarily serve a performance profile
1031 self.profiler_tempdir = None
1033 def environment(self, **kwargs):
1034 kwargs["log"] = self.log
1035 return test_environment(**kwargs)
1037 def getFullPath(self, path):
1038 "Get an absolute path relative to self.oldcwd."
1039 return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
1041 def getLogFilePath(self, logFile):
1042 """return the log file path relative to the device we are testing on, in most cases
1043 it will be the full path on the local system
1045 return self.getFullPath(logFile)
1047 @property
1048 def locations(self):
1049 if self._locations is not None:
1050 return self._locations
1051 locations_file = os.path.join(SCRIPT_DIR, "server-locations.txt")
1052 self._locations = ServerLocations(locations_file)
1053 return self._locations
1055 def buildURLOptions(self, options, env):
1056 """Add test control options from the command line to the url
1058 URL parameters to test URL:
1060 autorun -- kick off tests automatically
1061 closeWhenDone -- closes the browser after the tests
1062 hideResultsTable -- hides the table of individual test results
1063 logFile -- logs test run to an absolute path
1064 startAt -- name of test to start at
1065 endAt -- name of test to end at
1066 timeout -- per-test timeout in seconds
1067 repeat -- How many times to repeat the test, ie: repeat=1 will run the test twice.
1069 self.urlOpts = []
1071 if not hasattr(options, "logFile"):
1072 options.logFile = ""
1073 if not hasattr(options, "fileLevel"):
1074 options.fileLevel = "INFO"
1076 # allow relative paths for logFile
1077 if options.logFile:
1078 options.logFile = self.getLogFilePath(options.logFile)
1080 if options.flavor in ("a11y", "browser", "chrome"):
1081 self.makeTestConfig(options)
1082 else:
1083 if options.autorun:
1084 self.urlOpts.append("autorun=1")
1085 if options.timeout:
1086 self.urlOpts.append("timeout=%d" % options.timeout)
1087 if options.maxTimeouts:
1088 self.urlOpts.append("maxTimeouts=%d" % options.maxTimeouts)
1089 if not options.keep_open:
1090 self.urlOpts.append("closeWhenDone=1")
1091 if options.logFile:
1092 self.urlOpts.append("logFile=" + encodeURIComponent(options.logFile))
1093 self.urlOpts.append(
1094 "fileLevel=" + encodeURIComponent(options.fileLevel)
1096 if options.consoleLevel:
1097 self.urlOpts.append(
1098 "consoleLevel=" + encodeURIComponent(options.consoleLevel)
1100 if options.startAt:
1101 self.urlOpts.append("startAt=%s" % options.startAt)
1102 if options.endAt:
1103 self.urlOpts.append("endAt=%s" % options.endAt)
1104 if options.shuffle:
1105 self.urlOpts.append("shuffle=1")
1106 if "MOZ_HIDE_RESULTS_TABLE" in env and env["MOZ_HIDE_RESULTS_TABLE"] == "1":
1107 self.urlOpts.append("hideResultsTable=1")
1108 if options.runUntilFailure:
1109 self.urlOpts.append("runUntilFailure=1")
1110 if options.repeat:
1111 self.urlOpts.append("repeat=%d" % options.repeat)
1112 if len(options.test_paths) == 1 and os.path.isfile(
1113 os.path.join(
1114 self.oldcwd,
1115 os.path.dirname(__file__),
1116 self.TEST_PATH,
1117 options.test_paths[0],
1120 self.urlOpts.append(
1121 "testname=%s" % "/".join([self.TEST_PATH, options.test_paths[0]])
1123 if options.manifestFile:
1124 self.urlOpts.append("manifestFile=%s" % options.manifestFile)
1125 if options.failureFile:
1126 self.urlOpts.append(
1127 "failureFile=%s" % self.getFullPath(options.failureFile)
1129 if options.runSlower:
1130 self.urlOpts.append("runSlower=true")
1131 if options.debugOnFailure:
1132 self.urlOpts.append("debugOnFailure=true")
1133 if options.dumpOutputDirectory:
1134 self.urlOpts.append(
1135 "dumpOutputDirectory=%s"
1136 % encodeURIComponent(options.dumpOutputDirectory)
1138 if options.dumpAboutMemoryAfterTest:
1139 self.urlOpts.append("dumpAboutMemoryAfterTest=true")
1140 if options.dumpDMDAfterTest:
1141 self.urlOpts.append("dumpDMDAfterTest=true")
1142 if options.debugger or options.jsdebugger:
1143 self.urlOpts.append("interactiveDebugger=true")
1144 if options.jscov_dir_prefix:
1145 self.urlOpts.append("jscovDirPrefix=%s" % options.jscov_dir_prefix)
1146 if options.cleanupCrashes:
1147 self.urlOpts.append("cleanupCrashes=true")
1148 if "MOZ_XORIGIN_MOCHITEST" in env and env["MOZ_XORIGIN_MOCHITEST"] == "1":
1149 options.xOriginTests = True
1150 if options.xOriginTests:
1151 self.urlOpts.append("xOriginTests=true")
1152 if options.comparePrefs:
1153 self.urlOpts.append("comparePrefs=true")
1154 self.urlOpts.append("ignorePrefsFile=ignorePrefs.json")
1156 def normflavor(self, flavor):
1158 In some places the string 'browser-chrome' is expected instead of
1159 'browser' and 'mochitest' instead of 'plain'. Normalize the flavor
1160 strings for those instances.
1162 # TODO Use consistent flavor strings everywhere and remove this
1163 if flavor == "browser":
1164 return "browser-chrome"
1165 elif flavor == "plain":
1166 return "mochitest"
1167 return flavor
1169 # This check can be removed when bug 983867 is fixed.
1170 def isTest(self, options, filename):
1171 allow_js_css = False
1172 if options.flavor == "browser":
1173 allow_js_css = True
1174 testPattern = re.compile(r"browser_.+\.js")
1175 elif options.flavor in ("a11y", "chrome"):
1176 testPattern = re.compile(r"(browser|test)_.+\.(xul|html|js|xhtml)")
1177 else:
1178 testPattern = re.compile(r"test_")
1180 if not allow_js_css and (".js" in filename or ".css" in filename):
1181 return False
1183 pathPieces = filename.split("/")
1185 return testPattern.match(pathPieces[-1]) and not re.search(
1186 r"\^headers\^$", filename
1189 def setTestRoot(self, options):
1190 if options.flavor != "plain":
1191 self.testRoot = options.flavor
1192 else:
1193 self.testRoot = self.TEST_PATH
1194 self.testRootAbs = os.path.join(SCRIPT_DIR, self.testRoot)
1196 def buildTestURL(self, options, scheme="http"):
1197 if scheme == "https":
1198 testHost = "https://example.com:443"
1199 elif options.xOriginTests:
1200 testHost = "http://mochi.xorigin-test:8888"
1201 else:
1202 testHost = "http://mochi.test:8888"
1203 testURL = "/".join([testHost, self.TEST_PATH])
1205 if len(options.test_paths) == 1:
1206 if os.path.isfile(
1207 os.path.join(
1208 self.oldcwd,
1209 os.path.dirname(__file__),
1210 self.TEST_PATH,
1211 options.test_paths[0],
1214 testURL = "/".join([testURL, os.path.dirname(options.test_paths[0])])
1215 else:
1216 testURL = "/".join([testURL, options.test_paths[0]])
1218 if options.flavor in ("a11y", "chrome"):
1219 testURL = "/".join([testHost, self.CHROME_PATH])
1220 elif options.flavor == "browser":
1221 testURL = "about:blank"
1222 return testURL
1224 def parseAndCreateTestsDirs(self, m):
1225 testsDirs = list(self.tests_dirs_by_manifest[m])[0]
1226 self.extraTestsDirs = []
1227 if testsDirs:
1228 self.extraTestsDirs = testsDirs.strip().split()
1229 self.log.info(
1230 "The following extra test directories will be created:\n {}".format(
1231 "\n ".join(self.extraTestsDirs)
1234 self.createExtraTestsDirs(self.extraTestsDirs, m)
1236 def createExtraTestsDirs(self, extraTestsDirs=None, manifest=None):
1237 """Take a list of directories that might be needed to exist by the test
1238 prior to even the main process be executed, and:
1239 - verify it does not already exists
1240 - create it if it does
1241 Removal of those directories is handled in cleanup()
1243 if type(extraTestsDirs) != list:
1244 return
1246 for d in extraTestsDirs:
1247 if os.path.exists(d):
1248 raise FileExistsError(
1249 "Directory '{}' already exists. This is a member of "
1250 "test-directories in manifest {}.".format(d, manifest)
1253 created = []
1254 for d in extraTestsDirs:
1255 os.makedirs(d)
1256 created += [d]
1258 if created != extraTestsDirs:
1259 raise EnvironmentError(
1260 "Not all directories were created: extraTestsDirs={} -- created={}".format(
1261 extraTestsDirs, created
1265 def getTestsByScheme(
1266 self, options, testsToFilter=None, disabled=True, manifestToFilter=None
1268 """Build the url path to the specific test harness and test file or directory
1269 Build a manifest of tests to run and write out a json file for the harness to read
1270 testsToFilter option is used to filter/keep the tests provided in the list
1272 disabled -- This allows to add all disabled tests on the build side
1273 and then on the run side to only run the enabled ones
1276 tests = self.getActiveTests(options, disabled)
1277 paths = []
1278 for test in tests:
1279 if testsToFilter and (test["path"] not in testsToFilter):
1280 continue
1281 # If we are running a specific manifest, the previously computed set of active
1282 # tests should be filtered out based on the manifest that contains that entry.
1284 # This is especially important when a test file is listed in multiple
1285 # manifests (e.g. because the same test runs under a different configuration,
1286 # and so it is being included in multiple manifests), without filtering the
1287 # active tests based on the current manifest (configuration) that we are
1288 # running for each of the N manifests we would be executing the active tests
1289 # exactly N times (and so NxN runs instead of the expected N runs, one for each
1290 # manifest).
1291 if manifestToFilter and (test["manifest"] not in manifestToFilter):
1292 continue
1293 paths.append(test)
1295 # Generate test by schemes
1296 for scheme, grouped_tests in self.groupTestsByScheme(paths).items():
1297 # Bug 883865 - add this functionality into manifestparser
1298 with open(
1299 os.path.join(SCRIPT_DIR, options.testRunManifestFile), "w"
1300 ) as manifestFile:
1301 manifestFile.write(json.dumps({"tests": grouped_tests}))
1302 options.manifestFile = options.testRunManifestFile
1303 yield (scheme, grouped_tests)
1305 def startWebSocketServer(self, options, debuggerInfo):
1306 """Launch the websocket server"""
1307 self.wsserver = WebSocketServer(options, SCRIPT_DIR, self.log, debuggerInfo)
1308 self.wsserver.start()
1310 def startWebServer(self, options):
1311 """Create the webserver and start it up"""
1313 self.server = MochitestServer(options, self.log)
1314 self.server.start()
1316 if options.pidFile != "":
1317 with open(options.pidFile + ".xpcshell.pid", "w") as f:
1318 f.write("%s" % self.server._process.pid)
1320 def startWebsocketProcessBridge(self, options):
1321 """Create a websocket server that can launch various processes that
1322 JS needs (eg; ICE server for webrtc testing)
1325 command = [
1326 sys.executable,
1327 os.path.join("websocketprocessbridge", "websocketprocessbridge.py"),
1328 "--port",
1329 options.websocket_process_bridge_port,
1331 self.websocketProcessBridge = subprocess.Popen(command, cwd=SCRIPT_DIR)
1332 self.log.info(
1333 "runtests.py | websocket/process bridge pid: %d"
1334 % self.websocketProcessBridge.pid
1337 # ensure the server is up, wait for at most ten seconds
1338 for i in range(1, 100):
1339 if self.websocketProcessBridge.poll() is not None:
1340 self.log.error(
1341 "runtests.py | websocket/process bridge failed "
1342 "to launch. Are all the dependencies installed?"
1344 return
1346 try:
1347 sock = socket.create_connection(("127.0.0.1", 8191))
1348 sock.close()
1349 break
1350 except Exception:
1351 time.sleep(0.1)
1352 else:
1353 self.log.error(
1354 "runtests.py | Timed out while waiting for "
1355 "websocket/process bridge startup."
1358 def needsWebsocketProcessBridge(self, options):
1360 Returns a bool indicating if the current test configuration needs
1361 to start the websocket process bridge or not. The boils down to if
1362 WebRTC tests that need the bridge are present.
1364 tests = self.getActiveTests(options)
1365 is_webrtc_tag_present = False
1366 for test in tests:
1367 if "webrtc" in test.get("tags", ""):
1368 is_webrtc_tag_present = True
1369 break
1370 return is_webrtc_tag_present and options.subsuite in ["media"]
1372 def startHttp3Server(self, options):
1374 Start a Http3 test server.
1376 http3ServerPath = os.path.join(
1377 options.utilityPath, "http3server" + mozinfo.info["bin_suffix"]
1379 serverOptions = {}
1380 serverOptions["http3ServerPath"] = http3ServerPath
1381 serverOptions["profilePath"] = options.profilePath
1382 serverOptions["isMochitest"] = True
1383 serverOptions["isWin"] = mozinfo.isWin
1384 serverOptions["proxyPort"] = options.http3ServerPort
1385 env = test_environment(xrePath=options.xrePath, log=self.log)
1386 serverEnv = env.copy()
1387 serverLog = env.get("MOZHTTP3_SERVER_LOG")
1388 if serverLog is not None:
1389 serverEnv["RUST_LOG"] = serverLog
1390 self.http3Server = Http3Server(serverOptions, serverEnv, self.log)
1391 self.http3Server.start()
1393 port = self.http3Server.ports().get("MOZHTTP3_PORT_PROXY")
1394 if int(port) != options.http3ServerPort:
1395 self.http3Server = None
1396 raise RuntimeError("Error: Unable to start Http/3 server")
1398 def findNodeBin(self):
1399 # We try to find the node executable in the path given to us by the user in
1400 # the MOZ_NODE_PATH environment variable
1401 nodeBin = os.getenv("MOZ_NODE_PATH", None)
1402 self.log.info("Use MOZ_NODE_PATH at %s" % (nodeBin))
1403 if not nodeBin and build:
1404 nodeBin = build.substs.get("NODEJS")
1405 self.log.info("Use build node at %s" % (nodeBin))
1406 return nodeBin
1408 def startHttp2Server(self, options):
1410 Start a Http2 test server.
1412 serverOptions = {}
1413 serverOptions["serverPath"] = os.path.join(
1414 SCRIPT_DIR, "Http2Server", "http2_server.js"
1416 serverOptions["nodeBin"] = self.findNodeBin()
1417 serverOptions["isWin"] = mozinfo.isWin
1418 serverOptions["port"] = options.http2ServerPort
1419 env = test_environment(xrePath=options.xrePath, log=self.log)
1420 self.http2Server = Http2Server(serverOptions, env, self.log)
1421 self.http2Server.start()
1423 port = self.http2Server.port()
1424 if port != options.http2ServerPort:
1425 raise RuntimeError("Error: Unable to start Http2 server")
1427 def startDoHServer(self, options, dstServerPort, alpn):
1428 serverOptions = {}
1429 serverOptions["serverPath"] = os.path.join(
1430 SCRIPT_DIR, "DoHServer", "doh_server.js"
1432 serverOptions["nodeBin"] = self.findNodeBin()
1433 serverOptions["dstServerPort"] = dstServerPort
1434 serverOptions["isWin"] = mozinfo.isWin
1435 serverOptions["port"] = options.dohServerPort
1436 serverOptions["alpn"] = alpn
1437 env = test_environment(xrePath=options.xrePath, log=self.log)
1438 self.dohServer = DoHServer(serverOptions, env, self.log)
1439 self.dohServer.start()
1441 port = self.dohServer.port()
1442 if port != options.dohServerPort:
1443 raise RuntimeError("Error: Unable to start DoH server")
1445 def startServers(self, options, debuggerInfo, public=None):
1446 # start servers and set ports
1447 # TODO: pass these values, don't set on `self`
1448 self.webServer = options.webServer
1449 self.httpPort = options.httpPort
1450 self.sslPort = options.sslPort
1451 self.webSocketPort = options.webSocketPort
1453 # httpd-path is specified by standard makefile targets and may be specified
1454 # on the command line to select a particular version of httpd.js. If not
1455 # specified, try to select the one from hostutils.zip, as required in
1456 # bug 882932.
1457 if not options.httpdPath:
1458 options.httpdPath = os.path.join(options.utilityPath, "components")
1460 self.startWebServer(options)
1461 self.startWebSocketServer(options, debuggerInfo)
1463 # Only webrtc mochitests in the media suite need the websocketprocessbridge.
1464 if self.needsWebsocketProcessBridge(options):
1465 self.startWebsocketProcessBridge(options)
1467 # start SSL pipe
1468 self.sslTunnel = SSLTunnel(options, logger=self.log)
1469 self.sslTunnel.buildConfig(self.locations, public=public)
1470 self.sslTunnel.start()
1472 # If we're lucky, the server has fully started by now, and all paths are
1473 # ready, etc. However, xpcshell cold start times suck, at least for debug
1474 # builds. We'll try to connect to the server for awhile, and if we fail,
1475 # we'll try to kill the server and exit with an error.
1476 if self.server is not None:
1477 self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
1479 self.log.info("use http3 server: %d" % options.useHttp3Server)
1480 self.http3Server = None
1481 self.http2Server = None
1482 self.dohServer = None
1483 if options.useHttp3Server:
1484 self.startHttp3Server(options)
1485 self.startDoHServer(options, options.http3ServerPort, "h3")
1486 elif options.useHttp2Server:
1487 self.startHttp2Server(options)
1488 self.startDoHServer(options, options.http2ServerPort, "h2")
1490 def stopServers(self):
1491 """Servers are no longer needed, and perhaps more importantly, anything they
1492 might spew to console might confuse things."""
1493 if self.server is not None:
1494 try:
1495 self.log.info("Stopping web server")
1496 self.server.stop()
1497 except Exception:
1498 self.log.critical("Exception when stopping web server")
1500 if self.wsserver is not None:
1501 try:
1502 self.log.info("Stopping web socket server")
1503 self.wsserver.stop()
1504 except Exception:
1505 self.log.critical("Exception when stopping web socket server")
1507 if self.sslTunnel is not None:
1508 try:
1509 self.log.info("Stopping ssltunnel")
1510 self.sslTunnel.stop()
1511 except Exception:
1512 self.log.critical("Exception stopping ssltunnel")
1514 if self.websocketProcessBridge is not None:
1515 try:
1516 self.websocketProcessBridge.kill()
1517 self.websocketProcessBridge.wait()
1518 self.log.info("Stopping websocket/process bridge")
1519 except Exception:
1520 self.log.critical("Exception stopping websocket/process bridge")
1521 if self.http3Server is not None:
1522 try:
1523 self.http3Server.stop()
1524 except Exception:
1525 self.log.critical("Exception stopping http3 server")
1526 if self.http2Server is not None:
1527 try:
1528 self.http2Server.stop()
1529 except Exception:
1530 self.log.critical("Exception stopping http2 server")
1531 if self.dohServer is not None:
1532 try:
1533 self.dohServer.stop()
1534 except Exception:
1535 self.log.critical("Exception stopping doh server")
1537 if hasattr(self, "gstForV4l2loopbackProcess"):
1538 try:
1539 self.gstForV4l2loopbackProcess.kill()
1540 self.gstForV4l2loopbackProcess.wait()
1541 self.log.info("Stopping gst for v4l2loopback")
1542 except Exception:
1543 self.log.critical("Exception stopping gst for v4l2loopback")
1545 def copyExtraFilesToProfile(self, options):
1546 "Copy extra files or dirs specified on the command line to the testing profile."
1547 for f in options.extraProfileFiles:
1548 abspath = self.getFullPath(f)
1549 if os.path.isfile(abspath):
1550 shutil.copy2(abspath, options.profilePath)
1551 elif os.path.isdir(abspath):
1552 dest = os.path.join(options.profilePath, os.path.basename(abspath))
1553 shutil.copytree(abspath, dest)
1554 else:
1555 self.log.warning("runtests.py | Failed to copy %s to profile" % abspath)
1557 def getChromeTestDir(self, options):
1558 dir = os.path.join(os.path.abspath("."), SCRIPT_DIR) + "/"
1559 if mozinfo.isWin:
1560 dir = "file:///" + dir.replace("\\", "/")
1561 return dir
1563 def writeChromeManifest(self, options):
1564 manifest = os.path.join(options.profilePath, "tests.manifest")
1565 with open(manifest, "w") as manifestFile:
1566 # Register chrome directory.
1567 chrometestDir = self.getChromeTestDir(options)
1568 manifestFile.write(
1569 "content mochitests %s contentaccessible=yes\n" % chrometestDir
1571 manifestFile.write(
1572 "content mochitests-any %s contentaccessible=yes remoteenabled=yes\n"
1573 % chrometestDir
1575 manifestFile.write(
1576 "content mochitests-content %s contentaccessible=yes remoterequired=yes\n"
1577 % chrometestDir
1580 if options.testingModulesDir is not None:
1581 manifestFile.write(
1582 "resource testing-common file:///%s\n" % options.testingModulesDir
1584 if options.store_chrome_manifest:
1585 shutil.copyfile(manifest, options.store_chrome_manifest)
1586 return manifest
1588 def addChromeToProfile(self, options):
1589 "Adds MochiKit chrome tests to the profile."
1591 # Create (empty) chrome directory.
1592 chromedir = os.path.join(options.profilePath, "chrome")
1593 os.mkdir(chromedir)
1595 # Write userChrome.css.
1596 chrome = """
1597 /* set default namespace to XUL */
1598 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
1599 toolbar,
1600 toolbarpalette {
1601 background-color: rgb(235, 235, 235) !important;
1603 toolbar#nav-bar {
1604 background-image: none !important;
1607 with open(
1608 os.path.join(options.profilePath, "userChrome.css"), "a"
1609 ) as chromeFile:
1610 chromeFile.write(chrome)
1612 manifest = self.writeChromeManifest(options)
1614 return manifest
1616 def getExtensionsToInstall(self, options):
1617 "Return a list of extensions to install in the profile"
1618 extensions = []
1619 appDir = (
1620 options.app[: options.app.rfind(os.sep)]
1621 if options.app
1622 else options.utilityPath
1625 extensionDirs = [
1626 # Extensions distributed with the test harness.
1627 os.path.normpath(os.path.join(SCRIPT_DIR, "extensions")),
1629 if appDir:
1630 # Extensions distributed with the application.
1631 extensionDirs.append(os.path.join(appDir, "distribution", "extensions"))
1633 for extensionDir in extensionDirs:
1634 if os.path.isdir(extensionDir):
1635 for dirEntry in os.listdir(extensionDir):
1636 if dirEntry not in options.extensionsToExclude:
1637 path = os.path.join(extensionDir, dirEntry)
1638 if os.path.isdir(path) or (
1639 os.path.isfile(path) and path.endswith(".xpi")
1641 extensions.append(path)
1642 extensions.extend(options.extensionsToInstall)
1643 return extensions
1645 def logPreamble(self, tests):
1646 """Logs a suite_start message and test_start/test_end at the beginning of a run."""
1647 self.log.suite_start(
1648 self.tests_by_manifest, name="mochitest-{}".format(self.flavor)
1650 for test in tests:
1651 if "disabled" in test:
1652 self.log.test_start(test["path"])
1653 self.log.test_end(test["path"], "SKIP", message=test["disabled"])
1655 def loadFailurePatternFile(self, pat_file):
1656 if pat_file in self.patternFiles:
1657 return self.patternFiles[pat_file]
1658 if not os.path.isfile(pat_file):
1659 self.log.warning(
1660 "runtests.py | Cannot find failure pattern file " + pat_file
1662 return None
1664 # Using ":error" to ensure it shows up in the failure summary.
1665 self.log.warning(
1666 "[runtests.py:error] Using {} to filter failures. If there "
1667 "is any number mismatch below, you could have fixed "
1668 "something documented in that file. Please reduce the "
1669 "failure count appropriately.".format(pat_file)
1671 patternRE = re.compile(
1672 r"""
1673 ^\s*\*\s* # list bullet
1674 (test_\S+|\.{3}) # test name
1675 (?:\s*(`.+?`|asserts))? # failure pattern
1676 (?::.+)? # optional description
1677 \s*\[(\d+|\*)\] # expected count
1678 \s*$
1679 """,
1680 re.X,
1682 patterns = {}
1683 with open(pat_file) as f:
1684 last_name = None
1685 for line in f:
1686 match = patternRE.match(line)
1687 if not match:
1688 continue
1689 name = match.group(1)
1690 name = last_name if name == "..." else name
1691 last_name = name
1692 pat = match.group(2)
1693 if pat is not None:
1694 pat = "ASSERTION" if pat == "asserts" else pat[1:-1]
1695 count = match.group(3)
1696 count = None if count == "*" else int(count)
1697 if name not in patterns:
1698 patterns[name] = []
1699 patterns[name].append((pat, count))
1700 self.patternFiles[pat_file] = patterns
1701 return patterns
1703 def getFailurePatterns(self, pat_file, test_name):
1704 patterns = self.loadFailurePatternFile(pat_file)
1705 if patterns:
1706 return patterns.get(test_name, None)
1708 def getActiveTests(self, options, disabled=True):
1710 This method is used to parse the manifest and return active filtered tests.
1712 if self._active_tests:
1713 return self._active_tests
1715 tests = []
1716 manifest = self.getTestManifest(options)
1717 if manifest:
1718 if options.extra_mozinfo_json:
1719 mozinfo.update(options.extra_mozinfo_json)
1721 info = mozinfo.info
1723 filters = [
1724 subsuite(options.subsuite),
1727 # Allow for only running tests/manifests which match this tag
1728 if options.conditionedProfile:
1729 if not options.test_tags:
1730 options.test_tags = []
1731 options.test_tags.append("condprof")
1733 if options.test_tags:
1734 filters.append(tags(options.test_tags))
1736 if options.test_paths:
1737 options.test_paths = self.normalize_paths(options.test_paths)
1738 filters.append(pathprefix(options.test_paths))
1740 # Add chunking filters if specified
1741 if options.totalChunks:
1742 if options.chunkByDir:
1743 filters.append(
1744 chunk_by_dir(
1745 options.thisChunk, options.totalChunks, options.chunkByDir
1748 elif options.chunkByRuntime:
1749 if mozinfo.info["os"] == "android":
1750 platkey = "android"
1751 elif mozinfo.isWin:
1752 platkey = "windows"
1753 else:
1754 platkey = "unix"
1756 runtime_file = os.path.join(
1757 SCRIPT_DIR,
1758 "runtimes",
1759 "manifest-runtimes-{}.json".format(platkey),
1761 if not os.path.exists(runtime_file):
1762 self.log.error("runtime file %s not found!" % runtime_file)
1763 sys.exit(1)
1765 # Given the mochitest flavor, load the runtimes information
1766 # for only that flavor due to manifest runtime format change in Bug 1637463.
1767 with open(runtime_file, "r") as f:
1768 if "suite_name" in options:
1769 runtimes = json.load(f).get(options.suite_name, {})
1770 else:
1771 runtimes = {}
1773 filters.append(
1774 chunk_by_runtime(
1775 options.thisChunk, options.totalChunks, runtimes
1778 else:
1779 filters.append(
1780 chunk_by_slice(options.thisChunk, options.totalChunks)
1783 noDefaultFilters = False
1784 if options.runFailures:
1785 filters.append(failures(options.runFailures))
1786 noDefaultFilters = True
1788 tests = manifest.active_tests(
1789 exists=False,
1790 disabled=disabled,
1791 filters=filters,
1792 noDefaultFilters=noDefaultFilters,
1793 **info
1796 if len(tests) == 0:
1797 self.log.error(
1798 NO_TESTS_FOUND.format(options.flavor, manifest.fmt_filters())
1801 paths = []
1802 for test in tests:
1803 if len(tests) == 1 and "disabled" in test:
1804 del test["disabled"]
1806 pathAbs = os.path.abspath(test["path"])
1807 assert os.path.normcase(pathAbs).startswith(
1808 os.path.normcase(self.testRootAbs)
1810 tp = pathAbs[len(self.testRootAbs) :].replace("\\", "/").strip("/")
1812 if not self.isTest(options, tp):
1813 self.log.warning(
1814 "Warning: %s from manifest %s is not a valid test"
1815 % (test["name"], test["manifest"])
1817 continue
1819 manifest_key = test["manifest_relpath"]
1820 # Ignore ancestor_manifests that live at the root (e.g, don't have a
1821 # path separator).
1822 if "ancestor_manifest" in test and "/" in normsep(
1823 test["ancestor_manifest"]
1825 manifest_key = "{}:{}".format(test["ancestor_manifest"], manifest_key)
1827 manifest_key = manifest_key.replace("\\", "/")
1828 self.tests_by_manifest[manifest_key].append(tp)
1829 self.args_by_manifest[manifest_key].add(test.get("args"))
1830 self.prefs_by_manifest[manifest_key].add(test.get("prefs"))
1831 self.env_vars_by_manifest[manifest_key].add(test.get("environment"))
1832 self.tests_dirs_by_manifest[manifest_key].add(test.get("test-directories"))
1834 for key in ["args", "prefs", "environment", "test-directories"]:
1835 if key in test and not options.runByManifest and "disabled" not in test:
1836 self.log.error(
1837 "parsing {}: runByManifest mode must be enabled to "
1838 "set the `{}` key".format(test["manifest_relpath"], key)
1840 sys.exit(1)
1842 testob = {"path": tp, "manifest": manifest_key}
1843 if "disabled" in test:
1844 testob["disabled"] = test["disabled"]
1845 if "expected" in test:
1846 testob["expected"] = test["expected"]
1847 if "https_first_disabled" in test:
1848 testob["https_first_disabled"] = test["https_first_disabled"] == "true"
1849 if "allow_xul_xbl" in test:
1850 testob["allow_xul_xbl"] = test["allow_xul_xbl"] == "true"
1851 if "scheme" in test:
1852 testob["scheme"] = test["scheme"]
1853 if "tags" in test:
1854 testob["tags"] = test["tags"]
1855 if options.failure_pattern_file:
1856 pat_file = os.path.join(
1857 os.path.dirname(test["manifest"]), options.failure_pattern_file
1859 patterns = self.getFailurePatterns(pat_file, test["name"])
1860 if patterns:
1861 testob["expected"] = patterns
1862 paths.append(testob)
1864 # The 'args' key needs to be set in the DEFAULT section, unfortunately
1865 # we can't tell what comes from DEFAULT or not. So to validate this, we
1866 # stash all args from tests in the same manifest into a set. If the
1867 # length of the set > 1, then we know 'args' didn't come from DEFAULT.
1868 args_not_default = [
1869 m for m, p in six.iteritems(self.args_by_manifest) if len(p) > 1
1871 if args_not_default:
1872 self.log.error(
1873 "The 'args' key must be set in the DEFAULT section of a "
1874 "manifest. Fix the following manifests: {}".format(
1875 "\n".join(args_not_default)
1878 sys.exit(1)
1880 # The 'prefs' key needs to be set in the DEFAULT section too.
1881 pref_not_default = [
1882 m for m, p in six.iteritems(self.prefs_by_manifest) if len(p) > 1
1884 if pref_not_default:
1885 self.log.error(
1886 "The 'prefs' key must be set in the DEFAULT section of a "
1887 "manifest. Fix the following manifests: {}".format(
1888 "\n".join(pref_not_default)
1891 sys.exit(1)
1892 # The 'environment' key needs to be set in the DEFAULT section too.
1893 env_not_default = [
1894 m for m, p in six.iteritems(self.env_vars_by_manifest) if len(p) > 1
1896 if env_not_default:
1897 self.log.error(
1898 "The 'environment' key must be set in the DEFAULT section of a "
1899 "manifest. Fix the following manifests: {}".format(
1900 "\n".join(env_not_default)
1903 sys.exit(1)
1905 paths.sort(key=lambda p: p["path"].split("/"))
1906 if options.dump_tests:
1907 options.dump_tests = os.path.expanduser(options.dump_tests)
1908 assert os.path.exists(os.path.dirname(options.dump_tests))
1909 with open(options.dump_tests, "w") as dumpFile:
1910 dumpFile.write(json.dumps({"active_tests": paths}))
1912 self.log.info("Dumping active_tests to %s file." % options.dump_tests)
1913 sys.exit()
1915 # Upload a list of test manifests that were executed in this run.
1916 if "MOZ_UPLOAD_DIR" in os.environ:
1917 artifact = os.path.join(os.environ["MOZ_UPLOAD_DIR"], "manifests.list")
1918 with open(artifact, "a") as fh:
1919 fh.write("\n".join(sorted(self.tests_by_manifest.keys())))
1921 self._active_tests = paths
1922 return self._active_tests
1924 def getTestManifest(self, options):
1925 if isinstance(options.manifestFile, TestManifest):
1926 manifest = options.manifestFile
1927 elif options.manifestFile and os.path.isfile(options.manifestFile):
1928 manifestFileAbs = os.path.abspath(options.manifestFile)
1929 assert manifestFileAbs.startswith(SCRIPT_DIR)
1930 manifest = TestManifest([options.manifestFile], strict=False)
1931 elif options.manifestFile and os.path.isfile(
1932 os.path.join(SCRIPT_DIR, options.manifestFile)
1934 manifestFileAbs = os.path.abspath(
1935 os.path.join(SCRIPT_DIR, options.manifestFile)
1937 assert manifestFileAbs.startswith(SCRIPT_DIR)
1938 manifest = TestManifest([manifestFileAbs], strict=False)
1939 else:
1940 masterName = self.normflavor(options.flavor) + ".toml"
1941 masterPath = os.path.join(SCRIPT_DIR, self.testRoot, masterName)
1943 if not os.path.exists(masterPath):
1944 masterName = self.normflavor(options.flavor) + ".ini"
1945 masterPath = os.path.join(SCRIPT_DIR, self.testRoot, masterName)
1947 if os.path.exists(masterPath):
1948 manifest = TestManifest([masterPath], strict=False)
1949 else:
1950 manifest = None
1951 self.log.warning(
1952 "TestManifest masterPath %s does not exist" % masterPath
1955 return manifest
1957 def makeTestConfig(self, options):
1958 "Creates a test configuration file for customizing test execution."
1959 options.logFile = options.logFile.replace("\\", "\\\\")
1961 if (
1962 "MOZ_HIDE_RESULTS_TABLE" in os.environ
1963 and os.environ["MOZ_HIDE_RESULTS_TABLE"] == "1"
1965 options.hideResultsTable = True
1967 # strip certain unnecessary items to avoid serialization errors in json.dumps()
1968 d = dict(
1969 (k, v)
1970 for k, v in options.__dict__.items()
1971 if (v is None) or isinstance(v, (six.string_types, numbers.Number))
1973 d["testRoot"] = self.testRoot
1974 if options.jscov_dir_prefix:
1975 d["jscovDirPrefix"] = options.jscov_dir_prefix
1976 if not options.keep_open:
1977 d["closeWhenDone"] = "1"
1979 d["runFailures"] = False
1980 if options.runFailures:
1981 d["runFailures"] = True
1983 shutil.copy(
1984 os.path.join(SCRIPT_DIR, "ignorePrefs.json"),
1985 os.path.join(options.profilePath, "ignorePrefs.json"),
1987 d["ignorePrefsFile"] = "ignorePrefs.json"
1988 content = json.dumps(d)
1990 with open(os.path.join(options.profilePath, "testConfig.js"), "w") as config:
1991 config.write(content)
1993 def buildBrowserEnv(self, options, debugger=False, env=None):
1994 """build the environment variables for the specific test and operating system"""
1995 if mozinfo.info["asan"] and mozinfo.isLinux and mozinfo.bits == 64:
1996 useLSan = True
1997 else:
1998 useLSan = False
2000 browserEnv = self.environment(
2001 xrePath=options.xrePath, env=env, debugger=debugger, useLSan=useLSan
2004 if options.headless:
2005 browserEnv["MOZ_HEADLESS"] = "1"
2007 if not options.e10s:
2008 browserEnv["MOZ_FORCE_DISABLE_E10S"] = "1"
2010 if options.dmd:
2011 browserEnv["DMD"] = os.environ.get("DMD", "1")
2013 # bug 1443327: do not set MOZ_CRASHREPORTER_SHUTDOWN during browser-chrome
2014 # tests, since some browser-chrome tests test content process crashes;
2015 # also exclude non-e10s since at least one non-e10s mochitest is problematic
2016 if (
2017 options.flavor == "browser" or not options.e10s
2018 ) and "MOZ_CRASHREPORTER_SHUTDOWN" in browserEnv:
2019 del browserEnv["MOZ_CRASHREPORTER_SHUTDOWN"]
2021 try:
2022 browserEnv.update(
2023 dict(
2024 parse_key_value(
2025 self.extraEnv, context="environment variable in manifest"
2029 except KeyValueParseError as e:
2030 self.log.error(str(e))
2031 return None
2033 # These variables are necessary for correct application startup; change
2034 # via the commandline at your own risk.
2035 browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
2037 # interpolate environment passed with options
2038 try:
2039 browserEnv.update(
2040 dict(parse_key_value(options.environment, context="--setenv"))
2042 except KeyValueParseError as e:
2043 self.log.error(str(e))
2044 return None
2046 if (
2047 "MOZ_PROFILER_STARTUP_FEATURES" not in browserEnv
2048 or "nativeallocations"
2049 not in browserEnv["MOZ_PROFILER_STARTUP_FEATURES"].split(",")
2051 # Only turn on the bloat log if the profiler's native allocation feature is
2052 # not enabled. The two are not compatible.
2053 browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
2055 # If profiling options are enabled, turn on the gecko profiler by using the
2056 # profiler environmental variables.
2057 if options.profiler:
2058 # The user wants to capture a profile, and automatically view it. The
2059 # profile will be saved to a temporary folder, then deleted after
2060 # opening in profiler.firefox.com.
2061 self.profiler_tempdir = tempfile.mkdtemp()
2062 browserEnv["MOZ_PROFILER_SHUTDOWN"] = os.path.join(
2063 self.profiler_tempdir, "mochitest-profile.json"
2065 browserEnv["MOZ_PROFILER_STARTUP"] = "1"
2067 if options.profilerSaveOnly:
2068 # The user wants to capture a profile, but only to save it. This defaults
2069 # to the MOZ_UPLOAD_DIR.
2070 browserEnv["MOZ_PROFILER_STARTUP"] = "1"
2071 if "MOZ_UPLOAD_DIR" in browserEnv:
2072 browserEnv["MOZ_PROFILER_SHUTDOWN"] = os.path.join(
2073 browserEnv["MOZ_UPLOAD_DIR"], "mochitest-profile.json"
2075 else:
2076 self.log.error(
2077 "--profiler-save-only was specified, but no MOZ_UPLOAD_DIR "
2078 "environment variable was provided. Please set this "
2079 "environment variable to a directory path in order to save "
2080 "a performance profile."
2082 return None
2084 try:
2085 gmp_path = self.getGMPPluginPath(options)
2086 if gmp_path is not None:
2087 browserEnv["MOZ_GMP_PATH"] = gmp_path
2088 except EnvironmentError:
2089 self.log.error("Could not find path to gmp-fake plugin!")
2090 return None
2092 if options.fatalAssertions:
2093 browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
2095 # Produce a mozlog, if setup (see MOZ_LOG global at the top of
2096 # this script).
2097 self.mozLogs = MOZ_LOG and "MOZ_UPLOAD_DIR" in os.environ
2098 if self.mozLogs:
2099 browserEnv["MOZ_LOG"] = MOZ_LOG
2101 return browserEnv
2103 def killNamedProc(self, pname, orphans=True):
2104 """Kill processes matching the given command name"""
2105 self.log.info("Checking for %s processes..." % pname)
2107 if HAVE_PSUTIL:
2108 for proc in psutil.process_iter():
2109 try:
2110 if proc.name() == pname:
2111 procd = proc.as_dict(attrs=["pid", "ppid", "name", "username"])
2112 if proc.ppid() == 1 or not orphans:
2113 self.log.info("killing %s" % procd)
2114 killPid(proc.pid, self.log)
2115 else:
2116 self.log.info("NOT killing %s (not an orphan?)" % procd)
2117 except Exception as e:
2118 self.log.info(
2119 "Warning: Unable to kill process %s: %s" % (pname, str(e))
2121 # may not be able to access process info for all processes
2122 continue
2123 else:
2125 def _psInfo(_, line):
2126 if pname in line:
2127 self.log.info(line)
2129 mozprocess.run_and_wait(
2130 ["ps", "-f"],
2131 output_line_handler=_psInfo,
2134 def _psKill(_, line):
2135 parts = line.split()
2136 if len(parts) == 3 and parts[0].isdigit():
2137 pid = int(parts[0])
2138 ppid = int(parts[1])
2139 if parts[2] == pname:
2140 if ppid == 1 or not orphans:
2141 self.log.info("killing %s (pid %d)" % (pname, pid))
2142 killPid(pid, self.log)
2143 else:
2144 self.log.info(
2145 "NOT killing %s (pid %d) (not an orphan?)"
2146 % (pname, pid)
2149 mozprocess.run_and_wait(
2150 ["ps", "-o", "pid,ppid,comm"],
2151 output_line_handler=_psKill,
2154 def execute_start_script(self):
2155 if not self.start_script or not self.marionette:
2156 return
2158 if os.path.isfile(self.start_script):
2159 with open(self.start_script, "r") as fh:
2160 script = fh.read()
2161 else:
2162 script = self.start_script
2164 with self.marionette.using_context("chrome"):
2165 return self.marionette.execute_script(
2166 script, script_args=(self.start_script_kwargs,)
2169 def fillCertificateDB(self, options):
2170 # TODO: move -> mozprofile:
2171 # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c35
2173 pwfilePath = os.path.join(options.profilePath, ".crtdbpw")
2174 with open(pwfilePath, "w") as pwfile:
2175 pwfile.write("\n")
2177 # Pre-create the certification database for the profile
2178 env = self.environment(xrePath=options.xrePath)
2179 env["LD_LIBRARY_PATH"] = options.xrePath
2180 bin_suffix = mozinfo.info.get("bin_suffix", "")
2181 certutil = os.path.join(options.utilityPath, "certutil" + bin_suffix)
2182 pk12util = os.path.join(options.utilityPath, "pk12util" + bin_suffix)
2183 toolsEnv = env
2184 if mozinfo.info["asan"]:
2185 # Disable leak checking when running these tools
2186 toolsEnv["ASAN_OPTIONS"] = "detect_leaks=0"
2187 if mozinfo.info["tsan"]:
2188 # Disable race checking when running these tools
2189 toolsEnv["TSAN_OPTIONS"] = "report_bugs=0"
2191 if self.certdbNew:
2192 # android uses the new DB formats exclusively
2193 certdbPath = "sql:" + options.profilePath
2194 else:
2195 # desktop seems to use the old
2196 certdbPath = options.profilePath
2198 # certutil.exe depends on some DLLs in the app directory
2199 # When running tests against an MSIX-installed Firefox, these DLLs
2200 # cannot be used out of the install directory, they must be copied
2201 # elsewhere first.
2202 if "WindowsApps" in options.app:
2203 install_dir = os.path.dirname(options.app)
2204 for f in os.listdir(install_dir):
2205 if f.endswith(".dll"):
2206 shutil.copy(os.path.join(install_dir, f), options.utilityPath)
2208 status = call(
2209 [certutil, "-N", "-d", certdbPath, "-f", pwfilePath], env=toolsEnv
2211 if status:
2212 return status
2214 # Walk the cert directory and add custom CAs and client certs
2215 files = os.listdir(options.certPath)
2216 for item in files:
2217 root, ext = os.path.splitext(item)
2218 if ext == ".ca":
2219 trustBits = "CT,,"
2220 if root.endswith("-object"):
2221 trustBits = "CT,,CT"
2222 call(
2224 certutil,
2225 "-A",
2226 "-i",
2227 os.path.join(options.certPath, item),
2228 "-d",
2229 certdbPath,
2230 "-f",
2231 pwfilePath,
2232 "-n",
2233 root,
2234 "-t",
2235 trustBits,
2237 env=toolsEnv,
2239 elif ext == ".client":
2240 call(
2242 pk12util,
2243 "-i",
2244 os.path.join(options.certPath, item),
2245 "-w",
2246 pwfilePath,
2247 "-d",
2248 certdbPath,
2250 env=toolsEnv,
2253 os.unlink(pwfilePath)
2254 return 0
2256 def findFreePort(self, type):
2257 with closing(socket.socket(socket.AF_INET, type)) as s:
2258 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
2259 s.bind(("127.0.0.1", 0))
2260 return s.getsockname()[1]
2262 def proxy(self, options):
2263 # proxy
2264 # use SSL port for legacy compatibility; see
2265 # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
2266 # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
2267 # - https://github.com/mozilla/mozbase/commit/43f9510e3d58bfed32790c82a57edac5f928474d
2268 # 'ws': str(self.webSocketPort)
2269 proxyOptions = {
2270 "remote": options.webServer,
2271 "http": options.httpPort,
2272 "https": options.sslPort,
2273 "ws": options.sslPort,
2276 if options.useHttp3Server:
2277 options.dohServerPort = self.findFreePort(socket.SOCK_STREAM)
2278 options.http3ServerPort = self.findFreePort(socket.SOCK_DGRAM)
2279 proxyOptions["dohServerPort"] = options.dohServerPort
2280 self.log.info("use doh server at port: %d" % options.dohServerPort)
2281 self.log.info("use http3 server at port: %d" % options.http3ServerPort)
2282 elif options.useHttp2Server:
2283 options.dohServerPort = self.findFreePort(socket.SOCK_STREAM)
2284 options.http2ServerPort = self.findFreePort(socket.SOCK_STREAM)
2285 proxyOptions["dohServerPort"] = options.dohServerPort
2286 self.log.info("use doh server at port: %d" % options.dohServerPort)
2287 self.log.info("use http2 server at port: %d" % options.http2ServerPort)
2288 return proxyOptions
2290 def merge_base_profiles(self, options, category):
2291 """Merge extra profile data from testing/profiles."""
2293 # In test packages used in CI, the profile_data directory is installed
2294 # in the SCRIPT_DIR.
2295 profile_data_dir = os.path.join(SCRIPT_DIR, "profile_data")
2296 # If possible, read profile data from topsrcdir. This prevents us from
2297 # requiring a re-build to pick up newly added extensions in the
2298 # <profile>/extensions directory.
2299 if build_obj:
2300 path = os.path.join(build_obj.topsrcdir, "testing", "profiles")
2301 if os.path.isdir(path):
2302 profile_data_dir = path
2303 # Still not found? Look for testing/profiles relative to testing/mochitest.
2304 if not os.path.isdir(profile_data_dir):
2305 path = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "profiles"))
2306 if os.path.isdir(path):
2307 profile_data_dir = path
2309 with open(os.path.join(profile_data_dir, "profiles.json"), "r") as fh:
2310 base_profiles = json.load(fh)[category]
2312 # values to use when interpolating preferences
2313 interpolation = {
2314 "server": "%s:%s" % (options.webServer, options.httpPort),
2317 for profile in base_profiles:
2318 path = os.path.join(profile_data_dir, profile)
2319 self.profile.merge(path, interpolation=interpolation)
2321 @property
2322 def conditioned_profile_copy(self):
2323 """Returns a copy of the original conditioned profile that was created."""
2325 condprof_copy = os.path.join(tempfile.mkdtemp(), "profile")
2326 shutil.copytree(
2327 self.conditioned_profile_dir,
2328 condprof_copy,
2329 ignore=shutil.ignore_patterns("lock"),
2331 self.log.info("Created a conditioned-profile copy: %s" % condprof_copy)
2332 return condprof_copy
2334 def downloadConditionedProfile(self, profile_scenario, app):
2335 from condprof.client import get_profile
2336 from condprof.util import get_current_platform, get_version
2338 if self.conditioned_profile_dir:
2339 # We already have a directory, so provide a copy that
2340 # will get deleted after it's done with
2341 return self.conditioned_profile_copy
2343 temp_download_dir = tempfile.mkdtemp()
2345 # Call condprof's client API to yield our platform-specific
2346 # conditioned-profile binary
2347 platform = get_current_platform()
2349 if not profile_scenario:
2350 profile_scenario = "settled"
2352 version = get_version(app)
2353 try:
2354 cond_prof_target_dir = get_profile(
2355 temp_download_dir,
2356 platform,
2357 profile_scenario,
2358 repo="mozilla-central",
2359 version=version,
2360 retries=2, # quicker failure
2362 except Exception:
2363 if version is None:
2364 # any other error is a showstopper
2365 self.log.critical("Could not get the conditioned profile")
2366 traceback.print_exc()
2367 raise
2368 version = None
2369 try:
2370 self.log.info("retrying a profile with no version specified")
2371 cond_prof_target_dir = get_profile(
2372 temp_download_dir,
2373 platform,
2374 profile_scenario,
2375 repo="mozilla-central",
2376 version=version,
2378 except Exception:
2379 self.log.critical("Could not get the conditioned profile")
2380 traceback.print_exc()
2381 raise
2383 # Now get the full directory path to our fetched conditioned profile
2384 self.conditioned_profile_dir = os.path.join(
2385 temp_download_dir, cond_prof_target_dir
2387 if not os.path.exists(cond_prof_target_dir):
2388 self.log.critical(
2389 "Can't find target_dir {}, from get_profile()"
2390 "temp_download_dir {}, platform {}, scenario {}".format(
2391 cond_prof_target_dir, temp_download_dir, platform, profile_scenario
2394 raise OSError
2396 self.log.info(
2397 "Original self.conditioned_profile_dir is now set: {}".format(
2398 self.conditioned_profile_dir
2401 return self.conditioned_profile_copy
2403 def buildProfile(self, options):
2404 """create the profile and add optional chrome bits and files if requested"""
2405 # get extensions to install
2406 extensions = self.getExtensionsToInstall(options)
2408 # Whitelist the _tests directory (../..) so that TESTING_JS_MODULES work
2409 tests_dir = os.path.dirname(os.path.dirname(SCRIPT_DIR))
2410 sandbox_whitelist_paths = [tests_dir] + options.sandboxReadWhitelist
2411 if platform.system() == "Linux" or platform.system() in (
2412 "Windows",
2413 "Microsoft",
2415 # Trailing slashes are needed to indicate directories on Linux and Windows
2416 sandbox_whitelist_paths = [
2417 os.path.join(p, "") for p in sandbox_whitelist_paths
2420 if options.conditionedProfile:
2421 if options.profilePath and os.path.exists(options.profilePath):
2422 shutil.rmtree(options.profilePath, ignore_errors=True)
2423 options.profilePath = self.downloadConditionedProfile("full", options.app)
2425 # This is causing `certutil -N -d -f`` to not use -f (pwd file)
2426 try:
2427 os.remove(os.path.join(options.profilePath, "key4.db"))
2428 except Exception as e:
2429 self.log.info(
2430 "Caught exception while removing key4.db"
2431 "during setup of conditioned profile: %s" % e
2434 # Create the profile
2435 self.profile = Profile(
2436 profile=options.profilePath,
2437 addons=extensions,
2438 locations=self.locations,
2439 proxy=self.proxy(options),
2440 whitelistpaths=sandbox_whitelist_paths,
2443 # Fix options.profilePath for legacy consumers.
2444 options.profilePath = self.profile.profile
2446 manifest = self.addChromeToProfile(options)
2447 self.copyExtraFilesToProfile(options)
2449 # create certificate database for the profile
2450 # TODO: this should really be upstreamed somewhere, maybe mozprofile
2451 certificateStatus = self.fillCertificateDB(options)
2452 if certificateStatus:
2453 self.log.error(
2454 "TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed"
2456 return None
2458 # Set preferences in the following order (latter overrides former):
2459 # 1) Preferences from base profile (e.g from testing/profiles)
2460 # 2) Prefs hardcoded in this function
2461 # 3) Prefs from --setpref
2463 # Prefs from base profiles
2464 self.merge_base_profiles(options, "mochitest")
2466 # Hardcoded prefs (TODO move these into a base profile)
2467 prefs = {
2468 # Enable tracing output for detailed failures in case of
2469 # failing connection attempts, and hangs (bug 1397201)
2470 "remote.log.level": "Trace",
2471 # Disable async font fallback, because the unpredictable
2472 # extra reflow it can trigger (potentially affecting a later
2473 # test) results in spurious intermittent failures.
2474 "gfx.font_rendering.fallback.async": False,
2477 test_timeout = None
2478 if options.flavor == "browser" and options.timeout:
2479 test_timeout = options.timeout
2481 # browser-chrome tests use a fairly short default timeout of 45 seconds;
2482 # this is sometimes too short on asan and debug, where we expect reduced
2483 # performance.
2484 if (
2485 (mozinfo.info["asan"] or mozinfo.info["debug"])
2486 and options.flavor == "browser"
2487 and options.timeout is None
2489 self.log.info("Increasing default timeout to 90 seconds (asan or debug)")
2490 test_timeout = 90
2492 # tsan builds need even more time
2493 if (
2494 mozinfo.info["tsan"]
2495 and options.flavor == "browser"
2496 and options.timeout is None
2498 self.log.info("Increasing default timeout to 120 seconds (tsan)")
2499 test_timeout = 120
2501 if mozinfo.info["os"] == "win" and mozinfo.info["processor"] == "aarch64":
2502 test_timeout = self.DEFAULT_TIMEOUT * 4
2503 self.log.info(
2504 "Increasing default timeout to {} seconds (win aarch64)".format(
2505 test_timeout
2509 if "MOZ_CHAOSMODE=0xfb" in options.environment and test_timeout:
2510 test_timeout *= 2
2511 self.log.info(
2512 "Increasing default timeout to {} seconds (MOZ_CHAOSMODE)".format(
2513 test_timeout
2517 if test_timeout:
2518 prefs["testing.browserTestHarness.timeout"] = test_timeout
2520 if getattr(self, "testRootAbs", None):
2521 prefs["mochitest.testRoot"] = self.testRootAbs
2523 # See if we should use fake media devices.
2524 if options.useTestMediaDevices:
2525 prefs["media.audio_loopback_dev"] = self.mediaDevices["audio"]["name"]
2526 prefs["media.video_loopback_dev"] = self.mediaDevices["video"]["name"]
2527 prefs["media.cubeb.output_device"] = "Null Output"
2528 prefs["media.volume_scale"] = "1.0"
2529 self.gstForV4l2loopbackProcess = self.mediaDevices["video"]["process"]
2531 self.profile.set_preferences(prefs)
2533 # Extra prefs from --setpref
2534 self.profile.set_preferences(self.extraPrefs)
2535 return manifest
2537 def getGMPPluginPath(self, options):
2538 if options.gmp_path:
2539 return options.gmp_path
2541 gmp_parentdirs = [
2542 # For local builds, GMP plugins will be under dist/bin.
2543 options.xrePath,
2544 # For packaged builds, GMP plugins will get copied under
2545 # $profile/plugins.
2546 os.path.join(self.profile.profile, "plugins"),
2549 gmp_subdirs = [
2550 os.path.join("gmp-fake", "1.0"),
2551 os.path.join("gmp-fakeopenh264", "1.0"),
2552 os.path.join("gmp-clearkey", "0.1"),
2555 gmp_paths = [
2556 os.path.join(parent, sub)
2557 for parent in gmp_parentdirs
2558 for sub in gmp_subdirs
2559 if os.path.isdir(os.path.join(parent, sub))
2562 if not gmp_paths:
2563 # This is fatal for desktop environments.
2564 raise EnvironmentError("Could not find test gmp plugins")
2566 return os.pathsep.join(gmp_paths)
2568 def cleanup(self, options, final=False):
2569 """remove temporary files, profile and virtual audio input device"""
2570 if hasattr(self, "manifest") and self.manifest is not None:
2571 if os.path.exists(self.manifest):
2572 os.remove(self.manifest)
2573 if hasattr(self, "profile"):
2574 del self.profile
2575 if hasattr(self, "extraTestsDirs"):
2576 for d in self.extraTestsDirs:
2577 if os.path.exists(d):
2578 shutil.rmtree(d)
2579 if options.pidFile != "" and os.path.exists(options.pidFile):
2580 try:
2581 os.remove(options.pidFile)
2582 if os.path.exists(options.pidFile + ".xpcshell.pid"):
2583 os.remove(options.pidFile + ".xpcshell.pid")
2584 except Exception:
2585 self.log.warning(
2586 "cleaning up pidfile '%s' was unsuccessful from the test harness"
2587 % options.pidFile
2589 options.manifestFile = None
2591 if hasattr(self, "virtualDeviceIdList"):
2592 pactl = spawn.find_executable("pactl")
2594 if not pactl:
2595 self.log.error("Could not find pactl on system")
2596 return None
2598 for id in self.virtualDeviceIdList:
2599 try:
2600 subprocess.check_call([pactl, "unload-module", str(id)])
2601 except subprocess.CalledProcessError:
2602 self.log.error(
2603 "Could not remove pulse module with id {}".format(id)
2605 return None
2607 self.virtualDeviceIdList = []
2609 def dumpScreen(self, utilityPath):
2610 if self.haveDumpedScreen:
2611 self.log.info(
2612 "Not taking screenshot here: see the one that was previously logged"
2614 return
2615 self.haveDumpedScreen = True
2616 dump_screen(utilityPath, self.log)
2618 def killAndGetStack(self, processPID, utilityPath, debuggerInfo, dump_screen=False):
2620 Kill the process, preferrably in a way that gets us a stack trace.
2621 Also attempts to obtain a screenshot before killing the process
2622 if specified.
2624 self.log.info("Killing process: %s" % processPID)
2625 if dump_screen:
2626 self.dumpScreen(utilityPath)
2628 if mozinfo.info.get("crashreporter", True) and not debuggerInfo:
2629 try:
2630 minidump_path = os.path.join(self.profile.profile, "minidumps")
2631 mozcrash.kill_and_get_minidump(processPID, minidump_path, utilityPath)
2632 except OSError:
2633 # https://bugzilla.mozilla.org/show_bug.cgi?id=921509
2634 self.log.info("Can't trigger Breakpad, process no longer exists")
2635 return
2636 self.log.info("Can't trigger Breakpad, just killing process")
2637 killPid(processPID, self.log)
2639 def extract_child_pids(self, process_log, parent_pid=None):
2640 """Parses the given log file for the pids of any processes launched by
2641 the main process and returns them as a list.
2642 If parent_pid is provided, and psutil is available, returns children of
2643 parent_pid according to psutil.
2645 rv = []
2646 if parent_pid and HAVE_PSUTIL:
2647 self.log.info("Determining child pids from psutil...")
2648 try:
2649 rv = [p.pid for p in psutil.Process(parent_pid).children()]
2650 self.log.info(str(rv))
2651 except psutil.NoSuchProcess:
2652 self.log.warning("Failed to lookup children of pid %d" % parent_pid)
2654 rv = set(rv)
2655 pid_re = re.compile(r"==> process \d+ launched child process (\d+)")
2656 with open(process_log) as fd:
2657 for line in fd:
2658 self.log.info(line.rstrip())
2659 m = pid_re.search(line)
2660 if m:
2661 rv.add(int(m.group(1)))
2662 return rv
2664 def checkForZombies(self, processLog, utilityPath, debuggerInfo):
2665 """Look for hung processes"""
2667 if not os.path.exists(processLog):
2668 self.log.info("Automation Error: PID log not found: %s" % processLog)
2669 # Whilst no hung process was found, the run should still display as
2670 # a failure
2671 return True
2673 # scan processLog for zombies
2674 self.log.info("zombiecheck | Reading PID log: %s" % processLog)
2675 processList = self.extract_child_pids(processLog)
2676 # kill zombies
2677 foundZombie = False
2678 for processPID in processList:
2679 self.log.info(
2680 "zombiecheck | Checking for orphan process with PID: %d" % processPID
2682 if isPidAlive(processPID):
2683 foundZombie = True
2684 self.log.error(
2685 "TEST-UNEXPECTED-FAIL | zombiecheck | child process "
2686 "%d still alive after shutdown" % processPID
2688 self.killAndGetStack(
2689 processPID, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
2692 return foundZombie
2694 def checkForRunningBrowsers(self):
2695 firefoxes = ""
2696 if HAVE_PSUTIL:
2697 attrs = ["pid", "ppid", "name", "cmdline", "username"]
2698 for proc in psutil.process_iter():
2699 try:
2700 if "firefox" in proc.name():
2701 firefoxes = "%s%s\n" % (firefoxes, proc.as_dict(attrs=attrs))
2702 except Exception:
2703 # may not be able to access process info for all processes
2704 continue
2705 if len(firefoxes) > 0:
2706 # In automation, this warning is unexpected and should be investigated.
2707 # In local testing, this is probably okay, as long as the browser is not
2708 # running a marionette server.
2709 self.log.warning("Found 'firefox' running before starting test browser!")
2710 self.log.warning(firefoxes)
2712 def runApp(
2713 self,
2714 testUrl,
2715 env,
2716 app,
2717 profile,
2718 extraArgs,
2719 utilityPath,
2720 debuggerInfo=None,
2721 valgrindPath=None,
2722 valgrindArgs=None,
2723 valgrindSuppFiles=None,
2724 symbolsPath=None,
2725 timeout=-1,
2726 detectShutdownLeaks=False,
2727 screenshotOnFail=False,
2728 bisectChunk=None,
2729 restartAfterFailure=False,
2730 marionette_args=None,
2731 e10s=True,
2732 runFailures=False,
2733 crashAsPass=False,
2734 currentManifest=None,
2737 Run the app, log the duration it took to execute, return the status code.
2738 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing
2739 for |timeout| seconds.
2741 # It can't be the case that both a with-debugger and an
2742 # on-Valgrind run have been requested. doTests() should have
2743 # already excluded this possibility.
2744 assert not (valgrindPath and debuggerInfo)
2746 # debugger information
2747 interactive = False
2748 debug_args = None
2749 if debuggerInfo:
2750 interactive = debuggerInfo.interactive
2751 debug_args = [debuggerInfo.path] + debuggerInfo.args
2753 # Set up Valgrind arguments.
2754 if valgrindPath:
2755 interactive = False
2756 valgrindArgs_split = (
2757 [] if valgrindArgs is None else shlex.split(valgrindArgs)
2760 valgrindSuppFiles_final = []
2761 if valgrindSuppFiles is not None:
2762 valgrindSuppFiles_final = [
2763 "--suppressions=" + path for path in valgrindSuppFiles.split(",")
2766 debug_args = (
2767 [valgrindPath]
2768 + mozdebug.get_default_valgrind_args()
2769 + valgrindArgs_split
2770 + valgrindSuppFiles_final
2773 # fix default timeout
2774 if timeout == -1:
2775 timeout = self.DEFAULT_TIMEOUT
2777 # Note in the log if running on Valgrind
2778 if valgrindPath:
2779 self.log.info(
2780 "runtests.py | Running on Valgrind. "
2781 + "Using timeout of %d seconds." % timeout
2784 # copy env so we don't munge the caller's environment
2785 env = env.copy()
2787 # Used to defer a possible IOError exception from Marionette
2788 marionette_exception = None
2790 temp_file_paths = []
2792 # make sure we clean up after ourselves.
2793 try:
2794 # set process log environment variable
2795 tmpfd, processLog = tempfile.mkstemp(suffix="pidlog")
2796 os.close(tmpfd)
2797 env["MOZ_PROCESS_LOG"] = processLog
2799 if debuggerInfo:
2800 # If a debugger is attached, don't use timeouts, and don't
2801 # capture ctrl-c.
2802 timeout = None
2803 signal.signal(signal.SIGINT, lambda sigid, frame: None)
2805 # build command line
2806 cmd = os.path.abspath(app)
2807 args = list(extraArgs)
2808 args.append("-marionette")
2809 # TODO: mozrunner should use -foreground at least for mac
2810 # https://bugzilla.mozilla.org/show_bug.cgi?id=916512
2811 args.append("-foreground")
2812 self.start_script_kwargs["testUrl"] = testUrl or "about:blank"
2814 if detectShutdownLeaks:
2815 env["MOZ_LOG"] = (
2816 env["MOZ_LOG"] + "," if env["MOZ_LOG"] else ""
2817 ) + "DocShellAndDOMWindowLeak:3"
2818 shutdownLeaks = ShutdownLeaks(self.log)
2819 else:
2820 shutdownLeaks = None
2822 if mozinfo.info["asan"] and mozinfo.isLinux and mozinfo.bits == 64:
2823 lsanLeaks = LSANLeaks(self.log)
2824 else:
2825 lsanLeaks = None
2827 # create an instance to process the output
2828 outputHandler = self.OutputHandler(
2829 harness=self,
2830 utilityPath=utilityPath,
2831 symbolsPath=symbolsPath,
2832 dump_screen_on_timeout=not debuggerInfo,
2833 dump_screen_on_fail=screenshotOnFail,
2834 shutdownLeaks=shutdownLeaks,
2835 lsanLeaks=lsanLeaks,
2836 bisectChunk=bisectChunk,
2837 restartAfterFailure=restartAfterFailure,
2840 def timeoutHandler():
2841 browserProcessId = outputHandler.browserProcessId
2842 self.handleTimeout(
2843 timeout,
2844 proc,
2845 utilityPath,
2846 debuggerInfo,
2847 browserProcessId,
2848 processLog,
2851 kp_kwargs = {
2852 "kill_on_timeout": False,
2853 "cwd": SCRIPT_DIR,
2854 "onTimeout": [timeoutHandler],
2856 kp_kwargs["processOutputLine"] = [outputHandler]
2858 self.checkForRunningBrowsers()
2860 # create mozrunner instance and start the system under test process
2861 self.lastTestSeen = self.test_name
2862 self.lastManifest = currentManifest
2863 startTime = datetime.now()
2865 runner_cls = mozrunner.runners.get(
2866 mozinfo.info.get("appname", "firefox"), mozrunner.Runner
2868 runner = runner_cls(
2869 profile=self.profile,
2870 binary=cmd,
2871 cmdargs=args,
2872 env=env,
2873 process_class=mozprocess.ProcessHandlerMixin,
2874 process_args=kp_kwargs,
2877 # start the runner
2878 try:
2879 runner.start(
2880 debug_args=debug_args,
2881 interactive=interactive,
2882 outputTimeout=timeout,
2884 proc = runner.process_handler
2885 self.log.info("runtests.py | Application pid: %d" % proc.pid)
2887 gecko_id = "GECKO(%d)" % proc.pid
2888 self.log.process_start(gecko_id)
2889 self.message_logger.gecko_id = gecko_id
2890 except PermissionError:
2891 # treat machine as bad, return
2892 return TBPL_RETRY, "Failure to launch browser"
2893 except Exception as e:
2894 raise e # unknown error
2896 try:
2897 # start marionette and kick off the tests
2898 marionette_args = marionette_args or {}
2899 self.marionette = Marionette(**marionette_args)
2900 self.marionette.start_session()
2902 # install specialpowers and mochikit addons
2903 addons = Addons(self.marionette)
2905 if self.staged_addons:
2906 for addon_path in self.staged_addons:
2907 if not os.path.isdir(addon_path):
2908 self.log.error(
2909 "TEST-UNEXPECTED-FAIL | invalid setup: missing extension at %s"
2910 % addon_path
2912 return 1, self.lastTestSeen
2913 temp_addon_path = create_zip(addon_path)
2914 temp_file_paths.append(temp_addon_path)
2915 addons.install(temp_addon_path)
2917 self.execute_start_script()
2919 # an open marionette session interacts badly with mochitest,
2920 # delete it until we figure out why.
2921 self.marionette.delete_session()
2922 del self.marionette
2924 except IOError:
2925 # Any IOError as thrown by Marionette means that something is
2926 # wrong with the process, like a crash or the socket is no
2927 # longer open. We defer raising this specific error so that
2928 # post-test checks for leaks and crashes are performed and
2929 # reported first.
2930 marionette_exception = sys.exc_info()
2932 # wait until app is finished
2933 # XXX copy functionality from
2934 # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py#L61
2935 # until bug 913970 is fixed regarding mozrunner `wait` not returning status
2936 # see https://bugzilla.mozilla.org/show_bug.cgi?id=913970
2937 self.log.info("runtests.py | Waiting for browser...")
2938 status = proc.wait()
2939 if status is None:
2940 self.log.warning(
2941 "runtests.py | Failed to get app exit code - running/crashed?"
2943 # must report an integer to process_exit()
2944 status = 0
2945 self.log.process_exit("Main app process", status)
2946 runner.process_handler = None
2948 # finalize output handler
2949 outputHandler.finish()
2951 # record post-test information
2952 if status:
2953 # no need to keep return code 137, 245, etc.
2954 status = 1
2955 self.message_logger.dump_buffered()
2956 msg = "application terminated with exit code %s" % status
2957 # self.message_logger.is_test_running indicates we need to send a test_end
2958 if crashAsPass and self.message_logger.is_test_running:
2959 # this works for browser-chrome, mochitest-plain has status=0
2960 message = {
2961 "action": "test_end",
2962 "status": "CRASH",
2963 "expected": "CRASH",
2964 "thread": None,
2965 "pid": None,
2966 "source": "mochitest",
2967 "time": int(time.time()) * 1000,
2968 "test": self.lastTestSeen,
2969 "message": msg,
2971 # need to send a test_end in order to have mozharness process messages properly
2972 # this requires a custom message vs log.error/log.warning/etc.
2973 self.message_logger.process_message(message)
2974 else:
2975 self.lastTestSeen = (
2976 currentManifest or "Main app process exited normally"
2979 self.log.info(
2980 "runtests.py | Application ran for: %s"
2981 % str(datetime.now() - startTime)
2984 # Do a final check for zombie child processes.
2985 zombieProcesses = self.checkForZombies(
2986 processLog, utilityPath, debuggerInfo
2989 # check for crashes
2990 quiet = False
2991 if crashAsPass:
2992 quiet = True
2994 minidump_path = os.path.join(self.profile.profile, "minidumps")
2995 crash_count = mozcrash.log_crashes(
2996 self.log,
2997 minidump_path,
2998 symbolsPath,
2999 test=self.lastTestSeen,
3000 quiet=quiet,
3003 expected = None
3004 if crashAsPass or crash_count > 0:
3005 # self.message_logger.is_test_running indicates we need a test_end message
3006 if self.message_logger.is_test_running:
3007 # this works for browser-chrome, mochitest-plain has status=0
3008 expected = "CRASH"
3009 if crashAsPass:
3010 status = 0
3011 elif crash_count or zombieProcesses:
3012 if self.message_logger.is_test_running:
3013 expected = "PASS"
3014 status = 1
3016 if expected:
3017 # send this out so we always wrap up the test-end message
3018 message = {
3019 "action": "test_end",
3020 "status": "CRASH",
3021 "expected": expected,
3022 "thread": None,
3023 "pid": None,
3024 "source": "mochitest",
3025 "time": int(time.time()) * 1000,
3026 "test": self.lastTestSeen,
3027 "message": "application terminated with exit code %s" % status,
3029 # need to send a test_end in order to have mozharness process messages properly
3030 # this requires a custom message vs log.error/log.warning/etc.
3031 self.message_logger.process_message(message)
3032 finally:
3033 # cleanup
3034 if os.path.exists(processLog):
3035 os.remove(processLog)
3036 for p in temp_file_paths:
3037 os.remove(p)
3039 if marionette_exception is not None:
3040 exc, value, tb = marionette_exception
3041 six.reraise(exc, value, tb)
3043 return status, self.lastTestSeen
3045 def initializeLooping(self, options):
3047 This method is used to clear the contents before each run of for loop.
3048 This method is used for --run-by-dir and --bisect-chunk.
3050 if options.conditionedProfile:
3051 if options.profilePath and os.path.exists(options.profilePath):
3052 shutil.rmtree(options.profilePath, ignore_errors=True)
3053 if options.manifestFile and os.path.exists(options.manifestFile):
3054 os.remove(options.manifestFile)
3056 self.expectedError.clear()
3057 self.result.clear()
3058 options.manifestFile = None
3059 options.profilePath = None
3061 def initializeVirtualAudioDevices(self):
3063 Configure the system to have a number of virtual audio devices:
3064 2 output devices, and
3065 4 input devices that each produce a tone at a particular frequency.
3067 This method is only currently implemented for Linux.
3069 if not mozinfo.isLinux:
3070 return
3072 pactl = spawn.find_executable("pactl")
3074 if not pactl:
3075 self.log.error("Could not find pactl on system")
3076 return
3078 def getModuleIds(moduleName):
3079 o = subprocess.check_output([pactl, "list", "modules", "short"])
3080 list = []
3081 for input in o.splitlines():
3082 device = input.decode().split("\t")
3083 if device[1] == moduleName:
3084 list.append(int(device[0]))
3085 return list
3087 OUTPUT_DEVICES_COUNT = 2
3088 INPUT_DEVICES_COUNT = 4
3089 DEVICES_BASE_FREQUENCY = 110 # Hz
3090 # If the device are already present, find their id and return early
3091 outputDeviceIdList = getModuleIds("module-null-sink")
3092 inputDeviceIdList = getModuleIds("module-sine-source")
3094 if (
3095 len(outputDeviceIdList) == OUTPUT_DEVICES_COUNT
3096 and len(inputDeviceIdList) == INPUT_DEVICES_COUNT
3098 self.virtualDeviceIdList = outputDeviceIdList + inputDeviceIdList
3099 return
3100 else:
3101 # Remove any existing devices and reinitialize properly
3102 for id in outputDeviceIdList + inputDeviceIdList:
3103 try:
3104 subprocess.check_call([pactl, "unload-module", str(id)])
3105 except subprocess.CalledProcessError:
3106 log.error("Could not remove pulse module with id {}".format(id))
3107 return None
3109 idList = []
3110 command = [pactl, "load-module", "module-null-sink"]
3111 try: # device for "media.audio_loopback_dev" pref
3112 o = subprocess.check_output(command + ["rate=44100"])
3113 idList.append(int(o))
3114 except subprocess.CalledProcessError:
3115 self.log.error("Could not load module-null-sink")
3117 try:
3118 o = subprocess.check_output(
3119 command
3121 "rate=48000",
3122 "sink_properties='device.description=\"48000 Hz Null Output\"'",
3125 idList.append(int(o))
3126 except subprocess.CalledProcessError:
3127 self.log.error("Could not load module-null-sink at rate=48000")
3129 # We want quite a number of input devices, each with a different tone
3130 # frequency and device name so that we can recognize them easily during
3131 # testing.
3132 command = [pactl, "load-module", "module-sine-source", "rate=44100"]
3133 for i in range(1, INPUT_DEVICES_COUNT + 1):
3134 freq = i * DEVICES_BASE_FREQUENCY
3135 complete_command = command + [
3136 "source_name=sine-{}".format(freq),
3137 "frequency={}".format(freq),
3139 try:
3140 o = subprocess.check_output(complete_command)
3141 idList.append(int(o))
3143 except subprocess.CalledProcessError:
3144 self.log.error(
3145 "Could not create device with module-sine-source"
3146 " (freq={})".format(freq)
3149 self.virtualDeviceIdList = idList
3151 def normalize_paths(self, paths):
3152 # Normalize test paths so they are relative to test root
3153 norm_paths = []
3154 for p in paths:
3155 abspath = os.path.abspath(os.path.join(self.oldcwd, p))
3156 if abspath.startswith(self.testRootAbs):
3157 norm_paths.append(os.path.relpath(abspath, self.testRootAbs))
3158 else:
3159 norm_paths.append(p)
3160 return norm_paths
3162 def runMochitests(self, options, testsToRun, manifestToFilter=None):
3163 "This is a base method for calling other methods in this class for --bisect-chunk."
3164 # Making an instance of bisect class for --bisect-chunk option.
3165 bisect = bisection.Bisect(self)
3166 finished = False
3167 status = 0
3168 bisection_log = 0
3169 while not finished:
3170 if options.bisectChunk:
3171 testsToRun = bisect.pre_test(options, testsToRun, status)
3172 # To inform that we are in the process of bisection, and to
3173 # look for bleedthrough
3174 if options.bisectChunk != "default" and not bisection_log:
3175 self.log.error(
3176 "TEST-UNEXPECTED-FAIL | Bisection | Please ignore repeats "
3177 "and look for 'Bleedthrough' (if any) at the end of "
3178 "the failure list"
3180 bisection_log = 1
3182 result = self.doTests(options, testsToRun, manifestToFilter)
3183 if result == TBPL_RETRY: # terminate task
3184 return result
3186 if options.bisectChunk:
3187 status = bisect.post_test(options, self.expectedError, self.result)
3188 elif options.restartAfterFailure:
3189 # NOTE: ideally browser will halt on first failure, then this will always be the last test
3190 if not self.expectedError:
3191 status = -1
3192 else:
3193 firstFail = len(testsToRun)
3194 for key in self.expectedError:
3195 full_key = [x for x in testsToRun if key in x]
3196 if full_key:
3197 if testsToRun.index(full_key[0]) < firstFail:
3198 firstFail = testsToRun.index(full_key[0])
3199 testsToRun = testsToRun[firstFail + 1 :]
3200 if testsToRun == []:
3201 status = -1
3202 else:
3203 status = -1
3205 if status == -1:
3206 finished = True
3208 # We need to print the summary only if options.bisectChunk has a value.
3209 # Also we need to make sure that we do not print the summary in between
3210 # running tests via --run-by-dir.
3211 if options.bisectChunk and options.bisectChunk in self.result:
3212 bisect.print_summary()
3214 return result
3216 def groupTestsByScheme(self, tests):
3218 split tests into groups by schemes. test is classified as http if
3219 no scheme specified
3221 httpTests = []
3222 httpsTests = []
3223 for test in tests:
3224 if not test.get("scheme") or test.get("scheme") == "http":
3225 httpTests.append(test)
3226 elif test.get("scheme") == "https":
3227 httpsTests.append(test)
3228 return {"http": httpTests, "https": httpsTests}
3230 def verifyTests(self, options):
3232 Support --verify mode: Run test(s) many times in a variety of
3233 configurations/environments in an effort to find intermittent
3234 failures.
3237 # Number of times to repeat test(s) when running with --repeat
3238 VERIFY_REPEAT = 10
3239 # Number of times to repeat test(s) when running test in
3240 VERIFY_REPEAT_SINGLE_BROWSER = 5
3242 def step1():
3243 options.repeat = VERIFY_REPEAT
3244 options.keep_open = False
3245 options.runUntilFailure = True
3246 options.profilePath = None
3247 options.comparePrefs = True
3248 result = self.runTests(options)
3249 result = result or (-2 if self.countfail > 0 else 0)
3250 self.message_logger.finish()
3251 return result
3253 def step2():
3254 options.repeat = 0
3255 options.keep_open = False
3256 options.runUntilFailure = False
3257 for i in range(VERIFY_REPEAT_SINGLE_BROWSER):
3258 options.profilePath = None
3259 result = self.runTests(options)
3260 result = result or (-2 if self.countfail > 0 else 0)
3261 self.message_logger.finish()
3262 if result != 0:
3263 break
3264 return result
3266 def step3():
3267 options.repeat = VERIFY_REPEAT
3268 options.keep_open = False
3269 options.runUntilFailure = True
3270 options.environment.append("MOZ_CHAOSMODE=0xfb")
3271 options.profilePath = None
3272 result = self.runTests(options)
3273 options.environment.remove("MOZ_CHAOSMODE=0xfb")
3274 result = result or (-2 if self.countfail > 0 else 0)
3275 self.message_logger.finish()
3276 return result
3278 def step4():
3279 options.repeat = 0
3280 options.keep_open = False
3281 options.runUntilFailure = False
3282 options.environment.append("MOZ_CHAOSMODE=0xfb")
3283 for i in range(VERIFY_REPEAT_SINGLE_BROWSER):
3284 options.profilePath = None
3285 result = self.runTests(options)
3286 result = result or (-2 if self.countfail > 0 else 0)
3287 self.message_logger.finish()
3288 if result != 0:
3289 break
3290 options.environment.remove("MOZ_CHAOSMODE=0xfb")
3291 return result
3293 def fission_step(fission_pref):
3294 if fission_pref not in options.extraPrefs:
3295 options.extraPrefs.append(fission_pref)
3296 options.keep_open = False
3297 options.runUntilFailure = True
3298 options.profilePath = None
3299 result = self.runTests(options)
3300 result = result or (-2 if self.countfail > 0 else 0)
3301 self.message_logger.finish()
3302 return result
3304 def fission_step1():
3305 return fission_step("fission.autostart=false")
3307 def fission_step2():
3308 return fission_step("fission.autostart=true")
3310 if options.verify_fission:
3311 steps = [
3312 ("1. Run each test without fission.", fission_step1),
3313 ("2. Run each test with fission.", fission_step2),
3315 else:
3316 steps = [
3317 ("1. Run each test %d times in one browser." % VERIFY_REPEAT, step1),
3319 "2. Run each test %d times in a new browser each time."
3320 % VERIFY_REPEAT_SINGLE_BROWSER,
3321 step2,
3324 "3. Run each test %d times in one browser, in chaos mode."
3325 % VERIFY_REPEAT,
3326 step3,
3329 "4. Run each test %d times in a new browser each time, "
3330 "in chaos mode." % VERIFY_REPEAT_SINGLE_BROWSER,
3331 step4,
3335 stepResults = {}
3336 for descr, step in steps:
3337 stepResults[descr] = "not run / incomplete"
3339 startTime = datetime.now()
3340 maxTime = timedelta(seconds=options.verify_max_time)
3341 finalResult = "PASSED"
3342 for descr, step in steps:
3343 if (datetime.now() - startTime) > maxTime:
3344 self.log.info("::: Test verification is taking too long: Giving up!")
3345 self.log.info(
3346 "::: So far, all checks passed, but not all checks were run."
3348 break
3349 self.log.info(":::")
3350 self.log.info('::: Running test verification step "%s"...' % descr)
3351 self.log.info(":::")
3352 result = step()
3353 if result != 0:
3354 stepResults[descr] = "FAIL"
3355 finalResult = "FAILED!"
3356 break
3357 stepResults[descr] = "Pass"
3359 self.logPreamble([])
3361 self.log.info(":::")
3362 self.log.info("::: Test verification summary for:")
3363 self.log.info(":::")
3364 tests = self.getActiveTests(options)
3365 for test in tests:
3366 self.log.info("::: " + test["path"])
3367 self.log.info(":::")
3368 for descr in sorted(stepResults.keys()):
3369 self.log.info("::: %s : %s" % (descr, stepResults[descr]))
3370 self.log.info(":::")
3371 self.log.info("::: Test verification %s" % finalResult)
3372 self.log.info(":::")
3374 return 0
3376 def runTests(self, options):
3377 """Prepare, configure, run tests and cleanup"""
3378 self.extraPrefs = parse_preferences(options.extraPrefs)
3379 self.extraPrefs["fission.autostart"] = not options.disable_fission
3381 # for test manifest parsing.
3382 mozinfo.update(
3384 "a11y_checks": options.a11y_checks,
3385 "e10s": options.e10s,
3386 "fission": not options.disable_fission,
3387 "headless": options.headless,
3388 "http3": options.useHttp3Server,
3389 "http2": options.useHttp2Server,
3390 # Until the test harness can understand default pref values,
3391 # (https://bugzilla.mozilla.org/show_bug.cgi?id=1577912) this value
3392 # should by synchronized with the default pref value indicated in
3393 # StaticPrefList.yaml.
3395 # Currently for automation, the pref defaults to true (but can be
3396 # overridden with --setpref).
3397 "serviceworker_e10s": True,
3398 "sessionHistoryInParent": not options.disable_fission
3399 or not self.extraPrefs.get(
3400 "fission.disableSessionHistoryInParent",
3401 mozinfo.info["os"] == "android",
3403 "socketprocess_e10s": self.extraPrefs.get(
3404 "network.process.enabled", False
3406 "socketprocess_networking": self.extraPrefs.get(
3407 "network.http.network_access_on_socket_process.enabled", False
3409 "swgl": self.extraPrefs.get("gfx.webrender.software", False),
3410 "verify": options.verify,
3411 "verify_fission": options.verify_fission,
3412 "webgl_ipc": self.extraPrefs.get("webgl.out-of-process", False),
3413 "wmfme": (
3414 self.extraPrefs.get("media.wmf.media-engine.enabled", 0)
3415 and self.extraPrefs.get(
3416 "media.wmf.media-engine.channel-decoder.enabled", False
3419 "mda_gpu": self.extraPrefs.get(
3420 "media.hardware-video-decoding.force-enabled", False
3422 "xorigin": options.xOriginTests,
3423 "condprof": options.conditionedProfile,
3424 "msix": "WindowsApps" in options.app,
3428 if not self.mozinfo_variables_shown:
3429 self.mozinfo_variables_shown = True
3430 self.log.info(
3431 "These variables are available in the mozinfo environment and "
3432 "can be used to skip tests conditionally:"
3434 for info in sorted(mozinfo.info.items(), key=lambda item: item[0]):
3435 self.log.info(" {key}: {value}".format(key=info[0], value=info[1]))
3436 self.setTestRoot(options)
3438 # Despite our efforts to clean up servers started by this script, in practice
3439 # we still see infrequent cases where a process is orphaned and interferes
3440 # with future tests, typically because the old server is keeping the port in use.
3441 # Try to avoid those failures by checking for and killing servers before
3442 # trying to start new ones.
3443 self.killNamedProc("ssltunnel")
3444 self.killNamedProc("xpcshell")
3446 if options.cleanupCrashes:
3447 mozcrash.cleanup_pending_crash_reports()
3449 tests = self.getActiveTests(options)
3450 self.logPreamble(tests)
3452 if mozinfo.info["fission"] and not mozinfo.info["e10s"]:
3453 # Make sure this is logged *after* suite_start so it gets associated with the
3454 # current suite in the summary formatters.
3455 self.log.error("Fission is not supported without e10s.")
3456 return 1
3458 tests = [t for t in tests if "disabled" not in t]
3460 # Until we have all green, this does not run on a11y (for perf reasons)
3461 if not options.runByManifest:
3462 result = self.runMochitests(options, [t["path"] for t in tests])
3463 self.handleShutdownProfile(options)
3464 return result
3466 # code for --run-by-manifest
3467 manifests = set(t["manifest"].replace("\\", "/") for t in tests)
3468 result = 0
3470 origPrefs = self.extraPrefs.copy()
3471 for m in sorted(manifests):
3472 self.log.group_start(name=m)
3473 self.log.info("Running manifest: {}".format(m))
3474 self.message_logger.setManifest(m)
3476 args = list(self.args_by_manifest[m])[0]
3477 self.extraArgs = []
3478 if args:
3479 for arg in args.strip().split():
3480 # Split off the argument value if available so that both
3481 # name and value will be set individually
3482 self.extraArgs.extend(arg.split("="))
3484 self.log.info(
3485 "The following arguments will be set:\n {}".format(
3486 "\n ".join(self.extraArgs)
3490 prefs = list(self.prefs_by_manifest[m])[0]
3491 self.extraPrefs = origPrefs.copy()
3492 if prefs:
3493 prefs = prefs.strip().split()
3494 self.log.info(
3495 "The following extra prefs will be set:\n {}".format(
3496 "\n ".join(prefs)
3499 self.extraPrefs.update(parse_preferences(prefs))
3501 envVars = list(self.env_vars_by_manifest[m])[0]
3502 self.extraEnv = {}
3503 if envVars:
3504 self.extraEnv = envVars.strip().split()
3505 self.log.info(
3506 "The following extra environment variables will be set:\n {}".format(
3507 "\n ".join(self.extraEnv)
3511 self.parseAndCreateTestsDirs(m)
3513 # If we are using --run-by-manifest, we should not use the profile path (if) provided
3514 # by the user, since we need to create a new directory for each run. We would face
3515 # problems if we use the directory provided by the user.
3516 tests_in_manifest = [t["path"] for t in tests if t["manifest"] == m]
3517 res = self.runMochitests(options, tests_in_manifest, manifestToFilter=m)
3518 if res == TBPL_RETRY: # terminate task
3519 return res
3520 result = result or res
3522 # Dump the logging buffer
3523 self.message_logger.dump_buffered()
3524 self.log.group_end(name=m)
3526 if res == -1:
3527 break
3529 if self.manifest is not None:
3530 self.cleanup(options, True)
3532 e10s_mode = "e10s" if options.e10s else "non-e10s"
3534 # for failure mode: where browser window has crashed and we have no reported results
3535 if (
3536 self.countpass == self.countfail == self.counttodo == 0
3537 and options.crashAsPass
3539 self.countpass = 1
3540 self.result = 0
3542 # printing total number of tests
3543 if options.flavor == "browser":
3544 print("TEST-INFO | checking window state")
3545 print("Browser Chrome Test Summary")
3546 print("\tPassed: %s" % self.countpass)
3547 print("\tFailed: %s" % self.countfail)
3548 print("\tTodo: %s" % self.counttodo)
3549 print("\tMode: %s" % e10s_mode)
3550 print("*** End BrowserChrome Test Results ***")
3551 else:
3552 print("0 INFO TEST-START | Shutdown")
3553 print("1 INFO Passed: %s" % self.countpass)
3554 print("2 INFO Failed: %s" % self.countfail)
3555 print("3 INFO Todo: %s" % self.counttodo)
3556 print("4 INFO Mode: %s" % e10s_mode)
3557 print("5 INFO SimpleTest FINISHED")
3559 self.handleShutdownProfile(options)
3561 if not result:
3562 if self.countfail or not (self.countpass or self.counttodo):
3563 # at least one test failed, or
3564 # no tests passed, and no tests failed (possibly a crash)
3565 result = 1
3567 return result
3569 def handleShutdownProfile(self, options):
3570 # If shutdown profiling was enabled, then the user will want to access the
3571 # performance profile. The following code will display helpful log messages
3572 # and automatically open the profile if it is requested.
3573 if self.browserEnv and "MOZ_PROFILER_SHUTDOWN" in self.browserEnv:
3574 profile_path = self.browserEnv["MOZ_PROFILER_SHUTDOWN"]
3576 profiler_logger = get_proxy_logger("profiler")
3577 profiler_logger.info("Shutdown performance profiling was enabled")
3578 profiler_logger.info("Profile saved locally to: %s" % profile_path)
3580 if options.profilerSaveOnly or options.profiler:
3581 # Only do the extra work of symbolicating and viewing the profile if
3582 # officially requested through a command line flag. The MOZ_PROFILER_*
3583 # flags can be set by a user.
3584 symbolicate_profile_json(profile_path, options.topobjdir)
3585 view_gecko_profile_from_mochitest(
3586 profile_path, options, profiler_logger
3588 else:
3589 profiler_logger.info(
3590 "The profiler was enabled outside of the mochitests. "
3591 "Use --profiler instead of MOZ_PROFILER_SHUTDOWN to "
3592 "symbolicate and open the profile automatically."
3595 # Clean up the temporary file if it exists.
3596 if self.profiler_tempdir:
3597 shutil.rmtree(self.profiler_tempdir)
3599 def doTests(self, options, testsToFilter=None, manifestToFilter=None):
3600 # A call to initializeLooping method is required in case of --run-by-dir or --bisect-chunk
3601 # since we need to initialize variables for each loop.
3602 if options.bisectChunk or options.runByManifest:
3603 self.initializeLooping(options)
3605 # get debugger info, a dict of:
3606 # {'path': path to the debugger (string),
3607 # 'interactive': whether the debugger is interactive or not (bool)
3608 # 'args': arguments to the debugger (list)
3609 # TODO: use mozrunner.local.debugger_arguments:
3610 # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42
3612 debuggerInfo = None
3613 if options.debugger:
3614 debuggerInfo = mozdebug.get_debugger_info(
3615 options.debugger, options.debuggerArgs, options.debuggerInteractive
3618 if options.useTestMediaDevices:
3619 self.initializeVirtualAudioDevices()
3620 devices = findTestMediaDevices(self.log)
3621 if not devices:
3622 self.log.error("Could not find test media devices to use")
3623 return 1
3624 self.mediaDevices = devices
3626 # See if we were asked to run on Valgrind
3627 valgrindPath = None
3628 valgrindArgs = None
3629 valgrindSuppFiles = None
3630 if options.valgrind:
3631 valgrindPath = options.valgrind
3632 if options.valgrindArgs:
3633 valgrindArgs = options.valgrindArgs
3634 if options.valgrindSuppFiles:
3635 valgrindSuppFiles = options.valgrindSuppFiles
3637 if (valgrindArgs or valgrindSuppFiles) and not valgrindPath:
3638 self.log.error(
3639 "Specified --valgrind-args or --valgrind-supp-files,"
3640 " but not --valgrind"
3642 return 1
3644 if valgrindPath and debuggerInfo:
3645 self.log.error("Can't use both --debugger and --valgrind together")
3646 return 1
3648 if valgrindPath and not valgrindSuppFiles:
3649 valgrindSuppFiles = ",".join(get_default_valgrind_suppression_files())
3651 # buildProfile sets self.profile .
3652 # This relies on sideeffects and isn't very stateful:
3653 # https://bugzilla.mozilla.org/show_bug.cgi?id=919300
3654 self.manifest = self.buildProfile(options)
3655 if self.manifest is None:
3656 return 1
3658 self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log")
3660 self.browserEnv = self.buildBrowserEnv(options, debuggerInfo is not None)
3662 if self.browserEnv is None:
3663 return 1
3665 if self.mozLogs:
3666 self.browserEnv["MOZ_LOG_FILE"] = "{}/moz-pid=%PID-uid={}.log".format(
3667 self.browserEnv["MOZ_UPLOAD_DIR"], str(uuid.uuid4())
3670 status = 0
3671 try:
3672 self.startServers(options, debuggerInfo)
3674 if options.jsconsole:
3675 options.browserArgs.extend(["--jsconsole"])
3677 if options.jsdebugger:
3678 options.browserArgs.extend(["-wait-for-jsdebugger", "-jsdebugger"])
3680 # -jsdebugger takes a binary path as an optional argument.
3681 # Append jsdebuggerPath right after `-jsdebugger`.
3682 if options.jsdebuggerPath:
3683 options.browserArgs.extend([options.jsdebuggerPath])
3685 # Remove the leak detection file so it can't "leak" to the tests run.
3686 # The file is not there if leak logging was not enabled in the
3687 # application build.
3688 if os.path.exists(self.leak_report_file):
3689 os.remove(self.leak_report_file)
3691 # then again to actually run mochitest
3692 if options.timeout:
3693 timeout = options.timeout + 30
3694 elif options.debugger or options.jsdebugger or not options.autorun:
3695 timeout = None
3696 else:
3697 # We generally want the JS harness or marionette to handle
3698 # timeouts if they can.
3699 # The default JS harness timeout is currently 300 seconds.
3700 # The default Marionette socket timeout is currently 360 seconds.
3701 # Wait a little (10 seconds) more before timing out here.
3702 # See bug 479518 and bug 1414063.
3703 timeout = 370.0
3705 if "MOZ_CHAOSMODE=0xfb" in options.environment and timeout:
3706 timeout *= 2
3708 # Detect shutdown leaks for m-bc runs if
3709 # code coverage is not enabled.
3710 detectShutdownLeaks = False
3711 if options.jscov_dir_prefix is None:
3712 detectShutdownLeaks = (
3713 mozinfo.info["debug"]
3714 and options.flavor == "browser"
3715 and options.subsuite != "thunderbird"
3716 and not options.crashAsPass
3719 self.start_script_kwargs["flavor"] = self.normflavor(options.flavor)
3720 marionette_args = {
3721 "symbols_path": options.symbolsPath,
3722 "socket_timeout": options.marionette_socket_timeout,
3723 "startup_timeout": options.marionette_startup_timeout,
3726 if options.marionette:
3727 host, port = options.marionette.split(":")
3728 marionette_args["host"] = host
3729 marionette_args["port"] = int(port)
3731 # testsToFilter parameter is used to filter out the test list that
3732 # is sent to getTestsByScheme
3733 for scheme, tests in self.getTestsByScheme(
3734 options, testsToFilter, True, manifestToFilter
3736 # read the number of tests here, if we are not going to run any,
3737 # terminate early
3738 if not tests:
3739 continue
3741 self.currentTests = [t["path"] for t in tests]
3742 testURL = self.buildTestURL(options, scheme=scheme)
3744 self.buildURLOptions(options, self.browserEnv)
3745 if self.urlOpts:
3746 testURL += "?" + "&".join(self.urlOpts)
3748 if options.runFailures:
3749 testURL += "&runFailures=true"
3751 if options.timeoutAsPass:
3752 testURL += "&timeoutAsPass=true"
3754 if options.conditionedProfile:
3755 testURL += "&conditionedProfile=true"
3757 self.log.info("runtests.py | Running with scheme: {}".format(scheme))
3758 self.log.info(
3759 "runtests.py | Running with e10s: {}".format(options.e10s)
3761 self.log.info(
3762 "runtests.py | Running with fission: {}".format(
3763 mozinfo.info.get("fission", True)
3766 self.log.info(
3767 "runtests.py | Running with cross-origin iframes: {}".format(
3768 mozinfo.info.get("xorigin", False)
3771 self.log.info(
3772 "runtests.py | Running with serviceworker_e10s: {}".format(
3773 mozinfo.info.get("serviceworker_e10s", False)
3776 self.log.info(
3777 "runtests.py | Running with socketprocess_e10s: {}".format(
3778 mozinfo.info.get("socketprocess_e10s", False)
3781 self.log.info("runtests.py | Running tests: start.\n")
3782 ret, _ = self.runApp(
3783 testURL,
3784 self.browserEnv,
3785 options.app,
3786 profile=self.profile,
3787 extraArgs=options.browserArgs + self.extraArgs,
3788 utilityPath=options.utilityPath,
3789 debuggerInfo=debuggerInfo,
3790 valgrindPath=valgrindPath,
3791 valgrindArgs=valgrindArgs,
3792 valgrindSuppFiles=valgrindSuppFiles,
3793 symbolsPath=options.symbolsPath,
3794 timeout=timeout,
3795 detectShutdownLeaks=detectShutdownLeaks,
3796 screenshotOnFail=options.screenshotOnFail,
3797 bisectChunk=options.bisectChunk,
3798 restartAfterFailure=options.restartAfterFailure,
3799 marionette_args=marionette_args,
3800 e10s=options.e10s,
3801 runFailures=options.runFailures,
3802 crashAsPass=options.crashAsPass,
3803 currentManifest=manifestToFilter,
3805 status = ret or status
3806 except KeyboardInterrupt:
3807 self.log.info("runtests.py | Received keyboard interrupt.\n")
3808 status = -1
3809 except Exception as e:
3810 traceback.print_exc()
3811 self.log.error(
3812 "Automation Error: Received unexpected exception while running application\n"
3814 if "ADBTimeoutError" in repr(e):
3815 self.log.info("runtests.py | Device disconnected. Aborting test.\n")
3816 raise
3817 status = 1
3818 finally:
3819 self.stopServers()
3821 ignoreMissingLeaks = options.ignoreMissingLeaks
3822 leakThresholds = options.leakThresholds
3824 if options.crashAsPass:
3825 ignoreMissingLeaks.append("tab")
3826 ignoreMissingLeaks.append("socket")
3828 # Provide a floor for Windows chrome leak detection, because we know
3829 # we have some Windows-specific shutdown hangs that we avoid by timing
3830 # out and leaking memory.
3831 if options.flavor == "chrome" and mozinfo.isWin:
3832 leakThresholds["default"] += 1296
3834 # Stop leak detection if m-bc code coverage is enabled
3835 # by maxing out the leak threshold for all processes.
3836 if options.jscov_dir_prefix:
3837 for processType in leakThresholds:
3838 ignoreMissingLeaks.append(processType)
3839 leakThresholds[processType] = sys.maxsize
3841 utilityPath = options.utilityPath or options.xrePath
3842 if status == 0:
3843 # ignore leak checks for crashes
3844 mozleak.process_leak_log(
3845 self.leak_report_file,
3846 leak_thresholds=leakThresholds,
3847 ignore_missing_leaks=ignoreMissingLeaks,
3848 log=self.log,
3849 stack_fixer=get_stack_fixer_function(utilityPath, options.symbolsPath),
3850 scope=manifestToFilter,
3853 self.log.info("runtests.py | Running tests: end.")
3855 if self.manifest is not None:
3856 self.cleanup(options, False)
3858 return status
3860 def handleTimeout(
3861 self, timeout, proc, utilityPath, debuggerInfo, browser_pid, processLog
3863 """handle process output timeout"""
3864 # TODO: bug 913975 : _processOutput should call self.processOutputLine
3865 # one more time one timeout (I think)
3866 message = {
3867 "action": "test_end",
3868 "status": "TIMEOUT",
3869 "expected": "PASS",
3870 "thread": None,
3871 "pid": None,
3872 "source": "mochitest",
3873 "time": int(time.time()) * 1000,
3874 "test": self.lastTestSeen,
3875 "message": "application timed out after %d seconds with no output"
3876 % int(timeout),
3878 # need to send a test_end in order to have mozharness process messages properly
3879 # this requires a custom message vs log.error/log.warning/etc.
3880 self.message_logger.process_message(message)
3881 self.message_logger.dump_buffered()
3882 self.message_logger.buffering = False
3883 self.log.warning("Force-terminating active process(es).")
3885 browser_pid = browser_pid or proc.pid
3886 child_pids = self.extract_child_pids(processLog, browser_pid)
3887 self.log.info("Found child pids: %s" % child_pids)
3889 if HAVE_PSUTIL:
3890 try:
3891 browser_proc = [psutil.Process(browser_pid)]
3892 except Exception:
3893 self.log.info("Failed to get proc for pid %d" % browser_pid)
3894 browser_proc = []
3895 try:
3896 child_procs = [psutil.Process(pid) for pid in child_pids]
3897 except Exception:
3898 self.log.info("Failed to get child procs")
3899 child_procs = []
3900 for pid in child_pids:
3901 self.killAndGetStack(
3902 pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
3904 gone, alive = psutil.wait_procs(child_procs, timeout=30)
3905 for p in gone:
3906 self.log.info("psutil found pid %s dead" % p.pid)
3907 for p in alive:
3908 self.log.warning("failed to kill pid %d after 30s" % p.pid)
3909 self.killAndGetStack(
3910 browser_pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
3912 gone, alive = psutil.wait_procs(browser_proc, timeout=30)
3913 for p in gone:
3914 self.log.info("psutil found pid %s dead" % p.pid)
3915 for p in alive:
3916 self.log.warning("failed to kill pid %d after 30s" % p.pid)
3917 else:
3918 self.log.error(
3919 "psutil not available! Will wait 30s before "
3920 "attempting to kill parent process. This should "
3921 "not occur in mozilla automation. See bug 1143547."
3923 for pid in child_pids:
3924 self.killAndGetStack(
3925 pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
3927 if child_pids:
3928 time.sleep(30)
3930 self.killAndGetStack(
3931 browser_pid, utilityPath, debuggerInfo, dump_screen=not debuggerInfo
3934 def archiveMozLogs(self):
3935 if self.mozLogs:
3936 with zipfile.ZipFile(
3937 "{}/mozLogs.zip".format(os.environ["MOZ_UPLOAD_DIR"]),
3938 "w",
3939 zipfile.ZIP_DEFLATED,
3940 ) as logzip:
3941 for logfile in glob.glob(
3942 "{}/moz*.log*".format(os.environ["MOZ_UPLOAD_DIR"])
3944 logzip.write(logfile, os.path.basename(logfile))
3945 os.remove(logfile)
3946 logzip.close()
3948 class OutputHandler(object):
3950 """line output handler for mozrunner"""
3952 def __init__(
3953 self,
3954 harness,
3955 utilityPath,
3956 symbolsPath=None,
3957 dump_screen_on_timeout=True,
3958 dump_screen_on_fail=False,
3959 shutdownLeaks=None,
3960 lsanLeaks=None,
3961 bisectChunk=None,
3962 restartAfterFailure=None,
3965 harness -- harness instance
3966 dump_screen_on_timeout -- whether to dump the screen on timeout
3968 self.harness = harness
3969 self.utilityPath = utilityPath
3970 self.symbolsPath = symbolsPath
3971 self.dump_screen_on_timeout = dump_screen_on_timeout
3972 self.dump_screen_on_fail = dump_screen_on_fail
3973 self.shutdownLeaks = shutdownLeaks
3974 self.lsanLeaks = lsanLeaks
3975 self.bisectChunk = bisectChunk
3976 self.restartAfterFailure = restartAfterFailure
3977 self.browserProcessId = None
3978 self.stackFixerFunction = self.stackFixer()
3980 def processOutputLine(self, line):
3981 """per line handler of output for mozprocess"""
3982 # Parsing the line (by the structured messages logger).
3983 messages = self.harness.message_logger.parse_line(line)
3985 for message in messages:
3986 # Passing the message to the handlers
3987 msg = message
3988 for handler in self.outputHandlers():
3989 msg = handler(msg)
3991 # Processing the message by the logger
3992 self.harness.message_logger.process_message(msg)
3994 __call__ = processOutputLine
3996 def outputHandlers(self):
3997 """returns ordered list of output handlers"""
3998 handlers = [
3999 self.fix_stack,
4000 self.record_last_test,
4001 self.dumpScreenOnTimeout,
4002 self.dumpScreenOnFail,
4003 self.trackShutdownLeaks,
4004 self.trackLSANLeaks,
4005 self.countline,
4007 if self.bisectChunk or self.restartAfterFailure:
4008 handlers.append(self.record_result)
4009 handlers.append(self.first_error)
4011 return handlers
4013 def stackFixer(self):
4015 return get_stack_fixer_function, if any, to use on the output lines
4017 return get_stack_fixer_function(self.utilityPath, self.symbolsPath)
4019 def finish(self):
4020 if self.shutdownLeaks:
4021 numFailures, errorMessages = self.shutdownLeaks.process()
4022 self.harness.countfail += numFailures
4023 for message in errorMessages:
4024 msg = {
4025 "action": "test_end",
4026 "status": "FAIL",
4027 "expected": "PASS",
4028 "thread": None,
4029 "pid": None,
4030 "source": "mochitest",
4031 "time": int(time.time()) * 1000,
4032 "test": message["test"],
4033 "message": message["msg"],
4035 self.harness.message_logger.process_message(msg)
4037 if self.lsanLeaks:
4038 self.harness.countfail += self.lsanLeaks.process()
4040 # output message handlers:
4041 # these take a message and return a message
4043 def record_result(self, message):
4044 # by default make the result key equal to pass.
4045 if message["action"] == "test_start":
4046 key = message["test"].split("/")[-1].strip()
4047 self.harness.result[key] = "PASS"
4048 elif message["action"] == "test_status":
4049 if "expected" in message:
4050 key = message["test"].split("/")[-1].strip()
4051 self.harness.result[key] = "FAIL"
4052 elif message["status"] == "FAIL":
4053 key = message["test"].split("/")[-1].strip()
4054 self.harness.result[key] = "TODO"
4055 return message
4057 def first_error(self, message):
4058 if (
4059 message["action"] == "test_status"
4060 and "expected" in message
4061 and message["status"] == "FAIL"
4063 key = message["test"].split("/")[-1].strip()
4064 if key not in self.harness.expectedError:
4065 self.harness.expectedError[key] = message.get(
4066 "message", message["subtest"]
4067 ).strip()
4068 return message
4070 def countline(self, message):
4071 if message["action"] == "log":
4072 line = message.get("message", "")
4073 elif message["action"] == "process_output":
4074 line = message.get("data", "")
4075 else:
4076 return message
4077 val = 0
4078 try:
4079 val = int(line.split(":")[-1].strip())
4080 except (AttributeError, ValueError):
4081 return message
4083 if "Passed:" in line:
4084 self.harness.countpass += val
4085 elif "Failed:" in line:
4086 self.harness.countfail += val
4087 elif "Todo:" in line:
4088 self.harness.counttodo += val
4089 return message
4091 def fix_stack(self, message):
4092 if self.stackFixerFunction:
4093 if message["action"] == "log":
4094 message["message"] = self.stackFixerFunction(message["message"])
4095 elif message["action"] == "process_output":
4096 message["data"] = self.stackFixerFunction(message["data"])
4097 return message
4099 def record_last_test(self, message):
4100 """record last test on harness"""
4101 if message["action"] == "test_start":
4102 self.harness.lastTestSeen = message["test"]
4103 elif message["action"] == "test_end":
4104 self.harness.lastTestSeen = "{} (finished)".format(message["test"])
4105 return message
4107 def dumpScreenOnTimeout(self, message):
4108 if (
4109 not self.dump_screen_on_fail
4110 and self.dump_screen_on_timeout
4111 and message["action"] == "test_status"
4112 and "expected" in message
4113 and "Test timed out" in message["subtest"]
4115 self.harness.dumpScreen(self.utilityPath)
4116 return message
4118 def dumpScreenOnFail(self, message):
4119 if (
4120 self.dump_screen_on_fail
4121 and "expected" in message
4122 and message["status"] == "FAIL"
4124 self.harness.dumpScreen(self.utilityPath)
4125 return message
4127 def trackLSANLeaks(self, message):
4128 if self.lsanLeaks and message["action"] in ("log", "process_output"):
4129 line = (
4130 message.get("message", "")
4131 if message["action"] == "log"
4132 else message["data"]
4134 if "(finished)" in self.harness.lastTestSeen:
4135 self.lsanLeaks.log(line, self.harness.lastManifest)
4136 else:
4137 self.lsanLeaks.log(line, self.harness.lastTestSeen)
4138 return message
4140 def trackShutdownLeaks(self, message):
4141 if self.shutdownLeaks:
4142 self.shutdownLeaks.log(message)
4143 return message
4146 def view_gecko_profile_from_mochitest(profile_path, options, profiler_logger):
4147 """Getting shutdown performance profiles from just the command line arguments is
4148 difficult. This function makes the developer ergonomics a bit easier by taking the
4149 generated Gecko profile, and automatically serving it to profiler.firefox.com. The
4150 Gecko profile during shutdown is dumped to disk at:
4152 {objdir}/_tests/testing/mochitest/{profilename}
4154 This function takes that file, and launches a local webserver, and then points
4155 a browser to profiler.firefox.com to view it. From there it's easy to publish
4156 or save the profile.
4159 if options.profilerSaveOnly:
4160 # The user did not want this to automatically open, only share the location.
4161 return
4163 if not os.path.exists(profile_path):
4164 profiler_logger.error(
4165 "No profile was found at the profile path, cannot "
4166 "launch profiler.firefox.com."
4168 return
4170 profiler_logger.info("Loading this profile in the Firefox Profiler")
4172 view_gecko_profile(profile_path)
4175 def run_test_harness(parser, options):
4176 parser.validate(options)
4178 logger_options = {
4179 key: value
4180 for key, value in six.iteritems(vars(options))
4181 if key.startswith("log") or key == "valgrind"
4184 runner = MochitestDesktop(
4185 options.flavor, logger_options, options.stagedAddons, quiet=options.quiet
4188 options.runByManifest = False
4189 if options.flavor in ("plain", "a11y", "browser", "chrome"):
4190 options.runByManifest = True
4192 # run until failure, then loop until all tests have ran
4193 # using looping similar to bisection code
4194 if options.restartAfterFailure:
4195 options.runUntilFailure = True
4197 if options.verify or options.verify_fission:
4198 result = runner.verifyTests(options)
4199 else:
4200 result = runner.runTests(options)
4202 runner.archiveMozLogs()
4203 runner.message_logger.finish()
4204 return result
4207 def cli(args=sys.argv[1:]):
4208 # parse command line options
4209 parser = MochitestArgumentParser(app="generic")
4210 options = parser.parse_args(args)
4211 if options is None:
4212 # parsing error
4213 sys.exit(1)
4215 return run_test_harness(parser, options)
4218 if __name__ == "__main__":
4219 sys.exit(cli())