runxpcshelltests.py (102859B)
1 #!/usr/bin/env python 2 # 3 # This Source Code Form is subject to the terms of the Mozilla Public 4 # License, v. 2.0. If a copy of the MPL was not distributed with this 5 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7 import copy 8 import json 9 import os 10 import platform 11 import random 12 import re 13 import shlex 14 import shutil 15 import signal 16 import subprocess 17 import sys 18 import tempfile 19 import time 20 import traceback 21 from argparse import Namespace 22 from collections import defaultdict, deque, namedtuple 23 from contextlib import contextmanager 24 from datetime import datetime, timedelta 25 from functools import partial 26 from multiprocessing import cpu_count 27 from subprocess import PIPE, STDOUT, Popen 28 from tempfile import gettempdir, mkdtemp 29 from threading import Event, Thread, Timer, current_thread 30 31 import mozdebug 32 import six 33 from mozgeckoprofiler import ( 34 symbolicate_profile_json, 35 view_gecko_profile, 36 ) 37 from mozserve import Http3Server 38 39 try: 40 import psutil 41 42 HAVE_PSUTIL = True 43 except Exception: 44 HAVE_PSUTIL = False 45 46 from xpcshellcommandline import parser_desktop 47 48 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) 49 50 try: 51 from mozbuild.base import MozbuildObject 52 53 build = MozbuildObject.from_environment(cwd=SCRIPT_DIR) 54 except ImportError: 55 build = None 56 57 HARNESS_TIMEOUT = 30 58 TBPL_RETRY = 4 # defined in mozharness 59 60 # Based on recent benchmarking on highcpu pools, this value gives the best 61 # balance between runtime and memory usage 62 # 63 # Note: 64 # - NUM_THREADS defines the maximum number of tests that can run in parallel 65 # - With e10s/fission enabled, the actual number of underlying processes/threads 66 # can be much higher, so memory pressure may vary accordingly 67 # - For ASan/TSan variants, the thread count is reduced by half to avoid OOM 68 # 69 # This value can be overridden via the --threadCount CLI option if adjustments 70 # are needed for custom CPU/memory configurations 71 NUM_THREADS = int(cpu_count() * 2.5) 72 73 EXPECTED_LOG_ACTIONS = set([ 74 "crash_reporter_init", 75 "test_status", 76 "log", 77 ]) 78 79 # -------------------------------------------------------------- 80 # TODO: this is a hack for mozbase without virtualenv, remove with bug 849900 81 # 82 here = os.path.dirname(__file__) 83 mozbase = os.path.realpath(os.path.join(os.path.dirname(here), "mozbase")) 84 85 if os.path.isdir(mozbase): 86 for package in os.listdir(mozbase): 87 sys.path.append(os.path.join(mozbase, package)) 88 89 import mozcrash 90 import mozfile 91 import mozinfo 92 from manifestparser import TestManifest 93 from manifestparser.expression import parse 94 from manifestparser.filters import chunk_by_slice, failures, pathprefix, tags 95 from manifestparser.util import normsep 96 from mozlog import commandline 97 from mozprofile import Profile 98 from mozprofile.cli import parse_key_value, parse_preferences 99 from mozrunner.utils import get_stack_fixer_function 100 101 # -------------------------------------------------------------- 102 103 # TODO: perhaps this should be in a more generally shared location? 104 # This regex matches all of the C0 and C1 control characters 105 # (U+0000 through U+001F; U+007F; U+0080 through U+009F), 106 # except TAB (U+0009), CR (U+000D), LF (U+000A) and backslash (U+005C). 107 # A raw string is deliberately not used. 108 _cleanup_encoding_re = re.compile("[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\\\\]") 109 110 111 def get_full_group_name(test): 112 group = test["manifest"] 113 if "ancestor_manifest" in test: 114 ancestor_manifest = normsep(test["ancestor_manifest"]) 115 # Only change the group id if ancestor is not the generated root manifest. 116 if "/" in ancestor_manifest: 117 group = f"{ancestor_manifest}:{group}" 118 return group 119 120 121 def _cleanup_encoding_repl(m): 122 c = m.group(0) 123 return "\\\\" if c == "\\" else f"\\x{ord(c):02X}" 124 125 126 def cleanup_encoding(s): 127 """S is either a byte or unicode string. Either way it may 128 contain control characters, unpaired surrogates, reserved code 129 points, etc. If it is a byte string, it is assumed to be 130 UTF-8, but it may not be *correct* UTF-8. Return a 131 sanitized unicode object.""" 132 if not isinstance(s, str): 133 if isinstance(s, bytes): 134 return six.ensure_str(s) 135 else: 136 return str(s) 137 if isinstance(s, bytes): 138 s = s.decode("utf-8", "replace") 139 # Replace all C0 and C1 control characters with \xNN escapes. 140 return _cleanup_encoding_re.sub(_cleanup_encoding_repl, s) 141 142 143 @contextmanager 144 def popenCleanupHack(): 145 """ 146 Hack to work around https://bugs.python.org/issue37380 147 The basic idea is that on old versions of Python on Windows, 148 we need to clear subprocess._cleanup before we call Popen(), 149 then restore it afterwards. 150 """ 151 savedCleanup = None 152 if mozinfo.isWin and sys.version_info[0] == 3 and sys.version_info < (3, 7, 5): 153 savedCleanup = subprocess._cleanup 154 subprocess._cleanup = lambda: None 155 try: 156 yield 157 finally: 158 if savedCleanup: 159 subprocess._cleanup = savedCleanup 160 161 162 """ Control-C handling """ 163 gotSIGINT = False 164 165 166 def markGotSIGINT(signum, stackFrame): 167 global gotSIGINT 168 gotSIGINT = True 169 170 171 class XPCShellTestThread(Thread): 172 def __init__( 173 self, 174 test_object, 175 retry=None, 176 verbose=False, 177 usingTSan=False, 178 usingCrashReporter=False, 179 **kwargs, 180 ): 181 Thread.__init__(self) 182 self.daemon = True 183 184 self.test_object = test_object 185 self.retry = retry 186 if retry is None: 187 # Retry in CI, but report results without retry when run locally to 188 # avoid confusion and ease local debugging. 189 self.retry = os.environ.get("MOZ_AUTOMATION") is not None 190 self.verbose = verbose 191 self.usingTSan = usingTSan 192 self.usingCrashReporter = usingCrashReporter 193 194 self.appPath = kwargs.get("appPath") 195 self.xrePath = kwargs.get("xrePath") 196 self.utility_path = kwargs.get("utility_path") 197 self.testingModulesDir = kwargs.get("testingModulesDir") 198 self.debuggerInfo = kwargs.get("debuggerInfo") 199 self.jsDebuggerInfo = kwargs.get("jsDebuggerInfo") 200 self.headJSPath = kwargs.get("headJSPath") 201 self.testharnessdir = kwargs.get("testharnessdir") 202 self.profileName = kwargs.get("profileName") 203 self.singleFile = kwargs.get("singleFile") 204 self.env = copy.deepcopy(kwargs.get("env")) 205 self.symbolsPath = kwargs.get("symbolsPath") 206 self.logfiles = kwargs.get("logfiles") 207 self.app_binary = kwargs.get("app_binary") 208 self.xpcshell = kwargs.get("xpcshell") 209 self.xpcsRunArgs = kwargs.get("xpcsRunArgs") 210 self.failureManifest = kwargs.get("failureManifest") 211 self.jscovdir = kwargs.get("jscovdir") 212 self.stack_fixer_function = kwargs.get("stack_fixer_function") 213 self._rootTempDir = kwargs.get("tempDir") 214 self.cleanup_dir_list = kwargs.get("cleanup_dir_list") 215 self.pStdout = kwargs.get("pStdout") 216 self.pStderr = kwargs.get("pStderr") 217 self.keep_going = kwargs.get("keep_going") 218 self.log = kwargs.get("log") 219 self.app_dir_key = kwargs.get("app_dir_key") 220 self.interactive = kwargs.get("interactive") 221 self.rootPrefsFile = kwargs.get("rootPrefsFile") 222 self.extraPrefs = kwargs.get("extraPrefs") 223 self.verboseIfFails = kwargs.get("verboseIfFails") 224 self.headless = kwargs.get("headless") 225 self.selfTest = kwargs.get("selfTest") 226 self.runFailures = kwargs.get("runFailures") 227 self.timeoutAsPass = kwargs.get("timeoutAsPass") 228 self.crashAsPass = kwargs.get("crashAsPass") 229 self.conditionedProfileDir = kwargs.get("conditionedProfileDir") 230 self.profiler = kwargs.get("profiler") 231 if self.runFailures: 232 self.retry = False 233 234 # Default the test prefsFile to the rootPrefsFile. 235 self.prefsFile = self.rootPrefsFile 236 237 # only one of these will be set to 1. adding them to the totals in 238 # the harness 239 self.passCount = 0 240 self.todoCount = 0 241 self.failCount = 0 242 243 # Context for output processing 244 self.output_lines = [] 245 self.has_failure_output = False 246 self.saw_crash_reporter_init = False 247 self.saw_proc_start = False 248 self.saw_proc_end = False 249 self.command = None 250 self.harness_timeout = kwargs.get("harness_timeout") 251 self.timedout = False 252 self.infra = False 253 254 # event from main thread to signal work done 255 self.event = kwargs.get("event") 256 self.done = False # explicitly set flag so we don't rely on thread.isAlive 257 258 def run(self): 259 try: 260 self.run_test() 261 except PermissionError as e: 262 self.infra = True 263 self.exception = e 264 self.traceback = traceback.format_exc() 265 except Exception as e: 266 self.exception = e 267 self.traceback = traceback.format_exc() 268 else: 269 self.exception = None 270 self.traceback = None 271 if self.retry: 272 self.log.info( 273 "%s failed or timed out, will retry." % self.test_object["id"] 274 ) 275 self.done = True 276 self.event.set() 277 278 def kill(self, proc): 279 """ 280 Simple wrapper to kill a process. 281 On a remote system, this is overloaded to handle remote process communication. 282 """ 283 return proc.kill() 284 285 def removeDir(self, dirname): 286 """ 287 Simple wrapper to remove (recursively) a given directory. 288 On a remote system, we need to overload this to work on the remote filesystem. 289 """ 290 mozfile.remove(dirname) 291 292 def poll(self, proc): 293 """ 294 Simple wrapper to check if a process has terminated. 295 On a remote system, this is overloaded to handle remote process communication. 296 """ 297 return proc.poll() 298 299 def createLogFile(self, test_file, stdout): 300 """ 301 For a given test file and stdout buffer, create a log file. 302 On a remote system we have to fix the test name since it can contain directories. 303 """ 304 with open(test_file + ".log", "w") as f: 305 f.write(stdout) 306 307 def getReturnCode(self, proc): 308 """ 309 Simple wrapper to get the return code for a given process. 310 On a remote system we overload this to work with the remote process management. 311 """ 312 if proc is not None and hasattr(proc, "returncode"): 313 return proc.returncode 314 return -1 315 316 def communicate(self, proc): 317 """ 318 Simple wrapper to communicate with a process. 319 On a remote system, this is overloaded to handle remote process communication. 320 """ 321 # Processing of incremental output put here to 322 # sidestep issues on remote platforms, where what we know 323 # as proc is a file pulled off of a device. 324 if proc.stdout: 325 while True: 326 line = proc.stdout.readline() 327 if not line: 328 break 329 self.process_line(line) 330 331 if self.saw_proc_start and not self.saw_proc_end: 332 self.has_failure_output = True 333 334 return proc.communicate() 335 336 def launchProcess( 337 self, cmd, stdout, stderr, env, cwd, timeout=None, test_name=None 338 ): 339 """ 340 Simple wrapper to launch a process. 341 On a remote system, this is more complex and we need to overload this function. 342 """ 343 # timeout is needed by remote xpcshell to extend the 344 # remote device timeout. It is not used in this function. 345 if six.PY3: 346 cwd = six.ensure_str(cwd) 347 for i in range(len(cmd)): 348 cmd[i] = six.ensure_str(cmd[i]) 349 350 if HAVE_PSUTIL: 351 popen_func = psutil.Popen 352 else: 353 popen_func = Popen 354 355 with popenCleanupHack(): 356 proc = popen_func(cmd, stdout=stdout, stderr=stderr, env=env, cwd=cwd) 357 358 return proc 359 360 def checkForCrashes(self, dump_directory, symbols_path, test_name=None): 361 """ 362 Simple wrapper to check for crashes. 363 On a remote system, this is more complex and we need to overload this function. 364 """ 365 quiet = self.crashAsPass or self.retry 366 # For selftests, set dump_save_path to prevent crash dumps from being saved 367 # (they intentionally crash and the dumps aren't useful artifacts) 368 dump_save_path = "" if self.selfTest else None 369 return mozcrash.log_crashes( 370 self.log, 371 dump_directory, 372 symbols_path, 373 test=test_name, 374 quiet=quiet, 375 dump_save_path=dump_save_path, 376 ) 377 378 def logCommand(self, name, completeCmd, testdir): 379 self.log.info("%s | full command: %r" % (name, completeCmd)) 380 self.log.info("%s | current directory: %r" % (name, testdir)) 381 # Show only those environment variables that are changed from 382 # the ambient environment. 383 changedEnv = set("%s=%s" % i for i in self.env.items()) - set( 384 "%s=%s" % i for i in os.environ.items() 385 ) 386 self.log.info("%s | environment: %s" % (name, list(changedEnv))) 387 shell_command_tokens = [ 388 shlex.quote(tok) for tok in list(changedEnv) + completeCmd 389 ] 390 self.log.info( 391 "%s | as shell command: (cd %s; %s)" 392 % (name, shlex.quote(testdir), " ".join(shell_command_tokens)) 393 ) 394 395 def killTimeout(self, proc): 396 if proc is not None and hasattr(proc, "pid"): 397 mozcrash.kill_and_get_minidump( 398 proc.pid, self.tempDir, utility_path=self.utility_path 399 ) 400 else: 401 self.log.info("not killing -- proc or pid unknown") 402 403 def postCheck(self, proc): 404 """Checks for a still-running test process, kills it and fails the test if found. 405 We can sometimes get here before the process has terminated, which would 406 cause removeDir() to fail - so check for the process and kill it if needed. 407 """ 408 if proc and self.poll(proc) is None: 409 if HAVE_PSUTIL: 410 try: 411 self.kill(proc) 412 except psutil.NoSuchProcess: 413 pass 414 else: 415 self.kill(proc) 416 message = "%s | Process still running after test!" % self.test_object["id"] 417 if self.retry: 418 self.log.info(message) 419 return 420 421 self.log.error(message) 422 self.log_full_output() 423 self.failCount = 1 424 425 def testTimeout(self, proc): 426 # Set these flags first to prevent test_end from being logged again 427 # while we output the full log. 428 self.done = True 429 self.timedout = True 430 431 # Kill the test process before calling log_full_output that can take a 432 # a while due to stack fixing. 433 self.killTimeout(proc) 434 435 if self.test_object["expected"] == "pass": 436 expected = "PASS" 437 else: 438 expected = "FAIL" 439 440 extra = None 441 if self.timeout_factor > 1: 442 extra = {"timeoutfactor": self.timeout_factor} 443 444 if self.retry: 445 self.log.test_end( 446 self.test_object["id"], 447 "TIMEOUT", 448 expected="TIMEOUT", 449 message="Test timed out", 450 extra=extra, 451 ) 452 self.log_full_output(mark_failures_as_expected=True) 453 else: 454 result = "TIMEOUT" 455 if self.timeoutAsPass: 456 expected = "FAIL" 457 result = "FAIL" 458 self.failCount = 1 459 self.log.test_end( 460 self.test_object["id"], 461 result, 462 expected=expected, 463 message="Test timed out", 464 extra=extra, 465 ) 466 self.log_full_output() 467 468 self.log.info("xpcshell return code: %s" % self.getReturnCode(proc)) 469 self.postCheck(proc) 470 self.clean_temp_dirs(self.test_object["path"]) 471 472 def updateTestPrefsFile(self): 473 # If the Manifest file has some additional prefs, merge the 474 # prefs set in the user.js file stored in the _rootTempdir 475 # with the prefs from the manifest and the prefs specified 476 # in the extraPrefs option. 477 if "prefs" in self.test_object: 478 # Merge the user preferences in a fake profile dir in a 479 # local temporary dir (self.tempDir is the remoteTmpDir 480 # for the RemoteXPCShellTestThread subclass and so we 481 # can't use that tempDir here). 482 localTempDir = mkdtemp(prefix="xpc-other-", dir=self._rootTempDir) 483 484 filename = "user.js" 485 interpolation = {"server": "dummyserver"} 486 profile = Profile(profile=localTempDir, restore=False) 487 # _rootTempDir contains a user.js file, generated by buildPrefsFile 488 profile.merge(self._rootTempDir, interpolation=interpolation) 489 490 prefs = self.test_object["prefs"].strip() 491 if prefs: 492 prefs = [p.strip() for p in prefs.split("\n")] 493 name = self.test_object["id"] 494 if self.verbose: 495 self.log.info( 496 "%s: Per-test extra prefs will be set:\n {}".format( 497 "\n ".join(prefs) 498 ) 499 % name 500 ) 501 502 profile.set_preferences(parse_preferences(prefs), filename=filename) 503 # Make sure that the extra prefs form the command line are overriding 504 # any prefs inherited from the shared profile data or the manifest prefs. 505 profile.set_preferences( 506 parse_preferences(self.extraPrefs), filename=filename 507 ) 508 return os.path.join(profile.profile, filename) 509 510 # Return the root prefsFile if there is no other prefs to merge. 511 # This is the path set by buildPrefsFile. 512 return self.rootPrefsFile 513 514 def updateTestEnvironment(self): 515 # Add additional environment variables from the Manifest file 516 extraEnv = {} 517 if "environment" in self.test_object: 518 extraEnv = self.test_object["environment"].strip().split() 519 self.log.info( 520 "The following extra environment variables will be set:\n {}".format( 521 "\n ".join(extraEnv) 522 ) 523 ) 524 self.env.update( 525 dict( 526 parse_key_value( 527 extraEnv, context="environment variables in manifest" 528 ) 529 ) 530 ) 531 532 @property 533 def conditioned_profile_copy(self): 534 """Returns a copy of the original conditioned profile that was created.""" 535 536 condprof_copy = os.path.join(tempfile.mkdtemp(), "profile") 537 shutil.copytree( 538 self.conditionedProfileDir, 539 condprof_copy, 540 ignore=shutil.ignore_patterns("lock"), 541 ) 542 self.log.info("Created a conditioned-profile copy: %s" % condprof_copy) 543 return condprof_copy 544 545 def buildCmdTestFile(self, name): 546 """ 547 Build the command line arguments for the test file. 548 On a remote system, this may be overloaded to use a remote path structure. 549 """ 550 return ["-e", 'const _TEST_FILE = ["%s"];' % name.replace("\\", "/")] 551 552 def setupTempDir(self): 553 tempDir = mkdtemp(prefix="xpc-other-", dir=self._rootTempDir) 554 self.env["XPCSHELL_TEST_TEMP_DIR"] = tempDir 555 if self.interactive: 556 self.log.info("temp dir is %s" % tempDir) 557 return tempDir 558 559 def setupProfileDir(self): 560 """ 561 Create a temporary folder for the profile and set appropriate environment variables. 562 When running check-interactive and check-one, the directory is well-defined and 563 retained for inspection once the tests complete. 564 565 On a remote system, this may be overloaded to use a remote path structure. 566 """ 567 if self.conditionedProfileDir: 568 profileDir = self.conditioned_profile_copy 569 elif self.interactive or (self.singleFile and not self.selfTest): 570 profileDir = os.path.join(gettempdir(), self.profileName, "xpcshellprofile") 571 try: 572 # This could be left over from previous runs 573 self.removeDir(profileDir) 574 except Exception: 575 pass 576 os.makedirs(profileDir) 577 else: 578 profileDir = mkdtemp(prefix="xpc-profile-", dir=self._rootTempDir) 579 self.env["XPCSHELL_TEST_PROFILE_DIR"] = profileDir 580 if self.interactive or self.singleFile: 581 self.log.info("profile dir is %s" % profileDir) 582 return profileDir 583 584 def setupMozinfoJS(self): 585 mozInfoJSPath = os.path.join(self.profileDir, "mozinfo.json") 586 mozInfoJSPath = mozInfoJSPath.replace("\\", "\\\\") 587 mozinfo.output_to_file(mozInfoJSPath) 588 return mozInfoJSPath 589 590 def buildCmdHead(self): 591 """ 592 Build the command line arguments for the head files, 593 along with the address of the webserver which some tests require. 594 595 On a remote system, this is overloaded to resolve quoting issues over a 596 secondary command line. 597 """ 598 headfiles = self.getHeadFiles(self.test_object) 599 cmdH = ", ".join(['"' + f.replace("\\", "/") + '"' for f in headfiles]) 600 601 dbgport = 0 if self.jsDebuggerInfo is None else self.jsDebuggerInfo.port 602 603 return [ 604 "-e", 605 "const _HEAD_FILES = [%s];" % cmdH, 606 "-e", 607 "const _JSDEBUGGER_PORT = %d;" % dbgport, 608 ] 609 610 def getHeadFiles(self, test): 611 """Obtain lists of head- files. Returns a list of head files.""" 612 613 def sanitize_list(s, kind): 614 for f in s.strip().split(" "): 615 f = f.strip() 616 if len(f) < 1: 617 continue 618 619 path = os.path.normpath(os.path.join(test["here"], f)) 620 if not os.path.exists(path): 621 raise Exception("%s file does not exist: %s" % (kind, path)) 622 623 if not os.path.isfile(path): 624 raise Exception("%s file is not a file: %s" % (kind, path)) 625 626 yield path 627 628 headlist = test.get("head", "") 629 return list(sanitize_list(headlist, "head")) 630 631 def buildXpcsCmd(self): 632 """ 633 Load the root head.js file as the first file in our test path, before other head, 634 and test files. On a remote system, we overload this to add additional command 635 line arguments, so this gets overloaded. 636 """ 637 # - NOTE: if you rename/add any of the constants set here, update 638 # do_load_child_test_harness() in head.js 639 if not self.appPath: 640 self.appPath = self.xrePath 641 642 if self.app_binary: 643 xpcsCmd = [ 644 self.app_binary, 645 "--xpcshell", 646 ] 647 else: 648 xpcsCmd = [ 649 self.xpcshell, 650 ] 651 652 xpcsCmd += [ 653 "-g", 654 self.xrePath, 655 "-a", 656 self.appPath, 657 "-m", 658 "-e", 659 'const _HEAD_JS_PATH = "%s";' % self.headJSPath, 660 "-e", 661 'const _MOZINFO_JS_PATH = "%s";' % self.mozInfoJSPath, 662 "-e", 663 'const _PREFS_FILE = "%s";' % self.prefsFile.replace("\\", "\\\\"), 664 ] 665 666 if self.testingModulesDir: 667 # Escape backslashes in string literal. 668 sanitized = self.testingModulesDir.replace("\\", "\\\\") 669 xpcsCmd.extend(["-e", 'const _TESTING_MODULES_DIR = "%s";' % sanitized]) 670 671 xpcsCmd.extend(["-f", os.path.join(self.testharnessdir, "head.js")]) 672 673 if self.debuggerInfo: 674 xpcsCmd = [self.debuggerInfo.path] + self.debuggerInfo.args + xpcsCmd 675 676 return xpcsCmd 677 678 def cleanupDir(self, directory, name): 679 if not os.path.exists(directory): 680 return 681 682 # up to TRY_LIMIT attempts (one every second), because 683 # the Windows filesystem is slow to react to the changes 684 TRY_LIMIT = 25 685 try_count = 0 686 while try_count < TRY_LIMIT: 687 try: 688 self.removeDir(directory) 689 except OSError: 690 self.log.info("Failed to remove directory: %s. Waiting." % directory) 691 # We suspect the filesystem may still be making changes. Wait a 692 # little bit and try again. 693 time.sleep(1) 694 try_count += 1 695 else: 696 # removed fine 697 return 698 699 # we try cleaning up again later at the end of the run 700 self.cleanup_dir_list.append(directory) 701 702 def clean_temp_dirs(self, name): 703 # We don't want to delete the profile when running check-interactive 704 # or check-one. 705 if self.profileDir and not self.interactive and not self.singleFile: 706 self.cleanupDir(self.profileDir, name) 707 708 self.cleanupDir(self.tempDir, name) 709 710 def parse_output(self, output): 711 """Parses process output for structured messages and saves output as it is 712 read. Sets self.has_failure_output in case of evidence of a failure""" 713 for line_string in output.splitlines(): 714 self.process_line(line_string) 715 716 if self.saw_proc_start and not self.saw_proc_end: 717 self.has_failure_output = True 718 719 def fix_text_output(self, line): 720 line = cleanup_encoding(line) 721 if self.stack_fixer_function is not None: 722 line = self.stack_fixer_function(line) 723 724 if isinstance(line, bytes): 725 line = line.decode("utf-8") 726 return line 727 728 def log_line(self, line, time=None): 729 """Log a line of output (either a parser json object or text output from 730 the test process""" 731 if isinstance(line, (str, bytes)): 732 line = self.fix_text_output(line).rstrip("\r\n") 733 kwargs = {"command": self.command, "test": self.test_object["id"]} 734 if time is not None: 735 kwargs["time"] = time 736 self.log.process_output(self.proc_ident, line, **kwargs) 737 else: 738 if "message" in line: 739 line["message"] = self.fix_text_output(line["message"]) 740 if "xpcshell_process" in line: 741 line["thread"] = " ".join([ 742 current_thread().name, 743 line["xpcshell_process"], 744 ]) 745 else: 746 line["thread"] = current_thread().name 747 self.log.log_raw(line) 748 749 def log_full_output(self, mark_failures_as_expected=False): 750 """Logs any buffered output from the test process, and clears the buffer. 751 752 Args: 753 mark_failures_as_expected: If True, failures will be marked as expected 754 (TEST-EXPECTED-FAIL instead of TEST-UNEXPECTED-FAIL). This is used 755 when a test will be retried. 756 """ 757 if not self.output_lines: 758 return 759 log_message = f"full log for {self.test_object['id']}" 760 self.log.info(f">>>>>>> Begin of {log_message}") 761 self.log.group_start("replaying " + log_message) 762 for timestamp, line in self.output_lines: 763 if isinstance(line, dict): 764 # Always ensure the 'test' field is present for resource usage profiles 765 if "test" not in line: 766 line["test"] = self.test_object["id"] 767 768 if mark_failures_as_expected: 769 if line.get("action") == "test_status" and "expected" in line: 770 # Ensure the 'expected' field matches the 'status' to avoid failing the job 771 line["expected"] = line.get("status") 772 elif line.get("action") == "log" and line.get("level") == "ERROR": 773 # Convert ERROR log to test_status so it gets colored 774 # properly without causing a test failure. 775 line["action"] = "test_status" 776 line["status"] = "ERROR" 777 line["expected"] = "ERROR" 778 line["subtest"] = "" 779 del line["level"] 780 self.log_line(line) 781 else: 782 # For text lines, replace text matching error patterns to avoid 783 # mozharness log parsing forcing an error job exit code 784 line = re.sub( 785 r"ERROR: ((Address|Leak)Sanitizer)", r"ERROR (will retry): \1", line 786 ) 787 # Treeherder's log parser catches "fatal error" as an error 788 line = re.sub(r"fatal error", r"error", line) 789 # For text lines, we need to provide the timestamp that was 790 # recorded when appending the message to self.output_lines 791 self.log_line(line, time=timestamp) 792 self.log.info(f"<<<<<<< End of {log_message}") 793 self.log.group_end("replaying " + log_message) 794 self.output_lines = [] 795 796 def report_message(self, message): 797 """Stores or logs a json log message in mozlog format.""" 798 if self.verbose: 799 self.log_line(message) 800 else: 801 # Store timestamp only for string messages (dicts already have timestamps) 802 # We need valid timestamps to replay messages correctly in resource 803 # usage profiles. 804 import time 805 806 timestamp = ( 807 int(time.time() * 1000) if isinstance(message, (str, bytes)) else None 808 ) 809 self.output_lines.append((timestamp, message)) 810 811 def process_line(self, line_string): 812 """Parses a single line of output, determining its significance and 813 reporting a message. 814 """ 815 if isinstance(line_string, bytes): 816 # Transform binary to string representation 817 line_string = line_string.decode(sys.stdout.encoding, errors="replace") 818 819 if not line_string.strip(): 820 return 821 822 try: 823 line_object = json.loads(line_string) 824 if not isinstance(line_object, dict): 825 self.report_message(line_string) 826 return 827 except ValueError: 828 self.report_message(line_string) 829 return 830 831 if ( 832 "action" not in line_object 833 or line_object["action"] not in EXPECTED_LOG_ACTIONS 834 ): 835 # The test process output JSON. 836 self.report_message(line_string) 837 return 838 839 if line_object["action"] == "crash_reporter_init": 840 self.saw_crash_reporter_init = True 841 return 842 843 action = line_object["action"] 844 845 self.has_failure_output = ( 846 self.has_failure_output 847 or "expected" in line_object 848 or action == "log" 849 and line_object["level"] == "ERROR" 850 ) 851 852 self.report_message(line_object) 853 854 if action == "log" and line_object["message"] == "CHILD-TEST-STARTED": 855 self.saw_proc_start = True 856 elif action == "log" and line_object["message"] == "CHILD-TEST-COMPLETED": 857 self.saw_proc_end = True 858 859 def run_test(self): 860 """Run an individual xpcshell test.""" 861 global gotSIGINT 862 863 name = self.test_object["id"] 864 path = self.test_object["path"] 865 group = get_full_group_name(self.test_object) 866 867 # Check for skipped tests 868 if "disabled" in self.test_object: 869 message = self.test_object["disabled"] 870 if not message: 871 message = "disabled from xpcshell manifest" 872 self.log.test_start(name, group=group) 873 self.log.test_end(name, "SKIP", message=message, group=group) 874 875 self.retry = False 876 self.keep_going = True 877 return 878 879 # Check for known-fail tests 880 expect_pass = self.test_object["expected"] == "pass" 881 882 # By default self.appPath will equal the gre dir. If specified in the 883 # xpcshell.toml file, set a different app dir for this test. 884 if self.app_dir_key and self.app_dir_key in self.test_object: 885 rel_app_dir = self.test_object[self.app_dir_key] 886 rel_app_dir = os.path.join(self.xrePath, rel_app_dir) 887 self.appPath = os.path.abspath(rel_app_dir) 888 else: 889 self.appPath = None 890 891 test_dir = os.path.dirname(path) 892 893 # Create a profile and a temp dir that the JS harness can stick 894 # a profile and temporary data in 895 self.profileDir = self.setupProfileDir() 896 self.tempDir = self.setupTempDir() 897 self.mozInfoJSPath = self.setupMozinfoJS() 898 899 # Setup per-manifest prefs and write them into the tempdir. 900 self.prefsFile = self.updateTestPrefsFile() 901 902 # Setup per-manifest env variables 903 self.updateTestEnvironment() 904 905 # The order of the command line is important: 906 # 1) Arguments for xpcshell itself 907 self.command = self.buildXpcsCmd() 908 909 # 2) Arguments for the head files 910 self.command.extend(self.buildCmdHead()) 911 912 # 3) Arguments for the test file 913 self.command.extend(self.buildCmdTestFile(path)) 914 self.command.extend(["-e", 'const _TEST_NAME = "%s";' % name]) 915 self.command.extend([ 916 "-e", 917 'const _EXPECTED = "%s";' % self.test_object["expected"], 918 ]) 919 920 # 4) Arguments for code coverage 921 if self.jscovdir: 922 self.command.extend([ 923 "-e", 924 'const _JSCOV_DIR = "%s";' % self.jscovdir.replace("\\", "/"), 925 ]) 926 927 # 5) Runtime arguments 928 if "debug" in self.test_object: 929 self.command.append("-d") 930 931 self.command.extend(self.xpcsRunArgs) 932 933 if self.test_object.get("dmd") == "true": 934 self.env["PYTHON"] = sys.executable 935 self.env["BREAKPAD_SYMBOLS_PATH"] = self.symbolsPath 936 937 if self.test_object.get("subprocess") == "true": 938 self.env["PYTHON"] = sys.executable 939 940 if self.profiler: 941 if not self.singleFile: 942 self.log.error( 943 "The --profiler flag is currently only supported when running a single test" 944 ) 945 else: 946 self.env["MOZ_PROFILER_STARTUP"] = "1" 947 profile_path = os.path.join( 948 self.profileDir, "profile_" + os.path.basename(name) + ".json" 949 ) 950 self.env["MOZ_PROFILER_SHUTDOWN"] = profile_path 951 952 if ( 953 self.test_object.get("headless", "true" if self.headless else None) 954 == "true" 955 ): 956 self.env["MOZ_HEADLESS"] = "1" 957 self.env["DISPLAY"] = "77" # Set a fake display. 958 959 # Allow a test to request a multiple of the timeout if it is expected to take long 960 self.timeout_factor = 1 961 if "requesttimeoutfactor" in self.test_object: 962 self.timeout_factor = int(self.test_object["requesttimeoutfactor"]) 963 964 testTimeoutInterval = self.harness_timeout * self.timeout_factor 965 966 testTimer = None 967 if not self.interactive and not self.debuggerInfo and not self.jsDebuggerInfo: 968 testTimer = Timer(testTimeoutInterval, lambda: self.testTimeout(proc)) 969 testTimer.start() 970 self.env["MOZ_TEST_TIMEOUT_INTERVAL"] = str(testTimeoutInterval) 971 972 proc = None 973 process_output = None 974 975 try: 976 self.log.test_start(name, group=group) 977 if self.verbose: 978 self.logCommand(name, self.command, test_dir) 979 980 proc = self.launchProcess( 981 self.command, 982 stdout=self.pStdout, 983 stderr=self.pStderr, 984 env=self.env, 985 cwd=test_dir, 986 timeout=testTimeoutInterval, 987 test_name=name, 988 ) 989 990 if hasattr(proc, "pid"): 991 self.proc_ident = proc.pid 992 else: 993 # On mobile, "proc" is just a file. 994 self.proc_ident = name 995 996 if self.interactive: 997 self.log.info("%s | Process ID: %d" % (name, self.proc_ident)) 998 999 # Communicate returns a tuple of (stdout, stderr), however we always 1000 # redirect stderr to stdout, so the second element is ignored. 1001 process_output, _ = self.communicate(proc) 1002 1003 if self.interactive: 1004 # Not sure what else to do here... 1005 self.keep_going = True 1006 return 1007 1008 if testTimer: 1009 testTimer.cancel() 1010 1011 if process_output: 1012 # For the remote case, stdout is not yet depleted, so we parse 1013 # it here all at once. 1014 self.parse_output(process_output) 1015 1016 return_code = self.getReturnCode(proc) 1017 1018 # TSan'd processes return 66 if races are detected. This isn't 1019 # good in the sense that there's no way to distinguish between 1020 # a process that would normally have returned zero but has races, 1021 # and a race-free process that returns 66. But I don't see how 1022 # to do better. This ambiguity is at least constrained to the 1023 # with-TSan case. It doesn't affect normal builds. 1024 # 1025 # This also assumes that the magic value 66 isn't overridden by 1026 # a TSAN_OPTIONS=exitcode=<number> environment variable setting. 1027 # 1028 TSAN_EXIT_CODE_WITH_RACES = 66 1029 1030 return_code_ok = return_code == 0 or ( 1031 self.usingTSan and return_code == TSAN_EXIT_CODE_WITH_RACES 1032 ) 1033 1034 # Due to the limitation on the remote xpcshell test, the process 1035 # return code does not represent the process crash. 1036 # If crash_reporter_init log has not been seen and the return code 1037 # is 0, it means the process crashed before setting up the crash 1038 # reporter. 1039 # 1040 # NOTE: Crash reporter is not enabled on some configuration, such 1041 # as ASAN and TSAN. Those configuration shouldn't be using 1042 # remote xpcshell test, and the crash should be caught by 1043 # the process return code. 1044 # NOTE: self.saw_crash_reporter_init is False also when adb failed 1045 # to launch process, and in that case the return code is 1046 # not 0. 1047 # (see launchProcess in remotexpcshelltests.py) 1048 ended_before_crash_reporter_init = ( 1049 return_code_ok 1050 and self.usingCrashReporter 1051 and not self.saw_crash_reporter_init 1052 and len(process_output) > 0 1053 ) 1054 1055 passed = ( 1056 (not self.has_failure_output) 1057 and not ended_before_crash_reporter_init 1058 and return_code_ok 1059 ) 1060 1061 status = "PASS" if passed else "FAIL" 1062 expected = "PASS" if expect_pass else "FAIL" 1063 message = "xpcshell return code: %d" % return_code 1064 1065 if self.timedout: 1066 return 1067 1068 # Check for crashes before logging test_end 1069 found_crash = self.checkForCrashes( 1070 self.tempDir, self.symbolsPath, test_name=name 1071 ) 1072 if found_crash: 1073 status = "CRASH" 1074 message = "Test crashed" 1075 1076 # Include timeout factor in extra data if not default 1077 extra = None 1078 if self.timeout_factor > 1: 1079 extra = {"timeoutfactor": self.timeout_factor} 1080 1081 if status != expected or ended_before_crash_reporter_init: 1082 if ended_before_crash_reporter_init: 1083 self.log.test_end( 1084 name, 1085 "CRASH", 1086 expected=expected, 1087 message="Test ended before setting up the crash reporter", 1088 group=group, 1089 extra=extra, 1090 ) 1091 elif self.retry: 1092 retry_message = ( 1093 "Test crashed, will retry" 1094 if status == "CRASH" 1095 else "Test failed or timed out, will retry" 1096 ) 1097 self.log.test_end( 1098 name, 1099 status, 1100 expected=status, 1101 message=retry_message, 1102 group=group, 1103 extra=extra, 1104 ) 1105 self.clean_temp_dirs(path) 1106 if self.verboseIfFails and not self.verbose: 1107 self.log_full_output() 1108 return 1109 else: 1110 self.log.test_end( 1111 name, 1112 status, 1113 expected=expected, 1114 message=message, 1115 group=group, 1116 extra=extra, 1117 ) 1118 self.log_full_output() 1119 1120 self.failCount += 1 1121 1122 if self.failureManifest: 1123 with open(self.failureManifest, "a") as f: 1124 f.write("[%s]\n" % self.test_object["path"]) 1125 for k, v in self.test_object.items(): 1126 f.write("%s = %s\n" % (k, v)) 1127 1128 else: 1129 # If TSan reports a race, dump the output, else we can't 1130 # diagnose what the problem was. See comments above about 1131 # the significance of TSAN_EXIT_CODE_WITH_RACES. 1132 if self.usingTSan and return_code == TSAN_EXIT_CODE_WITH_RACES: 1133 self.log_full_output() 1134 1135 self.log.test_end( 1136 name, 1137 status, 1138 expected=expected, 1139 message=message, 1140 group=group, 1141 extra=extra, 1142 ) 1143 if self.verbose: 1144 self.log_full_output() 1145 1146 self.retry = False 1147 1148 if expect_pass: 1149 self.passCount = 1 1150 else: 1151 self.todoCount = 1 1152 1153 if self.logfiles and process_output: 1154 self.createLogFile(name, process_output) 1155 1156 finally: 1157 self.postCheck(proc) 1158 if self.profiler and self.singleFile: 1159 symbolicate_profile_json(profile_path, self.symbolsPath) 1160 view_gecko_profile(profile_path) 1161 self.clean_temp_dirs(path) 1162 1163 if gotSIGINT: 1164 self.log.error("Received SIGINT (control-C) during test execution") 1165 if self.keep_going: 1166 gotSIGINT = False 1167 else: 1168 self.keep_going = False 1169 return 1170 1171 self.keep_going = True 1172 1173 1174 class XPCShellTests: 1175 def __init__(self, log=None): 1176 """Initializes node status and logger.""" 1177 self.log = log 1178 self.harness_timeout = HARNESS_TIMEOUT 1179 self.nodeProc = {} 1180 self.http3Server = None 1181 self.conditioned_profile_dir = None 1182 1183 def getTestManifest(self, manifest): 1184 if isinstance(manifest, TestManifest): 1185 return manifest 1186 elif manifest is not None: 1187 manifest = os.path.normpath(os.path.abspath(manifest)) 1188 if os.path.isfile(manifest): 1189 return TestManifest([manifest], strict=True) 1190 else: 1191 toml_path = os.path.join(manifest, "xpcshell.toml") 1192 else: 1193 toml_path = os.path.join(SCRIPT_DIR, "tests", "xpcshell.toml") 1194 1195 if os.path.exists(toml_path): 1196 return TestManifest([toml_path], strict=True) 1197 else: 1198 self.log.error( 1199 "Failed to find manifest at %s; use --manifest " 1200 "to set path explicitly." % toml_path 1201 ) 1202 sys.exit(1) 1203 1204 def normalizeTest(self, root, test_object): 1205 path = test_object.get("file_relpath", test_object["relpath"]) 1206 if "dupe-manifest" in test_object and "ancestor_manifest" in test_object: 1207 # Use same logic as get_full_group_name() to determine which manifest to use 1208 ancestor_manifest = normsep(test_object["ancestor_manifest"]) 1209 # If ancestor is not the generated root (has path separator), use it 1210 manifest_for_id = ( 1211 test_object["ancestor_manifest"] 1212 if "/" in ancestor_manifest 1213 else test_object["manifest"] 1214 ) 1215 test_object["id"] = "%s:%s" % (os.path.basename(manifest_for_id), path) 1216 else: 1217 test_object["id"] = path 1218 1219 if root: 1220 test_object["manifest"] = os.path.relpath(test_object["manifest"], root) 1221 1222 if os.sep != "/": 1223 for key in ("id", "manifest"): 1224 test_object[key] = test_object[key].replace(os.sep, "/") 1225 1226 return test_object 1227 1228 def buildTestList(self, test_tags=None, test_paths=None, verify=False): 1229 """Reads the xpcshell.toml manifest and set self.alltests to an array. 1230 1231 Given the parameters, this method compiles a list of tests to be run 1232 that matches the criteria set by parameters. 1233 1234 If any chunking of tests are to occur, it is also done in this method. 1235 1236 If no tests are added to the list of tests to be run, an error 1237 is logged. A sys.exit() signal is sent to the caller. 1238 1239 Args: 1240 test_tags (list, optional): list of strings. 1241 test_paths (list, optional): list of strings derived from the command 1242 line argument provided by user, specifying 1243 tests to be run. 1244 verify (bool, optional): boolean value. 1245 """ 1246 if test_paths is None: 1247 test_paths = [] 1248 1249 mp = self.getTestManifest(self.manifest) 1250 1251 root = mp.rootdir 1252 if build and not root: 1253 root = build.topsrcdir 1254 normalize = partial(self.normalizeTest, root) 1255 1256 filters = [] 1257 if test_tags: 1258 filters.extend([tags(x) for x in test_tags]) 1259 1260 path_filter = None 1261 if test_paths: 1262 path_filter = pathprefix(test_paths) 1263 filters.append(path_filter) 1264 1265 noDefaultFilters = False 1266 if self.runFailures: 1267 filters.append(failures(self.runFailures)) 1268 noDefaultFilters = True 1269 1270 if self.totalChunks > 1: 1271 filters.append(chunk_by_slice(self.thisChunk, self.totalChunks)) 1272 try: 1273 self.alltests = list( 1274 map( 1275 normalize, 1276 mp.active_tests( 1277 filters=filters, 1278 noDefaultFilters=noDefaultFilters, 1279 strictExpressions=True, 1280 **mozinfo.info, 1281 ), 1282 ) 1283 ) 1284 except TypeError: 1285 sys.stderr.write("*** offending mozinfo.info: %s\n" % repr(mozinfo.info)) 1286 raise 1287 1288 if path_filter and path_filter.missing: 1289 self.log.warning( 1290 "The following path(s) didn't resolve any tests:\n {}".format( 1291 " \n".join(sorted(path_filter.missing)) 1292 ) 1293 ) 1294 1295 if len(self.alltests) == 0: 1296 if ( 1297 test_paths 1298 and path_filter.missing == set(test_paths) 1299 and os.environ.get("MOZ_AUTOMATION") == "1" 1300 ): 1301 # This can happen in CI when a manifest doesn't exist due to a 1302 # build config variable in moz.build traversal. Don't generate 1303 # an error in this case. Adding a todo count avoids mozharness 1304 # raising an error. 1305 self.todoCount += len(path_filter.missing) 1306 else: 1307 self.log.error( 1308 "no tests to run using specified " 1309 f"combination of filters: {mp.fmt_filters()}" 1310 ) 1311 sys.exit(1) 1312 1313 # Count non-disabled tests for --profiler validation 1314 enabled_tests = [t for t in self.alltests if "disabled" not in t] 1315 if len(enabled_tests) == 1 and not verify: 1316 self.singleFile = os.path.basename(enabled_tests[0]["path"]) 1317 else: 1318 self.singleFile = None 1319 1320 if self.dump_tests: 1321 self.dump_tests = os.path.expanduser(self.dump_tests) 1322 assert os.path.exists(os.path.dirname(self.dump_tests)) 1323 with open(self.dump_tests, "w") as dumpFile: 1324 dumpFile.write(json.dumps({"active_tests": self.alltests})) 1325 1326 self.log.info("Dumping active_tests to %s file." % self.dump_tests) 1327 sys.exit() 1328 1329 def setAbsPath(self): 1330 """ 1331 Set the absolute path for xpcshell and xrepath. These 3 variables 1332 depend on input from the command line and we need to allow for absolute paths. 1333 This function is overloaded for a remote solution as os.path* won't work remotely. 1334 """ 1335 self.testharnessdir = os.path.dirname(os.path.abspath(__file__)) 1336 self.headJSPath = self.testharnessdir.replace("\\", "/") + "/head.js" 1337 if self.xpcshell is not None: 1338 self.xpcshell = os.path.abspath(self.xpcshell) 1339 1340 if self.app_binary is not None: 1341 self.app_binary = os.path.abspath(self.app_binary) 1342 1343 if self.xrePath is None: 1344 binary_path = self.app_binary or self.xpcshell 1345 self.xrePath = os.path.dirname(binary_path) 1346 if mozinfo.isMac: 1347 # Check if we're run from an OSX app bundle and override 1348 # self.xrePath if we are. 1349 appBundlePath = os.path.join( 1350 os.path.dirname(os.path.dirname(self.xpcshell)), "Resources" 1351 ) 1352 if os.path.exists(os.path.join(appBundlePath, "application.ini")): 1353 self.xrePath = appBundlePath 1354 else: 1355 self.xrePath = os.path.abspath(self.xrePath) 1356 1357 if self.mozInfo is None: 1358 self.mozInfo = os.path.join(self.testharnessdir, "mozinfo.json") 1359 1360 def buildPrefsFile(self, extraPrefs): 1361 # Create the prefs.js file 1362 1363 # In test packages used in CI, the profile_data directory is installed 1364 # in the SCRIPT_DIR. 1365 profile_data_dir = os.path.join(SCRIPT_DIR, "profile_data") 1366 # If possible, read profile data from topsrcdir. This prevents us from 1367 # requiring a re-build to pick up newly added extensions in the 1368 # <profile>/extensions directory. 1369 if build: 1370 path = os.path.join(build.topsrcdir, "testing", "profiles") 1371 if os.path.isdir(path): 1372 profile_data_dir = path 1373 # Still not found? Look for testing/profiles relative to testing/xpcshell. 1374 if not os.path.isdir(profile_data_dir): 1375 path = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "profiles")) 1376 if os.path.isdir(path): 1377 profile_data_dir = path 1378 1379 with open(os.path.join(profile_data_dir, "profiles.json")) as fh: 1380 base_profiles = json.load(fh)["xpcshell"] 1381 1382 # values to use when interpolating preferences 1383 interpolation = { 1384 "server": "dummyserver", 1385 } 1386 1387 profile = Profile(profile=self.tempDir, restore=False) 1388 prefsFile = os.path.join(profile.profile, "user.js") 1389 1390 # Empty the user.js file in case the file existed before. 1391 with open(prefsFile, "w"): 1392 pass 1393 1394 for name in base_profiles: 1395 path = os.path.join(profile_data_dir, name) 1396 profile.merge(path, interpolation=interpolation) 1397 1398 # add command line prefs 1399 prefs = parse_preferences(extraPrefs) 1400 profile.set_preferences(prefs) 1401 1402 self.prefsFile = prefsFile 1403 return prefs 1404 1405 def buildCoreEnvironment(self): 1406 """ 1407 Add environment variables likely to be used across all platforms, including 1408 remote systems. 1409 """ 1410 # Make assertions fatal 1411 self.env["XPCOM_DEBUG_BREAK"] = "stack-and-abort" 1412 # Crash reporting interferes with debugging 1413 if not self.debuggerInfo: 1414 self.env["MOZ_CRASHREPORTER"] = "1" 1415 # Don't launch the crash reporter client 1416 self.env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" 1417 # Don't permit remote connections by default. 1418 # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily 1419 # enable non-local connections for the purposes of local testing. 1420 # Don't override the user's choice here. See bug 1049688. 1421 self.env.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1") 1422 if self.mozInfo.get("topsrcdir") is not None: 1423 self.env["MOZ_DEVELOPER_REPO_DIR"] = self.mozInfo["topsrcdir"] 1424 if self.mozInfo.get("topobjdir") is not None: 1425 self.env["MOZ_DEVELOPER_OBJ_DIR"] = self.mozInfo["topobjdir"] 1426 1427 # Disable the content process sandbox for the xpcshell tests. They 1428 # currently attempt to do things like bind() sockets, which is not 1429 # compatible with the sandbox. 1430 self.env["MOZ_DISABLE_CONTENT_SANDBOX"] = "1" 1431 if os.getenv("MOZ_FETCHES_DIR", None): 1432 self.env["MOZ_FETCHES_DIR"] = os.getenv("MOZ_FETCHES_DIR", None) 1433 1434 if self.mozInfo.get("socketprocess_networking"): 1435 self.env["MOZ_FORCE_USE_SOCKET_PROCESS"] = "1" 1436 else: 1437 self.env["MOZ_DISABLE_SOCKET_PROCESS"] = "1" 1438 1439 def buildEnvironment(self): 1440 """ 1441 Create and returns a dictionary of self.env to include all the appropriate env 1442 variables and values. On a remote system, we overload this to set different 1443 values and are missing things like os.environ and PATH. 1444 """ 1445 self.env = dict(os.environ) 1446 self.buildCoreEnvironment() 1447 if sys.platform == "win32": 1448 self.env["PATH"] = self.env["PATH"] + ";" + self.xrePath 1449 elif sys.platform in ("os2emx", "os2knix"): 1450 os.environ["BEGINLIBPATH"] = self.xrePath + ";" + self.env["BEGINLIBPATH"] 1451 os.environ["LIBPATHSTRICT"] = "T" 1452 elif sys.platform == "osx" or sys.platform == "darwin": 1453 self.env["DYLD_LIBRARY_PATH"] = os.path.join( 1454 os.path.dirname(self.xrePath), "MacOS" 1455 ) 1456 elif "LD_LIBRARY_PATH" not in self.env or self.env["LD_LIBRARY_PATH"] is None: 1457 self.env["LD_LIBRARY_PATH"] = self.xrePath 1458 else: 1459 self.env["LD_LIBRARY_PATH"] = ":".join([ 1460 self.xrePath, 1461 self.env["LD_LIBRARY_PATH"], 1462 ]) 1463 1464 usingASan = "asan" in self.mozInfo and self.mozInfo["asan"] 1465 usingTSan = "tsan" in self.mozInfo and self.mozInfo["tsan"] 1466 if usingASan or usingTSan: 1467 # symbolizer support 1468 if "ASAN_SYMBOLIZER_PATH" in self.env and os.path.isfile( 1469 self.env["ASAN_SYMBOLIZER_PATH"] 1470 ): 1471 llvmsym = self.env["ASAN_SYMBOLIZER_PATH"] 1472 else: 1473 llvmsym = os.path.join( 1474 self.xrePath, "llvm-symbolizer" + self.mozInfo["bin_suffix"] 1475 ) 1476 if os.path.isfile(llvmsym): 1477 if usingASan: 1478 self.env["ASAN_SYMBOLIZER_PATH"] = llvmsym 1479 else: 1480 oldTSanOptions = self.env.get("TSAN_OPTIONS", "") 1481 self.env["TSAN_OPTIONS"] = ( 1482 f"external_symbolizer_path={llvmsym} {oldTSanOptions}" 1483 ) 1484 self.log.info("runxpcshelltests.py | using symbolizer at %s" % llvmsym) 1485 else: 1486 self.log.error( 1487 "TEST-UNEXPECTED-FAIL | runxpcshelltests.py | " 1488 "Failed to find symbolizer at %s" % llvmsym 1489 ) 1490 1491 return self.env 1492 1493 def getPipes(self): 1494 """ 1495 Determine the value of the stdout and stderr for the test. 1496 Return value is a list (pStdout, pStderr). 1497 """ 1498 if self.interactive: 1499 pStdout = None 1500 pStderr = None 1501 elif self.debuggerInfo and self.debuggerInfo.interactive: 1502 pStdout = None 1503 pStderr = None 1504 else: 1505 if sys.platform == "os2emx": 1506 pStdout = None 1507 else: 1508 pStdout = PIPE 1509 pStderr = STDOUT 1510 return pStdout, pStderr 1511 1512 def verifyDirPath(self, dirname): 1513 """ 1514 Simple wrapper to get the absolute path for a given directory name. 1515 On a remote system, we need to overload this to work on the remote filesystem. 1516 """ 1517 return os.path.abspath(dirname) 1518 1519 def trySetupNode(self): 1520 """ 1521 Run node for HTTP/2 tests, if available, and updates mozinfo as appropriate. 1522 """ 1523 if os.getenv("MOZ_ASSUME_NODE_RUNNING", None): 1524 self.log.info("Assuming required node servers are already running") 1525 if not os.getenv("MOZHTTP2_PORT", None): 1526 self.log.warning( 1527 "MOZHTTP2_PORT environment variable not set. " 1528 "Tests requiring http/2 will fail." 1529 ) 1530 return 1531 1532 # We try to find the node executable in the path given to us by the user in 1533 # the MOZ_NODE_PATH environment variable 1534 nodeBin = os.getenv("MOZ_NODE_PATH", None) 1535 if not nodeBin and build: 1536 nodeBin = build.substs.get("NODEJS") 1537 if not nodeBin: 1538 self.log.warning( 1539 "MOZ_NODE_PATH environment variable not set. " 1540 "Tests requiring http/2 will fail." 1541 ) 1542 return 1543 1544 if not os.path.exists(nodeBin) or not os.path.isfile(nodeBin): 1545 error = "node not found at MOZ_NODE_PATH %s" % (nodeBin) 1546 self.log.error(error) 1547 raise OSError(error) 1548 1549 self.log.info("Found node at %s" % (nodeBin,)) 1550 1551 def read_streams(name, proc, pipe): 1552 output = "stdout" if pipe == proc.stdout else "stderr" 1553 for line in iter(pipe.readline, ""): 1554 self.log.info("node %s [%s] %s" % (name, output, line)) 1555 1556 def startServer(name, serverJs): 1557 if not os.path.exists(serverJs): 1558 error = "%s not found at %s" % (name, serverJs) 1559 self.log.error(error) 1560 raise OSError(error) 1561 1562 # OK, we found our server, let's try to get it running 1563 self.log.info("Found %s at %s" % (name, serverJs)) 1564 try: 1565 # We pipe stdin to node because the server will exit when its 1566 # stdin reaches EOF 1567 with popenCleanupHack(): 1568 process = Popen( 1569 [nodeBin, serverJs], 1570 stdin=PIPE, 1571 stdout=PIPE, 1572 stderr=PIPE, 1573 env=self.env, 1574 cwd=os.getcwd(), 1575 universal_newlines=True, 1576 start_new_session=True, 1577 ) 1578 self.nodeProc[name] = process 1579 1580 # Check to make sure the server starts properly by waiting for it to 1581 # tell us it's started 1582 msg = process.stdout.readline() 1583 if "server listening" in msg: 1584 searchObj = re.search( 1585 r"HTTP2 server listening on ports ([0-9]+),([0-9]+)", msg, 0 1586 ) 1587 if searchObj: 1588 self.env["MOZHTTP2_PORT"] = searchObj.group(1) 1589 self.env["MOZNODE_EXEC_PORT"] = searchObj.group(2) 1590 t1 = Thread( 1591 target=read_streams, 1592 args=(name, process, process.stdout), 1593 daemon=True, 1594 ) 1595 t1.start() 1596 t2 = Thread( 1597 target=read_streams, 1598 args=(name, process, process.stderr), 1599 daemon=True, 1600 ) 1601 t2.start() 1602 except OSError as e: 1603 # This occurs if the subprocess couldn't be started 1604 self.log.error("Could not run %s server: %s" % (name, str(e))) 1605 raise 1606 1607 myDir = os.path.split(os.path.abspath(__file__))[0] 1608 startServer("moz-http2", os.path.join(myDir, "moz-http2", "moz-http2.js")) 1609 1610 def shutdownNode(self): 1611 """ 1612 Shut down our node process, if it exists 1613 """ 1614 for name, proc in self.nodeProc.items(): 1615 self.log.info("Node %s server shutting down ..." % name) 1616 if proc.poll() is not None: 1617 self.log.info("Node server %s already dead %s" % (name, proc.poll())) 1618 elif sys.platform != "win32": 1619 # Kill process and all its spawned children. 1620 os.killpg(proc.pid, signal.SIGTERM) 1621 else: 1622 proc.terminate() 1623 1624 self.nodeProc = {} 1625 1626 def startHttp3Server(self): 1627 """ 1628 Start a Http3 test server. 1629 """ 1630 binSuffix = "" 1631 if sys.platform == "win32": 1632 binSuffix = ".exe" 1633 http3ServerPath = self.http3ServerPath 1634 serverEnv = self.env.copy() 1635 if not http3ServerPath: 1636 if self.mozInfo["buildapp"] == "mobile/android": 1637 # For android, use binary from host utilities. 1638 http3ServerPath = os.path.join(self.xrePath, "http3server" + binSuffix) 1639 serverEnv["LD_LIBRARY_PATH"] = self.xrePath 1640 elif build: 1641 http3ServerPath = os.path.join( 1642 build.topobjdir, "dist", "bin", "http3server" + binSuffix 1643 ) 1644 else: 1645 http3ServerPath = os.path.join( 1646 SCRIPT_DIR, "http3server", "http3server" + binSuffix 1647 ) 1648 1649 # Treat missing http3server as a non-fatal error, because tests that do not 1650 # depend on http3server may work just fine. 1651 if not os.path.exists(http3ServerPath): 1652 self.log.error("Cannot find http3server at path %s" % (http3ServerPath)) 1653 return 1654 1655 dbPath = os.path.join(SCRIPT_DIR, "http3server", "http3serverDB") 1656 if build: 1657 dbPath = os.path.join(build.topsrcdir, "netwerk", "test", "http3serverDB") 1658 options = {} 1659 options["http3ServerPath"] = http3ServerPath 1660 options["profilePath"] = dbPath 1661 options["isMochitest"] = False 1662 options["isWin"] = sys.platform == "win32" 1663 serverLog = self.env.get("MOZHTTP3_SERVER_LOG") 1664 if serverLog is not None: 1665 serverEnv["RUST_LOG"] = serverLog 1666 self.http3Server = Http3Server(options, serverEnv, self.log) 1667 self.http3Server.start() 1668 for key, value in self.http3Server.ports().items(): 1669 self.env[key] = value 1670 self.env["MOZHTTP3_ECH"] = self.http3Server.echConfig() 1671 self.env["MOZ_HTTP3_SERVER_PATH"] = http3ServerPath 1672 self.env["MOZ_HTTP3_CERT_DB_PATH"] = dbPath 1673 1674 def shutdownHttp3Server(self): 1675 if self.http3Server is None: 1676 return 1677 self.http3Server.stop() 1678 self.http3Server = None 1679 1680 def buildXpcsRunArgs(self): 1681 """ 1682 Add arguments to run the test or make it interactive. 1683 """ 1684 if self.interactive: 1685 self.xpcsRunArgs = [ 1686 "-e", 1687 'print("To start the test, type |_execute_test();|.");', 1688 "-i", 1689 ] 1690 else: 1691 self.xpcsRunArgs = ["-e", "_execute_test(); quit(0);"] 1692 1693 def addTestResults(self, test): 1694 self.passCount += test.passCount 1695 self.failCount += test.failCount 1696 self.todoCount += test.todoCount 1697 1698 def updateMozinfo(self, prefs, options): 1699 # Handle filenames in mozInfo 1700 if not isinstance(self.mozInfo, dict): 1701 mozInfoFile = self.mozInfo 1702 if not os.path.isfile(mozInfoFile): 1703 self.log.error( 1704 "Error: couldn't find mozinfo.json at '%s'. Perhaps you " 1705 "need to use --build-info-json?" % mozInfoFile 1706 ) 1707 return False 1708 self.mozInfo = json.load(open(mozInfoFile)) 1709 1710 # mozinfo.info is used as kwargs. Some builds are done with 1711 # an older Python that can't handle Unicode keys in kwargs. 1712 # All of the keys in question should be ASCII. 1713 fixedInfo = {} 1714 for k, v in self.mozInfo.items(): 1715 if isinstance(k, bytes): 1716 k = k.decode("utf-8") 1717 fixedInfo[k] = v 1718 self.mozInfo = fixedInfo 1719 1720 self.mozInfo["fission"] = prefs.get("fission.autostart", True) 1721 self.mozInfo["sessionHistoryInParent"] = self.mozInfo[ 1722 "fission" 1723 ] or not prefs.get("fission.disableSessionHistoryInParent", False) 1724 1725 self.mozInfo["verify"] = options.get("verify", False) 1726 1727 self.mozInfo["socketprocess_networking"] = prefs.get( 1728 "network.http.network_access_on_socket_process.enabled", False 1729 ) 1730 1731 self.mozInfo["inc_origin_init"] = ( 1732 os.environ.get("MOZ_ENABLE_INC_ORIGIN_INIT") == "1" 1733 ) 1734 1735 self.mozInfo["condprof"] = options.get("conditionedProfile", False) 1736 self.mozInfo["msix"] = options.get("variant", "") == "msix" 1737 1738 self.mozInfo["is_ubuntu"] = "Ubuntu" in platform.version() 1739 1740 # TODO: remove this when crashreporter is fixed on mac via bug 1910777 1741 if self.mozInfo["os"] == "mac": 1742 (release, versioninfo, machine) = platform.mac_ver() 1743 versionNums = release.split(".")[:2] 1744 os_version = "%s.%s" % (versionNums[0], versionNums[1].ljust(2, "0")) 1745 if os_version.split(".")[0] in ["14", "15"]: 1746 self.mozInfo["crashreporter"] = False 1747 1748 # we default to false for e10s on xpcshell 1749 self.mozInfo["e10s"] = self.mozInfo.get("e10s", False) 1750 1751 mozinfo.update(self.mozInfo) 1752 return True 1753 1754 @property 1755 def conditioned_profile_copy(self): 1756 """Returns a copy of the original conditioned profile that was created.""" 1757 condprof_copy = os.path.join(tempfile.mkdtemp(), "profile") 1758 shutil.copytree( 1759 self.conditioned_profile_dir, 1760 condprof_copy, 1761 ignore=shutil.ignore_patterns("lock"), 1762 ) 1763 self.log.info("Created a conditioned-profile copy: %s" % condprof_copy) 1764 return condprof_copy 1765 1766 def downloadConditionedProfile(self, profile_scenario, app): 1767 from condprof.client import get_profile 1768 from condprof.util import get_current_platform, get_version 1769 1770 if self.conditioned_profile_dir: 1771 # We already have a directory, so provide a copy that 1772 # will get deleted after it's done with 1773 return self.conditioned_profile_dir 1774 1775 # create a temp file to help ensure uniqueness 1776 temp_download_dir = tempfile.mkdtemp() 1777 self.log.info( 1778 f"Making temp_download_dir from inside get_conditioned_profile {temp_download_dir}" 1779 ) 1780 # call condprof's client API to yield our platform-specific 1781 # conditioned-profile binary 1782 platform = get_current_platform() 1783 version = None 1784 if isinstance(app, str): 1785 version = get_version(app) 1786 1787 if not profile_scenario: 1788 profile_scenario = "settled" 1789 try: 1790 cond_prof_target_dir = get_profile( 1791 temp_download_dir, 1792 platform, 1793 profile_scenario, 1794 repo="mozilla-central", 1795 version=version, 1796 retries=2, 1797 ) 1798 except Exception: 1799 if version is None: 1800 # any other error is a showstopper 1801 self.log.critical("Could not get the conditioned profile") 1802 traceback.print_exc() 1803 raise 1804 version = None 1805 try: 1806 self.log.info("Retrying a profile with no version specified") 1807 cond_prof_target_dir = get_profile( 1808 temp_download_dir, 1809 platform, 1810 profile_scenario, 1811 repo="mozilla-central", 1812 version=version, 1813 ) 1814 except Exception: 1815 self.log.critical("Could not get the conditioned profile") 1816 traceback.print_exc() 1817 raise 1818 1819 # now get the full directory path to our fetched conditioned profile 1820 self.conditioned_profile_dir = os.path.join( 1821 temp_download_dir, cond_prof_target_dir 1822 ) 1823 if not os.path.exists(cond_prof_target_dir): 1824 self.log.critical( 1825 f"Can't find target_dir {cond_prof_target_dir}, from get_profile()" 1826 f"temp_download_dir {temp_download_dir}, platform {platform}, scenario {profile_scenario}" 1827 ) 1828 raise OSError 1829 1830 self.log.info( 1831 f"Original self.conditioned_profile_dir is now set: {self.conditioned_profile_dir}" 1832 ) 1833 return self.conditioned_profile_copy 1834 1835 def runSelfTest(self): 1836 import unittest 1837 from concurrent.futures import ThreadPoolExecutor, as_completed 1838 1839 import selftest 1840 1841 this = self 1842 1843 class XPCShellTestsTests(selftest.XPCShellTestsTests): 1844 def __init__(self, name): 1845 unittest.TestCase.__init__(self, name) 1846 self.testing_modules = this.testingModulesDir 1847 self.xpcshellBin = this.xpcshell 1848 self.app_binary = this.app_binary 1849 self.utility_path = this.utility_path 1850 self.symbols_path = this.symbolsPath 1851 1852 old_info = dict(mozinfo.info) 1853 try: 1854 suite = unittest.TestLoader().loadTestsFromTestCase(XPCShellTestsTests) 1855 test_cases = list(suite) 1856 group = "xpcshell-selftest" 1857 tests_by_manifest = { 1858 "xpcshell-selftest": [tc._testMethodName for tc in test_cases] 1859 } 1860 self.log.suite_start(tests_by_manifest, name=group) 1861 self.log.group_start(name="selftests") 1862 1863 if self.sequential or len(test_cases) <= 1: 1864 return unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful() 1865 1866 def run_single_test(test_case): 1867 result = unittest.TestResult() 1868 test_name = test_case._testMethodName 1869 this.log.test_start(test_name, group=group) 1870 status = "PASS" 1871 try: 1872 test_case.run(result) 1873 if not result.wasSuccessful(): 1874 status = "FAIL" 1875 except Exception as e: 1876 result.addError(test_case, (type(e), e, None)) 1877 status = "ERROR" 1878 finally: 1879 this.log.test_end(test_name, status, expected="PASS", group=group) 1880 return { 1881 "result": result, 1882 "name": test_name, 1883 } 1884 1885 success = True 1886 1887 # Limit parallel self-tests to 32 on macOS to avoid "too many open files" error. 1888 max_workers = ( 1889 min(32, self.threadCount) 1890 if sys.platform == "darwin" 1891 else self.threadCount 1892 ) 1893 1894 self.log.info( 1895 f"Running {len(test_cases)} self-tests in parallel with up to {max_workers} workers..." 1896 ) 1897 with ThreadPoolExecutor(max_workers=max_workers) as executor: 1898 # Submit all tests 1899 future_to_test = { 1900 executor.submit(run_single_test, test): test for test in test_cases 1901 } 1902 1903 # Print the status of tests as they finish 1904 for future in as_completed(future_to_test): 1905 try: 1906 test_result = future.result() 1907 result_obj = test_result["result"] 1908 1909 if not result_obj.wasSuccessful(): 1910 success = False 1911 test_name = test_result["name"] 1912 if result_obj.failures: 1913 self.log.error(f"FAIL: {test_name}") 1914 for test, traceback in result_obj.failures: 1915 self.log.error(f" Failure: {traceback}") 1916 if result_obj.errors: 1917 self.log.error(f"ERROR: {test_name}") 1918 for test, traceback in result_obj.errors: 1919 self.log.error(f" Error: {traceback}") 1920 1921 except Exception as e: 1922 self.log.error(f"Exception in test execution: {e}") 1923 success = False 1924 1925 return success 1926 1927 finally: 1928 # The self tests modify mozinfo, so we need to reset it. 1929 mozinfo.info.clear() 1930 mozinfo.update(old_info) 1931 1932 self.log.group_end(name="selftests") 1933 self.log.suite_end() 1934 1935 def runTests(self, options, testClass=XPCShellTestThread, mobileArgs=None): 1936 """ 1937 Run xpcshell tests. 1938 """ 1939 1940 # Number of times to repeat test(s) in --verify mode 1941 VERIFY_REPEAT = 10 1942 1943 if isinstance(options, Namespace): 1944 options = vars(options) 1945 1946 # Try to guess modules directory. 1947 # This somewhat grotesque hack allows the buildbot machines to find the 1948 # modules directory without having to configure the buildbot hosts. This 1949 # code path should never be executed in local runs because the build system 1950 # should always set this argument. 1951 if not options.get("testingModulesDir"): 1952 possible = os.path.join(here, os.path.pardir, "modules") 1953 1954 if os.path.isdir(possible): 1955 testingModulesDir = possible 1956 1957 if options.get("rerun_failures"): 1958 if os.path.exists(options.get("failure_manifest")): 1959 rerun_manifest = os.path.join( 1960 os.path.dirname(options["failure_manifest"]), "rerun.toml" 1961 ) 1962 shutil.copyfile(options["failure_manifest"], rerun_manifest) 1963 os.remove(options["failure_manifest"]) 1964 else: 1965 self.log.error("No failures were found to re-run.") 1966 sys.exit(1) 1967 1968 if options.get("testingModulesDir"): 1969 # The resource loader expects native paths. Depending on how we were 1970 # invoked, a UNIX style path may sneak in on Windows. We try to 1971 # normalize that. 1972 testingModulesDir = os.path.normpath(options["testingModulesDir"]) 1973 1974 if not os.path.isabs(testingModulesDir): 1975 testingModulesDir = os.path.abspath(testingModulesDir) 1976 1977 if not testingModulesDir.endswith(os.path.sep): 1978 testingModulesDir += os.path.sep 1979 1980 self.debuggerInfo = None 1981 1982 if options.get("debugger"): 1983 self.debuggerInfo = mozdebug.get_debugger_info( 1984 options.get("debugger"), 1985 options.get("debuggerArgs"), 1986 options.get("debuggerInteractive"), 1987 ) 1988 1989 self.jsDebuggerInfo = None 1990 if options.get("jsDebugger"): 1991 # A namedtuple let's us keep .port instead of ['port'] 1992 JSDebuggerInfo = namedtuple("JSDebuggerInfo", ["port"]) 1993 self.jsDebuggerInfo = JSDebuggerInfo(port=options["jsDebuggerPort"]) 1994 1995 # Apply timeout factor 1996 timeout_factor = options.get("timeoutFactor", 1.0) 1997 self.harness_timeout = int(HARNESS_TIMEOUT * timeout_factor) 1998 self.log.info( 1999 f"Using harness timeout of {self.harness_timeout}s " 2000 f"(base={HARNESS_TIMEOUT}s, factor={timeout_factor})" 2001 ) 2002 2003 self.app_binary = options.get("app_binary") 2004 self.xpcshell = options.get("xpcshell") 2005 self.http3ServerPath = options.get("http3server") 2006 self.xrePath = options.get("xrePath") 2007 self.utility_path = options.get("utility_path") 2008 self.appPath = options.get("appPath") 2009 self.symbolsPath = options.get("symbolsPath") 2010 self.tempDir = os.path.normpath(options.get("tempDir") or tempfile.gettempdir()) 2011 self.manifest = options.get("manifest") 2012 self.dump_tests = options.get("dump_tests") 2013 self.interactive = options.get("interactive") 2014 self.verbose = options.get("verbose") 2015 self.verboseIfFails = options.get("verboseIfFails") 2016 self.keepGoing = options.get("keepGoing") 2017 self.logfiles = options.get("logfiles") 2018 self.totalChunks = options.get("totalChunks", 1) 2019 self.thisChunk = options.get("thisChunk") 2020 self.profileName = options.get("profileName") or "xpcshell" 2021 self.mozInfo = options.get("mozInfo") 2022 self.testingModulesDir = testingModulesDir 2023 self.sequential = options.get("sequential") 2024 self.failure_manifest = options.get("failure_manifest") 2025 self.threadCount = options.get("threadCount") or NUM_THREADS 2026 self.jscovdir = options.get("jscovdir") 2027 self.headless = options.get("headless") 2028 self.selfTest = options.get("selfTest") 2029 self.runFailures = options.get("runFailures") 2030 self.timeoutAsPass = options.get("timeoutAsPass") 2031 self.crashAsPass = options.get("crashAsPass") 2032 self.conditionedProfile = options.get("conditionedProfile") 2033 self.repeat = options.get("repeat", 0) 2034 self.variant = options.get("variant", "") 2035 self.profiler = options.get("profiler") 2036 2037 if self.variant == "msix": 2038 self.appPath = options.get("msixAppPath") 2039 self.xrePath = options.get("msixXrePath") 2040 self.app_binary = options.get("msix_app_binary") 2041 self.xpcshell = None 2042 2043 self.testCount = 0 2044 self.passCount = 0 2045 self.failCount = 0 2046 self.todoCount = 0 2047 2048 if self.conditionedProfile: 2049 self.conditioned_profile_dir = self.downloadConditionedProfile( 2050 "full", self.appPath 2051 ) 2052 options["self_test"] = False 2053 2054 self.setAbsPath() 2055 2056 eprefs = options.get("extraPrefs") or [] 2057 # enable fission by default 2058 if options.get("disableFission"): 2059 eprefs.append("fission.autostart=false") 2060 else: 2061 # should be by default, just in case 2062 eprefs.append("fission.autostart=true") 2063 2064 prefs = self.buildPrefsFile(eprefs) 2065 self.buildXpcsRunArgs() 2066 2067 self.event = Event() 2068 2069 if not self.updateMozinfo(prefs, options): 2070 return False 2071 2072 self.log.info( 2073 "These variables are available in the mozinfo environment and " 2074 "can be used to skip tests conditionally:" 2075 ) 2076 for info in sorted(self.mozInfo.items(), key=lambda item: item[0]): 2077 self.log.info(f" {info[0]}: {info[1]}") 2078 2079 if options.get("self_test"): 2080 if not self.runSelfTest(): 2081 return False 2082 2083 if ( 2084 ("tsan" in self.mozInfo and self.mozInfo["tsan"]) 2085 or ("asan" in self.mozInfo and self.mozInfo["asan"]) 2086 ) and not options.get("threadCount"): 2087 # TSan/ASan require significantly more memory, so reduce the amount of parallel 2088 # tests we run to avoid OOMs and timeouts. We always keep a minimum of 2 for 2089 # non-sequential execution. 2090 # pylint --py3k W1619 2091 self.threadCount = max(self.threadCount / 2, 2) 2092 2093 self.stack_fixer_function = None 2094 if self.utility_path and os.path.exists(self.utility_path): 2095 self.stack_fixer_function = get_stack_fixer_function( 2096 self.utility_path, self.symbolsPath 2097 ) 2098 2099 # buildEnvironment() needs mozInfo, so we call it after mozInfo is initialized. 2100 self.buildEnvironment() 2101 extraEnv = parse_key_value(options.get("extraEnv") or [], context="--setenv") 2102 for k, v in extraEnv: 2103 if k in self.env: 2104 self.log.info( 2105 "Using environment variable %s instead of %s." % (v, self.env[k]) 2106 ) 2107 self.env[k] = v 2108 2109 # The appDirKey is a optional entry in either the default or individual test 2110 # sections that defines a relative application directory for test runs. If 2111 # defined we pass 'grePath/$appDirKey' for the -a parameter of the xpcshell 2112 # test harness. 2113 appDirKey = None 2114 if "appname" in self.mozInfo: 2115 appDirKey = self.mozInfo["appname"] + "-appdir" 2116 2117 # We have to do this before we run tests that depend on having the node 2118 # http/2 server. 2119 self.trySetupNode() 2120 2121 self.startHttp3Server() 2122 2123 pStdout, pStderr = self.getPipes() 2124 2125 self.buildTestList( 2126 options.get("test_tags"), options.get("testPaths"), options.get("verify") 2127 ) 2128 if self.singleFile and not self.selfTest: 2129 self.sequential = True 2130 2131 if options.get("shuffle"): 2132 random.shuffle(self.alltests) 2133 2134 self.cleanup_dir_list = [] 2135 2136 # If any of the tests that are about to be run uses npm packages 2137 # we should install them now. It would also be possible for tests 2138 # to define the location where they want the npm modules to be 2139 # installed, but for now only netwerk xpcshell tests use it. 2140 installNPM = False 2141 for test in self.alltests: 2142 if "usesNPM" in test: 2143 installNPM = True 2144 break 2145 2146 if installNPM: 2147 env = os.environ.copy() 2148 nodePath = os.environ.get("MOZ_NODE_PATH", "") 2149 if nodePath: 2150 node_bin_path = os.path.dirname(nodePath) 2151 env["PATH"] = f"{node_bin_path}{os.pathsep}{env.get('PATH', '')}" 2152 2153 # Try to find npm in PATH 2154 npm_executable = shutil.which("npm", path=env.get("PATH")) 2155 2156 if npm_executable: 2157 command = [npm_executable, "ci"] 2158 working_directory = os.path.join(SCRIPT_DIR, "moz-http2") 2159 result = subprocess.run( 2160 command, 2161 cwd=working_directory, 2162 env=env, 2163 capture_output=True, 2164 text=True, 2165 check=False, 2166 ) 2167 2168 # Print the output 2169 self.log.info("npm output: " + result.stdout) 2170 self.log.info("npm error: " + result.stderr) 2171 self.log.info("npm return code: " + str(result.returncode)) 2172 else: 2173 self.log.warning( 2174 "npm step was skipped because no executable could be resolved." 2175 ) 2176 2177 kwargs = { 2178 "appPath": self.appPath, 2179 "xrePath": self.xrePath, 2180 "utility_path": self.utility_path, 2181 "testingModulesDir": self.testingModulesDir, 2182 "debuggerInfo": self.debuggerInfo, 2183 "jsDebuggerInfo": self.jsDebuggerInfo, 2184 "headJSPath": self.headJSPath, 2185 "tempDir": self.tempDir, 2186 "testharnessdir": self.testharnessdir, 2187 "profileName": self.profileName, 2188 "singleFile": self.singleFile, 2189 "env": self.env, # making a copy of this in the testthreads 2190 "symbolsPath": self.symbolsPath, 2191 "logfiles": self.logfiles, 2192 "app_binary": self.app_binary, 2193 "xpcshell": self.xpcshell, 2194 "xpcsRunArgs": self.xpcsRunArgs, 2195 "failureManifest": self.failure_manifest, 2196 "jscovdir": self.jscovdir, 2197 "harness_timeout": self.harness_timeout, 2198 "stack_fixer_function": self.stack_fixer_function, 2199 "event": self.event, 2200 "cleanup_dir_list": self.cleanup_dir_list, 2201 "pStdout": pStdout, 2202 "pStderr": pStderr, 2203 "keep_going": self.keepGoing, 2204 "log": self.log, 2205 "interactive": self.interactive, 2206 "app_dir_key": appDirKey, 2207 "rootPrefsFile": self.prefsFile, 2208 "extraPrefs": options.get("extraPrefs") or [], 2209 "verboseIfFails": self.verboseIfFails, 2210 "headless": self.headless, 2211 "selfTest": self.selfTest, 2212 "runFailures": self.runFailures, 2213 "timeoutAsPass": self.timeoutAsPass, 2214 "crashAsPass": self.crashAsPass, 2215 "conditionedProfileDir": self.conditioned_profile_dir, 2216 "repeat": self.repeat, 2217 "profiler": self.profiler, 2218 } 2219 2220 # Only set retry if explicitly provided (avoid overriding default behavior) 2221 if options.get("retry") is not None: 2222 kwargs["retry"] = options.get("retry") 2223 2224 if self.sequential: 2225 # Allow user to kill hung xpcshell subprocess with SIGINT 2226 # when we are only running tests sequentially. 2227 signal.signal(signal.SIGINT, markGotSIGINT) 2228 2229 if self.debuggerInfo: 2230 # Force a sequential run 2231 self.sequential = True 2232 2233 # If we have an interactive debugger, disable SIGINT entirely. 2234 if self.debuggerInfo.interactive: 2235 signal.signal(signal.SIGINT, lambda signum, frame: None) 2236 2237 if "lldb" in self.debuggerInfo.path: 2238 # Ask people to start debugging using 'process launch', see bug 952211. 2239 self.log.info( 2240 "It appears that you're using LLDB to debug this test. " 2241 + "Please use the 'process launch' command instead of " 2242 "the 'run' command to start xpcshell." 2243 ) 2244 2245 if self.jsDebuggerInfo: 2246 # The js debugger magic needs more work to do the right thing 2247 # if debugging multiple files. 2248 if len(self.alltests) != 1: 2249 self.log.error( 2250 "Error: --jsdebugger can only be used with a single test!" 2251 ) 2252 return False 2253 2254 # The test itself needs to know whether it is a tsan build, since 2255 # that has an effect on interpretation of the process return value. 2256 usingTSan = "tsan" in self.mozInfo and self.mozInfo["tsan"] 2257 2258 usingCrashReporter = ( 2259 "crashreporter" in self.mozInfo and self.mozInfo["crashreporter"] 2260 ) 2261 2262 # create a queue of all tests that will run 2263 tests_queue = deque() 2264 # also a list for the tests that need to be run sequentially 2265 sequential_tests = [] 2266 status = None 2267 2268 if options.get("repeat", 0) > 0: 2269 self.sequential = True 2270 2271 def _match_run_sequentially(value, **values): 2272 """Helper function to evaluate run-sequentially conditions like skip-if/run-if""" 2273 return any(parse(e, strict=True, **values) for e in value.splitlines() if e) 2274 2275 if not options.get("verify"): 2276 for test_object in self.alltests: 2277 # Test identifiers are provided for the convenience of logging. These 2278 # start as path names but are rewritten in case tests from the same path 2279 # are re-run. 2280 2281 path = test_object["path"] 2282 2283 if self.singleFile and not path.endswith(self.singleFile): 2284 continue 2285 2286 # if we have --repeat, duplicate the tests as needed 2287 for i in range(0, options.get("repeat", 0) + 1): 2288 self.testCount += 1 2289 2290 test = testClass( 2291 test_object, 2292 verbose=self.verbose or test_object.get("verbose") == "true", 2293 usingTSan=usingTSan, 2294 usingCrashReporter=usingCrashReporter, 2295 mobileArgs=mobileArgs, 2296 **kwargs, 2297 ) 2298 if ( 2299 "run-sequentially" in test_object 2300 and _match_run_sequentially( 2301 test_object["run-sequentially"], **mozinfo.info 2302 ) 2303 ) or self.sequential: 2304 sequential_tests.append(test) 2305 else: 2306 tests_queue.append(test) 2307 2308 # Sort parallel tests by timeout factor (descending) to start slower tests first 2309 # This helps optimize parallel execution by avoiding long-running tests at the end 2310 if tests_queue: 2311 tests_queue = deque( 2312 sorted( 2313 tests_queue, 2314 key=lambda t: int(t.test_object.get("requesttimeoutfactor", 1)), 2315 reverse=True, 2316 ) 2317 ) 2318 2319 status = self.runTestList( 2320 tests_queue, sequential_tests, testClass, mobileArgs, **kwargs 2321 ) 2322 else: 2323 # 2324 # Test verification: Run each test many times, in various configurations, 2325 # in hopes of finding intermittent failures. 2326 # 2327 2328 def step1(): 2329 # Run tests sequentially. Parallel mode would also work, except that 2330 # the logging system gets confused when 2 or more tests with the same 2331 # name run at the same time. 2332 sequential_tests = [] 2333 for i in range(VERIFY_REPEAT): 2334 self.testCount += 1 2335 test = testClass( 2336 test_object, retry=False, mobileArgs=mobileArgs, **kwargs 2337 ) 2338 sequential_tests.append(test) 2339 status = self.runTestList( 2340 tests_queue, sequential_tests, testClass, mobileArgs, **kwargs 2341 ) 2342 return status 2343 2344 def step2(): 2345 # Run tests sequentially, with MOZ_CHAOSMODE enabled. 2346 sequential_tests = [] 2347 self.env["MOZ_CHAOSMODE"] = "0xfb" 2348 2349 # for android, adjust flags to avoid slow down 2350 if self.env.get("MOZ_ANDROID_DATA_DIR", ""): 2351 self.env["MOZ_CHAOSMODE"] = "0x3b" 2352 2353 # chaosmode runs really slow, allow tests extra time to pass 2354 kwargs["harness_timeout"] = self.harness_timeout * 2 2355 for i in range(VERIFY_REPEAT): 2356 self.testCount += 1 2357 test = testClass( 2358 test_object, retry=False, mobileArgs=mobileArgs, **kwargs 2359 ) 2360 sequential_tests.append(test) 2361 status = self.runTestList( 2362 tests_queue, sequential_tests, testClass, mobileArgs, **kwargs 2363 ) 2364 kwargs["harness_timeout"] = self.harness_timeout 2365 return status 2366 2367 steps = [ 2368 ("1. Run each test %d times, sequentially." % VERIFY_REPEAT, step1), 2369 ( 2370 "2. Run each test %d times, sequentially, in chaos mode." 2371 % VERIFY_REPEAT, 2372 step2, 2373 ), 2374 ] 2375 startTime = datetime.now() 2376 maxTime = timedelta(seconds=options["verifyMaxTime"]) 2377 for test_object in self.alltests: 2378 stepResults = {} 2379 for descr, step in steps: 2380 stepResults[descr] = "not run / incomplete" 2381 finalResult = "PASSED" 2382 for descr, step in steps: 2383 if (datetime.now() - startTime) > maxTime: 2384 self.log.info( 2385 "::: Test verification is taking too long: Giving up!" 2386 ) 2387 self.log.info( 2388 "::: So far, all checks passed, but not " 2389 "all checks were run." 2390 ) 2391 break 2392 self.log.info(":::") 2393 self.log.info('::: Running test verification step "%s"...' % descr) 2394 self.log.info(":::") 2395 status = step() 2396 if status is not True: 2397 stepResults[descr] = "FAIL" 2398 finalResult = "FAILED!" 2399 break 2400 stepResults[descr] = "Pass" 2401 self.log.info(":::") 2402 self.log.info( 2403 "::: Test verification summary for: %s" % test_object["path"] 2404 ) 2405 self.log.info(":::") 2406 for descr in sorted(stepResults.keys()): 2407 self.log.info("::: %s : %s" % (descr, stepResults[descr])) 2408 self.log.info(":::") 2409 self.log.info("::: Test verification %s" % finalResult) 2410 self.log.info(":::") 2411 2412 self.shutdownNode() 2413 self.shutdownHttp3Server() 2414 2415 return status 2416 2417 def start_test(self, test): 2418 test.start() 2419 2420 def test_ended(self, test): 2421 pass 2422 2423 def runTestList( 2424 self, tests_queue, sequential_tests, testClass, mobileArgs, **kwargs 2425 ): 2426 if self.sequential: 2427 self.log.info("Running tests sequentially.") 2428 else: 2429 self.log.info("Using at most %d threads." % self.threadCount) 2430 2431 # keep a set of threadCount running tests and start running the 2432 # tests in the queue at most threadCount at a time 2433 running_tests = set() 2434 keep_going = True 2435 infra_abort = False 2436 exceptions = [] 2437 tracebacks = [] 2438 self.try_again_list = [] 2439 2440 tests_by_manifest = defaultdict(list) 2441 for test in self.alltests: 2442 group = get_full_group_name(test) 2443 tests_by_manifest[group].append(test["id"]) 2444 2445 self.log.suite_start(tests_by_manifest, name="xpcshell") 2446 2447 # Start group for parallel test execution 2448 parallel_group_started = False 2449 if tests_queue: 2450 self.log.group_start(name="parallel") 2451 parallel_group_started = True 2452 2453 while tests_queue or running_tests: 2454 # if we're not supposed to continue and all of the running tests 2455 # are done, stop 2456 if not keep_going and not running_tests: 2457 break 2458 2459 # if there's room to run more tests, start running them 2460 while ( 2461 keep_going and tests_queue and (len(running_tests) < self.threadCount) 2462 ): 2463 test = tests_queue.popleft() 2464 running_tests.add(test) 2465 self.start_test(test) 2466 2467 # queue is full (for now) or no more new tests, 2468 # process the finished tests so far 2469 2470 # wait for at least one of the tests to finish 2471 self.event.wait(1) 2472 self.event.clear() 2473 2474 # find what tests are done (might be more than 1) 2475 done_tests = set() 2476 for test in running_tests: 2477 if test.done: 2478 self.test_ended(test) 2479 done_tests.add(test) 2480 test.join( 2481 1 2482 ) # join with timeout so we don't hang on blocked threads 2483 # if the test had trouble, we will try running it again 2484 # at the end of the run 2485 if test.retry or test.is_alive(): 2486 # if the join call timed out, test.is_alive => True 2487 self.try_again_list.append(test.test_object) 2488 # Print the failure output now, marking failures as expected 2489 # since we'll retry sequentially 2490 test.log_full_output(mark_failures_as_expected=True) 2491 continue 2492 # did the test encounter any exception? 2493 if test.exception: 2494 exceptions.append(test.exception) 2495 tracebacks.append(test.traceback) 2496 # we won't add any more tests, will just wait for 2497 # the currently running ones to finish 2498 keep_going = False 2499 infra_abort = infra_abort and test.infra 2500 keep_going = keep_going and test.keep_going 2501 self.addTestResults(test) 2502 2503 # make room for new tests to run 2504 running_tests.difference_update(done_tests) 2505 2506 # End group for parallel test execution 2507 if parallel_group_started: 2508 self.log.group_end(name="parallel") 2509 2510 if infra_abort: 2511 return TBPL_RETRY # terminate early 2512 2513 if keep_going: 2514 # run the other tests sequentially 2515 if sequential_tests: 2516 self.log.group_start(name="sequential") 2517 for test in sequential_tests: 2518 if not keep_going: 2519 self.log.error( 2520 "TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so " 2521 "stopped run. (Use --keep-going to keep running tests " 2522 "after killing one with SIGINT)" 2523 ) 2524 break 2525 self.start_test(test) 2526 test.join() 2527 self.test_ended(test) 2528 if (test.failCount > 0 or test.passCount <= 0) and test.retry: 2529 self.try_again_list.append(test.test_object) 2530 # Print the failure output now, marking failures as expected 2531 # since we'll retry sequentially 2532 test.log_full_output(mark_failures_as_expected=True) 2533 continue 2534 self.addTestResults(test) 2535 # did the test encounter any exception? 2536 if test.exception: 2537 exceptions.append(test.exception) 2538 tracebacks.append(test.traceback) 2539 break 2540 keep_going = test.keep_going 2541 2542 if sequential_tests: 2543 self.log.group_end(name="sequential") 2544 2545 # retry tests that failed when run in parallel 2546 if self.try_again_list: 2547 self.log.info("Retrying tests that failed when run in parallel.") 2548 self.log.group_start(name="retry") 2549 for test_object in self.try_again_list: 2550 test = testClass( 2551 test_object, 2552 retry=False, 2553 verbose=self.verbose, 2554 mobileArgs=mobileArgs, 2555 **kwargs, 2556 ) 2557 self.start_test(test) 2558 test.join() 2559 self.test_ended(test) 2560 self.addTestResults(test) 2561 # did the test encounter any exception? 2562 if test.exception: 2563 exceptions.append(test.exception) 2564 tracebacks.append(test.traceback) 2565 break 2566 keep_going = test.keep_going 2567 2568 if self.try_again_list: 2569 self.log.group_end(name="retry") 2570 2571 # restore default SIGINT behaviour 2572 if self.sequential: 2573 signal.signal(signal.SIGINT, signal.SIG_DFL) 2574 2575 # Clean up any slacker directories that might be lying around 2576 # Some might fail because of windows taking too long to unlock them. 2577 # We don't do anything if this fails because the test machines will have 2578 # their $TEMP dirs cleaned up on reboot anyway. 2579 for directory in self.cleanup_dir_list: 2580 try: 2581 shutil.rmtree(directory) 2582 except Exception: 2583 self.log.info("%s could not be cleaned up." % directory) 2584 2585 if exceptions: 2586 self.log.info("Following exceptions were raised:") 2587 for t in tracebacks: 2588 self.log.error(t) 2589 raise exceptions[0] 2590 2591 if self.testCount == 0 and os.environ.get("MOZ_AUTOMATION") != "1": 2592 self.log.error("No tests run. Did you pass an invalid --test-path?") 2593 self.failCount = 1 2594 2595 # doing this allows us to pass the mozharness parsers that 2596 # report an orange job for failCount>0 2597 if self.runFailures: 2598 passed = self.passCount 2599 self.passCount = self.failCount 2600 self.failCount = passed 2601 2602 self.log.info("INFO | Result summary:") 2603 self.log.info("INFO | Passed: %d" % self.passCount) 2604 self.log.info("INFO | Failed: %d" % self.failCount) 2605 self.log.info("INFO | Todo: %d" % self.todoCount) 2606 self.log.info("INFO | Retried: %d" % len(self.try_again_list)) 2607 2608 if gotSIGINT and not keep_going: 2609 self.log.error( 2610 "TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so stopped run. " 2611 "(Use --keep-going to keep running tests after " 2612 "killing one with SIGINT)" 2613 ) 2614 return False 2615 2616 self.log.suite_end() 2617 return self.runFailures or self.failCount == 0 2618 2619 2620 def main(): 2621 parser = parser_desktop() 2622 options = parser.parse_args() 2623 2624 log = commandline.setup_logging("XPCShell", options, {"tbpl": sys.stdout}) 2625 2626 if options.xpcshell is None and options.app_binary is None: 2627 log.error( 2628 "Must provide path to xpcshell using --xpcshell or Firefox using --app-binary" 2629 ) 2630 sys.exit(1) 2631 2632 if options.xpcshell is not None and options.app_binary is not None: 2633 log.error( 2634 "Cannot provide --xpcshell and --app-binary - they are mutually exclusive options. Choose one." 2635 ) 2636 sys.exit(1) 2637 2638 xpcsh = XPCShellTests(log) 2639 2640 if options.interactive and not options.testPath: 2641 log.error("Error: You must specify a test filename in interactive mode!") 2642 sys.exit(1) 2643 2644 result = xpcsh.runTests(options) 2645 if result == TBPL_RETRY: 2646 sys.exit(4) 2647 2648 if not result: 2649 sys.exit(1) 2650 2651 2652 if __name__ == "__main__": 2653 main()