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/.
6 Runs the Mochitest test harness.
12 SCRIPT_DIR
= os
.path
.abspath(os
.path
.realpath(os
.path
.dirname(__file__
)))
13 sys
.path
.insert(0, SCRIPT_DIR
)
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
45 from manifestparser
import TestManifest
46 from manifestparser
.filters
import (
55 from manifestparser
.util
import normsep
56 from mozgeckoprofiler
import symbolicate_profile_json
, view_gecko_profile
57 from mozserve
import DoHServer
, Http2Server
, Http3Server
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
):
70 from leaks
import LSANLeaks
, ShutdownLeaks
71 from mochitest_options
import (
72 MochitestArgumentParser
,
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
92 from six
.moves
.urllib
.parse
import quote_plus
as encodeURIComponent
93 from six
.moves
.urllib_request
import urlopen
96 from mozbuild
.base
import MozbuildObject
98 build
= MozbuildObject
.from_environment(cwd
=SCRIPT_DIR
)
102 here
= os
.path
.abspath(os
.path
.dirname(__file__
))
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.
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.
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
132 MOCHITEST_SERVER_LOGGING
= False
134 #####################
135 # Test log handling #
136 #####################
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"])
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
168 TEST_PATH_PREFIXES
= [
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):
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
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)"""
200 isinstance(obj
, dict)
202 and obj
["action"] in MessageLogger
.VALID_ACTIONS
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
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")
237 # line is in unicode - so let's use it as it is
241 for fragment
in line
.split(MessageLogger
.DELIMITER
):
245 message
= json
.loads(fragment
)
246 self
.validate(message
)
250 action
="process_output",
251 process
=self
.gecko_id
,
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
)
271 if not self
._buffering
_initially
_enabled
:
273 return self
._buffering
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
286 if message
["action"] == "buffering_off":
287 self
.buffering
= False
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
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
315 and message
["action"] in self
.BUFFERED_ACTIONS
317 self
.buffered_messages
.append(message
)
318 # Otherwise log the message directly
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
)
344 def dump_buffered(self
, limit
=False):
346 dumped_messages
= self
.buffered_messages
[-self
.BUFFERING_THRESHOLD
:]
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
= []
365 self
.buffering
= False
366 self
.logger
.suite_end()
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
)
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
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")
398 parent
= psutil
.Process(pid
)
399 children
= parent
.children(recursive
=True)
400 children
.append(parent
)
402 p
.send_signal(signal
.SIGTERM
)
403 gone
, alive
= psutil
.wait_procs(children
, timeout
=30)
405 log
.info("psutil found pid %s dead" % p
.pid
)
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
)))
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
)))
418 import ctypes
.wintypes
422 PROCESS_QUERY_LIMITED_INFORMATION
= 0x1000
423 pHandle
= ctypes
.windll
.kernel32
.OpenProcess(
424 PROCESS_QUERY_LIMITED_INFORMATION
, 0, pid
430 pExitCode
= ctypes
.wintypes
.DWORD()
431 ctypes
.windll
.kernel32
.GetExitCodeProcess(pHandle
, ctypes
.byref(pExitCode
))
433 if pExitCode
.value
!= STILL_ACTIVE
:
436 # We have a live process handle. But Windows aggressively
437 # re-uses pids, so let's attempt to verify that this is
440 pName
= ctypes
.create_string_buffer(namesize
)
441 namelen
= ctypes
.windll
.psapi
.GetProcessImageFileNameA(
442 pHandle
, pName
, namesize
445 # Still an active process, so conservatively assume it's Firefox.
448 return pName
.value
.endswith((b
"firefox.exe", b
"plugin-container.exe"))
450 ctypes
.windll
.kernel32
.CloseHandle(pHandle
)
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
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
)
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
):
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."
486 def __init__(self
, options
, logger
):
487 if isinstance(options
, Namespace
):
488 options
= vars(options
)
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"
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"]
515 self
._httpdPath
= SCRIPT_DIR
516 self
._httpdPath
= os
.path
.abspath(self
._httpdPath
)
518 MochitestServer
.instance_count
+= 1
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
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
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"
546 env
["PATH"] = env
["PATH"] + ";" + str(self
._xrePath
)
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(),
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(
582 stdout
=self
.server_logfile
,
583 stderr
=subprocess
.STDOUT
,
586 self
.server_logfile
= None
587 self
._process
= subprocess
.Popen(
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...")
600 with
closing(urlopen(self
.debugURL
)) as c
:
601 self
._log
.info(six
.ensure_text(c
.read()))
603 except Exception as e
:
604 self
._log
.info("exception when enabling debugging: %s" % str(e
))
608 def ensureReady(self
, timeout
):
611 aliveFile
= os
.path
.join(self
._profileDir
, "server_alive.txt")
614 if os
.path
.exists(aliveFile
):
620 "TEST-UNEXPECTED-FAIL | runtests.py | Timed out while waiting for server startup."
627 with
closing(urlopen(self
.shutdownURL
)) as c
:
628 self
._log
.info(six
.ensure_text(c
.read()))
630 self
._log
.info("Failed to stop web server on %s" % self
.shutdownURL
)
631 traceback
.print_exc()
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.
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
649 self
._scriptdir
= scriptdir
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
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
676 os
.path
.join(self
._scriptdir
, "websock.log"),
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
)
691 if self
._process
is not None:
696 def __init__(self
, options
, logger
):
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
)
715 customcert
= match
.group("nickname")
717 "listen:%s:%s:%s:%s\n"
718 % (loc
.host
, loc
.port
, self
.sslPort
, customcert
)
721 match
= self
.clientAuthRE
.match(option
)
723 clientauth
= match
.group("clientauth")
725 "clientauth:%s:%s:%s:%s\n"
726 % (loc
.host
, loc
.port
, self
.sslPort
, clientauth
)
729 match
= self
.redirRE
.match(option
)
731 redirhost
= match
.group("redirhost")
733 "redirhost:%s:%s:%s:%s\n"
734 % (loc
.host
, loc
.port
, self
.sslPort
, redirhost
)
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
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
)
780 """Starts the SSL Tunnel"""
782 # start ssltunnel to provide https:// URLs capability
783 ssltunnel
= os
.path
.join(self
.utilityPath
, "ssltunnel")
786 if not os
.path
.exists(ssltunnel
):
788 "INFO | runtests.py | expected to find ssltunnel at %s" % ssltunnel
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
)
798 """Stops the SSL Tunnel and cleans up"""
799 if self
.process
is not None:
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
:
815 libc
= ctypes
.cdll
.LoadLibrary(find_library("c"))
817 # These are from linux/videodev2.h
819 class v4l2_capability(ctypes
.Structure
):
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
)
836 vcap
= v4l2_capability()
837 if libc
.ioctl(fd
, VIDIOC_QUERYCAP
, ctypes
.byref(vcap
)) != 0:
840 if six
.ensure_text(vcap
.driver
) != "v4l2 loopback":
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
855 libc
.ioctl(fd
, VIDIOC_S_CTRL
, ctypes
.byref(control
))
857 control
.id = SUSTAIN_FRAMERATE
859 libc
.ioctl(fd
, VIDIOC_S_CTRL
, ctypes
.byref(control
))
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
:
879 # Look for a v4l2loopback device.
882 for dev
in sorted(glob
.glob("/dev/video*")):
883 result
, name_
= checkAndConfigureV4l2loopback(dev
)
889 if not (name
and device
):
890 log
.error("Couldn't find a v4l2loopback video device")
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")
903 process
= subprocess
.Popen(
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"}
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
939 while path
!= os
.path
.expanduser("~"):
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.
955 # Path to the test script on the server
957 CHROME_PATH
= "redirect.html"
961 DEFAULT_TIMEOUT
= 60.0
963 mozinfo_variables_shown
= False
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):
974 self
.staged_addons
= staged_addons
977 self
.websocketProcessBridge
= None
978 self
.sslTunnel
= 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
993 self
.start_script_kwargs
= {}
997 self
.extraTestsDirs
= []
998 self
.conditioned_profile_dir
= None
1000 if logger_options
.get("log"):
1001 self
.log
= logger_options
["log"]
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.
1025 self
.expectedError
= {}
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
)
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.
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
1078 options
.logFile
= self
.getLogFilePath(options
.logFile
)
1080 if options
.flavor
in ("a11y", "browser", "chrome"):
1081 self
.makeTestConfig(options
)
1084 self
.urlOpts
.append("autorun=1")
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")
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
)
1101 self
.urlOpts
.append("startAt=%s" % options
.startAt
)
1103 self
.urlOpts
.append("endAt=%s" % options
.endAt
)
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")
1111 self
.urlOpts
.append("repeat=%d" % options
.repeat
)
1112 if len(options
.test_paths
) == 1 and os
.path
.isfile(
1115 os
.path
.dirname(__file__
),
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":
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":
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)")
1178 testPattern
= re
.compile(r
"test_")
1180 if not allow_js_css
and (".js" in filename
or ".css" in filename
):
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
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"
1202 testHost
= "http://mochi.test:8888"
1203 testURL
= "/".join([testHost
, self
.TEST_PATH
])
1205 if len(options
.test_paths
) == 1:
1209 os
.path
.dirname(__file__
),
1211 options
.test_paths
[0],
1214 testURL
= "/".join([testURL
, os
.path
.dirname(options
.test_paths
[0])])
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"
1224 def parseAndCreateTestsDirs(self
, m
):
1225 testsDirs
= list(self
.tests_dirs_by_manifest
[m
])[0]
1226 self
.extraTestsDirs
= []
1228 self
.extraTestsDirs
= testsDirs
.strip().split()
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:
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
)
1254 for d
in extraTestsDirs
:
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
)
1279 if testsToFilter
and (test
["path"] not in testsToFilter
):
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
1291 if manifestToFilter
and (test
["manifest"] not in manifestToFilter
):
1295 # Generate test by schemes
1296 for scheme
, grouped_tests
in self
.groupTestsByScheme(paths
).items():
1297 # Bug 883865 - add this functionality into manifestparser
1299 os
.path
.join(SCRIPT_DIR
, options
.testRunManifestFile
), "w"
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
)
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)
1327 os
.path
.join("websocketprocessbridge", "websocketprocessbridge.py"),
1329 options
.websocket_process_bridge_port
,
1331 self
.websocketProcessBridge
= subprocess
.Popen(command
, cwd
=SCRIPT_DIR
)
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:
1341 "runtests.py | websocket/process bridge failed "
1342 "to launch. Are all the dependencies installed?"
1347 sock
= socket
.create_connection(("127.0.0.1", 8191))
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
1367 if "webrtc" in test
.get("tags", ""):
1368 is_webrtc_tag_present
= True
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"]
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
))
1408 def startHttp2Server(self
, options
):
1410 Start a Http2 test server.
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
):
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
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
)
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:
1495 self
.log
.info("Stopping web server")
1498 self
.log
.critical("Exception when stopping web server")
1500 if self
.wsserver
is not None:
1502 self
.log
.info("Stopping web socket server")
1503 self
.wsserver
.stop()
1505 self
.log
.critical("Exception when stopping web socket server")
1507 if self
.sslTunnel
is not None:
1509 self
.log
.info("Stopping ssltunnel")
1510 self
.sslTunnel
.stop()
1512 self
.log
.critical("Exception stopping ssltunnel")
1514 if self
.websocketProcessBridge
is not None:
1516 self
.websocketProcessBridge
.kill()
1517 self
.websocketProcessBridge
.wait()
1518 self
.log
.info("Stopping websocket/process bridge")
1520 self
.log
.critical("Exception stopping websocket/process bridge")
1521 if self
.http3Server
is not None:
1523 self
.http3Server
.stop()
1525 self
.log
.critical("Exception stopping http3 server")
1526 if self
.http2Server
is not None:
1528 self
.http2Server
.stop()
1530 self
.log
.critical("Exception stopping http2 server")
1531 if self
.dohServer
is not None:
1533 self
.dohServer
.stop()
1535 self
.log
.critical("Exception stopping doh server")
1537 if hasattr(self
, "gstForV4l2loopbackProcess"):
1539 self
.gstForV4l2loopbackProcess
.kill()
1540 self
.gstForV4l2loopbackProcess
.wait()
1541 self
.log
.info("Stopping gst for v4l2loopback")
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
)
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
) + "/"
1560 dir = "file:///" + dir.replace("\\", "/")
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
)
1569 "content mochitests %s contentaccessible=yes\n" % chrometestDir
1572 "content mochitests-any %s contentaccessible=yes remoteenabled=yes\n"
1576 "content mochitests-content %s contentaccessible=yes remoterequired=yes\n"
1580 if options
.testingModulesDir
is not None:
1582 "resource testing-common file:///%s\n" % options
.testingModulesDir
1584 if options
.store_chrome_manifest
:
1585 shutil
.copyfile(manifest
, options
.store_chrome_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")
1595 # Write userChrome.css.
1597 /* set default namespace to XUL */
1598 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
1601 background-color: rgb(235, 235, 235) !important;
1604 background-image: none !important;
1608 os
.path
.join(options
.profilePath
, "userChrome.css"), "a"
1610 chromeFile
.write(chrome
)
1612 manifest
= self
.writeChromeManifest(options
)
1616 def getExtensionsToInstall(self
, options
):
1617 "Return a list of extensions to install in the profile"
1620 options
.app
[: options
.app
.rfind(os
.sep
)]
1622 else options
.utilityPath
1626 # Extensions distributed with the test harness.
1627 os
.path
.normpath(os
.path
.join(SCRIPT_DIR
, "extensions")),
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
)
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
)
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
):
1660 "runtests.py | Cannot find failure pattern file " + pat_file
1664 # Using ":error" to ensure it shows up in the failure summary.
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(
1673 ^\s*\*\s* # list bullet
1674 (test_\S+|\.{3}) # test name
1675 (?:\s*(`.+?`|asserts))? # failure pattern
1676 (?::.+)? # optional description
1677 \s*\[(\d+|\*)\] # expected count
1683 with
open(pat_file
) as f
:
1686 match
= patternRE
.match(line
)
1689 name
= match
.group(1)
1690 name
= last_name
if name
== "..." else name
1692 pat
= match
.group(2)
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
:
1699 patterns
[name
].append((pat
, count
))
1700 self
.patternFiles
[pat_file
] = patterns
1703 def getFailurePatterns(self
, pat_file
, test_name
):
1704 patterns
= self
.loadFailurePatternFile(pat_file
)
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
1716 manifest
= self
.getTestManifest(options
)
1718 if options
.extra_mozinfo_json
:
1719 mozinfo
.update(options
.extra_mozinfo_json
)
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
:
1745 options
.thisChunk
, options
.totalChunks
, options
.chunkByDir
1748 elif options
.chunkByRuntime
:
1749 if mozinfo
.info
["os"] == "android":
1756 runtime_file
= os
.path
.join(
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
)
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
, {})
1775 options
.thisChunk
, options
.totalChunks
, runtimes
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(
1792 noDefaultFilters
=noDefaultFilters
,
1798 NO_TESTS_FOUND
.format(options
.flavor
, manifest
.fmt_filters())
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
):
1814 "Warning: %s from manifest %s is not a valid test"
1815 % (test
["name"], test
["manifest"])
1819 manifest_key
= test
["manifest_relpath"]
1820 # Ignore ancestor_manifests that live at the root (e.g, don't have a
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
:
1837 "parsing {}: runByManifest mode must be enabled to "
1838 "set the `{}` key".format(test
["manifest_relpath"], key
)
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"]
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"])
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
:
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
)
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
:
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
)
1892 # The 'environment' key needs to be set in the DEFAULT section too.
1894 m
for m
, p
in six
.iteritems(self
.env_vars_by_manifest
) if len(p
) > 1
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
)
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
)
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)
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)
1952 "TestManifest masterPath %s does not exist" % masterPath
1957 def makeTestConfig(self
, options
):
1958 "Creates a test configuration file for customizing test execution."
1959 options
.logFile
= options
.logFile
.replace("\\", "\\\\")
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()
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
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:
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"
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
2017 options
.flavor
== "browser" or not options
.e10s
2018 ) and "MOZ_CRASHREPORTER_SHUTDOWN" in browserEnv
:
2019 del browserEnv
["MOZ_CRASHREPORTER_SHUTDOWN"]
2025 self
.extraEnv
, context
="environment variable in manifest"
2029 except KeyValueParseError
as e
:
2030 self
.log
.error(str(e
))
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
2040 dict(parse_key_value(options
.environment
, context
="--setenv"))
2042 except KeyValueParseError
as e
:
2043 self
.log
.error(str(e
))
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"
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."
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!")
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
2097 self
.mozLogs
= MOZ_LOG
and "MOZ_UPLOAD_DIR" in os
.environ
2099 browserEnv
["MOZ_LOG"] = MOZ_LOG
2103 def killNamedProc(self
, pname
, orphans
=True):
2104 """Kill processes matching the given command name"""
2105 self
.log
.info("Checking for %s processes..." % pname
)
2108 for proc
in psutil
.process_iter():
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
)
2116 self
.log
.info("NOT killing %s (not an orphan?)" % procd
)
2117 except Exception as e
:
2119 "Warning: Unable to kill process %s: %s" % (pname
, str(e
))
2121 # may not be able to access process info for all processes
2125 def _psInfo(_
, line
):
2129 mozprocess
.run_and_wait(
2131 output_line_handler
=_psInfo
,
2134 def _psKill(_
, line
):
2135 parts
= line
.split()
2136 if len(parts
) == 3 and parts
[0].isdigit():
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
)
2145 "NOT killing %s (pid %d) (not an orphan?)"
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
:
2158 if os
.path
.isfile(self
.start_script
):
2159 with
open(self
.start_script
, "r") as fh
:
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
:
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
)
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"
2192 # android uses the new DB formats exclusively
2193 certdbPath
= "sql:" + options
.profilePath
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
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
)
2209 [certutil
, "-N", "-d", certdbPath
, "-f", pwfilePath
], env
=toolsEnv
2214 # Walk the cert directory and add custom CAs and client certs
2215 files
= os
.listdir(options
.certPath
)
2217 root
, ext
= os
.path
.splitext(item
)
2220 if root
.endswith("-object"):
2221 trustBits
= "CT,,CT"
2227 os
.path
.join(options
.certPath
, item
),
2239 elif ext
== ".client":
2244 os
.path
.join(options
.certPath
, item
),
2253 os
.unlink(pwfilePath
)
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
):
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)
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
)
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.
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
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
)
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")
2327 self
.conditioned_profile_dir
,
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
)
2354 cond_prof_target_dir
= get_profile(
2358 repo
="mozilla-central",
2360 retries
=2, # quicker failure
2364 # any other error is a showstopper
2365 self
.log
.critical("Could not get the conditioned profile")
2366 traceback
.print_exc()
2370 self
.log
.info("retrying a profile with no version specified")
2371 cond_prof_target_dir
= get_profile(
2375 repo
="mozilla-central",
2379 self
.log
.critical("Could not get the conditioned profile")
2380 traceback
.print_exc()
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
):
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
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 (
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)
2427 os
.remove(os
.path
.join(options
.profilePath
, "key4.db"))
2428 except Exception as e
:
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
,
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
:
2454 "TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed"
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)
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,
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
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)")
2492 # tsan builds need even more time
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)")
2501 if mozinfo
.info
["os"] == "win" and mozinfo
.info
["processor"] == "aarch64":
2502 test_timeout
= self
.DEFAULT_TIMEOUT
* 4
2504 "Increasing default timeout to {} seconds (win aarch64)".format(
2509 if "MOZ_CHAOSMODE=0xfb" in options
.environment
and test_timeout
:
2512 "Increasing default timeout to {} seconds (MOZ_CHAOSMODE)".format(
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
)
2537 def getGMPPluginPath(self
, options
):
2538 if options
.gmp_path
:
2539 return options
.gmp_path
2542 # For local builds, GMP plugins will be under dist/bin.
2544 # For packaged builds, GMP plugins will get copied under
2546 os
.path
.join(self
.profile
.profile
, "plugins"),
2550 os
.path
.join("gmp-fake", "1.0"),
2551 os
.path
.join("gmp-fakeopenh264", "1.0"),
2552 os
.path
.join("gmp-clearkey", "0.1"),
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
))
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"):
2575 if hasattr(self
, "extraTestsDirs"):
2576 for d
in self
.extraTestsDirs
:
2577 if os
.path
.exists(d
):
2579 if options
.pidFile
!= "" and os
.path
.exists(options
.pidFile
):
2581 os
.remove(options
.pidFile
)
2582 if os
.path
.exists(options
.pidFile
+ ".xpcshell.pid"):
2583 os
.remove(options
.pidFile
+ ".xpcshell.pid")
2586 "cleaning up pidfile '%s' was unsuccessful from the test harness"
2589 options
.manifestFile
= None
2591 if hasattr(self
, "virtualDeviceIdList"):
2592 pactl
= spawn
.find_executable("pactl")
2595 self
.log
.error("Could not find pactl on system")
2598 for id in self
.virtualDeviceIdList
:
2600 subprocess
.check_call([pactl
, "unload-module", str(id)])
2601 except subprocess
.CalledProcessError
:
2603 "Could not remove pulse module with id {}".format(id)
2607 self
.virtualDeviceIdList
= []
2609 def dumpScreen(self
, utilityPath
):
2610 if self
.haveDumpedScreen
:
2612 "Not taking screenshot here: see the one that was previously logged"
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
2624 self
.log
.info("Killing process: %s" % processPID
)
2626 self
.dumpScreen(utilityPath
)
2628 if mozinfo
.info
.get("crashreporter", True) and not debuggerInfo
:
2630 minidump_path
= os
.path
.join(self
.profile
.profile
, "minidumps")
2631 mozcrash
.kill_and_get_minidump(processPID
, minidump_path
, utilityPath
)
2633 # https://bugzilla.mozilla.org/show_bug.cgi?id=921509
2634 self
.log
.info("Can't trigger Breakpad, process no longer exists")
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.
2646 if parent_pid
and HAVE_PSUTIL
:
2647 self
.log
.info("Determining child pids from psutil...")
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
)
2655 pid_re
= re
.compile(r
"==> process \d+ launched child process (\d+)")
2656 with
open(process_log
) as fd
:
2658 self
.log
.info(line
.rstrip())
2659 m
= pid_re
.search(line
)
2661 rv
.add(int(m
.group(1)))
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
2673 # scan processLog for zombies
2674 self
.log
.info("zombiecheck | Reading PID log: %s" % processLog
)
2675 processList
= self
.extract_child_pids(processLog
)
2678 for processPID
in processList
:
2680 "zombiecheck | Checking for orphan process with PID: %d" % processPID
2682 if isPidAlive(processPID
):
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
2694 def checkForRunningBrowsers(self
):
2697 attrs
= ["pid", "ppid", "name", "cmdline", "username"]
2698 for proc
in psutil
.process_iter():
2700 if "firefox" in proc
.name():
2701 firefoxes
= "%s%s\n" % (firefoxes
, proc
.as_dict(attrs
=attrs
))
2703 # may not be able to access process info for all processes
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
)
2723 valgrindSuppFiles
=None,
2726 detectShutdownLeaks
=False,
2727 screenshotOnFail
=False,
2729 restartAfterFailure
=False,
2730 marionette_args
=None,
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
2750 interactive
= debuggerInfo
.interactive
2751 debug_args
= [debuggerInfo
.path
] + debuggerInfo
.args
2753 # Set up Valgrind arguments.
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(",")
2768 + mozdebug
.get_default_valgrind_args()
2769 + valgrindArgs_split
2770 + valgrindSuppFiles_final
2773 # fix default timeout
2775 timeout
= self
.DEFAULT_TIMEOUT
2777 # Note in the log if running on Valgrind
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
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.
2794 # set process log environment variable
2795 tmpfd
, processLog
= tempfile
.mkstemp(suffix
="pidlog")
2797 env
["MOZ_PROCESS_LOG"] = processLog
2800 # If a debugger is attached, don't use timeouts, and don't
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
:
2816 env
["MOZ_LOG"] + "," if env
["MOZ_LOG"] else ""
2817 ) + "DocShellAndDOMWindowLeak:3"
2818 shutdownLeaks
= ShutdownLeaks(self
.log
)
2820 shutdownLeaks
= None
2822 if mozinfo
.info
["asan"] and mozinfo
.isLinux
and mozinfo
.bits
== 64:
2823 lsanLeaks
= LSANLeaks(self
.log
)
2827 # create an instance to process the output
2828 outputHandler
= self
.OutputHandler(
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
2852 "kill_on_timeout": False,
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
,
2873 process_class
=mozprocess
.ProcessHandlerMixin
,
2874 process_args
=kp_kwargs
,
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
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
):
2909 "TEST-UNEXPECTED-FAIL | invalid setup: missing extension at %s"
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()
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
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()
2941 "runtests.py | Failed to get app exit code - running/crashed?"
2943 # must report an integer to process_exit()
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
2953 # no need to keep return code 137, 245, etc.
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
2961 "action": "test_end",
2963 "expected": "CRASH",
2966 "source": "mochitest",
2967 "time": int(time
.time()) * 1000,
2968 "test": self
.lastTestSeen
,
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
)
2975 self
.lastTestSeen
= (
2976 currentManifest
or "Main app process exited normally"
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
2994 minidump_path
= os
.path
.join(self
.profile
.profile
, "minidumps")
2995 crash_count
= mozcrash
.log_crashes(
2999 test
=self
.lastTestSeen
,
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
3011 elif crash_count
or zombieProcesses
:
3012 if self
.message_logger
.is_test_running
:
3017 # send this out so we always wrap up the test-end message
3019 "action": "test_end",
3021 "expected": expected
,
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
)
3034 if os
.path
.exists(processLog
):
3035 os
.remove(processLog
)
3036 for p
in temp_file_paths
:
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()
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
:
3072 pactl
= spawn
.find_executable("pactl")
3075 self
.log
.error("Could not find pactl on system")
3078 def getModuleIds(moduleName
):
3079 o
= subprocess
.check_output([pactl
, "list", "modules", "short"])
3081 for input in o
.splitlines():
3082 device
= input.decode().split("\t")
3083 if device
[1] == moduleName
:
3084 list.append(int(device
[0]))
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")
3095 len(outputDeviceIdList
) == OUTPUT_DEVICES_COUNT
3096 and len(inputDeviceIdList
) == INPUT_DEVICES_COUNT
3098 self
.virtualDeviceIdList
= outputDeviceIdList
+ inputDeviceIdList
3101 # Remove any existing devices and reinitialize properly
3102 for id in outputDeviceIdList
+ inputDeviceIdList
:
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))
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")
3118 o
= subprocess
.check_output(
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
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
),
3140 o
= subprocess
.check_output(complete_command
)
3141 idList
.append(int(o
))
3143 except subprocess
.CalledProcessError
:
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
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
))
3159 norm_paths
.append(p
)
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
)
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
:
3176 "TEST-UNEXPECTED-FAIL | Bisection | Please ignore repeats "
3177 "and look for 'Bleedthrough' (if any) at the end of "
3182 result
= self
.doTests(options
, testsToRun
, manifestToFilter
)
3183 if result
== TBPL_RETRY
: # terminate task
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
:
3193 firstFail
= len(testsToRun
)
3194 for key
in self
.expectedError
:
3195 full_key
= [x
for x
in testsToRun
if key
in x
]
3197 if testsToRun
.index(full_key
[0]) < firstFail
:
3198 firstFail
= testsToRun
.index(full_key
[0])
3199 testsToRun
= testsToRun
[firstFail
+ 1 :]
3200 if testsToRun
== []:
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()
3216 def groupTestsByScheme(self
, tests
):
3218 split tests into groups by schemes. test is classified as http if
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
3237 # Number of times to repeat test(s) when running with --repeat
3239 # Number of times to repeat test(s) when running test in
3240 VERIFY_REPEAT_SINGLE_BROWSER
= 5
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()
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()
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()
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()
3290 options
.environment
.remove("MOZ_CHAOSMODE=0xfb")
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()
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
:
3312 ("1. Run each test without fission.", fission_step1
),
3313 ("2. Run each test with fission.", fission_step2
),
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
,
3324 "3. Run each test %d times in one browser, in chaos mode."
3329 "4. Run each test %d times in a new browser each time, "
3330 "in chaos mode." % VERIFY_REPEAT_SINGLE_BROWSER
,
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!")
3346 "::: So far, all checks passed, but not all checks were run."
3349 self
.log
.info(":::")
3350 self
.log
.info('::: Running test verification step "%s"...' % descr
)
3351 self
.log
.info(":::")
3354 stepResults
[descr
] = "FAIL"
3355 finalResult
= "FAILED!"
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
)
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(":::")
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.
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),
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
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.")
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
)
3466 # code for --run-by-manifest
3467 manifests
= set(t
["manifest"].replace("\\", "/") for t
in tests
)
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]
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("="))
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()
3493 prefs
= prefs
.strip().split()
3495 "The following extra prefs will be set:\n {}".format(
3499 self
.extraPrefs
.update(parse_preferences(prefs
))
3501 envVars
= list(self
.env_vars_by_manifest
[m
])[0]
3504 self
.extraEnv
= envVars
.strip().split()
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
3520 result
= result
or res
3522 # Dump the logging buffer
3523 self
.message_logger
.dump_buffered()
3524 self
.log
.group_end(name
=m
)
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
3536 self
.countpass
== self
.countfail
== self
.counttodo
== 0
3537 and options
.crashAsPass
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 ***")
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
)
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)
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
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
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
)
3622 self
.log
.error("Could not find test media devices to use")
3624 self
.mediaDevices
= devices
3626 # See if we were asked to run on Valgrind
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
:
3639 "Specified --valgrind-args or --valgrind-supp-files,"
3640 " but not --valgrind"
3644 if valgrindPath
and debuggerInfo
:
3645 self
.log
.error("Can't use both --debugger and --valgrind together")
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:
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:
3666 self
.browserEnv
["MOZ_LOG_FILE"] = "{}/moz-pid=%PID-uid={}.log".format(
3667 self
.browserEnv
["MOZ_UPLOAD_DIR"], str(uuid
.uuid4())
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
3693 timeout
= options
.timeout
+ 30
3694 elif options
.debugger
or options
.jsdebugger
or not options
.autorun
:
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.
3705 if "MOZ_CHAOSMODE=0xfb" in options
.environment
and timeout
:
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
)
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,
3741 self
.currentTests
= [t
["path"] for t
in tests
]
3742 testURL
= self
.buildTestURL(options
, scheme
=scheme
)
3744 self
.buildURLOptions(options
, self
.browserEnv
)
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
))
3759 "runtests.py | Running with e10s: {}".format(options
.e10s
)
3762 "runtests.py | Running with fission: {}".format(
3763 mozinfo
.info
.get("fission", True)
3767 "runtests.py | Running with cross-origin iframes: {}".format(
3768 mozinfo
.info
.get("xorigin", False)
3772 "runtests.py | Running with serviceworker_e10s: {}".format(
3773 mozinfo
.info
.get("serviceworker_e10s", False)
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(
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
,
3795 detectShutdownLeaks
=detectShutdownLeaks
,
3796 screenshotOnFail
=options
.screenshotOnFail
,
3797 bisectChunk
=options
.bisectChunk
,
3798 restartAfterFailure
=options
.restartAfterFailure
,
3799 marionette_args
=marionette_args
,
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")
3809 except Exception as e
:
3810 traceback
.print_exc()
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")
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
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
,
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)
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)
3867 "action": "test_end",
3868 "status": "TIMEOUT",
3872 "source": "mochitest",
3873 "time": int(time
.time()) * 1000,
3874 "test": self
.lastTestSeen
,
3875 "message": "application timed out after %d seconds with no output"
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
)
3891 browser_proc
= [psutil
.Process(browser_pid
)]
3893 self
.log
.info("Failed to get proc for pid %d" % browser_pid
)
3896 child_procs
= [psutil
.Process(pid
) for pid
in child_pids
]
3898 self
.log
.info("Failed to get 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)
3906 self
.log
.info("psutil found pid %s dead" % p
.pid
)
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)
3914 self
.log
.info("psutil found pid %s dead" % p
.pid
)
3916 self
.log
.warning("failed to kill pid %d after 30s" % p
.pid
)
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
3930 self
.killAndGetStack(
3931 browser_pid
, utilityPath
, debuggerInfo
, dump_screen
=not debuggerInfo
3934 def archiveMozLogs(self
):
3936 with zipfile
.ZipFile(
3937 "{}/mozLogs.zip".format(os
.environ
["MOZ_UPLOAD_DIR"]),
3939 zipfile
.ZIP_DEFLATED
,
3941 for logfile
in glob
.glob(
3942 "{}/moz*.log*".format(os
.environ
["MOZ_UPLOAD_DIR"])
3944 logzip
.write(logfile
, os
.path
.basename(logfile
))
3948 class OutputHandler(object):
3950 """line output handler for mozrunner"""
3957 dump_screen_on_timeout
=True,
3958 dump_screen_on_fail
=False,
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
3988 for handler
in self
.outputHandlers():
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"""
4000 self
.record_last_test
,
4001 self
.dumpScreenOnTimeout
,
4002 self
.dumpScreenOnFail
,
4003 self
.trackShutdownLeaks
,
4004 self
.trackLSANLeaks
,
4007 if self
.bisectChunk
or self
.restartAfterFailure
:
4008 handlers
.append(self
.record_result
)
4009 handlers
.append(self
.first_error
)
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
)
4020 if self
.shutdownLeaks
:
4021 numFailures
, errorMessages
= self
.shutdownLeaks
.process()
4022 self
.harness
.countfail
+= numFailures
4023 for message
in errorMessages
:
4025 "action": "test_end",
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
)
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"
4057 def first_error(self
, message
):
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"]
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", "")
4079 val
= int(line
.split(":")[-1].strip())
4080 except (AttributeError, ValueError):
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
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"])
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"])
4107 def dumpScreenOnTimeout(self
, message
):
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
)
4118 def dumpScreenOnFail(self
, message
):
4120 self
.dump_screen_on_fail
4121 and "expected" in message
4122 and message
["status"] == "FAIL"
4124 self
.harness
.dumpScreen(self
.utilityPath
)
4127 def trackLSANLeaks(self
, message
):
4128 if self
.lsanLeaks
and message
["action"] in ("log", "process_output"):
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
)
4137 self
.lsanLeaks
.log(line
, self
.harness
.lastTestSeen
)
4140 def trackShutdownLeaks(self
, message
):
4141 if self
.shutdownLeaks
:
4142 self
.shutdownLeaks
.log(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.
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."
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
)
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
)
4200 result
= runner
.runTests(options
)
4202 runner
.archiveMozLogs()
4203 runner
.message_logger
.finish()
4207 def cli(args
=sys
.argv
[1:]):
4208 # parse command line options
4209 parser
= MochitestArgumentParser(app
="generic")
4210 options
= parser
.parse_args(args
)
4215 return run_test_harness(parser
, options
)
4218 if __name__
== "__main__":