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