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