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/.
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
33 from mozserve
import Http3Server
42 from xpcshellcommandline
import parser_desktop
44 SCRIPT_DIR
= os
.path
.abspath(os
.path
.realpath(os
.path
.dirname(__file__
)))
47 from mozbuild
.base
import MozbuildObject
49 build
= MozbuildObject
.from_environment(cwd
=SCRIPT_DIR
)
53 HARNESS_TIMEOUT
= 5 * 60
54 TBPL_RETRY
= 4 # defined in mozharness
56 # benchmarking on tbpl revealed that this works best for now
57 # TODO: This has been evaluated/set many years ago and we might want to
58 # benchmark this again.
59 # These days with e10s/fission the number of real processes/threads running
60 # can be significantly higher, with both consequences on runtime and memory
61 # consumption. So be aware that NUM_THREADS is just saying how many tests will
62 # be started maximum in parallel and that depending on the tests there is
63 # only a weak correlation to the effective number of processes or threads.
64 # Be also aware that we can override this value with the threadCount option
65 # on the command line to tweak it for a concrete CPU/memory combination.
66 NUM_THREADS
= int(cpu_count() * 4)
67 if sys
.platform
== "win32":
68 NUM_THREADS
= NUM_THREADS
/ 2
70 EXPECTED_LOG_ACTIONS
= set(
72 "crash_reporter_init",
78 # --------------------------------------------------------------
79 # TODO: this is a hack for mozbase without virtualenv, remove with bug 849900
81 here
= os
.path
.dirname(__file__
)
82 mozbase
= os
.path
.realpath(os
.path
.join(os
.path
.dirname(here
), "mozbase"))
84 if os
.path
.isdir(mozbase
):
85 for package
in os
.listdir(mozbase
):
86 sys
.path
.append(os
.path
.join(mozbase
, package
))
91 from manifestparser
import TestManifest
92 from manifestparser
.filters
import chunk_by_slice
, failures
, pathprefix
, tags
93 from manifestparser
.util
import normsep
94 from mozlog
import commandline
95 from mozprofile
import Profile
96 from mozprofile
.cli
import parse_preferences
97 from mozrunner
.utils
import get_stack_fixer_function
99 # --------------------------------------------------------------
101 # TODO: perhaps this should be in a more generally shared location?
102 # This regex matches all of the C0 and C1 control characters
103 # (U+0000 through U+001F; U+007F; U+0080 through U+009F),
104 # except TAB (U+0009), CR (U+000D), LF (U+000A) and backslash (U+005C).
105 # A raw string is deliberately not used.
106 _cleanup_encoding_re
= re
.compile("[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\\\\]")
109 def _cleanup_encoding_repl(m
):
111 return "\\\\" if c
== "\\" else "\\x{0:02X}".format(ord(c
))
114 def cleanup_encoding(s
):
115 """S is either a byte or unicode string. Either way it may
116 contain control characters, unpaired surrogates, reserved code
117 points, etc. If it is a byte string, it is assumed to be
118 UTF-8, but it may not be *correct* UTF-8. Return a
119 sanitized unicode object."""
120 if not isinstance(s
, six
.string_types
):
121 if isinstance(s
, six
.binary_type
):
122 return six
.ensure_str(s
)
124 return six
.text_type(s
)
125 if isinstance(s
, six
.binary_type
):
126 s
= s
.decode("utf-8", "replace")
127 # Replace all C0 and C1 control characters with \xNN escapes.
128 return _cleanup_encoding_re
.sub(_cleanup_encoding_repl
, s
)
132 def popenCleanupHack():
134 Hack to work around https://bugs.python.org/issue37380
135 The basic idea is that on old versions of Python on Windows,
136 we need to clear subprocess._cleanup before we call Popen(),
137 then restore it afterwards.
140 if mozinfo
.isWin
and sys
.version_info
[0] == 3 and sys
.version_info
< (3, 7, 5):
141 savedCleanup
= subprocess
._cleanup
142 subprocess
._cleanup
= lambda: None
147 subprocess
._cleanup
= savedCleanup
150 """ Control-C handling """
154 def markGotSIGINT(signum
, stackFrame
):
159 class XPCShellTestThread(Thread
):
166 usingCrashReporter
=False,
169 Thread
.__init
__(self
)
172 self
.test_object
= test_object
174 self
.verbose
= verbose
175 self
.usingTSan
= usingTSan
176 self
.usingCrashReporter
= usingCrashReporter
178 self
.appPath
= kwargs
.get("appPath")
179 self
.xrePath
= kwargs
.get("xrePath")
180 self
.utility_path
= kwargs
.get("utility_path")
181 self
.testingModulesDir
= kwargs
.get("testingModulesDir")
182 self
.debuggerInfo
= kwargs
.get("debuggerInfo")
183 self
.jsDebuggerInfo
= kwargs
.get("jsDebuggerInfo")
184 self
.headJSPath
= kwargs
.get("headJSPath")
185 self
.testharnessdir
= kwargs
.get("testharnessdir")
186 self
.profileName
= kwargs
.get("profileName")
187 self
.singleFile
= kwargs
.get("singleFile")
188 self
.env
= copy
.deepcopy(kwargs
.get("env"))
189 self
.symbolsPath
= kwargs
.get("symbolsPath")
190 self
.logfiles
= kwargs
.get("logfiles")
191 self
.app_binary
= kwargs
.get("app_binary")
192 self
.xpcshell
= kwargs
.get("xpcshell")
193 self
.xpcsRunArgs
= kwargs
.get("xpcsRunArgs")
194 self
.failureManifest
= kwargs
.get("failureManifest")
195 self
.jscovdir
= kwargs
.get("jscovdir")
196 self
.stack_fixer_function
= kwargs
.get("stack_fixer_function")
197 self
._rootTempDir
= kwargs
.get("tempDir")
198 self
.cleanup_dir_list
= kwargs
.get("cleanup_dir_list")
199 self
.pStdout
= kwargs
.get("pStdout")
200 self
.pStderr
= kwargs
.get("pStderr")
201 self
.keep_going
= kwargs
.get("keep_going")
202 self
.log
= kwargs
.get("log")
203 self
.app_dir_key
= kwargs
.get("app_dir_key")
204 self
.interactive
= kwargs
.get("interactive")
205 self
.rootPrefsFile
= kwargs
.get("rootPrefsFile")
206 self
.extraPrefs
= kwargs
.get("extraPrefs")
207 self
.verboseIfFails
= kwargs
.get("verboseIfFails")
208 self
.headless
= kwargs
.get("headless")
209 self
.runFailures
= kwargs
.get("runFailures")
210 self
.timeoutAsPass
= kwargs
.get("timeoutAsPass")
211 self
.crashAsPass
= kwargs
.get("crashAsPass")
212 self
.conditionedProfileDir
= kwargs
.get("conditionedProfileDir")
216 # Default the test prefsFile to the rootPrefsFile.
217 self
.prefsFile
= self
.rootPrefsFile
219 # only one of these will be set to 1. adding them to the totals in
225 # Context for output processing
226 self
.output_lines
= []
227 self
.has_failure_output
= False
228 self
.saw_crash_reporter_init
= False
229 self
.saw_proc_start
= False
230 self
.saw_proc_end
= False
232 self
.harness_timeout
= kwargs
.get("harness_timeout")
233 self
.timedout
= False
236 # event from main thread to signal work done
237 self
.event
= kwargs
.get("event")
238 self
.done
= False # explicitly set flag so we don't rely on thread.isAlive
243 except PermissionError
as e
:
246 self
.traceback
= traceback
.format_exc()
247 except Exception as e
:
249 self
.traceback
= traceback
.format_exc()
251 self
.exception
= None
252 self
.traceback
= None
255 "%s failed or timed out, will retry." % self
.test_object
["id"]
260 def kill(self
, proc
):
262 Simple wrapper to kill a process.
263 On a remote system, this is overloaded to handle remote process communication.
267 def removeDir(self
, dirname
):
269 Simple wrapper to remove (recursively) a given directory.
270 On a remote system, we need to overload this to work on the remote filesystem.
272 mozfile
.remove(dirname
)
274 def poll(self
, proc
):
276 Simple wrapper to check if a process has terminated.
277 On a remote system, this is overloaded to handle remote process communication.
281 def createLogFile(self
, test_file
, stdout
):
283 For a given test file and stdout buffer, create a log file.
284 On a remote system we have to fix the test name since it can contain directories.
286 with
open(test_file
+ ".log", "w") as f
:
289 def getReturnCode(self
, proc
):
291 Simple wrapper to get the return code for a given process.
292 On a remote system we overload this to work with the remote process management.
294 if proc
is not None and hasattr(proc
, "returncode"):
295 return proc
.returncode
298 def communicate(self
, proc
):
300 Simple wrapper to communicate with a process.
301 On a remote system, this is overloaded to handle remote process communication.
303 # Processing of incremental output put here to
304 # sidestep issues on remote platforms, where what we know
305 # as proc is a file pulled off of a device.
308 line
= proc
.stdout
.readline()
311 self
.process_line(line
)
313 if self
.saw_proc_start
and not self
.saw_proc_end
:
314 self
.has_failure_output
= True
316 return proc
.communicate()
319 self
, cmd
, stdout
, stderr
, env
, cwd
, timeout
=None, test_name
=None
322 Simple wrapper to launch a process.
323 On a remote system, this is more complex and we need to overload this function.
325 # timeout is needed by remote xpcshell to extend the
326 # remote device timeout. It is not used in this function.
328 cwd
= six
.ensure_str(cwd
)
329 for i
in range(len(cmd
)):
330 cmd
[i
] = six
.ensure_str(cmd
[i
])
333 popen_func
= psutil
.Popen
337 with
popenCleanupHack():
338 proc
= popen_func(cmd
, stdout
=stdout
, stderr
=stderr
, env
=env
, cwd
=cwd
)
342 def checkForCrashes(self
, dump_directory
, symbols_path
, test_name
=None):
344 Simple wrapper to check for crashes.
345 On a remote system, this is more complex and we need to overload this function.
351 return mozcrash
.log_crashes(
352 self
.log
, dump_directory
, symbols_path
, test
=test_name
, quiet
=quiet
355 def logCommand(self
, name
, completeCmd
, testdir
):
356 self
.log
.info("%s | full command: %r" % (name
, completeCmd
))
357 self
.log
.info("%s | current directory: %r" % (name
, testdir
))
358 # Show only those environment variables that are changed from
359 # the ambient environment.
360 changedEnv
= set("%s=%s" % i
for i
in six
.iteritems(self
.env
)) - set(
361 "%s=%s" % i
for i
in six
.iteritems(os
.environ
)
363 self
.log
.info("%s | environment: %s" % (name
, list(changedEnv
)))
364 shell_command_tokens
= [
365 pipes
.quote(tok
) for tok
in list(changedEnv
) + completeCmd
368 "%s | as shell command: (cd %s; %s)"
369 % (name
, pipes
.quote(testdir
), " ".join(shell_command_tokens
))
372 def killTimeout(self
, proc
):
373 if proc
is not None and hasattr(proc
, "pid"):
374 mozcrash
.kill_and_get_minidump(
375 proc
.pid
, self
.tempDir
, utility_path
=self
.utility_path
378 self
.log
.info("not killing -- proc or pid unknown")
380 def postCheck(self
, proc
):
381 """Checks for a still-running test process, kills it and fails the test if found.
382 We can sometimes get here before the process has terminated, which would
383 cause removeDir() to fail - so check for the process and kill it if needed.
385 if proc
and self
.poll(proc
) is None:
389 except psutil
.NoSuchProcess
:
393 message
= "%s | Process still running after test!" % self
.test_object
["id"]
395 self
.log
.info(message
)
398 self
.log
.error(message
)
399 self
.log_full_output()
402 def testTimeout(self
, proc
):
403 if self
.test_object
["expected"] == "pass":
410 self
.test_object
["id"],
413 message
="Test timed out",
417 if self
.timeoutAsPass
:
422 self
.test_object
["id"],
425 message
="Test timed out",
427 self
.log_full_output()
431 self
.killTimeout(proc
)
432 self
.log
.info("xpcshell return code: %s" % self
.getReturnCode(proc
))
434 self
.clean_temp_dirs(self
.test_object
["path"])
436 def updateTestPrefsFile(self
):
437 # If the Manifest file has some additional prefs, merge the
438 # prefs set in the user.js file stored in the _rootTempdir
439 # with the prefs from the manifest and the prefs specified
440 # in the extraPrefs option.
441 if "prefs" in self
.test_object
:
442 # Merge the user preferences in a fake profile dir in a
443 # local temporary dir (self.tempDir is the remoteTmpDir
444 # for the RemoteXPCShellTestThread subclass and so we
445 # can't use that tempDir here).
446 localTempDir
= mkdtemp(prefix
="xpc-other-", dir=self
._rootTempDir
)
449 interpolation
= {"server": "dummyserver"}
450 profile
= Profile(profile
=localTempDir
, restore
=False)
451 # _rootTempDir contains a user.js file, generated by buildPrefsFile
452 profile
.merge(self
._rootTempDir
, interpolation
=interpolation
)
454 prefs
= self
.test_object
["prefs"].strip().split()
455 name
= self
.test_object
["id"]
458 "%s: Per-test extra prefs will be set:\n {}".format(
464 profile
.set_preferences(parse_preferences(prefs
), filename
=filename
)
465 # Make sure that the extra prefs form the command line are overriding
466 # any prefs inherited from the shared profile data or the manifest prefs.
467 profile
.set_preferences(
468 parse_preferences(self
.extraPrefs
), filename
=filename
470 return os
.path
.join(profile
.profile
, filename
)
472 # Return the root prefsFile if there is no other prefs to merge.
473 # This is the path set by buildPrefsFile.
474 return self
.rootPrefsFile
477 def conditioned_profile_copy(self
):
478 """Returns a copy of the original conditioned profile that was created."""
480 condprof_copy
= os
.path
.join(tempfile
.mkdtemp(), "profile")
482 self
.conditionedProfileDir
,
484 ignore
=shutil
.ignore_patterns("lock"),
486 self
.log
.info("Created a conditioned-profile copy: %s" % condprof_copy
)
489 def buildCmdTestFile(self
, name
):
491 Build the command line arguments for the test file.
492 On a remote system, this may be overloaded to use a remote path structure.
494 return ["-e", 'const _TEST_FILE = ["%s"];' % name
.replace("\\", "/")]
496 def setupTempDir(self
):
497 tempDir
= mkdtemp(prefix
="xpc-other-", dir=self
._rootTempDir
)
498 self
.env
["XPCSHELL_TEST_TEMP_DIR"] = tempDir
500 self
.log
.info("temp dir is %s" % tempDir
)
503 def setupProfileDir(self
):
505 Create a temporary folder for the profile and set appropriate environment variables.
506 When running check-interactive and check-one, the directory is well-defined and
507 retained for inspection once the tests complete.
509 On a remote system, this may be overloaded to use a remote path structure.
511 if self
.conditionedProfileDir
:
512 profileDir
= self
.conditioned_profile_copy
513 elif self
.interactive
or self
.singleFile
:
514 profileDir
= os
.path
.join(gettempdir(), self
.profileName
, "xpcshellprofile")
516 # This could be left over from previous runs
517 self
.removeDir(profileDir
)
520 os
.makedirs(profileDir
)
522 profileDir
= mkdtemp(prefix
="xpc-profile-", dir=self
._rootTempDir
)
523 self
.env
["XPCSHELL_TEST_PROFILE_DIR"] = profileDir
524 if self
.interactive
or self
.singleFile
:
525 self
.log
.info("profile dir is %s" % profileDir
)
528 def setupMozinfoJS(self
):
529 mozInfoJSPath
= os
.path
.join(self
.profileDir
, "mozinfo.json")
530 mozInfoJSPath
= mozInfoJSPath
.replace("\\", "\\\\")
531 mozinfo
.output_to_file(mozInfoJSPath
)
534 def buildCmdHead(self
):
536 Build the command line arguments for the head files,
537 along with the address of the webserver which some tests require.
539 On a remote system, this is overloaded to resolve quoting issues over a
540 secondary command line.
542 headfiles
= self
.getHeadFiles(self
.test_object
)
543 cmdH
= ", ".join(['"' + f
.replace("\\", "/") + '"' for f
in headfiles
])
545 dbgport
= 0 if self
.jsDebuggerInfo
is None else self
.jsDebuggerInfo
.port
549 "const _HEAD_FILES = [%s];" % cmdH
,
551 "const _JSDEBUGGER_PORT = %d;" % dbgport
,
554 def getHeadFiles(self
, test
):
555 """Obtain lists of head- files. Returns a list of head files."""
557 def sanitize_list(s
, kind
):
558 for f
in s
.strip().split(" "):
563 path
= os
.path
.normpath(os
.path
.join(test
["here"], f
))
564 if not os
.path
.exists(path
):
565 raise Exception("%s file does not exist: %s" % (kind
, path
))
567 if not os
.path
.isfile(path
):
568 raise Exception("%s file is not a file: %s" % (kind
, path
))
572 headlist
= test
.get("head", "")
573 return list(sanitize_list(headlist
, "head"))
575 def buildXpcsCmd(self
):
577 Load the root head.js file as the first file in our test path, before other head,
578 and test files. On a remote system, we overload this to add additional command
579 line arguments, so this gets overloaded.
581 # - NOTE: if you rename/add any of the constants set here, update
582 # do_load_child_test_harness() in head.js
584 self
.appPath
= self
.xrePath
603 'const _HEAD_JS_PATH = "%s";' % self
.headJSPath
,
605 'const _MOZINFO_JS_PATH = "%s";' % self
.mozInfoJSPath
,
607 'const _PREFS_FILE = "%s";' % self
.prefsFile
.replace("\\", "\\\\"),
610 if self
.testingModulesDir
:
611 # Escape backslashes in string literal.
612 sanitized
= self
.testingModulesDir
.replace("\\", "\\\\")
613 xpcsCmd
.extend(["-e", 'const _TESTING_MODULES_DIR = "%s";' % sanitized
])
615 xpcsCmd
.extend(["-f", os
.path
.join(self
.testharnessdir
, "head.js")])
617 if self
.debuggerInfo
:
618 xpcsCmd
= [self
.debuggerInfo
.path
] + self
.debuggerInfo
.args
+ xpcsCmd
622 def cleanupDir(self
, directory
, name
):
623 if not os
.path
.exists(directory
):
626 # up to TRY_LIMIT attempts (one every second), because
627 # the Windows filesystem is slow to react to the changes
630 while try_count
< TRY_LIMIT
:
632 self
.removeDir(directory
)
634 self
.log
.info("Failed to remove directory: %s. Waiting." % directory
)
635 # We suspect the filesystem may still be making changes. Wait a
636 # little bit and try again.
643 # we try cleaning up again later at the end of the run
644 self
.cleanup_dir_list
.append(directory
)
646 def clean_temp_dirs(self
, name
):
647 # We don't want to delete the profile when running check-interactive
649 if self
.profileDir
and not self
.interactive
and not self
.singleFile
:
650 self
.cleanupDir(self
.profileDir
, name
)
652 self
.cleanupDir(self
.tempDir
, name
)
654 def parse_output(self
, output
):
655 """Parses process output for structured messages and saves output as it is
656 read. Sets self.has_failure_output in case of evidence of a failure"""
657 for line_string
in output
.splitlines():
658 self
.process_line(line_string
)
660 if self
.saw_proc_start
and not self
.saw_proc_end
:
661 self
.has_failure_output
= True
663 def fix_text_output(self
, line
):
664 line
= cleanup_encoding(line
)
665 if self
.stack_fixer_function
is not None:
666 line
= self
.stack_fixer_function(line
)
668 if isinstance(line
, bytes
):
669 line
= line
.decode("utf-8")
672 def log_line(self
, line
):
673 """Log a line of output (either a parser json object or text output from
675 if isinstance(line
, six
.string_types
) or isinstance(line
, bytes
):
676 line
= self
.fix_text_output(line
).rstrip("\r\n")
677 self
.log
.process_output(self
.proc_ident
, line
, command
=self
.command
)
679 if "message" in line
:
680 line
["message"] = self
.fix_text_output(line
["message"])
681 if "xpcshell_process" in line
:
682 line
["thread"] = " ".join(
683 [current_thread().name
, line
["xpcshell_process"]]
686 line
["thread"] = current_thread().name
687 self
.log
.log_raw(line
)
689 def log_full_output(self
):
690 """Logs any buffered output from the test process, and clears the buffer."""
691 if not self
.output_lines
:
693 self
.log
.info(">>>>>>>")
694 for line
in self
.output_lines
:
696 self
.log
.info("<<<<<<<")
697 self
.output_lines
= []
699 def report_message(self
, message
):
700 """Stores or logs a json log message in mozlog format."""
702 self
.log_line(message
)
704 self
.output_lines
.append(message
)
706 def process_line(self
, line_string
):
707 """Parses a single line of output, determining its significance and
710 if isinstance(line_string
, bytes
):
711 # Transform binary to string representation
712 line_string
= line_string
.decode(sys
.stdout
.encoding
, errors
="replace")
714 if not line_string
.strip():
718 line_object
= json
.loads(line_string
)
719 if not isinstance(line_object
, dict):
720 self
.report_message(line_string
)
723 self
.report_message(line_string
)
727 "action" not in line_object
728 or line_object
["action"] not in EXPECTED_LOG_ACTIONS
730 # The test process output JSON.
731 self
.report_message(line_string
)
734 if line_object
["action"] == "crash_reporter_init":
735 self
.saw_crash_reporter_init
= True
738 action
= line_object
["action"]
740 self
.has_failure_output
= (
741 self
.has_failure_output
742 or "expected" in line_object
744 and line_object
["level"] == "ERROR"
747 self
.report_message(line_object
)
749 if action
== "log" and line_object
["message"] == "CHILD-TEST-STARTED":
750 self
.saw_proc_start
= True
751 elif action
== "log" and line_object
["message"] == "CHILD-TEST-COMPLETED":
752 self
.saw_proc_end
= True
755 """Run an individual xpcshell test."""
758 name
= self
.test_object
["id"]
759 path
= self
.test_object
["path"]
761 # Check for skipped tests
762 if "disabled" in self
.test_object
:
763 message
= self
.test_object
["disabled"]
765 message
= "disabled from xpcshell manifest"
766 self
.log
.test_start(name
)
767 self
.log
.test_end(name
, "SKIP", message
=message
)
770 self
.keep_going
= True
773 # Check for known-fail tests
774 expect_pass
= self
.test_object
["expected"] == "pass"
776 # By default self.appPath will equal the gre dir. If specified in the
777 # xpcshell.toml file, set a different app dir for this test.
778 if self
.app_dir_key
and self
.app_dir_key
in self
.test_object
:
779 rel_app_dir
= self
.test_object
[self
.app_dir_key
]
780 rel_app_dir
= os
.path
.join(self
.xrePath
, rel_app_dir
)
781 self
.appPath
= os
.path
.abspath(rel_app_dir
)
785 test_dir
= os
.path
.dirname(path
)
787 # Create a profile and a temp dir that the JS harness can stick
788 # a profile and temporary data in
789 self
.profileDir
= self
.setupProfileDir()
790 self
.tempDir
= self
.setupTempDir()
791 self
.mozInfoJSPath
= self
.setupMozinfoJS()
793 # Setup per-manifest prefs and write them into the tempdir.
794 self
.prefsFile
= self
.updateTestPrefsFile()
796 # The order of the command line is important:
797 # 1) Arguments for xpcshell itself
798 self
.command
= self
.buildXpcsCmd()
800 # 2) Arguments for the head files
801 self
.command
.extend(self
.buildCmdHead())
803 # 3) Arguments for the test file
804 self
.command
.extend(self
.buildCmdTestFile(path
))
805 self
.command
.extend(["-e", 'const _TEST_NAME = "%s";' % name
])
807 # 4) Arguments for code coverage
810 ["-e", 'const _JSCOV_DIR = "%s";' % self
.jscovdir
.replace("\\", "/")]
813 # 5) Runtime arguments
814 if "debug" in self
.test_object
:
815 self
.command
.append("-d")
817 self
.command
.extend(self
.xpcsRunArgs
)
819 if self
.test_object
.get("dmd") == "true":
820 self
.env
["PYTHON"] = sys
.executable
821 self
.env
["BREAKPAD_SYMBOLS_PATH"] = self
.symbolsPath
823 if self
.test_object
.get("snap") == "true":
824 self
.env
["SNAP_NAME"] = "firefox"
825 self
.env
["SNAP_INSTANCE_NAME"] = "firefox"
827 if self
.test_object
.get("subprocess") == "true":
828 self
.env
["PYTHON"] = sys
.executable
831 self
.test_object
.get("headless", "true" if self
.headless
else None)
834 self
.env
["MOZ_HEADLESS"] = "1"
835 self
.env
["DISPLAY"] = "77" # Set a fake display.
837 testTimeoutInterval
= self
.harness_timeout
838 # Allow a test to request a multiple of the timeout if it is expected to take long
839 if "requesttimeoutfactor" in self
.test_object
:
840 testTimeoutInterval
*= int(self
.test_object
["requesttimeoutfactor"])
843 if not self
.interactive
and not self
.debuggerInfo
and not self
.jsDebuggerInfo
:
844 testTimer
= Timer(testTimeoutInterval
, lambda: self
.testTimeout(proc
))
848 process_output
= None
851 self
.log
.test_start(name
)
853 self
.logCommand(name
, self
.command
, test_dir
)
855 proc
= self
.launchProcess(
861 timeout
=testTimeoutInterval
,
865 if hasattr(proc
, "pid"):
866 self
.proc_ident
= proc
.pid
868 # On mobile, "proc" is just a file.
869 self
.proc_ident
= name
872 self
.log
.info("%s | Process ID: %d" % (name
, self
.proc_ident
))
874 # Communicate returns a tuple of (stdout, stderr), however we always
875 # redirect stderr to stdout, so the second element is ignored.
876 process_output
, _
= self
.communicate(proc
)
879 # Not sure what else to do here...
880 self
.keep_going
= True
887 # For the remote case, stdout is not yet depleted, so we parse
888 # it here all at once.
889 self
.parse_output(process_output
)
891 return_code
= self
.getReturnCode(proc
)
893 # TSan'd processes return 66 if races are detected. This isn't
894 # good in the sense that there's no way to distinguish between
895 # a process that would normally have returned zero but has races,
896 # and a race-free process that returns 66. But I don't see how
897 # to do better. This ambiguity is at least constrained to the
898 # with-TSan case. It doesn't affect normal builds.
900 # This also assumes that the magic value 66 isn't overridden by
901 # a TSAN_OPTIONS=exitcode=<number> environment variable setting.
903 TSAN_EXIT_CODE_WITH_RACES
= 66
905 return_code_ok
= return_code
== 0 or (
906 self
.usingTSan
and return_code
== TSAN_EXIT_CODE_WITH_RACES
909 # Due to the limitation on the remote xpcshell test, the process
910 # return code does not represent the process crash.
911 # If crash_reporter_init log has not been seen and the return code
912 # is 0, it means the process crashed before setting up the crash
915 # NOTE: Crash reporter is not enabled on some configuration, such
916 # as ASAN and TSAN. Those configuration shouldn't be using
917 # remote xpcshell test, and the crash should be caught by
918 # the process return code.
919 # NOTE: self.saw_crash_reporter_init is False also when adb failed
920 # to launch process, and in that case the return code is
922 # (see launchProcess in remotexpcshelltests.py)
923 ended_before_crash_reporter_init
= (
925 and self
.usingCrashReporter
926 and not self
.saw_crash_reporter_init
930 (not self
.has_failure_output
)
931 and not ended_before_crash_reporter_init
935 status
= "PASS" if passed
else "FAIL"
936 expected
= "PASS" if expect_pass
else "FAIL"
937 message
= "xpcshell return code: %d" % return_code
942 if status
!= expected
or ended_before_crash_reporter_init
:
943 if ended_before_crash_reporter_init
:
948 message
="Test ended before setting up the crash reporter",
955 message
="Test failed or timed out, will retry",
957 self
.clean_temp_dirs(path
)
958 if self
.verboseIfFails
and not self
.verbose
:
959 self
.log_full_output()
962 self
.log
.test_end(name
, status
, expected
=expected
, message
=message
)
963 self
.log_full_output()
967 if self
.failureManifest
:
968 with
open(self
.failureManifest
, "a") as f
:
969 f
.write("[%s]\n" % self
.test_object
["path"])
970 for k
, v
in self
.test_object
.items():
971 f
.write("%s = %s\n" % (k
, v
))
974 # If TSan reports a race, dump the output, else we can't
975 # diagnose what the problem was. See comments above about
976 # the significance of TSAN_EXIT_CODE_WITH_RACES.
977 if self
.usingTSan
and return_code
== TSAN_EXIT_CODE_WITH_RACES
:
978 self
.log_full_output()
980 self
.log
.test_end(name
, status
, expected
=expected
, message
=message
)
982 self
.log_full_output()
991 if self
.checkForCrashes(self
.tempDir
, self
.symbolsPath
, test_name
=name
):
993 self
.clean_temp_dirs(path
)
996 # If we assert during shutdown there's a chance the test has passed
997 # but we haven't logged full output, so do so here.
998 self
.log_full_output()
1001 if self
.logfiles
and process_output
:
1002 self
.createLogFile(name
, process_output
)
1005 self
.postCheck(proc
)
1006 self
.clean_temp_dirs(path
)
1009 self
.log
.error("Received SIGINT (control-C) during test execution")
1013 self
.keep_going
= False
1016 self
.keep_going
= True
1019 class XPCShellTests(object):
1020 def __init__(self
, log
=None):
1021 """Initializes node status and logger."""
1023 self
.harness_timeout
= HARNESS_TIMEOUT
1025 self
.http3Server
= None
1026 self
.conditioned_profile_dir
= None
1028 def getTestManifest(self
, manifest
):
1029 if isinstance(manifest
, TestManifest
):
1031 elif manifest
is not None:
1032 manifest
= os
.path
.normpath(os
.path
.abspath(manifest
))
1033 if os
.path
.isfile(manifest
):
1034 return TestManifest([manifest
], strict
=True)
1036 toml_path
= os
.path
.join(manifest
, "xpcshell.toml")
1038 toml_path
= os
.path
.join(SCRIPT_DIR
, "tests", "xpcshell.toml")
1040 if os
.path
.exists(toml_path
):
1041 return TestManifest([toml_path
], strict
=True)
1044 "Failed to find manifest at %s; use --manifest "
1045 "to set path explicitly." % toml_path
1049 def normalizeTest(self
, root
, test_object
):
1050 path
= test_object
.get("file_relpath", test_object
["relpath"])
1051 if "dupe-manifest" in test_object
and "ancestor_manifest" in test_object
:
1052 test_object
["id"] = "%s:%s" % (
1053 os
.path
.basename(test_object
["ancestor_manifest"]),
1057 test_object
["id"] = path
1060 test_object
["manifest"] = os
.path
.relpath(test_object
["manifest"], root
)
1063 for key
in ("id", "manifest"):
1064 test_object
[key
] = test_object
[key
].replace(os
.sep
, "/")
1068 def buildTestList(self
, test_tags
=None, test_paths
=None, verify
=False):
1069 """Reads the xpcshell.toml manifest and set self.alltests to an array.
1071 Given the parameters, this method compiles a list of tests to be run
1072 that matches the criteria set by parameters.
1074 If any chunking of tests are to occur, it is also done in this method.
1076 If no tests are added to the list of tests to be run, an error
1077 is logged. A sys.exit() signal is sent to the caller.
1080 test_tags (list, optional): list of strings.
1081 test_paths (list, optional): list of strings derived from the command
1082 line argument provided by user, specifying
1084 verify (bool, optional): boolean value.
1086 if test_paths
is None:
1089 mp
= self
.getTestManifest(self
.manifest
)
1092 if build
and not root
:
1093 root
= build
.topsrcdir
1094 normalize
= partial(self
.normalizeTest
, root
)
1098 filters
.append(tags(test_tags
))
1102 path_filter
= pathprefix(test_paths
)
1103 filters
.append(path_filter
)
1105 noDefaultFilters
= False
1106 if self
.runFailures
:
1107 filters
.append(failures(self
.runFailures
))
1108 noDefaultFilters
= True
1110 if self
.totalChunks
> 1:
1111 filters
.append(chunk_by_slice(self
.thisChunk
, self
.totalChunks
))
1113 self
.alltests
= list(
1118 noDefaultFilters
=noDefaultFilters
,
1124 sys
.stderr
.write("*** offending mozinfo.info: %s\n" % repr(mozinfo
.info
))
1127 if path_filter
and path_filter
.missing
:
1129 "The following path(s) didn't resolve any tests:\n {}".format(
1130 " \n".join(sorted(path_filter
.missing
))
1134 if len(self
.alltests
) == 0:
1137 and path_filter
.missing
== set(test_paths
)
1138 and os
.environ
.get("MOZ_AUTOMATION") == "1"
1140 # This can happen in CI when a manifest doesn't exist due to a
1141 # build config variable in moz.build traversal. Don't generate
1142 # an error in this case. Adding a todo count avoids mozharness
1144 self
.todoCount
+= len(path_filter
.missing
)
1147 "no tests to run using specified "
1148 "combination of filters: {}".format(mp
.fmt_filters())
1152 if len(self
.alltests
) == 1 and not verify
:
1153 self
.singleFile
= os
.path
.basename(self
.alltests
[0]["path"])
1155 self
.singleFile
= None
1158 self
.dump_tests
= os
.path
.expanduser(self
.dump_tests
)
1159 assert os
.path
.exists(os
.path
.dirname(self
.dump_tests
))
1160 with
open(self
.dump_tests
, "w") as dumpFile
:
1161 dumpFile
.write(json
.dumps({"active_tests": self
.alltests
}))
1163 self
.log
.info("Dumping active_tests to %s file." % self
.dump_tests
)
1166 def setAbsPath(self
):
1168 Set the absolute path for xpcshell and xrepath. These 3 variables
1169 depend on input from the command line and we need to allow for absolute paths.
1170 This function is overloaded for a remote solution as os.path* won't work remotely.
1172 self
.testharnessdir
= os
.path
.dirname(os
.path
.abspath(__file__
))
1173 self
.headJSPath
= self
.testharnessdir
.replace("\\", "/") + "/head.js"
1174 if self
.xpcshell
is not None:
1175 self
.xpcshell
= os
.path
.abspath(self
.xpcshell
)
1177 if self
.app_binary
is not None:
1178 self
.app_binary
= os
.path
.abspath(self
.app_binary
)
1180 if self
.xrePath
is None:
1181 binary_path
= self
.app_binary
or self
.xpcshell
1182 self
.xrePath
= os
.path
.dirname(binary_path
)
1184 # Check if we're run from an OSX app bundle and override
1185 # self.xrePath if we are.
1186 appBundlePath
= os
.path
.join(
1187 os
.path
.dirname(os
.path
.dirname(self
.xpcshell
)), "Resources"
1189 if os
.path
.exists(os
.path
.join(appBundlePath
, "application.ini")):
1190 self
.xrePath
= appBundlePath
1192 self
.xrePath
= os
.path
.abspath(self
.xrePath
)
1194 if self
.mozInfo
is None:
1195 self
.mozInfo
= os
.path
.join(self
.testharnessdir
, "mozinfo.json")
1197 def buildPrefsFile(self
, extraPrefs
):
1198 # Create the prefs.js file
1200 # In test packages used in CI, the profile_data directory is installed
1201 # in the SCRIPT_DIR.
1202 profile_data_dir
= os
.path
.join(SCRIPT_DIR
, "profile_data")
1203 # If possible, read profile data from topsrcdir. This prevents us from
1204 # requiring a re-build to pick up newly added extensions in the
1205 # <profile>/extensions directory.
1207 path
= os
.path
.join(build
.topsrcdir
, "testing", "profiles")
1208 if os
.path
.isdir(path
):
1209 profile_data_dir
= path
1210 # Still not found? Look for testing/profiles relative to testing/xpcshell.
1211 if not os
.path
.isdir(profile_data_dir
):
1212 path
= os
.path
.abspath(os
.path
.join(SCRIPT_DIR
, "..", "profiles"))
1213 if os
.path
.isdir(path
):
1214 profile_data_dir
= path
1216 with
open(os
.path
.join(profile_data_dir
, "profiles.json"), "r") as fh
:
1217 base_profiles
= json
.load(fh
)["xpcshell"]
1219 # values to use when interpolating preferences
1221 "server": "dummyserver",
1224 profile
= Profile(profile
=self
.tempDir
, restore
=False)
1225 prefsFile
= os
.path
.join(profile
.profile
, "user.js")
1227 # Empty the user.js file in case the file existed before.
1228 with
open(prefsFile
, "w"):
1231 for name
in base_profiles
:
1232 path
= os
.path
.join(profile_data_dir
, name
)
1233 profile
.merge(path
, interpolation
=interpolation
)
1235 # add command line prefs
1236 prefs
= parse_preferences(extraPrefs
)
1237 profile
.set_preferences(prefs
)
1239 self
.prefsFile
= prefsFile
1242 def buildCoreEnvironment(self
):
1244 Add environment variables likely to be used across all platforms, including
1247 # Make assertions fatal
1248 self
.env
["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
1249 # Crash reporting interferes with debugging
1250 if not self
.debuggerInfo
:
1251 self
.env
["MOZ_CRASHREPORTER"] = "1"
1252 # Don't launch the crash reporter client
1253 self
.env
["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
1254 # Don't permit remote connections by default.
1255 # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
1256 # enable non-local connections for the purposes of local testing.
1257 # Don't override the user's choice here. See bug 1049688.
1258 self
.env
.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1")
1259 if self
.mozInfo
.get("topsrcdir") is not None:
1260 self
.env
["MOZ_DEVELOPER_REPO_DIR"] = self
.mozInfo
["topsrcdir"]
1261 if self
.mozInfo
.get("topobjdir") is not None:
1262 self
.env
["MOZ_DEVELOPER_OBJ_DIR"] = self
.mozInfo
["topobjdir"]
1264 # Disable the content process sandbox for the xpcshell tests. They
1265 # currently attempt to do things like bind() sockets, which is not
1266 # compatible with the sandbox.
1267 self
.env
["MOZ_DISABLE_CONTENT_SANDBOX"] = "1"
1268 if os
.getenv("MOZ_FETCHES_DIR", None):
1269 self
.env
["MOZ_FETCHES_DIR"] = os
.getenv("MOZ_FETCHES_DIR", None)
1271 if self
.mozInfo
.get("socketprocess_networking"):
1272 self
.env
["MOZ_FORCE_USE_SOCKET_PROCESS"] = "1"
1274 self
.env
["MOZ_DISABLE_SOCKET_PROCESS"] = "1"
1276 def buildEnvironment(self
):
1278 Create and returns a dictionary of self.env to include all the appropriate env
1279 variables and values. On a remote system, we overload this to set different
1280 values and are missing things like os.environ and PATH.
1282 self
.env
= dict(os
.environ
)
1283 self
.buildCoreEnvironment()
1284 if sys
.platform
== "win32":
1285 self
.env
["PATH"] = self
.env
["PATH"] + ";" + self
.xrePath
1286 elif sys
.platform
in ("os2emx", "os2knix"):
1287 os
.environ
["BEGINLIBPATH"] = self
.xrePath
+ ";" + self
.env
["BEGINLIBPATH"]
1288 os
.environ
["LIBPATHSTRICT"] = "T"
1289 elif sys
.platform
== "osx" or sys
.platform
== "darwin":
1290 self
.env
["DYLD_LIBRARY_PATH"] = os
.path
.join(
1291 os
.path
.dirname(self
.xrePath
), "MacOS"
1293 else: # unix or linux?
1294 if "LD_LIBRARY_PATH" not in self
.env
or self
.env
["LD_LIBRARY_PATH"] is None:
1295 self
.env
["LD_LIBRARY_PATH"] = self
.xrePath
1297 self
.env
["LD_LIBRARY_PATH"] = ":".join(
1298 [self
.xrePath
, self
.env
["LD_LIBRARY_PATH"]]
1301 usingASan
= "asan" in self
.mozInfo
and self
.mozInfo
["asan"]
1302 usingTSan
= "tsan" in self
.mozInfo
and self
.mozInfo
["tsan"]
1303 if usingASan
or usingTSan
:
1304 # symbolizer support
1305 if "ASAN_SYMBOLIZER_PATH" in self
.env
and os
.path
.isfile(
1306 self
.env
["ASAN_SYMBOLIZER_PATH"]
1308 llvmsym
= self
.env
["ASAN_SYMBOLIZER_PATH"]
1310 llvmsym
= os
.path
.join(
1311 self
.xrePath
, "llvm-symbolizer" + self
.mozInfo
["bin_suffix"]
1313 if os
.path
.isfile(llvmsym
):
1315 self
.env
["ASAN_SYMBOLIZER_PATH"] = llvmsym
1317 oldTSanOptions
= self
.env
.get("TSAN_OPTIONS", "")
1318 self
.env
["TSAN_OPTIONS"] = "external_symbolizer_path={} {}".format(
1319 llvmsym
, oldTSanOptions
1321 self
.log
.info("runxpcshelltests.py | using symbolizer at %s" % llvmsym
)
1324 "TEST-UNEXPECTED-FAIL | runxpcshelltests.py | "
1325 "Failed to find symbolizer at %s" % llvmsym
1332 Determine the value of the stdout and stderr for the test.
1333 Return value is a list (pStdout, pStderr).
1335 if self
.interactive
:
1339 if self
.debuggerInfo
and self
.debuggerInfo
.interactive
:
1343 if sys
.platform
== "os2emx":
1348 return pStdout
, pStderr
1350 def verifyDirPath(self
, dirname
):
1352 Simple wrapper to get the absolute path for a given directory name.
1353 On a remote system, we need to overload this to work on the remote filesystem.
1355 return os
.path
.abspath(dirname
)
1357 def trySetupNode(self
):
1359 Run node for HTTP/2 tests, if available, and updates mozinfo as appropriate.
1361 if os
.getenv("MOZ_ASSUME_NODE_RUNNING", None):
1362 self
.log
.info("Assuming required node servers are already running")
1363 if not os
.getenv("MOZHTTP2_PORT", None):
1365 "MOZHTTP2_PORT environment variable not set. "
1366 "Tests requiring http/2 will fail."
1370 # We try to find the node executable in the path given to us by the user in
1371 # the MOZ_NODE_PATH environment variable
1372 nodeBin
= os
.getenv("MOZ_NODE_PATH", None)
1373 if not nodeBin
and build
:
1374 nodeBin
= build
.substs
.get("NODEJS")
1377 "MOZ_NODE_PATH environment variable not set. "
1378 "Tests requiring http/2 will fail."
1382 if not os
.path
.exists(nodeBin
) or not os
.path
.isfile(nodeBin
):
1383 error
= "node not found at MOZ_NODE_PATH %s" % (nodeBin
)
1384 self
.log
.error(error
)
1385 raise IOError(error
)
1387 self
.log
.info("Found node at %s" % (nodeBin
,))
1389 def read_streams(name
, proc
, pipe
):
1390 output
= "stdout" if pipe
== proc
.stdout
else "stderr"
1391 for line
in iter(pipe
.readline
, ""):
1392 self
.log
.info("node %s [%s] %s" % (name
, output
, line
))
1394 def startServer(name
, serverJs
):
1395 if not os
.path
.exists(serverJs
):
1396 error
= "%s not found at %s" % (name
, serverJs
)
1397 self
.log
.error(error
)
1398 raise IOError(error
)
1400 # OK, we found our server, let's try to get it running
1401 self
.log
.info("Found %s at %s" % (name
, serverJs
))
1403 # We pipe stdin to node because the server will exit when its
1405 with
popenCleanupHack():
1407 [nodeBin
, serverJs
],
1413 universal_newlines
=True,
1414 start_new_session
=True,
1416 self
.nodeProc
[name
] = process
1418 # Check to make sure the server starts properly by waiting for it to
1419 # tell us it's started
1420 msg
= process
.stdout
.readline()
1421 if "server listening" in msg
:
1422 searchObj
= re
.search(
1423 r
"HTTP2 server listening on ports ([0-9]+),([0-9]+)", msg
, 0
1426 self
.env
["MOZHTTP2_PORT"] = searchObj
.group(1)
1427 self
.env
["MOZNODE_EXEC_PORT"] = searchObj
.group(2)
1429 target
=read_streams
,
1430 args
=(name
, process
, process
.stdout
),
1435 target
=read_streams
,
1436 args
=(name
, process
, process
.stderr
),
1440 except OSError as e
:
1441 # This occurs if the subprocess couldn't be started
1442 self
.log
.error("Could not run %s server: %s" % (name
, str(e
)))
1445 myDir
= os
.path
.split(os
.path
.abspath(__file__
))[0]
1446 startServer("moz-http2", os
.path
.join(myDir
, "moz-http2", "moz-http2.js"))
1448 def shutdownNode(self
):
1450 Shut down our node process, if it exists
1452 for name
, proc
in six
.iteritems(self
.nodeProc
):
1453 self
.log
.info("Node %s server shutting down ..." % name
)
1454 if proc
.poll() is not None:
1455 self
.log
.info("Node server %s already dead %s" % (name
, proc
.poll()))
1456 elif sys
.platform
!= "win32":
1457 # Kill process and all its spawned children.
1458 os
.killpg(proc
.pid
, signal
.SIGTERM
)
1464 def startHttp3Server(self
):
1466 Start a Http3 test server.
1469 if sys
.platform
== "win32":
1471 http3ServerPath
= self
.http3ServerPath
1472 if not http3ServerPath
:
1473 http3ServerPath
= os
.path
.join(
1474 SCRIPT_DIR
, "http3server", "http3server" + binSuffix
1477 http3ServerPath
= os
.path
.join(
1478 build
.topobjdir
, "dist", "bin", "http3server" + binSuffix
1480 dbPath
= os
.path
.join(SCRIPT_DIR
, "http3server", "http3serverDB")
1482 dbPath
= os
.path
.join(build
.topsrcdir
, "netwerk", "test", "http3serverDB")
1484 options
["http3ServerPath"] = http3ServerPath
1485 options
["profilePath"] = dbPath
1486 options
["isMochitest"] = False
1487 options
["isWin"] = sys
.platform
== "win32"
1488 serverEnv
= self
.env
.copy()
1489 serverLog
= self
.env
.get("MOZHTTP3_SERVER_LOG")
1490 if serverLog
is not None:
1491 serverEnv
["RUST_LOG"] = serverLog
1492 self
.http3Server
= Http3Server(options
, serverEnv
, self
.log
)
1493 self
.http3Server
.start()
1494 for key
, value
in self
.http3Server
.ports().items():
1495 self
.env
[key
] = value
1496 self
.env
["MOZHTTP3_ECH"] = self
.http3Server
.echConfig()
1498 def shutdownHttp3Server(self
):
1499 if self
.http3Server
is None:
1501 self
.http3Server
.stop()
1502 self
.http3Server
= None
1504 def buildXpcsRunArgs(self
):
1506 Add arguments to run the test or make it interactive.
1508 if self
.interactive
:
1509 self
.xpcsRunArgs
= [
1511 'print("To start the test, type |_execute_test();|.");',
1515 self
.xpcsRunArgs
= ["-e", "_execute_test(); quit(0);"]
1517 def addTestResults(self
, test
):
1518 self
.passCount
+= test
.passCount
1519 self
.failCount
+= test
.failCount
1520 self
.todoCount
+= test
.todoCount
1522 def updateMozinfo(self
, prefs
, options
):
1523 # Handle filenames in mozInfo
1524 if not isinstance(self
.mozInfo
, dict):
1525 mozInfoFile
= self
.mozInfo
1526 if not os
.path
.isfile(mozInfoFile
):
1528 "Error: couldn't find mozinfo.json at '%s'. Perhaps you "
1529 "need to use --build-info-json?" % mozInfoFile
1532 self
.mozInfo
= json
.load(open(mozInfoFile
))
1534 # mozinfo.info is used as kwargs. Some builds are done with
1535 # an older Python that can't handle Unicode keys in kwargs.
1536 # All of the keys in question should be ASCII.
1538 for k
, v
in self
.mozInfo
.items():
1539 if isinstance(k
, bytes
):
1540 k
= k
.decode("utf-8")
1542 self
.mozInfo
= fixedInfo
1544 self
.mozInfo
["fission"] = prefs
.get("fission.autostart", True)
1545 self
.mozInfo
["sessionHistoryInParent"] = self
.mozInfo
[
1547 ] or not prefs
.get("fission.disableSessionHistoryInParent", False)
1549 self
.mozInfo
["serviceworker_e10s"] = True
1551 self
.mozInfo
["verify"] = options
.get("verify", False)
1553 self
.mozInfo
["socketprocess_networking"] = prefs
.get(
1554 "network.http.network_access_on_socket_process.enabled", False
1557 self
.mozInfo
["condprof"] = options
.get("conditionedProfile", False)
1559 self
.mozInfo
["msix"] = options
.get(
1561 ) is not None and "WindowsApps" in options
.get("app_binary", "")
1563 self
.mozInfo
["is_ubuntu"] = "Ubuntu" in platform
.version()
1565 mozinfo
.update(self
.mozInfo
)
1570 def conditioned_profile_copy(self
):
1571 """Returns a copy of the original conditioned profile that was created."""
1572 condprof_copy
= os
.path
.join(tempfile
.mkdtemp(), "profile")
1574 self
.conditioned_profile_dir
,
1576 ignore
=shutil
.ignore_patterns("lock"),
1578 self
.log
.info("Created a conditioned-profile copy: %s" % condprof_copy
)
1579 return condprof_copy
1581 def downloadConditionedProfile(self
, profile_scenario
, app
):
1582 from condprof
.client
import get_profile
1583 from condprof
.util
import get_current_platform
, get_version
1585 if self
.conditioned_profile_dir
:
1586 # We already have a directory, so provide a copy that
1587 # will get deleted after it's done with
1588 return self
.conditioned_profile_dir
1590 # create a temp file to help ensure uniqueness
1591 temp_download_dir
= tempfile
.mkdtemp()
1593 "Making temp_download_dir from inside get_conditioned_profile {}".format(
1597 # call condprof's client API to yield our platform-specific
1598 # conditioned-profile binary
1599 platform
= get_current_platform()
1601 if isinstance(app
, str):
1602 version
= get_version(app
)
1604 if not profile_scenario
:
1605 profile_scenario
= "settled"
1607 cond_prof_target_dir
= get_profile(
1611 repo
="mozilla-central",
1617 # any other error is a showstopper
1618 self
.log
.critical("Could not get the conditioned profile")
1619 traceback
.print_exc()
1623 self
.log
.info("Retrying a profile with no version specified")
1624 cond_prof_target_dir
= get_profile(
1628 repo
="mozilla-central",
1632 self
.log
.critical("Could not get the conditioned profile")
1633 traceback
.print_exc()
1636 # now get the full directory path to our fetched conditioned profile
1637 self
.conditioned_profile_dir
= os
.path
.join(
1638 temp_download_dir
, cond_prof_target_dir
1640 if not os
.path
.exists(cond_prof_target_dir
):
1642 "Can't find target_dir {}, from get_profile()"
1643 "temp_download_dir {}, platform {}, scenario {}".format(
1644 cond_prof_target_dir
, temp_download_dir
, platform
, profile_scenario
1650 "Original self.conditioned_profile_dir is now set: {}".format(
1651 self
.conditioned_profile_dir
1654 return self
.conditioned_profile_copy
1656 def runSelfTest(self
):
1663 class XPCShellTestsTests(selftest
.XPCShellTestsTests
):
1664 def __init__(self
, name
):
1665 unittest
.TestCase
.__init
__(self
, name
)
1666 self
.testing_modules
= this
.testingModulesDir
1667 self
.xpcshellBin
= this
.xpcshell
1668 self
.app_binary
= this
.app_binary
1669 self
.utility_path
= this
.utility_path
1670 self
.symbols_path
= this
.symbolsPath
1672 old_info
= dict(mozinfo
.info
)
1674 suite
= unittest
.TestLoader().loadTestsFromTestCase(XPCShellTestsTests
)
1675 return unittest
.TextTestRunner(verbosity
=2).run(suite
).wasSuccessful()
1677 # The self tests modify mozinfo, so we need to reset it.
1678 mozinfo
.info
.clear()
1679 mozinfo
.update(old_info
)
1681 def runTests(self
, options
, testClass
=XPCShellTestThread
, mobileArgs
=None):
1687 # Number of times to repeat test(s) in --verify mode
1690 if isinstance(options
, Namespace
):
1691 options
= vars(options
)
1693 # Try to guess modules directory.
1694 # This somewhat grotesque hack allows the buildbot machines to find the
1695 # modules directory without having to configure the buildbot hosts. This
1696 # code path should never be executed in local runs because the build system
1697 # should always set this argument.
1698 if not options
.get("testingModulesDir"):
1699 possible
= os
.path
.join(here
, os
.path
.pardir
, "modules")
1701 if os
.path
.isdir(possible
):
1702 testingModulesDir
= possible
1704 if options
.get("rerun_failures"):
1705 if os
.path
.exists(options
.get("failure_manifest")):
1706 rerun_manifest
= os
.path
.join(
1707 os
.path
.dirname(options
["failure_manifest"]), "rerun.toml"
1709 shutil
.copyfile(options
["failure_manifest"], rerun_manifest
)
1710 os
.remove(options
["failure_manifest"])
1712 self
.log
.error("No failures were found to re-run.")
1715 if options
.get("testingModulesDir"):
1716 # The resource loader expects native paths. Depending on how we were
1717 # invoked, a UNIX style path may sneak in on Windows. We try to
1719 testingModulesDir
= os
.path
.normpath(options
["testingModulesDir"])
1721 if not os
.path
.isabs(testingModulesDir
):
1722 testingModulesDir
= os
.path
.abspath(testingModulesDir
)
1724 if not testingModulesDir
.endswith(os
.path
.sep
):
1725 testingModulesDir
+= os
.path
.sep
1727 self
.debuggerInfo
= None
1729 if options
.get("debugger"):
1730 self
.debuggerInfo
= mozdebug
.get_debugger_info(
1731 options
.get("debugger"),
1732 options
.get("debuggerArgs"),
1733 options
.get("debuggerInteractive"),
1736 self
.jsDebuggerInfo
= None
1737 if options
.get("jsDebugger"):
1738 # A namedtuple let's us keep .port instead of ['port']
1739 JSDebuggerInfo
= namedtuple("JSDebuggerInfo", ["port"])
1740 self
.jsDebuggerInfo
= JSDebuggerInfo(port
=options
["jsDebuggerPort"])
1742 self
.app_binary
= options
.get("app_binary")
1743 self
.xpcshell
= options
.get("xpcshell")
1744 self
.http3ServerPath
= options
.get("http3server")
1745 self
.xrePath
= options
.get("xrePath")
1746 self
.utility_path
= options
.get("utility_path")
1747 self
.appPath
= options
.get("appPath")
1748 self
.symbolsPath
= options
.get("symbolsPath")
1749 self
.tempDir
= os
.path
.normpath(options
.get("tempDir") or tempfile
.gettempdir())
1750 self
.manifest
= options
.get("manifest")
1751 self
.dump_tests
= options
.get("dump_tests")
1752 self
.interactive
= options
.get("interactive")
1753 self
.verbose
= options
.get("verbose")
1754 self
.verboseIfFails
= options
.get("verboseIfFails")
1755 self
.keepGoing
= options
.get("keepGoing")
1756 self
.logfiles
= options
.get("logfiles")
1757 self
.totalChunks
= options
.get("totalChunks", 1)
1758 self
.thisChunk
= options
.get("thisChunk")
1759 self
.profileName
= options
.get("profileName") or "xpcshell"
1760 self
.mozInfo
= options
.get("mozInfo")
1761 self
.testingModulesDir
= testingModulesDir
1762 self
.sequential
= options
.get("sequential")
1763 self
.failure_manifest
= options
.get("failure_manifest")
1764 self
.threadCount
= options
.get("threadCount") or NUM_THREADS
1765 self
.jscovdir
= options
.get("jscovdir")
1766 self
.headless
= options
.get("headless")
1767 self
.runFailures
= options
.get("runFailures")
1768 self
.timeoutAsPass
= options
.get("timeoutAsPass")
1769 self
.crashAsPass
= options
.get("crashAsPass")
1770 self
.conditionedProfile
= options
.get("conditionedProfile")
1771 self
.repeat
= options
.get("repeat", 0)
1778 if self
.conditionedProfile
:
1779 self
.conditioned_profile_dir
= self
.downloadConditionedProfile(
1780 "full", self
.appPath
1782 options
["self_test"] = False
1783 if not options
["test_tags"]:
1784 options
["test_tags"] = []
1785 options
["test_tags"].append("condprof")
1789 eprefs
= options
.get("extraPrefs") or []
1790 # enable fission by default
1791 if options
.get("disableFission"):
1792 eprefs
.append("fission.autostart=false")
1794 # should be by default, just in case
1795 eprefs
.append("fission.autostart=true")
1797 prefs
= self
.buildPrefsFile(eprefs
)
1798 self
.buildXpcsRunArgs()
1800 self
.event
= Event()
1802 if not self
.updateMozinfo(prefs
, options
):
1806 "These variables are available in the mozinfo environment and "
1807 "can be used to skip tests conditionally:"
1809 for info
in sorted(self
.mozInfo
.items(), key
=lambda item
: item
[0]):
1810 self
.log
.info(" {key}: {value}".format(key
=info
[0], value
=info
[1]))
1812 if options
.get("self_test"):
1813 if not self
.runSelfTest():
1817 ("tsan" in self
.mozInfo
and self
.mozInfo
["tsan"])
1818 or ("asan" in self
.mozInfo
and self
.mozInfo
["asan"])
1819 ) and not options
.get("threadCount"):
1820 # TSan/ASan require significantly more memory, so reduce the amount of parallel
1821 # tests we run to avoid OOMs and timeouts. We always keep a minimum of 2 for
1822 # non-sequential execution.
1823 # pylint --py3k W1619
1824 self
.threadCount
= max(self
.threadCount
/ 2, 2)
1826 self
.stack_fixer_function
= None
1827 if self
.utility_path
and os
.path
.exists(self
.utility_path
):
1828 self
.stack_fixer_function
= get_stack_fixer_function(
1829 self
.utility_path
, self
.symbolsPath
1832 # buildEnvironment() needs mozInfo, so we call it after mozInfo is initialized.
1833 self
.buildEnvironment()
1835 # The appDirKey is a optional entry in either the default or individual test
1836 # sections that defines a relative application directory for test runs. If
1837 # defined we pass 'grePath/$appDirKey' for the -a parameter of the xpcshell
1840 if "appname" in self
.mozInfo
:
1841 appDirKey
= self
.mozInfo
["appname"] + "-appdir"
1843 # We have to do this before we run tests that depend on having the node
1847 self
.startHttp3Server()
1849 pStdout
, pStderr
= self
.getPipes()
1852 options
.get("test_tags"), options
.get("testPaths"), options
.get("verify")
1855 self
.sequential
= True
1857 if options
.get("shuffle"):
1858 random
.shuffle(self
.alltests
)
1860 self
.cleanup_dir_list
= []
1863 "appPath": self
.appPath
,
1864 "xrePath": self
.xrePath
,
1865 "utility_path": self
.utility_path
,
1866 "testingModulesDir": self
.testingModulesDir
,
1867 "debuggerInfo": self
.debuggerInfo
,
1868 "jsDebuggerInfo": self
.jsDebuggerInfo
,
1869 "headJSPath": self
.headJSPath
,
1870 "tempDir": self
.tempDir
,
1871 "testharnessdir": self
.testharnessdir
,
1872 "profileName": self
.profileName
,
1873 "singleFile": self
.singleFile
,
1874 "env": self
.env
, # making a copy of this in the testthreads
1875 "symbolsPath": self
.symbolsPath
,
1876 "logfiles": self
.logfiles
,
1877 "app_binary": self
.app_binary
,
1878 "xpcshell": self
.xpcshell
,
1879 "xpcsRunArgs": self
.xpcsRunArgs
,
1880 "failureManifest": self
.failure_manifest
,
1881 "jscovdir": self
.jscovdir
,
1882 "harness_timeout": self
.harness_timeout
,
1883 "stack_fixer_function": self
.stack_fixer_function
,
1884 "event": self
.event
,
1885 "cleanup_dir_list": self
.cleanup_dir_list
,
1888 "keep_going": self
.keepGoing
,
1890 "interactive": self
.interactive
,
1891 "app_dir_key": appDirKey
,
1892 "rootPrefsFile": self
.prefsFile
,
1893 "extraPrefs": options
.get("extraPrefs") or [],
1894 "verboseIfFails": self
.verboseIfFails
,
1895 "headless": self
.headless
,
1896 "runFailures": self
.runFailures
,
1897 "timeoutAsPass": self
.timeoutAsPass
,
1898 "crashAsPass": self
.crashAsPass
,
1899 "conditionedProfileDir": self
.conditioned_profile_dir
,
1900 "repeat": self
.repeat
,
1904 # Allow user to kill hung xpcshell subprocess with SIGINT
1905 # when we are only running tests sequentially.
1906 signal
.signal(signal
.SIGINT
, markGotSIGINT
)
1908 if self
.debuggerInfo
:
1909 # Force a sequential run
1910 self
.sequential
= True
1912 # If we have an interactive debugger, disable SIGINT entirely.
1913 if self
.debuggerInfo
.interactive
:
1914 signal
.signal(signal
.SIGINT
, lambda signum
, frame
: None)
1916 if "lldb" in self
.debuggerInfo
.path
:
1917 # Ask people to start debugging using 'process launch', see bug 952211.
1919 "It appears that you're using LLDB to debug this test. "
1920 + "Please use the 'process launch' command instead of "
1921 "the 'run' command to start xpcshell."
1924 if self
.jsDebuggerInfo
:
1925 # The js debugger magic needs more work to do the right thing
1926 # if debugging multiple files.
1927 if len(self
.alltests
) != 1:
1929 "Error: --jsdebugger can only be used with a single test!"
1933 # The test itself needs to know whether it is a tsan build, since
1934 # that has an effect on interpretation of the process return value.
1935 usingTSan
= "tsan" in self
.mozInfo
and self
.mozInfo
["tsan"]
1937 usingCrashReporter
= (
1938 "crashreporter" in self
.mozInfo
and self
.mozInfo
["crashreporter"]
1941 # create a queue of all tests that will run
1942 tests_queue
= deque()
1943 # also a list for the tests that need to be run sequentially
1944 sequential_tests
= []
1947 if options
.get("repeat", 0) > 0:
1948 self
.sequential
= True
1950 if not options
.get("verify"):
1951 for test_object
in self
.alltests
:
1952 # Test identifiers are provided for the convenience of logging. These
1953 # start as path names but are rewritten in case tests from the same path
1956 path
= test_object
["path"]
1958 if self
.singleFile
and not path
.endswith(self
.singleFile
):
1961 # if we have --repeat, duplicate the tests as needed
1962 for i
in range(0, options
.get("repeat", 0) + 1):
1967 verbose
=self
.verbose
or test_object
.get("verbose") == "true",
1968 usingTSan
=usingTSan
,
1969 usingCrashReporter
=usingCrashReporter
,
1970 mobileArgs
=mobileArgs
,
1973 if "run-sequentially" in test_object
or self
.sequential
:
1974 sequential_tests
.append(test
)
1976 tests_queue
.append(test
)
1978 status
= self
.runTestList(
1979 tests_queue
, sequential_tests
, testClass
, mobileArgs
, **kwargs
1983 # Test verification: Run each test many times, in various configurations,
1984 # in hopes of finding intermittent failures.
1988 # Run tests sequentially. Parallel mode would also work, except that
1989 # the logging system gets confused when 2 or more tests with the same
1990 # name run at the same time.
1991 sequential_tests
= []
1992 for i
in range(VERIFY_REPEAT
):
1995 test_object
, retry
=False, mobileArgs
=mobileArgs
, **kwargs
1997 sequential_tests
.append(test
)
1998 status
= self
.runTestList(
1999 tests_queue
, sequential_tests
, testClass
, mobileArgs
, **kwargs
2004 # Run tests sequentially, with MOZ_CHAOSMODE enabled.
2005 sequential_tests
= []
2006 self
.env
["MOZ_CHAOSMODE"] = "0xfb"
2007 # chaosmode runs really slow, allow tests extra time to pass
2008 self
.harness_timeout
= self
.harness_timeout
* 2
2009 for i
in range(VERIFY_REPEAT
):
2012 test_object
, retry
=False, mobileArgs
=mobileArgs
, **kwargs
2014 sequential_tests
.append(test
)
2015 status
= self
.runTestList(
2016 tests_queue
, sequential_tests
, testClass
, mobileArgs
, **kwargs
2018 self
.harness_timeout
= self
.harness_timeout
/ 2
2022 ("1. Run each test %d times, sequentially." % VERIFY_REPEAT
, step1
),
2024 "2. Run each test %d times, sequentially, in chaos mode."
2029 startTime
= datetime
.now()
2030 maxTime
= timedelta(seconds
=options
["verifyMaxTime"])
2031 for test_object
in self
.alltests
:
2033 for descr
, step
in steps
:
2034 stepResults
[descr
] = "not run / incomplete"
2035 finalResult
= "PASSED"
2036 for descr
, step
in steps
:
2037 if (datetime
.now() - startTime
) > maxTime
:
2039 "::: Test verification is taking too long: Giving up!"
2042 "::: So far, all checks passed, but not "
2043 "all checks were run."
2046 self
.log
.info(":::")
2047 self
.log
.info('::: Running test verification step "%s"...' % descr
)
2048 self
.log
.info(":::")
2050 if status
is not True:
2051 stepResults
[descr
] = "FAIL"
2052 finalResult
= "FAILED!"
2054 stepResults
[descr
] = "Pass"
2055 self
.log
.info(":::")
2057 "::: Test verification summary for: %s" % test_object
["path"]
2059 self
.log
.info(":::")
2060 for descr
in sorted(stepResults
.keys()):
2061 self
.log
.info("::: %s : %s" % (descr
, stepResults
[descr
]))
2062 self
.log
.info(":::")
2063 self
.log
.info("::: Test verification %s" % finalResult
)
2064 self
.log
.info(":::")
2067 self
.shutdownHttp3Server()
2071 def start_test(self
, test
):
2074 def test_ended(self
, test
):
2078 self
, tests_queue
, sequential_tests
, testClass
, mobileArgs
, **kwargs
2081 self
.log
.info("Running tests sequentially.")
2083 self
.log
.info("Using at most %d threads." % self
.threadCount
)
2085 # keep a set of threadCount running tests and start running the
2086 # tests in the queue at most threadCount at a time
2087 running_tests
= set()
2092 self
.try_again_list
= []
2094 tests_by_manifest
= defaultdict(list)
2095 for test
in self
.alltests
:
2096 group
= test
["manifest"]
2097 if "ancestor_manifest" in test
:
2098 ancestor_manifest
= normsep(test
["ancestor_manifest"])
2099 # Only change the group id if ancestor is not the generated root manifest.
2100 if "/" in ancestor_manifest
:
2101 group
= "{}:{}".format(ancestor_manifest
, group
)
2102 tests_by_manifest
[group
].append(test
["id"])
2104 self
.log
.suite_start(tests_by_manifest
, name
="xpcshell")
2106 while tests_queue
or running_tests
:
2107 # if we're not supposed to continue and all of the running tests
2109 if not keep_going
and not running_tests
:
2112 # if there's room to run more tests, start running them
2114 keep_going
and tests_queue
and (len(running_tests
) < self
.threadCount
)
2116 test
= tests_queue
.popleft()
2117 running_tests
.add(test
)
2118 self
.start_test(test
)
2120 # queue is full (for now) or no more new tests,
2121 # process the finished tests so far
2123 # wait for at least one of the tests to finish
2127 # find what tests are done (might be more than 1)
2129 for test
in running_tests
:
2131 self
.test_ended(test
)
2132 done_tests
.add(test
)
2135 ) # join with timeout so we don't hang on blocked threads
2136 # if the test had trouble, we will try running it again
2137 # at the end of the run
2138 if test
.retry
or test
.is_alive():
2139 # if the join call timed out, test.is_alive => True
2140 self
.try_again_list
.append(test
.test_object
)
2142 # did the test encounter any exception?
2144 exceptions
.append(test
.exception
)
2145 tracebacks
.append(test
.traceback
)
2146 # we won't add any more tests, will just wait for
2147 # the currently running ones to finish
2149 infra_abort
= infra_abort
and test
.infra
2150 keep_going
= keep_going
and test
.keep_going
2151 self
.addTestResults(test
)
2153 # make room for new tests to run
2154 running_tests
.difference_update(done_tests
)
2157 return TBPL_RETRY
# terminate early
2160 # run the other tests sequentially
2161 for test
in sequential_tests
:
2164 "TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so "
2165 "stopped run. (Use --keep-going to keep running tests "
2166 "after killing one with SIGINT)"
2169 self
.start_test(test
)
2171 self
.test_ended(test
)
2172 if (test
.failCount
> 0 or test
.passCount
<= 0) and os
.environ
.get(
2175 self
.try_again_list
.append(test
.test_object
)
2177 self
.addTestResults(test
)
2178 # did the test encounter any exception?
2180 exceptions
.append(test
.exception
)
2181 tracebacks
.append(test
.traceback
)
2183 keep_going
= test
.keep_going
2185 # retry tests that failed when run in parallel
2186 if self
.try_again_list
:
2187 self
.log
.info("Retrying tests that failed when run in parallel.")
2188 for test_object
in self
.try_again_list
:
2192 verbose
=self
.verbose
,
2193 mobileArgs
=mobileArgs
,
2196 self
.start_test(test
)
2198 self
.test_ended(test
)
2199 self
.addTestResults(test
)
2200 # did the test encounter any exception?
2202 exceptions
.append(test
.exception
)
2203 tracebacks
.append(test
.traceback
)
2205 keep_going
= test
.keep_going
2207 # restore default SIGINT behaviour
2208 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
)
2210 # Clean up any slacker directories that might be lying around
2211 # Some might fail because of windows taking too long to unlock them.
2212 # We don't do anything if this fails because the test machines will have
2213 # their $TEMP dirs cleaned up on reboot anyway.
2214 for directory
in self
.cleanup_dir_list
:
2216 shutil
.rmtree(directory
)
2218 self
.log
.info("%s could not be cleaned up." % directory
)
2221 self
.log
.info("Following exceptions were raised:")
2222 for t
in tracebacks
:
2226 if self
.testCount
== 0 and os
.environ
.get("MOZ_AUTOMATION") != "1":
2227 self
.log
.error("No tests run. Did you pass an invalid --test-path?")
2230 # doing this allows us to pass the mozharness parsers that
2231 # report an orange job for failCount>0
2232 if self
.runFailures
:
2233 passed
= self
.passCount
2234 self
.passCount
= self
.failCount
2235 self
.failCount
= passed
2237 self
.log
.info("INFO | Result summary:")
2238 self
.log
.info("INFO | Passed: %d" % self
.passCount
)
2239 self
.log
.info("INFO | Failed: %d" % self
.failCount
)
2240 self
.log
.info("INFO | Todo: %d" % self
.todoCount
)
2241 self
.log
.info("INFO | Retried: %d" % len(self
.try_again_list
))
2243 if gotSIGINT
and not keep_going
:
2245 "TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so stopped run. "
2246 "(Use --keep-going to keep running tests after "
2247 "killing one with SIGINT)"
2251 self
.log
.suite_end()
2252 return self
.runFailures
or self
.failCount
== 0
2256 parser
= parser_desktop()
2257 options
= parser
.parse_args()
2259 log
= commandline
.setup_logging("XPCShell", options
, {"tbpl": sys
.stdout
})
2261 if options
.xpcshell
is None and options
.app_binary
is None:
2263 "Must provide path to xpcshell using --xpcshell or Firefox using --app-binary"
2267 if options
.xpcshell
is not None and options
.app_binary
is not None:
2269 "Cannot provide --xpcshell and --app-binary - they are mutually exclusive options. Choose one."
2273 xpcsh
= XPCShellTests(log
)
2275 if options
.interactive
and not options
.testPath
:
2276 log
.error("Error: You must specify a test filename in interactive mode!")
2279 result
= xpcsh
.runTests(options
)
2280 if result
== TBPL_RETRY
:
2287 if __name__
== "__main__":