runreftest.py (43159B)
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 reftest test harness. 7 """ 8 9 import json 10 import os 11 import platform 12 import posixpath 13 import re 14 import shutil 15 import signal 16 import subprocess 17 import sys 18 import tempfile 19 import threading 20 from collections import defaultdict 21 from datetime import datetime, timedelta 22 23 SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) 24 if SCRIPT_DIRECTORY not in sys.path: 25 sys.path.insert(0, SCRIPT_DIRECTORY) 26 27 import mozcrash 28 import mozdebug 29 import mozfile 30 import mozinfo 31 import mozleak 32 import mozlog 33 import mozprocess 34 import mozprofile 35 import mozrunner 36 from manifestparser import TestManifest 37 from manifestparser import filters as mpf 38 from mozrunner.utils import get_stack_fixer_function, test_environment 39 from mozscreenshot import dump_screen, printstatus 40 41 try: 42 from marionette_driver.addons import Addons 43 from marionette_driver.marionette import Marionette 44 except ImportError as e: # noqa 45 # Defer ImportError until attempt to use Marionette. 46 # Python 3 deletes the exception once the except block 47 # is exited. Save a version to raise later. 48 e_save = ImportError(str(e)) 49 50 def reraise_(*args, **kwargs): 51 raise (e_save) # noqa 52 53 Marionette = reraise_ 54 55 import reftestcommandline 56 from output import OutputHandler, ReftestFormatter 57 58 here = os.path.abspath(os.path.dirname(__file__)) 59 60 try: 61 from mozbuild.base import MozbuildObject 62 from mozbuild.util import cpu_count 63 64 build_obj = MozbuildObject.from_environment(cwd=here) 65 except ImportError: 66 build_obj = None 67 from multiprocessing import cpu_count 68 69 70 def categoriesToRegex(categoryList): 71 return "\\(" + ", ".join(["(?P<%s>\\d+) %s" % c for c in categoryList]) + "\\)" 72 73 74 summaryLines = [ 75 ("Successful", [("pass", "pass"), ("loadOnly", "load only")]), 76 ( 77 "Unexpected", 78 [ 79 ("fail", "unexpected fail"), 80 ("pass", "unexpected pass"), 81 ("asserts", "unexpected asserts"), 82 ("fixedAsserts", "unexpected fixed asserts"), 83 ("failedLoad", "failed load"), 84 ("exception", "exception"), 85 ], 86 ), 87 ( 88 "Known problems", 89 [ 90 ("knownFail", "known fail"), 91 ("knownAsserts", "known asserts"), 92 ("random", "random"), 93 ("skipped", "skipped"), 94 ("slow", "slow"), 95 ], 96 ), 97 ] 98 99 100 def reraise_(tp_, value_, tb_=None): 101 if value_ is None: 102 value_ = tp_() 103 if value_.__traceback__ is not tb_: 104 raise value_.with_traceback(tb_) 105 raise value_ 106 107 108 def update_mozinfo(): 109 """walk up directories to find mozinfo.json update the info""" 110 # TODO: This should go in a more generic place, e.g. mozinfo 111 112 path = SCRIPT_DIRECTORY 113 dirs = set() 114 while path != os.path.expanduser("~"): 115 if path in dirs: 116 break 117 dirs.add(path) 118 path = os.path.split(path)[0] 119 mozinfo.find_and_update_from_json(*dirs) 120 121 122 # Python's print is not threadsafe. 123 printLock = threading.Lock() 124 125 126 class ReftestThread(threading.Thread): 127 def __init__(self, cmdargs): 128 threading.Thread.__init__(self) 129 self.cmdargs = cmdargs 130 self.summaryMatches = {} 131 self.retcode = -1 132 for text, _ in summaryLines: 133 self.summaryMatches[text] = None 134 135 def run(self): 136 with printLock: 137 print("Starting thread with", self.cmdargs) 138 sys.stdout.flush() 139 process = subprocess.Popen(self.cmdargs, stdout=subprocess.PIPE) 140 for chunk in self.chunkForMergedOutput(process.stdout): 141 with printLock: 142 print(chunk, end=" ") 143 sys.stdout.flush() 144 self.retcode = process.wait() 145 146 def chunkForMergedOutput(self, logsource): 147 """Gather lines together that should be printed as one atomic unit. 148 Individual test results--anything between 'REFTEST TEST-START' and 149 'REFTEST TEST-END' lines--are an atomic unit. Lines with data from 150 summaries are parsed and the data stored for later aggregation. 151 Other lines are considered their own atomic units and are permitted 152 to intermix freely.""" 153 testStartRegex = re.compile("^REFTEST TEST-START") 154 testEndRegex = re.compile("^REFTEST TEST-END") 155 summaryHeadRegex = re.compile("^REFTEST INFO \\| Result summary:") 156 summaryRegexFormatString = ( 157 "^REFTEST INFO \\| (?P<message>{text}): (?P<total>\\d+) {regex}" 158 ) 159 summaryRegexStrings = [ 160 summaryRegexFormatString.format( 161 text=text, regex=categoriesToRegex(categories) 162 ) 163 for (text, categories) in summaryLines 164 ] 165 summaryRegexes = [re.compile(regex) for regex in summaryRegexStrings] 166 167 for line in logsource: 168 if testStartRegex.search(line) is not None: 169 chunkedLines = [line] 170 for lineToBeChunked in logsource: 171 chunkedLines.append(lineToBeChunked) 172 if testEndRegex.search(lineToBeChunked) is not None: 173 break 174 yield "".join(chunkedLines) 175 continue 176 177 haveSuppressedSummaryLine = False 178 for regex in summaryRegexes: 179 match = regex.search(line) 180 if match is not None: 181 self.summaryMatches[match.group("message")] = match 182 haveSuppressedSummaryLine = True 183 break 184 if haveSuppressedSummaryLine: 185 continue 186 187 if summaryHeadRegex.search(line) is None: 188 yield line 189 190 191 class ReftestResolver: 192 def defaultManifest(self, suite): 193 return { 194 "reftest": "reftest.list", 195 "crashtest": "crashtests.list", 196 "jstestbrowser": "jstests.list", 197 }[suite] 198 199 def directoryManifest(self, suite, path): 200 return os.path.join(path, self.defaultManifest(suite)) 201 202 def findManifest(self, suite, test_file, subdirs=True): 203 """Return a tuple of (manifest-path, filter-string) for running test_file. 204 205 test_file is a path to a test or a manifest file 206 """ 207 rv = [] 208 default_manifest = self.defaultManifest(suite) 209 relative_path = None 210 if not os.path.isabs(test_file): 211 relative_path = test_file 212 test_file = self.absManifestPath(test_file) 213 214 if os.path.isdir(test_file): 215 for dirpath, dirnames, filenames in os.walk(test_file): 216 if default_manifest in filenames: 217 rv.append((os.path.join(dirpath, default_manifest), None)) 218 # We keep recursing into subdirectories which means that in the case 219 # of include directives we get the same manifest multiple times. 220 # However reftest.js will only read each manifest once 221 222 if ( 223 len(rv) == 0 224 and relative_path 225 and suite == "jstestbrowser" 226 and build_obj 227 ): 228 # The relative path can be from staging area. 229 staged_js_dir = os.path.join( 230 build_obj.topobjdir, "dist", "test-stage", "jsreftest" 231 ) 232 staged_file = os.path.join(staged_js_dir, "tests", relative_path) 233 return self.findManifest(suite, staged_file, subdirs) 234 elif test_file.endswith(".list"): 235 if os.path.exists(test_file): 236 rv = [(test_file, None)] 237 else: 238 dirname, pathname = os.path.split(test_file) 239 found = True 240 while not os.path.exists(os.path.join(dirname, default_manifest)): 241 dirname, suffix = os.path.split(dirname) 242 pathname = posixpath.join(suffix, pathname) 243 if os.path.dirname(dirname) == dirname: 244 found = False 245 break 246 if found: 247 rv = [ 248 ( 249 os.path.join(dirname, default_manifest), 250 r".*%s(?:[#?].*)?$" % pathname.replace("?", r"\?"), 251 ) 252 ] 253 254 return rv 255 256 def absManifestPath(self, path): 257 return os.path.normpath(os.path.abspath(path)) 258 259 def manifestURL(self, options, path): 260 return "file://%s" % path 261 262 def resolveManifests(self, options, tests): 263 suite = options.suite 264 manifests = {} 265 for testPath in tests: 266 for manifest, filter_str in self.findManifest(suite, testPath): 267 if manifest not in manifests: 268 manifests[manifest] = set() 269 manifests[manifest].add(filter_str) 270 manifests_by_url = {} 271 for key in manifests.keys(): 272 id = os.path.relpath( 273 os.path.abspath(os.path.dirname(key)), options.topsrcdir 274 ) 275 id = id.replace(os.sep, posixpath.sep) 276 if None in manifests[key]: 277 manifests[key] = (None, id) 278 else: 279 manifests[key] = ("|".join(list(manifests[key])), id) 280 url = self.manifestURL(options, key) 281 manifests_by_url[url] = manifests[key] 282 return manifests_by_url 283 284 285 class RefTest: 286 oldcwd = os.getcwd() 287 resolver_cls = ReftestResolver 288 use_marionette = True 289 290 def __init__(self, suite): 291 update_mozinfo() 292 self.lastTestSeen = None 293 self.lastTest = None 294 self.haveDumpedScreen = False 295 self.resolver = self.resolver_cls() 296 self.log = None 297 self.outputHandler = None 298 self.testDumpFile = os.path.join(tempfile.gettempdir(), "reftests.json") 299 self.currentManifest = "No test started" 300 self.gtkTheme = self.getGtkTheme() 301 302 self.run_by_manifest = True 303 if suite in ("crashtest", "jstestbrowser"): 304 self.run_by_manifest = False 305 306 def _populate_logger(self, options): 307 if self.log: 308 return 309 310 self.log = getattr(options, "log", None) 311 if self.log: 312 return 313 314 mozlog.commandline.log_formatters["tbpl"] = ( 315 ReftestFormatter, 316 "Reftest specific formatter for the" 317 "benefit of legacy log parsers and" 318 "tools such as the reftest analyzer", 319 ) 320 fmt_options = {} 321 if not options.log_tbpl_level and os.environ.get("MOZ_REFTEST_VERBOSE"): 322 options.log_tbpl_level = fmt_options["level"] = "debug" 323 self.log = mozlog.commandline.setup_logging( 324 "reftest harness", options, {"tbpl": sys.stdout}, fmt_options 325 ) 326 327 def getGtkTheme(self): 328 if not platform.system() == "Linux": 329 return "" 330 331 try: 332 theme_cmd = "gsettings get org.gnome.desktop.interface gtk-theme" 333 theme = subprocess.check_output( 334 theme_cmd, shell=True, universal_newlines=True 335 ) 336 if theme: 337 theme = theme.strip("\n") 338 theme = theme.strip("'") 339 return theme.strip() 340 except subprocess.CalledProcessError: 341 return "" 342 343 def getFullPath(self, path): 344 "Get an absolute path relative to self.oldcwd." 345 return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path))) 346 347 def createReftestProfile( 348 self, 349 options, 350 tests=None, 351 manifests=None, 352 server="localhost", 353 port=0, 354 profile_to_clone=None, 355 prefs=None, 356 ): 357 """Sets up a profile for reftest. 358 359 :param options: Object containing command line options 360 :param tests: List of test objects to run 361 :param manifests: List of manifest files to parse (only takes effect 362 if tests were not passed in) 363 :param server: Server name to use for http tests 364 :param profile_to_clone: Path to a profile to use as the basis for the 365 test profile 366 :param prefs: Extra preferences to set in the profile 367 """ 368 locations = mozprofile.permissions.ServerLocations() 369 locations.add_host(server, scheme="http", port=port) 370 locations.add_host(server, scheme="https", port=port) 371 372 sandbox_allowlist_paths = options.sandboxReadWhitelist 373 if platform.system() == "Linux" or platform.system() in ( 374 "Windows", 375 "Microsoft", 376 ): 377 # Trailing slashes are needed to indicate directories on Linux and Windows 378 sandbox_allowlist_paths = map( 379 lambda p: os.path.join(p, ""), sandbox_allowlist_paths 380 ) 381 382 addons = [] 383 if not self.use_marionette: 384 addons.append(options.reftestExtensionPath) 385 386 if options.specialPowersExtensionPath is not None: 387 if not self.use_marionette: 388 addons.append(options.specialPowersExtensionPath) 389 390 # Install distributed extensions, if application has any. 391 distExtDir = os.path.join( 392 options.app[: options.app.rfind(os.sep)], "distribution", "extensions" 393 ) 394 if os.path.isdir(distExtDir): 395 for f in os.listdir(distExtDir): 396 addons.append(os.path.join(distExtDir, f)) 397 398 # Install custom extensions. 399 for f in options.extensionsToInstall: 400 addons.append(self.getFullPath(f)) 401 402 kwargs = { 403 "addons": addons, 404 "locations": locations, 405 "allowlistpaths": sandbox_allowlist_paths, 406 } 407 if profile_to_clone: 408 profile = mozprofile.Profile.clone(profile_to_clone, **kwargs) 409 else: 410 profile = mozprofile.Profile(**kwargs) 411 412 # First set prefs from the base profiles under testing/profiles. 413 414 # In test packages used in CI, the profile_data directory is installed 415 # in the SCRIPT_DIRECTORY. 416 profile_data_dir = os.path.join(SCRIPT_DIRECTORY, "profile_data") 417 # If possible, read profile data from topsrcdir. This prevents us from 418 # requiring a re-build to pick up newly added extensions in the 419 # <profile>/extensions directory. 420 if build_obj: 421 path = os.path.join(build_obj.topsrcdir, "testing", "profiles") 422 if os.path.isdir(path): 423 profile_data_dir = path 424 # Still not found? Look for testing/profiles relative to layout/tools/reftest. 425 if not os.path.isdir(profile_data_dir): 426 path = os.path.abspath( 427 os.path.join(SCRIPT_DIRECTORY, "..", "..", "..", "testing", "profiles") 428 ) 429 if os.path.isdir(path): 430 profile_data_dir = path 431 432 with open(os.path.join(profile_data_dir, "profiles.json")) as fh: 433 base_profiles = json.load(fh)["reftest"] 434 435 for name in base_profiles: 436 path = os.path.join(profile_data_dir, name) 437 profile.merge(path) 438 439 # Second set preferences for communication between our command line 440 # arguments and the reftest harness. Preferences that are required for 441 # reftest to work should instead be set under srcdir/testing/profiles. 442 prefs = prefs or {} 443 prefs["reftest.timeout"] = options.timeout * 1000 444 if options.logFile: 445 prefs["reftest.logFile"] = options.logFile 446 if options.ignoreWindowSize: 447 prefs["reftest.ignoreWindowSize"] = True 448 if options.shuffle: 449 prefs["reftest.shuffle"] = True 450 if options.repeat: 451 prefs["reftest.repeat"] = options.repeat 452 if options.runUntilFailure: 453 prefs["reftest.runUntilFailure"] = True 454 if not options.repeat: 455 prefs["reftest.repeat"] = 30 456 if options.verify: 457 prefs["reftest.verify"] = True 458 if options.cleanupCrashes: 459 prefs["reftest.cleanupPendingCrashes"] = True 460 prefs["reftest.focusFilterMode"] = options.focusFilterMode 461 prefs["reftest.logLevel"] = options.log_tbpl_level or "info" 462 prefs["reftest.suite"] = options.suite 463 prefs["sandbox.mozinfo"] = json.dumps(mozinfo.info) 464 465 # Set tests to run or manifests to parse. 466 if tests: 467 testlist = os.path.join(profile.profile, "reftests.json") 468 with open(testlist, "w") as fh: 469 json.dump(tests, fh) 470 prefs["reftest.tests"] = testlist 471 elif manifests: 472 prefs["reftest.manifests"] = json.dumps(manifests) 473 474 # Avoid unncessary recursion when MOZHARNESS_TEST_PATHS is set 475 prefs["reftest.mozharness_test_paths"] = ( 476 len(os.environ.get("MOZHARNESS_TEST_PATHS", "")) > 0 477 ) 478 479 # default fission to True 480 prefs["fission.autostart"] = True 481 if options.disableFission: 482 prefs["fission.autostart"] = False 483 484 if not self.run_by_manifest: 485 if options.totalChunks: 486 prefs["reftest.totalChunks"] = options.totalChunks 487 if options.thisChunk: 488 prefs["reftest.thisChunk"] = options.thisChunk 489 490 if options.marionette: 491 # options.marionette can specify host:port 492 port = options.marionette.split(":")[1] 493 prefs["marionette.port"] = int(port) 494 495 # Enable tracing output for detailed failures in case of 496 # failing connection attempts, and hangs (bug 1397201) 497 prefs["remote.log.level"] = "Trace" 498 499 # Third, set preferences passed in via the command line. 500 for v in options.extraPrefs: 501 thispref = v.split("=") 502 if len(thispref) < 2: 503 print("Error: syntax error in --setpref=" + v) 504 sys.exit(1) 505 prefs[thispref[0]] = thispref[1].strip() 506 507 for pref in prefs: 508 prefs[pref] = mozprofile.Preferences.cast(prefs[pref]) 509 profile.set_preferences(prefs) 510 511 if os.path.join(here, "chrome") not in options.extraProfileFiles: 512 options.extraProfileFiles.append(os.path.join(here, "chrome")) 513 514 self.copyExtraFilesToProfile(options, profile) 515 516 self.log.info(f"Running with e10s: {options.e10s}") 517 self.log.info("Running with fission: {}".format(prefs["fission.autostart"])) 518 519 return profile 520 521 def environment(self, **kwargs): 522 kwargs["log"] = self.log 523 return test_environment(**kwargs) 524 525 def buildBrowserEnv(self, options, profileDir): 526 browserEnv = self.environment( 527 xrePath=options.xrePath, debugger=options.debugger 528 ) 529 browserEnv["XPCOM_DEBUG_BREAK"] = "stack" 530 531 if mozinfo.info["asan"]: 532 # Disable leak checking for reftests for now 533 if "ASAN_OPTIONS" in browserEnv: 534 browserEnv["ASAN_OPTIONS"] += ":detect_leaks=0" 535 else: 536 browserEnv["ASAN_OPTIONS"] = "detect_leaks=0" 537 538 # Set environment defaults for jstestbrowser. Keep in sync with the 539 # defaults used in js/src/tests/lib/tests.py. 540 if options.suite == "jstestbrowser": 541 browserEnv["TZ"] = "PST8PDT" 542 browserEnv["LC_ALL"] = "en_US.UTF-8" 543 544 for v in options.environment: 545 ix = v.find("=") 546 if ix <= 0: 547 print("Error: syntax error in --setenv=" + v) 548 return None 549 browserEnv[v[:ix]] = v[ix + 1 :] 550 551 # Enable leaks detection to its own log file. 552 self.leakLogFile = os.path.join(profileDir, "runreftest_leaks.log") 553 browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leakLogFile 554 555 # TODO: this is always defined (as part of --enable-webrender which is default) 556 # can we make this default in the browser? 557 browserEnv["MOZ_ACCELERATED"] = "1" 558 559 if options.headless: 560 browserEnv["MOZ_HEADLESS"] = "1" 561 562 if not options.e10s: 563 browserEnv["MOZ_FORCE_DISABLE_E10S"] = "1" 564 565 return browserEnv 566 567 def cleanup(self, profileDir): 568 if profileDir: 569 shutil.rmtree(profileDir, True) 570 571 def verifyTests(self, tests, options): 572 """ 573 Support --verify mode: Run test(s) many times in a variety of 574 configurations/environments in an effort to find intermittent 575 failures. 576 """ 577 578 self._populate_logger(options) 579 580 # Number of times to repeat test(s) when running with --repeat 581 VERIFY_REPEAT = 10 582 # Number of times to repeat test(s) when running test in separate browser 583 VERIFY_REPEAT_SINGLE_BROWSER = 5 584 585 def step1(): 586 options.repeat = VERIFY_REPEAT 587 options.runUntilFailure = True 588 result = self.runTests(tests, options) 589 return result 590 591 def step2(): 592 options.repeat = 0 593 options.runUntilFailure = False 594 for i in range(VERIFY_REPEAT_SINGLE_BROWSER): 595 result = self.runTests(tests, options) 596 if result != 0: 597 break 598 return result 599 600 def step3(): 601 options.repeat = VERIFY_REPEAT 602 options.runUntilFailure = True 603 options.environment.append("MOZ_CHAOSMODE=0xfb") 604 result = self.runTests(tests, options) 605 options.environment.remove("MOZ_CHAOSMODE=0xfb") 606 return result 607 608 def step4(): 609 options.repeat = 0 610 options.runUntilFailure = False 611 options.environment.append("MOZ_CHAOSMODE=0xfb") 612 for i in range(VERIFY_REPEAT_SINGLE_BROWSER): 613 result = self.runTests(tests, options) 614 if result != 0: 615 break 616 options.environment.remove("MOZ_CHAOSMODE=0xfb") 617 return result 618 619 steps = [ 620 ("1. Run each test %d times in one browser." % VERIFY_REPEAT, step1), 621 ( 622 "2. Run each test %d times in a new browser each time." 623 % VERIFY_REPEAT_SINGLE_BROWSER, 624 step2, 625 ), 626 ( 627 "3. Run each test %d times in one browser, in chaos mode." 628 % VERIFY_REPEAT, 629 step3, 630 ), 631 ( 632 "4. Run each test %d times in a new browser each time, in chaos mode." 633 % VERIFY_REPEAT_SINGLE_BROWSER, 634 step4, 635 ), 636 ] 637 638 stepResults = {} 639 for descr, step in steps: 640 stepResults[descr] = "not run / incomplete" 641 642 startTime = datetime.now() 643 maxTime = timedelta(seconds=options.verify_max_time) 644 finalResult = "PASSED" 645 for descr, step in steps: 646 if (datetime.now() - startTime) > maxTime: 647 self.log.info("::: Test verification is taking too long: Giving up!") 648 self.log.info( 649 "::: So far, all checks passed, but not all checks were run." 650 ) 651 break 652 self.log.info(":::") 653 self.log.info('::: Running test verification step "%s"...' % descr) 654 self.log.info(":::") 655 result = step() 656 if result != 0: 657 stepResults[descr] = "FAIL" 658 finalResult = "FAILED!" 659 break 660 stepResults[descr] = "Pass" 661 662 self.log.info(":::") 663 self.log.info("::: Test verification summary for:") 664 self.log.info(":::") 665 for test in tests: 666 self.log.info("::: " + test) 667 self.log.info(":::") 668 for descr in sorted(stepResults.keys()): 669 self.log.info("::: %s : %s" % (descr, stepResults[descr])) 670 self.log.info(":::") 671 self.log.info("::: Test verification %s" % finalResult) 672 self.log.info(":::") 673 674 return result 675 676 def runTests(self, tests, options, cmdargs=None): 677 cmdargs = cmdargs or [] 678 self._populate_logger(options) 679 self.outputHandler = OutputHandler( 680 self.log, options.utilityPath, options.symbolsPath 681 ) 682 683 if options.cleanupCrashes: 684 mozcrash.cleanup_pending_crash_reports() 685 686 manifests = self.resolver.resolveManifests(options, tests) 687 if options.filter: 688 manifests[""] = (options.filter, None) 689 690 if not getattr(options, "runTestsInParallel", False): 691 return self.runSerialTests(manifests, options, cmdargs) 692 693 cpuCount = cpu_count() 694 695 # We have the directive, technology, and machine to run multiple test instances. 696 # Experimentation says that reftests are not overly CPU-intensive, so we can run 697 # multiple jobs per CPU core. 698 # 699 # Our Windows machines in automation seem to get upset when we run a lot of 700 # simultaneous tests on them, so tone things down there. 701 if sys.platform == "win32": 702 jobsWithoutFocus = cpuCount 703 else: 704 jobsWithoutFocus = 2 * cpuCount 705 706 totalJobs = jobsWithoutFocus + 1 707 perProcessArgs = [sys.argv[:] for i in range(0, totalJobs)] 708 709 host = "localhost" 710 port = 2828 711 if options.marionette: 712 host, port = options.marionette.split(":") 713 714 # First job is only needs-focus tests. Remaining jobs are 715 # non-needs-focus and chunked. 716 perProcessArgs[0].insert(-1, "--focus-filter-mode=needs-focus") 717 for chunkNumber, jobArgs in enumerate(perProcessArgs[1:], start=1): 718 jobArgs[-1:-1] = [ 719 "--focus-filter-mode=non-needs-focus", 720 "--total-chunks=%d" % jobsWithoutFocus, 721 "--this-chunk=%d" % chunkNumber, 722 "--marionette=%s:%d" % (host, port), 723 ] 724 port += 1 725 726 for jobArgs in perProcessArgs: 727 try: 728 jobArgs.remove("--run-tests-in-parallel") 729 except Exception: 730 pass 731 jobArgs[0:0] = [sys.executable, "-u"] 732 733 threads = [ReftestThread(args) for args in perProcessArgs[1:]] 734 for t in threads: 735 t.start() 736 737 while True: 738 # The test harness in each individual thread will be doing timeout 739 # handling on its own, so we shouldn't need to worry about any of 740 # the threads hanging for arbitrarily long. 741 for t in threads: 742 t.join(10) 743 if not any(t.is_alive() for t in threads): 744 break 745 746 # Run the needs-focus tests serially after the other ones, so we don't 747 # have to worry about races between the needs-focus tests *actually* 748 # needing focus and the dummy windows in the non-needs-focus tests 749 # trying to focus themselves. 750 focusThread = ReftestThread(perProcessArgs[0]) 751 focusThread.start() 752 focusThread.join() 753 754 # Output the summaries that the ReftestThread filters suppressed. 755 summaryObjects = [defaultdict(int) for s in summaryLines] 756 for t in threads: 757 for summaryObj, (text, categories) in zip(summaryObjects, summaryLines): 758 threadMatches = t.summaryMatches[text] 759 for attribute, description in categories: 760 amount = int(threadMatches.group(attribute) if threadMatches else 0) 761 summaryObj[attribute] += amount 762 amount = int(threadMatches.group("total") if threadMatches else 0) 763 summaryObj["total"] += amount 764 765 print("REFTEST INFO | Result summary:") 766 for summaryObj, (text, categories) in zip(summaryObjects, summaryLines): 767 details = ", ".join([ 768 "%d %s" % (summaryObj[attribute], description) 769 for (attribute, description) in categories 770 ]) 771 print( 772 "REFTEST INFO | " 773 + text 774 + ": " 775 + str(summaryObj["total"]) 776 + " (" 777 + details 778 + ")" 779 ) 780 781 return int(any(t.retcode != 0 for t in threads)) 782 783 def handleTimeout(self, timeout, proc, utilityPath, debuggerInfo): 784 """handle process output timeout""" 785 # TODO: bug 913975 : _processOutput should call self.processOutputLine 786 # one more time one timeout (I think) 787 self.log.error( 788 "%s | application timed out after %d seconds with no output" 789 % (self.lastTestSeen, int(timeout)) 790 ) 791 self.log.warning("Force-terminating active process(es).") 792 self.killAndGetStack( 793 proc, utilityPath, debuggerInfo, dump_screen=not debuggerInfo 794 ) 795 796 def dumpScreen(self, utilityPath): 797 if self.haveDumpedScreen: 798 self.log.info( 799 "Not taking screenshot here: see the one that was previously logged" 800 ) 801 return 802 self.haveDumpedScreen = True 803 dump_screen(utilityPath, self.log) 804 805 def killAndGetStack(self, process, utilityPath, debuggerInfo, dump_screen=False): 806 """ 807 Kill the process, preferrably in a way that gets us a stack trace. 808 Also attempts to obtain a screenshot before killing the process 809 if specified. 810 """ 811 812 if dump_screen: 813 self.dumpScreen(utilityPath) 814 815 if mozinfo.info.get("crashreporter", True) and not debuggerInfo: 816 if mozinfo.isWin: 817 # We should have a "crashinject" program in our utility path 818 crashinject = os.path.normpath( 819 os.path.join(utilityPath, "crashinject.exe") 820 ) 821 if os.path.exists(crashinject): 822 status = subprocess.Popen([crashinject, str(process.pid)]).wait() 823 printstatus("crashinject", status) 824 if status == 0: 825 return 826 else: 827 try: 828 process.kill(sig=signal.SIGABRT) 829 except OSError: 830 # https://bugzilla.mozilla.org/show_bug.cgi?id=921509 831 self.log.info("Can't trigger Breakpad, process no longer exists") 832 return 833 self.log.info("Can't trigger Breakpad, just killing process") 834 process.kill() 835 836 def runApp( 837 self, 838 options, 839 cmdargs=None, 840 timeout=None, 841 debuggerInfo=None, 842 symbolsPath=None, 843 valgrindPath=None, 844 valgrindArgs=None, 845 valgrindSuppFiles=None, 846 **profileArgs, 847 ): 848 if cmdargs is None: 849 cmdargs = [] 850 cmdargs = cmdargs[:] 851 852 if self.use_marionette: 853 cmdargs.append("-marionette") 854 855 binary = options.app 856 profile = self.createReftestProfile(options, **profileArgs) 857 858 # browser environment 859 env = self.buildBrowserEnv(options, profile.profile) 860 861 def timeoutHandler(): 862 self.handleTimeout(timeout, proc, options.utilityPath, debuggerInfo) 863 864 interactive = False 865 debug_args = None 866 if debuggerInfo: 867 interactive = debuggerInfo.interactive 868 debug_args = [debuggerInfo.path] + debuggerInfo.args 869 870 def record_last_test(message): 871 """Records the last test seen by this harness for the benefit of crash logging.""" 872 873 def testid(test): 874 if " " in test: 875 return test.split(" ")[0] 876 return test 877 878 if message["action"] == "test_start": 879 self.lastTestSeen = testid(message["test"]) 880 elif message["action"] == "test_end": 881 if self.lastTest and message["test"] == self.lastTest: 882 self.lastTestSeen = self.currentManifest 883 else: 884 self.lastTestSeen = "{} (finished)".format(testid(message["test"])) 885 886 self.log.add_handler(record_last_test) 887 888 kp_kwargs = { 889 "kill_on_timeout": False, 890 "cwd": SCRIPT_DIRECTORY, 891 "onTimeout": [timeoutHandler], 892 "processOutputLine": [self.outputHandler], 893 } 894 895 if mozinfo.isWin or mozinfo.isMac: 896 # Prevents log interleaving on Windows at the expense of losing 897 # true log order. See bug 798300 and bug 1324961 for more details. 898 kp_kwargs["processStderrLine"] = [self.outputHandler] 899 900 if interactive: 901 # If an interactive debugger is attached, 902 # don't use timeouts, and don't capture ctrl-c. 903 timeout = None 904 signal.signal(signal.SIGINT, lambda sigid, frame: None) 905 906 runner_cls = mozrunner.runners.get( 907 mozinfo.info.get("appname", "firefox"), mozrunner.Runner 908 ) 909 runner = runner_cls( 910 profile=profile, 911 binary=binary, 912 process_class=mozprocess.ProcessHandlerMixin, 913 cmdargs=cmdargs, 914 env=env, 915 process_args=kp_kwargs, 916 ) 917 runner.start( 918 debug_args=debug_args, interactive=interactive, outputTimeout=timeout 919 ) 920 proc = runner.process_handler 921 self.outputHandler.proc_name = f"GECKO({proc.pid})" 922 923 # Used to defer a possible IOError exception from Marionette 924 marionette_exception = None 925 926 if self.use_marionette: 927 marionette_args = { 928 "socket_timeout": options.marionette_socket_timeout, 929 "startup_timeout": options.marionette_startup_timeout, 930 "symbols_path": options.symbolsPath, 931 } 932 if options.marionette: 933 host, port = options.marionette.split(":") 934 marionette_args["host"] = host 935 marionette_args["port"] = int(port) 936 937 try: 938 marionette = Marionette(**marionette_args) 939 marionette.start_session() 940 941 addons = Addons(marionette) 942 if options.specialPowersExtensionPath: 943 addons.install(options.specialPowersExtensionPath, temp=True) 944 945 addons.install(options.reftestExtensionPath, temp=True) 946 947 marionette.delete_session() 948 except OSError as e: 949 # Any IOError as thrown by Marionette means that something is 950 # wrong with the process, like a crash or the socket is no 951 # longer open. We defer raising this specific error so that 952 # post-test checks for leaks and crashes are performed and 953 # reported first. 954 marionette_exception = e 955 956 status = runner.wait() 957 runner.process_handler = None 958 self.outputHandler.proc_name = None 959 960 crashed = mozcrash.log_crashes( 961 self.log, 962 os.path.join(profile.profile, "minidumps"), 963 options.symbolsPath, 964 test=self.lastTestSeen, 965 ) 966 967 if crashed: 968 # log suite_end to wrap up, this is usually done with in in-browser harness 969 if not self.outputHandler.results: 970 # TODO: while .results is a defaultdict(int), it is proxied via log_actions as data, not type 971 self.outputHandler.results = { 972 "Pass": 0, 973 "LoadOnly": 0, 974 "Exception": 0, 975 "FailedLoad": 0, 976 "UnexpectedFail": 1, 977 "UnexpectedPass": 0, 978 "AssertionUnexpected": 0, 979 "AssertionUnexpectedFixed": 0, 980 "KnownFail": 0, 981 "AssertionKnown": 0, 982 "Random": 0, 983 "Skip": 0, 984 "Slow": 0, 985 } 986 self.log.suite_end(extra={"results": self.outputHandler.results}) 987 988 if not status and crashed: 989 status = 1 990 991 if status and not crashed: 992 msg = "application terminated with exit code %s" % (status) 993 self.log.shutdown_failure(group=self.lastTestSeen, message=msg) 994 995 runner.cleanup() 996 self.cleanup(profile.profile) 997 998 if marionette_exception is not None: 999 raise marionette_exception 1000 1001 self.log.info("Process mode: {}".format("e10s" if options.e10s else "non-e10s")) 1002 return status 1003 1004 def getActiveTests(self, manifests, options, testDumpFile=None): 1005 # These prefs will cause reftest.sys.mjs to parse the manifests, 1006 # dump the resulting tests to a file, and exit. 1007 prefs = { 1008 "reftest.manifests": json.dumps(manifests), 1009 "reftest.manifests.dumpTests": testDumpFile or self.testDumpFile, 1010 } 1011 cmdargs = [] 1012 self.runApp(options, cmdargs=cmdargs, prefs=prefs) 1013 1014 if not os.path.isfile(self.testDumpFile): 1015 print("Error: parsing manifests failed!") 1016 sys.exit(1) 1017 1018 with open(self.testDumpFile) as fh: 1019 tests = json.load(fh) 1020 1021 if os.path.isfile(self.testDumpFile): 1022 mozfile.remove(self.testDumpFile) 1023 1024 for test in tests: 1025 # Name and path are expected by manifestparser, but not used in reftest. 1026 test["name"] = test["path"] = test["url1"] 1027 1028 mp = TestManifest(strict=False) 1029 mp.tests = tests 1030 1031 filters = [] 1032 if options.totalChunks: 1033 filters.append( 1034 mpf.chunk_by_manifest(options.thisChunk, options.totalChunks) 1035 ) 1036 1037 tests = mp.active_tests(exists=False, filters=filters) 1038 return tests 1039 1040 def runSerialTests(self, manifests, options, cmdargs=None): 1041 debuggerInfo = None 1042 if options.debugger: 1043 debuggerInfo = mozdebug.get_debugger_info( 1044 options.debugger, options.debuggerArgs, options.debuggerInteractive 1045 ) 1046 1047 def run(**kwargs): 1048 if kwargs.get("tests"): 1049 self.lastTest = kwargs["tests"][-1]["identifier"] 1050 if not isinstance(self.lastTest, str): 1051 self.lastTest = " ".join(self.lastTest) 1052 1053 status = self.runApp( 1054 options, 1055 manifests=manifests, 1056 cmdargs=cmdargs, 1057 # We generally want the JS harness or marionette 1058 # to handle timeouts if they can. 1059 # The default JS harness timeout is currently 1060 # 300 seconds (default options.timeout). 1061 # The default Marionette socket timeout is 1062 # currently 360 seconds. 1063 # Give the JS harness extra time to deal with 1064 # its own timeouts and try to usually exceed 1065 # the 360 second marionette socket timeout. 1066 # See bug 479518 and bug 1414063. 1067 timeout=options.timeout + 70.0, 1068 debuggerInfo=debuggerInfo, 1069 symbolsPath=options.symbolsPath, 1070 **kwargs, 1071 ) 1072 1073 # do not process leak log when we crash/assert 1074 if status == 0: 1075 mozleak.process_leak_log( 1076 self.leakLogFile, 1077 leak_thresholds=options.leakThresholds, 1078 stack_fixer=get_stack_fixer_function( 1079 options.utilityPath, options.symbolsPath 1080 ), 1081 ) 1082 return status 1083 1084 if not self.run_by_manifest: 1085 return run() 1086 1087 tests = self.getActiveTests(manifests, options) 1088 tests_by_manifest = defaultdict(list) 1089 ids_by_manifest = defaultdict(list) 1090 for t in tests: 1091 tests_by_manifest[t["manifest"]].append(t) 1092 test_id = t["identifier"] 1093 if not isinstance(test_id, str): 1094 test_id = " ".join(test_id) 1095 ids_by_manifest[t["manifestID"]].append(test_id) 1096 1097 self.log.suite_start(ids_by_manifest, name=options.suite) 1098 1099 overall = 0 1100 status = -1 1101 for manifest, tests in tests_by_manifest.items(): 1102 if self.getGtkTheme() != self.gtkTheme: 1103 self.log.error( 1104 "Theme (%s) has changed to (%s), terminating job as this is unstable" 1105 % (self.gtkTheme, self.getGtkTheme()) 1106 ) 1107 return 1 1108 1109 self.log.info(f"Running tests in {manifest}") 1110 self.currentManifest = manifest 1111 status = run(tests=tests) 1112 overall = overall or status 1113 if status == -1: 1114 # we didn't run anything 1115 overall = 1 1116 1117 self.log.suite_end(extra={"results": self.outputHandler.results}) 1118 return overall 1119 1120 def copyExtraFilesToProfile(self, options, profile): 1121 "Copy extra files or dirs specified on the command line to the testing profile." 1122 profileDir = profile.profile 1123 for f in options.extraProfileFiles: 1124 abspath = self.getFullPath(f) 1125 if os.path.isfile(abspath): 1126 if os.path.basename(abspath) == "user.js": 1127 extra_prefs = mozprofile.Preferences.read_prefs(abspath) 1128 profile.set_preferences(extra_prefs) 1129 elif os.path.basename(abspath).endswith(".dic"): 1130 hyphDir = os.path.join(profileDir, "hyphenation") 1131 if not os.path.exists(hyphDir): 1132 os.makedirs(hyphDir) 1133 shutil.copy2(abspath, hyphDir) 1134 else: 1135 shutil.copy2(abspath, profileDir) 1136 elif os.path.isdir(abspath): 1137 dest = os.path.join(profileDir, os.path.basename(abspath)) 1138 shutil.copytree(abspath, dest) 1139 else: 1140 self.log.warning( 1141 "runreftest.py | Failed to copy %s to profile" % abspath 1142 ) 1143 continue 1144 1145 1146 def run_test_harness(parser, options): 1147 reftest = RefTest(options.suite) 1148 parser.validate(options, reftest) 1149 1150 # We have to validate options.app here for the case when the mach 1151 # command is able to find it after argument parsing. This can happen 1152 # when running from a tests archive. 1153 if not options.app: 1154 parser.error("could not find the application path, --appname must be specified") 1155 1156 options.app = reftest.getFullPath(options.app) 1157 if not os.path.exists(options.app): 1158 parser.error( 1159 "Error: Path %(app)s doesn't exist. Are you executing " 1160 "$objdir/_tests/reftest/runreftest.py?" % {"app": options.app} 1161 ) 1162 1163 if options.xrePath is None: 1164 options.xrePath = os.path.dirname(options.app) 1165 1166 if options.verify: 1167 result = reftest.verifyTests(options.tests, options) 1168 else: 1169 result = reftest.runTests(options.tests, options) 1170 1171 return result 1172 1173 1174 if __name__ == "__main__": 1175 parser = reftestcommandline.DesktopArgumentsParser() 1176 options = parser.parse_args() 1177 sys.exit(run_test_harness(parser, options))