1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 Runs the reftest test harness.
20 from collections
import defaultdict
21 from datetime
import datetime
, timedelta
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
)
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 from six
import reraise
, string_types
41 from six
.moves
import range
44 from marionette_driver
.addons
import Addons
45 from marionette_driver
.marionette
import Marionette
46 except ImportError as e
: # noqa
47 # Defer ImportError until attempt to use Marionette.
48 # Python 3 deletes the exception once the except block
49 # is exited. Save a version to raise later.
50 e_save
= ImportError(str(e
))
52 def reraise_(*args
, **kwargs
):
57 import reftestcommandline
58 from output
import OutputHandler
, ReftestFormatter
60 here
= os
.path
.abspath(os
.path
.dirname(__file__
))
63 from mozbuild
.base
import MozbuildObject
65 build_obj
= MozbuildObject
.from_environment(cwd
=here
)
70 def categoriesToRegex(categoryList
):
71 return "\\(" + ", ".join(["(?P<%s>\\d+) %s" % c
for c
in categoryList
]) + "\\)"
75 ("Successful", [("pass", "pass"), ("loadOnly", "load only")]),
79 ("fail", "unexpected fail"),
80 ("pass", "unexpected pass"),
81 ("asserts", "unexpected asserts"),
82 ("fixedAsserts", "unexpected fixed asserts"),
83 ("failedLoad", "failed load"),
84 ("exception", "exception"),
90 ("knownFail", "known fail"),
91 ("knownAsserts", "known asserts"),
93 ("skipped", "skipped"),
100 if sys
.version_info
[0] == 3:
102 def reraise_(tp_
, value_
, tb_
=None):
105 if value_
.__traceback
__ is not tb_
:
106 raise value_
.with_traceback(tb_
)
111 exec("def reraise_(tp_, value_, tb_=None):\n raise tp_, value_, tb_\n")
114 def update_mozinfo():
115 """walk up directories to find mozinfo.json update the info"""
116 # TODO: This should go in a more generic place, e.g. mozinfo
118 path
= SCRIPT_DIRECTORY
120 while path
!= os
.path
.expanduser("~"):
124 path
= os
.path
.split(path
)[0]
125 mozinfo
.find_and_update_from_json(*dirs
)
128 # Python's print is not threadsafe.
129 printLock
= threading
.Lock()
132 class ReftestThread(threading
.Thread
):
133 def __init__(self
, cmdargs
):
134 threading
.Thread
.__init
__(self
)
135 self
.cmdargs
= cmdargs
136 self
.summaryMatches
= {}
138 for text
, _
in summaryLines
:
139 self
.summaryMatches
[text
] = None
143 print("Starting thread with", self
.cmdargs
)
145 process
= subprocess
.Popen(self
.cmdargs
, stdout
=subprocess
.PIPE
)
146 for chunk
in self
.chunkForMergedOutput(process
.stdout
):
148 print(chunk
, end
=" ")
150 self
.retcode
= process
.wait()
152 def chunkForMergedOutput(self
, logsource
):
153 """Gather lines together that should be printed as one atomic unit.
154 Individual test results--anything between 'REFTEST TEST-START' and
155 'REFTEST TEST-END' lines--are an atomic unit. Lines with data from
156 summaries are parsed and the data stored for later aggregation.
157 Other lines are considered their own atomic units and are permitted
158 to intermix freely."""
159 testStartRegex
= re
.compile("^REFTEST TEST-START")
160 testEndRegex
= re
.compile("^REFTEST TEST-END")
161 summaryHeadRegex
= re
.compile("^REFTEST INFO \\| Result summary:")
162 summaryRegexFormatString
= (
163 "^REFTEST INFO \\| (?P<message>{text}): (?P<total>\\d+) {regex}"
165 summaryRegexStrings
= [
166 summaryRegexFormatString
.format(
167 text
=text
, regex
=categoriesToRegex(categories
)
169 for (text
, categories
) in summaryLines
171 summaryRegexes
= [re
.compile(regex
) for regex
in summaryRegexStrings
]
173 for line
in logsource
:
174 if testStartRegex
.search(line
) is not None:
175 chunkedLines
= [line
]
176 for lineToBeChunked
in logsource
:
177 chunkedLines
.append(lineToBeChunked
)
178 if testEndRegex
.search(lineToBeChunked
) is not None:
180 yield "".join(chunkedLines
)
183 haveSuppressedSummaryLine
= False
184 for regex
in summaryRegexes
:
185 match
= regex
.search(line
)
186 if match
is not None:
187 self
.summaryMatches
[match
.group("message")] = match
188 haveSuppressedSummaryLine
= True
190 if haveSuppressedSummaryLine
:
193 if summaryHeadRegex
.search(line
) is None:
197 class ReftestResolver(object):
198 def defaultManifest(self
, suite
):
200 "reftest": "reftest.list",
201 "crashtest": "crashtests.list",
202 "jstestbrowser": "jstests.list",
205 def directoryManifest(self
, suite
, path
):
206 return os
.path
.join(path
, self
.defaultManifest(suite
))
208 def findManifest(self
, suite
, test_file
, subdirs
=True):
209 """Return a tuple of (manifest-path, filter-string) for running test_file.
211 test_file is a path to a test or a manifest file
214 default_manifest
= self
.defaultManifest(suite
)
216 if not os
.path
.isabs(test_file
):
217 relative_path
= test_file
218 test_file
= self
.absManifestPath(test_file
)
220 if os
.path
.isdir(test_file
):
221 for dirpath
, dirnames
, filenames
in os
.walk(test_file
):
222 if default_manifest
in filenames
:
223 rv
.append((os
.path
.join(dirpath
, default_manifest
), None))
224 # We keep recursing into subdirectories which means that in the case
225 # of include directives we get the same manifest multiple times.
226 # However reftest.js will only read each manifest once
231 and suite
== "jstestbrowser"
234 # The relative path can be from staging area.
235 staged_js_dir
= os
.path
.join(
236 build_obj
.topobjdir
, "dist", "test-stage", "jsreftest"
238 staged_file
= os
.path
.join(staged_js_dir
, "tests", relative_path
)
239 return self
.findManifest(suite
, staged_file
, subdirs
)
240 elif test_file
.endswith(".list"):
241 if os
.path
.exists(test_file
):
242 rv
= [(test_file
, None)]
244 dirname
, pathname
= os
.path
.split(test_file
)
246 while not os
.path
.exists(os
.path
.join(dirname
, default_manifest
)):
247 dirname
, suffix
= os
.path
.split(dirname
)
248 pathname
= posixpath
.join(suffix
, pathname
)
249 if os
.path
.dirname(dirname
) == dirname
:
255 os
.path
.join(dirname
, default_manifest
),
256 r
".*%s(?:[#?].*)?$" % pathname
.replace("?", "\?"),
262 def absManifestPath(self
, path
):
263 return os
.path
.normpath(os
.path
.abspath(path
))
265 def manifestURL(self
, options
, path
):
266 return "file://%s" % path
268 def resolveManifests(self
, options
, tests
):
269 suite
= options
.suite
271 for testPath
in tests
:
272 for manifest
, filter_str
in self
.findManifest(suite
, testPath
):
273 if manifest
not in manifests
:
274 manifests
[manifest
] = set()
275 manifests
[manifest
].add(filter_str
)
276 manifests_by_url
= {}
277 for key
in manifests
.keys():
278 id = os
.path
.relpath(
279 os
.path
.abspath(os
.path
.dirname(key
)), options
.topsrcdir
281 id = id.replace(os
.sep
, posixpath
.sep
)
282 if None in manifests
[key
]:
283 manifests
[key
] = (None, id)
285 manifests
[key
] = ("|".join(list(manifests
[key
])), id)
286 url
= self
.manifestURL(options
, key
)
287 manifests_by_url
[url
] = manifests
[key
]
288 return manifests_by_url
291 class RefTest(object):
293 resolver_cls
= ReftestResolver
294 use_marionette
= True
296 def __init__(self
, suite
):
298 self
.lastTestSeen
= None
300 self
.haveDumpedScreen
= False
301 self
.resolver
= self
.resolver_cls()
303 self
.outputHandler
= None
304 self
.testDumpFile
= os
.path
.join(tempfile
.gettempdir(), "reftests.json")
305 self
.currentManifest
= "No test started"
307 self
.run_by_manifest
= True
308 if suite
in ("crashtest", "jstestbrowser"):
309 self
.run_by_manifest
= False
311 def _populate_logger(self
, options
):
315 self
.log
= getattr(options
, "log", None)
319 mozlog
.commandline
.log_formatters
["tbpl"] = (
321 "Reftest specific formatter for the"
322 "benefit of legacy log parsers and"
323 "tools such as the reftest analyzer",
326 if not options
.log_tbpl_level
and os
.environ
.get("MOZ_REFTEST_VERBOSE"):
327 options
.log_tbpl_level
= fmt_options
["level"] = "debug"
328 self
.log
= mozlog
.commandline
.setup_logging(
329 "reftest harness", options
, {"tbpl": sys
.stdout
}, fmt_options
332 def getFullPath(self
, path
):
333 "Get an absolute path relative to self.oldcwd."
334 return os
.path
.normpath(os
.path
.join(self
.oldcwd
, os
.path
.expanduser(path
)))
336 def createReftestProfile(
343 profile_to_clone
=None,
346 """Sets up a profile for reftest.
348 :param options: Object containing command line options
349 :param tests: List of test objects to run
350 :param manifests: List of manifest files to parse (only takes effect
351 if tests were not passed in)
352 :param server: Server name to use for http tests
353 :param profile_to_clone: Path to a profile to use as the basis for the
355 :param prefs: Extra preferences to set in the profile
357 locations
= mozprofile
.permissions
.ServerLocations()
358 locations
.add_host(server
, scheme
="http", port
=port
)
359 locations
.add_host(server
, scheme
="https", port
=port
)
361 sandbox_whitelist_paths
= options
.sandboxReadWhitelist
362 if platform
.system() == "Linux" or platform
.system() in (
366 # Trailing slashes are needed to indicate directories on Linux and Windows
367 sandbox_whitelist_paths
= map(
368 lambda p
: os
.path
.join(p
, ""), sandbox_whitelist_paths
372 if not self
.use_marionette
:
373 addons
.append(options
.reftestExtensionPath
)
375 if options
.specialPowersExtensionPath
is not None:
376 if not self
.use_marionette
:
377 addons
.append(options
.specialPowersExtensionPath
)
379 # Install distributed extensions, if application has any.
380 distExtDir
= os
.path
.join(
381 options
.app
[: options
.app
.rfind(os
.sep
)], "distribution", "extensions"
383 if os
.path
.isdir(distExtDir
):
384 for f
in os
.listdir(distExtDir
):
385 addons
.append(os
.path
.join(distExtDir
, f
))
387 # Install custom extensions.
388 for f
in options
.extensionsToInstall
:
389 addons
.append(self
.getFullPath(f
))
393 "locations": locations
,
394 "whitelistpaths": sandbox_whitelist_paths
,
397 profile
= mozprofile
.Profile
.clone(profile_to_clone
, **kwargs
)
399 profile
= mozprofile
.Profile(**kwargs
)
401 # First set prefs from the base profiles under testing/profiles.
403 # In test packages used in CI, the profile_data directory is installed
404 # in the SCRIPT_DIRECTORY.
405 profile_data_dir
= os
.path
.join(SCRIPT_DIRECTORY
, "profile_data")
406 # If possible, read profile data from topsrcdir. This prevents us from
407 # requiring a re-build to pick up newly added extensions in the
408 # <profile>/extensions directory.
410 path
= os
.path
.join(build_obj
.topsrcdir
, "testing", "profiles")
411 if os
.path
.isdir(path
):
412 profile_data_dir
= path
413 # Still not found? Look for testing/profiles relative to layout/tools/reftest.
414 if not os
.path
.isdir(profile_data_dir
):
415 path
= os
.path
.abspath(
416 os
.path
.join(SCRIPT_DIRECTORY
, "..", "..", "..", "testing", "profiles")
418 if os
.path
.isdir(path
):
419 profile_data_dir
= path
421 with
open(os
.path
.join(profile_data_dir
, "profiles.json"), "r") as fh
:
422 base_profiles
= json
.load(fh
)["reftest"]
424 for name
in base_profiles
:
425 path
= os
.path
.join(profile_data_dir
, name
)
428 # Second set preferences for communication between our command line
429 # arguments and the reftest harness. Preferences that are required for
430 # reftest to work should instead be set under srcdir/testing/profiles.
432 prefs
["reftest.timeout"] = options
.timeout
* 1000
434 prefs
["reftest.logFile"] = options
.logFile
435 if options
.ignoreWindowSize
:
436 prefs
["reftest.ignoreWindowSize"] = True
438 prefs
["reftest.shuffle"] = True
440 prefs
["reftest.repeat"] = options
.repeat
441 if options
.runUntilFailure
:
442 prefs
["reftest.runUntilFailure"] = True
443 if not options
.repeat
:
444 prefs
["reftest.repeat"] = 30
446 prefs
["reftest.verify"] = True
447 if options
.cleanupCrashes
:
448 prefs
["reftest.cleanupPendingCrashes"] = True
449 prefs
["reftest.focusFilterMode"] = options
.focusFilterMode
450 prefs
["reftest.logLevel"] = options
.log_tbpl_level
or "info"
451 prefs
["reftest.suite"] = options
.suite
452 prefs
["gfx.font_rendering.ahem_antialias_none"] = True
453 # Run the "deferred" font-loader immediately, because if it finishes
454 # mid-test, the extra reflow that is triggered can disrupt the test.
455 prefs
["gfx.font_loader.delay"] = 0
456 # Ensure bundled fonts are activated, even if not enabled by default
457 # on the platform, so that tests can rely on them.
458 prefs
["gfx.bundled-fonts.activate"] = 1
459 # Disable dark scrollbars because it's semi-transparent.
460 prefs
["widget.disable-dark-scrollbar"] = True
461 prefs
["reftest.isCoverageBuild"] = mozinfo
.info
.get("ccov", False)
463 # config specific flags
464 prefs
["sandbox.apple_silicon"] = mozinfo
.info
.get("apple_silicon", False)
466 # Set tests to run or manifests to parse.
468 testlist
= os
.path
.join(profile
.profile
, "reftests.json")
469 with
open(testlist
, "w") as fh
:
471 prefs
["reftest.tests"] = testlist
473 prefs
["reftest.manifests"] = json
.dumps(manifests
)
475 # Unconditionally update the e10s pref, default True
476 prefs
["browser.tabs.remote.autostart"] = True
478 prefs
["browser.tabs.remote.autostart"] = False
480 # default fission to True
481 prefs
["fission.autostart"] = True
482 if options
.disableFission
:
483 prefs
["fission.autostart"] = False
485 if not self
.run_by_manifest
:
486 if options
.totalChunks
:
487 prefs
["reftest.totalChunks"] = options
.totalChunks
488 if options
.thisChunk
:
489 prefs
["reftest.thisChunk"] = options
.thisChunk
491 # Bug 1262954: For winXP + e10s disable acceleration
493 platform
.system() in ("Windows", "Microsoft")
494 and "5.1" in platform
.version()
497 prefs
["layers.acceleration.disabled"] = True
499 # Bug 1300355: Disable canvas cache for win7 as it uses
500 # too much memory and causes OOMs.
502 platform
.system() in ("Windows", "Microsoft")
503 and "6.1" in platform
.version()
505 prefs
["reftest.nocache"] = True
507 if options
.marionette
:
508 # options.marionette can specify host:port
509 port
= options
.marionette
.split(":")[1]
510 prefs
["marionette.port"] = int(port
)
512 # Enable tracing output for detailed failures in case of
513 # failing connection attempts, and hangs (bug 1397201)
514 prefs
["remote.log.level"] = "Trace"
516 # Third, set preferences passed in via the command line.
517 for v
in options
.extraPrefs
:
518 thispref
= v
.split("=")
519 if len(thispref
) < 2:
520 print("Error: syntax error in --setpref=" + v
)
522 prefs
[thispref
[0]] = thispref
[1].strip()
525 prefs
[pref
] = mozprofile
.Preferences
.cast(prefs
[pref
])
526 profile
.set_preferences(prefs
)
528 if os
.path
.join(here
, "chrome") not in options
.extraProfileFiles
:
529 options
.extraProfileFiles
.append(os
.path
.join(here
, "chrome"))
531 self
.copyExtraFilesToProfile(options
, profile
)
534 "Running with e10s: {}".format(prefs
["browser.tabs.remote.autostart"])
536 self
.log
.info("Running with fission: {}".format(prefs
["fission.autostart"]))
540 def environment(self
, **kwargs
):
541 kwargs
["log"] = self
.log
542 return test_environment(**kwargs
)
544 def buildBrowserEnv(self
, options
, profileDir
):
545 browserEnv
= self
.environment(
546 xrePath
=options
.xrePath
, debugger
=options
.debugger
548 browserEnv
["XPCOM_DEBUG_BREAK"] = "stack"
549 if options
.topsrcdir
:
550 browserEnv
["MOZ_DEVELOPER_REPO_DIR"] = options
.topsrcdir
551 if hasattr(options
, "topobjdir"):
552 browserEnv
["MOZ_DEVELOPER_OBJ_DIR"] = options
.topobjdir
554 if mozinfo
.info
["asan"]:
555 # Disable leak checking for reftests for now
556 if "ASAN_OPTIONS" in browserEnv
:
557 browserEnv
["ASAN_OPTIONS"] += ":detect_leaks=0"
559 browserEnv
["ASAN_OPTIONS"] = "detect_leaks=0"
561 # Set environment defaults for jstestbrowser. Keep in sync with the
562 # defaults used in js/src/tests/lib/tests.py.
563 if options
.suite
== "jstestbrowser":
564 browserEnv
["TZ"] = "PST8PDT"
565 browserEnv
["LC_ALL"] = "en_US.UTF-8"
567 for v
in options
.environment
:
570 print("Error: syntax error in --setenv=" + v
)
572 browserEnv
[v
[:ix
]] = v
[ix
+ 1 :]
574 # Enable leaks detection to its own log file.
575 self
.leakLogFile
= os
.path
.join(profileDir
, "runreftest_leaks.log")
576 browserEnv
["XPCOM_MEM_BLOAT_LOG"] = self
.leakLogFile
578 # TODO: this is always defined (as part of --enable-webrender which is default)
579 # can we make this default in the browser?
580 browserEnv
["MOZ_ACCELERATED"] = "1"
583 browserEnv
["MOZ_HEADLESS"] = "1"
587 def cleanup(self
, profileDir
):
589 shutil
.rmtree(profileDir
, True)
591 def verifyTests(self
, tests
, options
):
593 Support --verify mode: Run test(s) many times in a variety of
594 configurations/environments in an effort to find intermittent
598 self
._populate
_logger
(options
)
600 # Number of times to repeat test(s) when running with --repeat
602 # Number of times to repeat test(s) when running test in separate browser
603 VERIFY_REPEAT_SINGLE_BROWSER
= 5
606 options
.repeat
= VERIFY_REPEAT
607 options
.runUntilFailure
= True
608 result
= self
.runTests(tests
, options
)
613 options
.runUntilFailure
= False
614 for i
in range(VERIFY_REPEAT_SINGLE_BROWSER
):
615 result
= self
.runTests(tests
, options
)
621 options
.repeat
= VERIFY_REPEAT
622 options
.runUntilFailure
= True
623 options
.environment
.append("MOZ_CHAOSMODE=0xfb")
624 result
= self
.runTests(tests
, options
)
625 options
.environment
.remove("MOZ_CHAOSMODE=0xfb")
630 options
.runUntilFailure
= False
631 options
.environment
.append("MOZ_CHAOSMODE=0xfb")
632 for i
in range(VERIFY_REPEAT_SINGLE_BROWSER
):
633 result
= self
.runTests(tests
, options
)
636 options
.environment
.remove("MOZ_CHAOSMODE=0xfb")
640 ("1. Run each test %d times in one browser." % VERIFY_REPEAT
, step1
),
642 "2. Run each test %d times in a new browser each time."
643 % VERIFY_REPEAT_SINGLE_BROWSER
,
647 "3. Run each test %d times in one browser, in chaos mode."
652 "4. Run each test %d times in a new browser each time, in chaos mode."
653 % VERIFY_REPEAT_SINGLE_BROWSER
,
659 for (descr
, step
) in steps
:
660 stepResults
[descr
] = "not run / incomplete"
662 startTime
= datetime
.now()
663 maxTime
= timedelta(seconds
=options
.verify_max_time
)
664 finalResult
= "PASSED"
665 for (descr
, step
) in steps
:
666 if (datetime
.now() - startTime
) > maxTime
:
667 self
.log
.info("::: Test verification is taking too long: Giving up!")
669 "::: So far, all checks passed, but not all checks were run."
673 self
.log
.info('::: Running test verification step "%s"...' % descr
)
677 stepResults
[descr
] = "FAIL"
678 finalResult
= "FAILED!"
680 stepResults
[descr
] = "Pass"
683 self
.log
.info("::: Test verification summary for:")
686 self
.log
.info("::: " + test
)
688 for descr
in sorted(stepResults
.keys()):
689 self
.log
.info("::: %s : %s" % (descr
, stepResults
[descr
]))
691 self
.log
.info("::: Test verification %s" % finalResult
)
696 def runTests(self
, tests
, options
, cmdargs
=None):
697 cmdargs
= cmdargs
or []
698 self
._populate
_logger
(options
)
699 self
.outputHandler
= OutputHandler(
700 self
.log
, options
.utilityPath
, options
.symbolsPath
703 if options
.cleanupCrashes
:
704 mozcrash
.cleanup_pending_crash_reports()
706 manifests
= self
.resolver
.resolveManifests(options
, tests
)
708 manifests
[""] = (options
.filter, None)
710 if not getattr(options
, "runTestsInParallel", False):
711 return self
.runSerialTests(manifests
, options
, cmdargs
)
713 cpuCount
= multiprocessing
.cpu_count()
715 # We have the directive, technology, and machine to run multiple test instances.
716 # Experimentation says that reftests are not overly CPU-intensive, so we can run
717 # multiple jobs per CPU core.
719 # Our Windows machines in automation seem to get upset when we run a lot of
720 # simultaneous tests on them, so tone things down there.
721 if sys
.platform
== "win32":
722 jobsWithoutFocus
= cpuCount
724 jobsWithoutFocus
= 2 * cpuCount
726 totalJobs
= jobsWithoutFocus
+ 1
727 perProcessArgs
= [sys
.argv
[:] for i
in range(0, totalJobs
)]
731 if options
.marionette
:
732 host
, port
= options
.marionette
.split(":")
734 # First job is only needs-focus tests. Remaining jobs are
735 # non-needs-focus and chunked.
736 perProcessArgs
[0].insert(-1, "--focus-filter-mode=needs-focus")
737 for (chunkNumber
, jobArgs
) in enumerate(perProcessArgs
[1:], start
=1):
739 "--focus-filter-mode=non-needs-focus",
740 "--total-chunks=%d" % jobsWithoutFocus
,
741 "--this-chunk=%d" % chunkNumber
,
742 "--marionette=%s:%d" % (host
, port
),
746 for jobArgs
in perProcessArgs
:
748 jobArgs
.remove("--run-tests-in-parallel")
751 jobArgs
[0:0] = [sys
.executable
, "-u"]
753 threads
= [ReftestThread(args
) for args
in perProcessArgs
[1:]]
758 # The test harness in each individual thread will be doing timeout
759 # handling on its own, so we shouldn't need to worry about any of
760 # the threads hanging for arbitrarily long.
763 if not any(t
.is_alive() for t
in threads
):
766 # Run the needs-focus tests serially after the other ones, so we don't
767 # have to worry about races between the needs-focus tests *actually*
768 # needing focus and the dummy windows in the non-needs-focus tests
769 # trying to focus themselves.
770 focusThread
= ReftestThread(perProcessArgs
[0])
774 # Output the summaries that the ReftestThread filters suppressed.
775 summaryObjects
= [defaultdict(int) for s
in summaryLines
]
777 for (summaryObj
, (text
, categories
)) in zip(summaryObjects
, summaryLines
):
778 threadMatches
= t
.summaryMatches
[text
]
779 for (attribute
, description
) in categories
:
780 amount
= int(threadMatches
.group(attribute
) if threadMatches
else 0)
781 summaryObj
[attribute
] += amount
782 amount
= int(threadMatches
.group("total") if threadMatches
else 0)
783 summaryObj
["total"] += amount
785 print("REFTEST INFO | Result summary:")
786 for (summaryObj
, (text
, categories
)) in zip(summaryObjects
, summaryLines
):
789 "%d %s" % (summaryObj
[attribute
], description
)
790 for (attribute
, description
) in categories
797 + str(summaryObj
["total"])
803 return int(any(t
.retcode
!= 0 for t
in threads
))
805 def handleTimeout(self
, timeout
, proc
, utilityPath
, debuggerInfo
):
806 """handle process output timeout"""
807 # TODO: bug 913975 : _processOutput should call self.processOutputLine
808 # one more time one timeout (I think)
810 "%s | application timed out after %d seconds with no output"
811 % (self
.lastTestSeen
, int(timeout
))
813 self
.log
.warning("Force-terminating active process(es).")
814 self
.killAndGetStack(
815 proc
, utilityPath
, debuggerInfo
, dump_screen
=not debuggerInfo
818 def dumpScreen(self
, utilityPath
):
819 if self
.haveDumpedScreen
:
821 "Not taking screenshot here: see the one that was previously logged"
824 self
.haveDumpedScreen
= True
825 dump_screen(utilityPath
, self
.log
)
827 def killAndGetStack(self
, process
, utilityPath
, debuggerInfo
, dump_screen
=False):
829 Kill the process, preferrably in a way that gets us a stack trace.
830 Also attempts to obtain a screenshot before killing the process
835 self
.dumpScreen(utilityPath
)
837 if mozinfo
.info
.get("crashreporter", True) and not debuggerInfo
:
839 # We should have a "crashinject" program in our utility path
840 crashinject
= os
.path
.normpath(
841 os
.path
.join(utilityPath
, "crashinject.exe")
843 if os
.path
.exists(crashinject
):
844 status
= subprocess
.Popen([crashinject
, str(process
.pid
)]).wait()
845 printstatus("crashinject", status
)
850 process
.kill(sig
=signal
.SIGABRT
)
852 # https://bugzilla.mozilla.org/show_bug.cgi?id=921509
853 self
.log
.info("Can't trigger Breakpad, process no longer exists")
855 self
.log
.info("Can't trigger Breakpad, just killing process")
867 valgrindSuppFiles
=None,
875 if self
.use_marionette
:
876 cmdargs
.append("-marionette")
879 profile
= self
.createReftestProfile(options
, **profileArgs
)
881 # browser environment
882 env
= self
.buildBrowserEnv(options
, profile
.profile
)
884 def timeoutHandler():
885 self
.handleTimeout(timeout
, proc
, options
.utilityPath
, debuggerInfo
)
890 interactive
= debuggerInfo
.interactive
891 debug_args
= [debuggerInfo
.path
] + debuggerInfo
.args
893 def record_last_test(message
):
894 """Records the last test seen by this harness for the benefit of crash logging."""
898 return test
.split(" ")[0]
901 if message
["action"] == "test_start":
902 self
.lastTestSeen
= testid(message
["test"])
903 elif message
["action"] == "test_end":
904 if self
.lastTest
and message
["test"] == self
.lastTest
:
905 self
.lastTestSeen
= self
.currentManifest
907 self
.lastTestSeen
= "{} (finished)".format(testid(message
["test"]))
909 self
.log
.add_handler(record_last_test
)
912 "kill_on_timeout": False,
913 "cwd": SCRIPT_DIRECTORY
,
914 "onTimeout": [timeoutHandler
],
915 "processOutputLine": [self
.outputHandler
],
918 if mozinfo
.isWin
or mozinfo
.isMac
:
919 # Prevents log interleaving on Windows at the expense of losing
920 # true log order. See bug 798300 and bug 1324961 for more details.
921 kp_kwargs
["processStderrLine"] = [self
.outputHandler
]
924 # If an interactive debugger is attached,
925 # don't use timeouts, and don't capture ctrl-c.
927 signal
.signal(signal
.SIGINT
, lambda sigid
, frame
: None)
929 runner_cls
= mozrunner
.runners
.get(
930 mozinfo
.info
.get("appname", "firefox"), mozrunner
.Runner
935 process_class
=mozprocess
.ProcessHandlerMixin
,
938 process_args
=kp_kwargs
,
941 debug_args
=debug_args
, interactive
=interactive
, outputTimeout
=timeout
943 proc
= runner
.process_handler
944 self
.outputHandler
.proc_name
= "GECKO({})".format(proc
.pid
)
946 # Used to defer a possible IOError exception from Marionette
947 marionette_exception
= None
949 if self
.use_marionette
:
951 "socket_timeout": options
.marionette_socket_timeout
,
952 "startup_timeout": options
.marionette_startup_timeout
,
953 "symbols_path": options
.symbolsPath
,
955 if options
.marionette
:
956 host
, port
= options
.marionette
.split(":")
957 marionette_args
["host"] = host
958 marionette_args
["port"] = int(port
)
961 marionette
= Marionette(**marionette_args
)
962 marionette
.start_session()
964 addons
= Addons(marionette
)
965 if options
.specialPowersExtensionPath
:
966 addons
.install(options
.specialPowersExtensionPath
, temp
=True)
968 addons
.install(options
.reftestExtensionPath
, temp
=True)
970 marionette
.delete_session()
972 # Any IOError as thrown by Marionette means that something is
973 # wrong with the process, like a crash or the socket is no
974 # longer open. We defer raising this specific error so that
975 # post-test checks for leaks and crashes are performed and
977 marionette_exception
= sys
.exc_info()
979 status
= runner
.wait()
980 runner
.process_handler
= None
981 self
.outputHandler
.proc_name
= None
983 crashed
= mozcrash
.log_crashes(
985 os
.path
.join(profile
.profile
, "minidumps"),
987 test
=self
.lastTestSeen
,
991 # log suite_end to wrap up, this is usually done with in in-browser harness
992 if not self
.outputHandler
.results
:
993 # TODO: while .results is a defaultdict(int), it is proxied via log_actions as data, not type
994 self
.outputHandler
.results
= {
1000 "UnexpectedPass": 0,
1001 "AssertionUnexpected": 0,
1002 "AssertionUnexpectedFixed": 0,
1004 "AssertionKnown": 0,
1009 self
.log
.suite_end(extra
={"results": self
.outputHandler
.results
})
1011 if not status
and crashed
:
1014 if status
and not crashed
:
1016 "TEST-UNEXPECTED-FAIL | %s | application terminated with exit code %s"
1017 % (self
.lastTestSeen
, status
)
1019 # use process_output so message is logged verbatim
1020 self
.log
.process_output(None, msg
)
1023 self
.cleanup(profile
.profile
)
1025 if marionette_exception
is not None:
1026 exc
, value
, tb
= marionette_exception
1027 raise reraise(exc
, value
, tb
)
1029 self
.log
.info("Process mode: {}".format("e10s" if options
.e10s
else "non-e10s"))
1032 def getActiveTests(self
, manifests
, options
, testDumpFile
=None):
1033 # These prefs will cause reftest.jsm to parse the manifests,
1034 # dump the resulting tests to a file, and exit.
1036 "reftest.manifests": json
.dumps(manifests
),
1037 "reftest.manifests.dumpTests": testDumpFile
or self
.testDumpFile
,
1040 self
.runApp(options
, cmdargs
=cmdargs
, prefs
=prefs
)
1042 if not os
.path
.isfile(self
.testDumpFile
):
1043 print("Error: parsing manifests failed!")
1046 with
open(self
.testDumpFile
, "r") as fh
:
1047 tests
= json
.load(fh
)
1049 if os
.path
.isfile(self
.testDumpFile
):
1050 mozfile
.remove(self
.testDumpFile
)
1053 # Name and path are expected by manifestparser, but not used in reftest.
1054 test
["name"] = test
["path"] = test
["url1"]
1056 mp
= TestManifest(strict
=False)
1060 if options
.totalChunks
:
1062 mpf
.chunk_by_manifest(options
.thisChunk
, options
.totalChunks
)
1065 tests
= mp
.active_tests(exists
=False, filters
=filters
)
1068 def runSerialTests(self
, manifests
, options
, cmdargs
=None):
1070 if options
.debugger
:
1071 debuggerInfo
= mozdebug
.get_debugger_info(
1072 options
.debugger
, options
.debuggerArgs
, options
.debuggerInteractive
1076 if kwargs
.get("tests"):
1077 self
.lastTest
= kwargs
["tests"][-1]["identifier"]
1078 if not isinstance(self
.lastTest
, string_types
):
1079 self
.lastTest
= " ".join(self
.lastTest
)
1081 status
= self
.runApp(
1083 manifests
=manifests
,
1085 # We generally want the JS harness or marionette
1086 # to handle timeouts if they can.
1087 # The default JS harness timeout is currently
1088 # 300 seconds (default options.timeout).
1089 # The default Marionette socket timeout is
1090 # currently 360 seconds.
1091 # Give the JS harness extra time to deal with
1092 # its own timeouts and try to usually exceed
1093 # the 360 second marionette socket timeout.
1094 # See bug 479518 and bug 1414063.
1095 timeout
=options
.timeout
+ 70.0,
1096 debuggerInfo
=debuggerInfo
,
1097 symbolsPath
=options
.symbolsPath
,
1101 # do not process leak log when we crash/assert
1103 mozleak
.process_leak_log(
1105 leak_thresholds
=options
.leakThresholds
,
1106 stack_fixer
=get_stack_fixer_function(
1107 options
.utilityPath
, options
.symbolsPath
1112 if not self
.run_by_manifest
:
1115 tests
= self
.getActiveTests(manifests
, options
)
1116 tests_by_manifest
= defaultdict(list)
1117 ids_by_manifest
= defaultdict(list)
1119 tests_by_manifest
[t
["manifest"]].append(t
)
1120 test_id
= t
["identifier"]
1121 if not isinstance(test_id
, string_types
):
1122 test_id
= " ".join(test_id
)
1123 ids_by_manifest
[t
["manifestID"]].append(test_id
)
1125 self
.log
.suite_start(ids_by_manifest
, name
=options
.suite
)
1129 for manifest
, tests
in tests_by_manifest
.items():
1130 self
.log
.info("Running tests in {}".format(manifest
))
1131 self
.currentManifest
= manifest
1132 status
= run(tests
=tests
)
1133 overall
= overall
or status
1135 # we didn't run anything
1138 self
.log
.suite_end(extra
={"results": self
.outputHandler
.results
})
1141 def copyExtraFilesToProfile(self
, options
, profile
):
1142 "Copy extra files or dirs specified on the command line to the testing profile."
1143 profileDir
= profile
.profile
1144 for f
in options
.extraProfileFiles
:
1145 abspath
= self
.getFullPath(f
)
1146 if os
.path
.isfile(abspath
):
1147 if os
.path
.basename(abspath
) == "user.js":
1148 extra_prefs
= mozprofile
.Preferences
.read_prefs(abspath
)
1149 profile
.set_preferences(extra_prefs
)
1150 elif os
.path
.basename(abspath
).endswith(".dic"):
1151 hyphDir
= os
.path
.join(profileDir
, "hyphenation")
1152 if not os
.path
.exists(hyphDir
):
1153 os
.makedirs(hyphDir
)
1154 shutil
.copy2(abspath
, hyphDir
)
1156 shutil
.copy2(abspath
, profileDir
)
1157 elif os
.path
.isdir(abspath
):
1158 dest
= os
.path
.join(profileDir
, os
.path
.basename(abspath
))
1159 shutil
.copytree(abspath
, dest
)
1162 "runreftest.py | Failed to copy %s to profile" % abspath
1167 def run_test_harness(parser
, options
):
1168 reftest
= RefTest(options
.suite
)
1169 parser
.validate(options
, reftest
)
1171 # We have to validate options.app here for the case when the mach
1172 # command is able to find it after argument parsing. This can happen
1173 # when running from a tests archive.
1175 parser
.error("could not find the application path, --appname must be specified")
1177 options
.app
= reftest
.getFullPath(options
.app
)
1178 if not os
.path
.exists(options
.app
):
1180 "Error: Path %(app)s doesn't exist. Are you executing "
1181 "$objdir/_tests/reftest/runreftest.py?" % {"app": options
.app
}
1184 if options
.xrePath
is None:
1185 options
.xrePath
= os
.path
.dirname(options
.app
)
1188 result
= reftest
.verifyTests(options
.tests
, options
)
1190 result
= reftest
.runTests(options
.tests
, options
)
1195 if __name__
== "__main__":
1196 parser
= reftestcommandline
.DesktopArgumentsParser()
1197 options
= parser
.parse_args()
1198 sys
.exit(run_test_harness(parser
, options
))