Backed out changeset 2b70f662a797 (bug 1867644) for causing bc failures on browser_ov...
[gecko.git] / testing / xpcshell / runxpcshelltests.py
blobd72172a25bde8a89f87d7229dfe56ae7a4199ad0
1 #!/usr/bin/env python
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/.
7 import copy
8 import json
9 import os
10 import pipes
11 import platform
12 import random
13 import re
14 import shutil
15 import signal
16 import subprocess
17 import sys
18 import tempfile
19 import time
20 import traceback
21 from argparse import Namespace
22 from collections import defaultdict, deque, namedtuple
23 from contextlib import contextmanager
24 from datetime import datetime, timedelta
25 from functools import partial
26 from multiprocessing import cpu_count
27 from subprocess import PIPE, STDOUT, Popen
28 from tempfile import gettempdir, mkdtemp
29 from threading import Event, Thread, Timer, current_thread
31 import mozdebug
32 import six
33 from mozserve import Http3Server
35 try:
36 import psutil
38 HAVE_PSUTIL = True
39 except Exception:
40 HAVE_PSUTIL = False
42 from xpcshellcommandline import parser_desktop
44 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
46 try:
47 from mozbuild.base import MozbuildObject
49 build = MozbuildObject.from_environment(cwd=SCRIPT_DIR)
50 except ImportError:
51 build = None
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",
73 "test_status",
74 "log",
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))
88 import mozcrash
89 import mozfile
90 import mozinfo
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):
110 c = m.group(0)
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)
123 else:
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)
131 @contextmanager
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.
139 savedCleanup = None
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
143 try:
144 yield
145 finally:
146 if savedCleanup:
147 subprocess._cleanup = savedCleanup
150 """ Control-C handling """
151 gotSIGINT = False
154 def markGotSIGINT(signum, stackFrame):
155 global gotSIGINT
156 gotSIGINT = True
159 class XPCShellTestThread(Thread):
160 def __init__(
161 self,
162 test_object,
163 retry=True,
164 verbose=False,
165 usingTSan=False,
166 usingCrashReporter=False,
167 **kwargs
169 Thread.__init__(self)
170 self.daemon = True
172 self.test_object = test_object
173 self.retry = retry
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")
213 if self.runFailures:
214 self.retry = False
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
220 # the harness
221 self.passCount = 0
222 self.todoCount = 0
223 self.failCount = 0
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
231 self.command = None
232 self.harness_timeout = kwargs.get("harness_timeout")
233 self.timedout = False
234 self.infra = 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
240 def run(self):
241 try:
242 self.run_test()
243 except PermissionError as e:
244 self.infra = True
245 self.exception = e
246 self.traceback = traceback.format_exc()
247 except Exception as e:
248 self.exception = e
249 self.traceback = traceback.format_exc()
250 else:
251 self.exception = None
252 self.traceback = None
253 if self.retry:
254 self.log.info(
255 "%s failed or timed out, will retry." % self.test_object["id"]
257 self.done = True
258 self.event.set()
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.
265 return proc.kill()
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.
279 return proc.poll()
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:
287 f.write(stdout)
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
296 return -1
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.
306 if proc.stdout:
307 while True:
308 line = proc.stdout.readline()
309 if not line:
310 break
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()
318 def launchProcess(
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.
327 if six.PY3:
328 cwd = six.ensure_str(cwd)
329 for i in range(len(cmd)):
330 cmd[i] = six.ensure_str(cmd[i])
332 if HAVE_PSUTIL:
333 popen_func = psutil.Popen
334 else:
335 popen_func = Popen
337 with popenCleanupHack():
338 proc = popen_func(cmd, stdout=stdout, stderr=stderr, env=env, cwd=cwd)
340 return proc
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.
347 quiet = False
348 if self.crashAsPass:
349 quiet = True
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
367 self.log.info(
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
377 else:
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:
386 if HAVE_PSUTIL:
387 try:
388 self.kill(proc)
389 except psutil.NoSuchProcess:
390 pass
391 else:
392 self.kill(proc)
393 message = "%s | Process still running after test!" % self.test_object["id"]
394 if self.retry:
395 self.log.info(message)
396 return
398 self.log.error(message)
399 self.log_full_output()
400 self.failCount = 1
402 def testTimeout(self, proc):
403 if self.test_object["expected"] == "pass":
404 expected = "PASS"
405 else:
406 expected = "FAIL"
408 if self.retry:
409 self.log.test_end(
410 self.test_object["id"],
411 "TIMEOUT",
412 expected="TIMEOUT",
413 message="Test timed out",
415 else:
416 result = "TIMEOUT"
417 if self.timeoutAsPass:
418 expected = "FAIL"
419 result = "FAIL"
420 self.failCount = 1
421 self.log.test_end(
422 self.test_object["id"],
423 result,
424 expected=expected,
425 message="Test timed out",
427 self.log_full_output()
429 self.done = True
430 self.timedout = True
431 self.killTimeout(proc)
432 self.log.info("xpcshell return code: %s" % self.getReturnCode(proc))
433 self.postCheck(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)
448 filename = "user.js"
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"]
456 if self.verbose:
457 self.log.info(
458 "%s: Per-test extra prefs will be set:\n {}".format(
459 "\n ".join(prefs)
461 % name
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
476 @property
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")
481 shutil.copytree(
482 self.conditionedProfileDir,
483 condprof_copy,
484 ignore=shutil.ignore_patterns("lock"),
486 self.log.info("Created a conditioned-profile copy: %s" % condprof_copy)
487 return 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
499 if self.interactive:
500 self.log.info("temp dir is %s" % tempDir)
501 return 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")
515 try:
516 # This could be left over from previous runs
517 self.removeDir(profileDir)
518 except Exception:
519 pass
520 os.makedirs(profileDir)
521 else:
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)
526 return profileDir
528 def setupMozinfoJS(self):
529 mozInfoJSPath = os.path.join(self.profileDir, "mozinfo.json")
530 mozInfoJSPath = mozInfoJSPath.replace("\\", "\\\\")
531 mozinfo.output_to_file(mozInfoJSPath)
532 return 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
547 return [
548 "-e",
549 "const _HEAD_FILES = [%s];" % cmdH,
550 "-e",
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(" "):
559 f = f.strip()
560 if len(f) < 1:
561 continue
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))
570 yield 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
583 if not self.appPath:
584 self.appPath = self.xrePath
586 if self.app_binary:
587 xpcsCmd = [
588 self.app_binary,
589 "--xpcshell",
591 else:
592 xpcsCmd = [
593 self.xpcshell,
596 xpcsCmd += [
597 "-g",
598 self.xrePath,
599 "-a",
600 self.appPath,
601 "-m",
602 "-e",
603 'const _HEAD_JS_PATH = "%s";' % self.headJSPath,
604 "-e",
605 'const _MOZINFO_JS_PATH = "%s";' % self.mozInfoJSPath,
606 "-e",
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
620 return xpcsCmd
622 def cleanupDir(self, directory, name):
623 if not os.path.exists(directory):
624 return
626 # up to TRY_LIMIT attempts (one every second), because
627 # the Windows filesystem is slow to react to the changes
628 TRY_LIMIT = 25
629 try_count = 0
630 while try_count < TRY_LIMIT:
631 try:
632 self.removeDir(directory)
633 except OSError:
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.
637 time.sleep(1)
638 try_count += 1
639 else:
640 # removed fine
641 return
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
648 # or check-one.
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")
670 return line
672 def log_line(self, line):
673 """Log a line of output (either a parser json object or text output from
674 the test process"""
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)
678 else:
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"]]
685 else:
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:
692 return
693 self.log.info(">>>>>>>")
694 for line in self.output_lines:
695 self.log_line(line)
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."""
701 if self.verbose:
702 self.log_line(message)
703 else:
704 self.output_lines.append(message)
706 def process_line(self, line_string):
707 """Parses a single line of output, determining its significance and
708 reporting a message.
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():
715 return
717 try:
718 line_object = json.loads(line_string)
719 if not isinstance(line_object, dict):
720 self.report_message(line_string)
721 return
722 except ValueError:
723 self.report_message(line_string)
724 return
726 if (
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)
732 return
734 if line_object["action"] == "crash_reporter_init":
735 self.saw_crash_reporter_init = True
736 return
738 action = line_object["action"]
740 self.has_failure_output = (
741 self.has_failure_output
742 or "expected" in line_object
743 or action == "log"
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
754 def run_test(self):
755 """Run an individual xpcshell test."""
756 global gotSIGINT
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"]
764 if not message:
765 message = "disabled from xpcshell manifest"
766 self.log.test_start(name)
767 self.log.test_end(name, "SKIP", message=message)
769 self.retry = False
770 self.keep_going = True
771 return
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)
782 else:
783 self.appPath = None
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
808 if self.jscovdir:
809 self.command.extend(
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
830 if (
831 self.test_object.get("headless", "true" if self.headless else None)
832 == "true"
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"])
842 testTimer = None
843 if not self.interactive and not self.debuggerInfo and not self.jsDebuggerInfo:
844 testTimer = Timer(testTimeoutInterval, lambda: self.testTimeout(proc))
845 testTimer.start()
847 proc = None
848 process_output = None
850 try:
851 self.log.test_start(name)
852 if self.verbose:
853 self.logCommand(name, self.command, test_dir)
855 proc = self.launchProcess(
856 self.command,
857 stdout=self.pStdout,
858 stderr=self.pStderr,
859 env=self.env,
860 cwd=test_dir,
861 timeout=testTimeoutInterval,
862 test_name=name,
865 if hasattr(proc, "pid"):
866 self.proc_ident = proc.pid
867 else:
868 # On mobile, "proc" is just a file.
869 self.proc_ident = name
871 if self.interactive:
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)
878 if self.interactive:
879 # Not sure what else to do here...
880 self.keep_going = True
881 return
883 if testTimer:
884 testTimer.cancel()
886 if process_output:
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
913 # reporter.
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
921 # not 0.
922 # (see launchProcess in remotexpcshelltests.py)
923 ended_before_crash_reporter_init = (
924 return_code_ok
925 and self.usingCrashReporter
926 and not self.saw_crash_reporter_init
929 passed = (
930 (not self.has_failure_output)
931 and not ended_before_crash_reporter_init
932 and return_code_ok
935 status = "PASS" if passed else "FAIL"
936 expected = "PASS" if expect_pass else "FAIL"
937 message = "xpcshell return code: %d" % return_code
939 if self.timedout:
940 return
942 if status != expected or ended_before_crash_reporter_init:
943 if ended_before_crash_reporter_init:
944 self.log.test_end(
945 name,
946 "CRASH",
947 expected=expected,
948 message="Test ended before setting up the crash reporter",
950 elif self.retry:
951 self.log.test_end(
952 name,
953 status,
954 expected=status,
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()
960 return
961 else:
962 self.log.test_end(name, status, expected=expected, message=message)
963 self.log_full_output()
965 self.failCount += 1
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))
973 else:
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)
981 if self.verbose:
982 self.log_full_output()
984 self.retry = False
986 if expect_pass:
987 self.passCount = 1
988 else:
989 self.todoCount = 1
991 if self.checkForCrashes(self.tempDir, self.symbolsPath, test_name=name):
992 if self.retry:
993 self.clean_temp_dirs(path)
994 return
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()
999 self.failCount = 1
1001 if self.logfiles and process_output:
1002 self.createLogFile(name, process_output)
1004 finally:
1005 self.postCheck(proc)
1006 self.clean_temp_dirs(path)
1008 if gotSIGINT:
1009 self.log.error("Received SIGINT (control-C) during test execution")
1010 if self.keep_going:
1011 gotSIGINT = False
1012 else:
1013 self.keep_going = False
1014 return
1016 self.keep_going = True
1019 class XPCShellTests(object):
1020 def __init__(self, log=None):
1021 """Initializes node status and logger."""
1022 self.log = log
1023 self.harness_timeout = HARNESS_TIMEOUT
1024 self.nodeProc = {}
1025 self.http3Server = None
1026 self.conditioned_profile_dir = None
1028 def getTestManifest(self, manifest):
1029 if isinstance(manifest, TestManifest):
1030 return manifest
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)
1035 else:
1036 toml_path = os.path.join(manifest, "xpcshell.toml")
1037 else:
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)
1042 else:
1043 self.log.error(
1044 "Failed to find manifest at %s; use --manifest "
1045 "to set path explicitly." % toml_path
1047 sys.exit(1)
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"]),
1054 path,
1056 else:
1057 test_object["id"] = path
1059 if root:
1060 test_object["manifest"] = os.path.relpath(test_object["manifest"], root)
1062 if os.sep != "/":
1063 for key in ("id", "manifest"):
1064 test_object[key] = test_object[key].replace(os.sep, "/")
1066 return test_object
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.
1079 Args:
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
1083 tests to be run.
1084 verify (bool, optional): boolean value.
1086 if test_paths is None:
1087 test_paths = []
1089 mp = self.getTestManifest(self.manifest)
1091 root = mp.rootdir
1092 if build and not root:
1093 root = build.topsrcdir
1094 normalize = partial(self.normalizeTest, root)
1096 filters = []
1097 if test_tags:
1098 filters.append(tags(test_tags))
1100 path_filter = None
1101 if test_paths:
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))
1112 try:
1113 self.alltests = list(
1114 map(
1115 normalize,
1116 mp.active_tests(
1117 filters=filters,
1118 noDefaultFilters=noDefaultFilters,
1119 **mozinfo.info,
1123 except TypeError:
1124 sys.stderr.write("*** offending mozinfo.info: %s\n" % repr(mozinfo.info))
1125 raise
1127 if path_filter and path_filter.missing:
1128 self.log.warning(
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:
1135 if (
1136 test_paths
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
1143 # raising an error.
1144 self.todoCount += len(path_filter.missing)
1145 else:
1146 self.log.error(
1147 "no tests to run using specified "
1148 "combination of filters: {}".format(mp.fmt_filters())
1150 sys.exit(1)
1152 if len(self.alltests) == 1 and not verify:
1153 self.singleFile = os.path.basename(self.alltests[0]["path"])
1154 else:
1155 self.singleFile = None
1157 if self.dump_tests:
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)
1164 sys.exit()
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)
1183 if mozinfo.isMac:
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
1191 else:
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.
1206 if build:
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
1220 interpolation = {
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"):
1229 pass
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
1240 return prefs
1242 def buildCoreEnvironment(self):
1244 Add environment variables likely to be used across all platforms, including
1245 remote systems.
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"
1273 else:
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
1296 else:
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"]
1309 else:
1310 llvmsym = os.path.join(
1311 self.xrePath, "llvm-symbolizer" + self.mozInfo["bin_suffix"]
1313 if os.path.isfile(llvmsym):
1314 if usingASan:
1315 self.env["ASAN_SYMBOLIZER_PATH"] = llvmsym
1316 else:
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)
1322 else:
1323 self.log.error(
1324 "TEST-UNEXPECTED-FAIL | runxpcshelltests.py | "
1325 "Failed to find symbolizer at %s" % llvmsym
1328 return self.env
1330 def getPipes(self):
1332 Determine the value of the stdout and stderr for the test.
1333 Return value is a list (pStdout, pStderr).
1335 if self.interactive:
1336 pStdout = None
1337 pStderr = None
1338 else:
1339 if self.debuggerInfo and self.debuggerInfo.interactive:
1340 pStdout = None
1341 pStderr = None
1342 else:
1343 if sys.platform == "os2emx":
1344 pStdout = None
1345 else:
1346 pStdout = PIPE
1347 pStderr = STDOUT
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):
1364 self.log.warning(
1365 "MOZHTTP2_PORT environment variable not set. "
1366 "Tests requiring http/2 will fail."
1368 return
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")
1375 if not nodeBin:
1376 self.log.warning(
1377 "MOZ_NODE_PATH environment variable not set. "
1378 "Tests requiring http/2 will fail."
1380 return
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))
1402 try:
1403 # We pipe stdin to node because the server will exit when its
1404 # stdin reaches EOF
1405 with popenCleanupHack():
1406 process = Popen(
1407 [nodeBin, serverJs],
1408 stdin=PIPE,
1409 stdout=PIPE,
1410 stderr=PIPE,
1411 env=self.env,
1412 cwd=os.getcwd(),
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
1425 if searchObj:
1426 self.env["MOZHTTP2_PORT"] = searchObj.group(1)
1427 self.env["MOZNODE_EXEC_PORT"] = searchObj.group(2)
1428 t1 = Thread(
1429 target=read_streams,
1430 args=(name, process, process.stdout),
1431 daemon=True,
1433 t1.start()
1434 t2 = Thread(
1435 target=read_streams,
1436 args=(name, process, process.stderr),
1437 daemon=True,
1439 t2.start()
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)))
1443 raise
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)
1459 else:
1460 proc.terminate()
1462 self.nodeProc = {}
1464 def startHttp3Server(self):
1466 Start a Http3 test server.
1468 binSuffix = ""
1469 if sys.platform == "win32":
1470 binSuffix = ".exe"
1471 http3ServerPath = self.http3ServerPath
1472 if not http3ServerPath:
1473 http3ServerPath = os.path.join(
1474 SCRIPT_DIR, "http3server", "http3server" + binSuffix
1476 if build:
1477 http3ServerPath = os.path.join(
1478 build.topobjdir, "dist", "bin", "http3server" + binSuffix
1480 dbPath = os.path.join(SCRIPT_DIR, "http3server", "http3serverDB")
1481 if build:
1482 dbPath = os.path.join(build.topsrcdir, "netwerk", "test", "http3serverDB")
1483 options = {}
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:
1500 return
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 = [
1510 "-e",
1511 'print("To start the test, type |_execute_test();|.");',
1512 "-i",
1514 else:
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):
1527 self.log.error(
1528 "Error: couldn't find mozinfo.json at '%s'. Perhaps you "
1529 "need to use --build-info-json?" % mozInfoFile
1531 return False
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.
1537 fixedInfo = {}
1538 for k, v in self.mozInfo.items():
1539 if isinstance(k, bytes):
1540 k = k.decode("utf-8")
1541 fixedInfo[k] = v
1542 self.mozInfo = fixedInfo
1544 self.mozInfo["fission"] = prefs.get("fission.autostart", True)
1545 self.mozInfo["sessionHistoryInParent"] = self.mozInfo[
1546 "fission"
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(
1560 "app_binary"
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)
1567 return True
1569 @property
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")
1573 shutil.copytree(
1574 self.conditioned_profile_dir,
1575 condprof_copy,
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()
1592 self.log.info(
1593 "Making temp_download_dir from inside get_conditioned_profile {}".format(
1594 temp_download_dir
1597 # call condprof's client API to yield our platform-specific
1598 # conditioned-profile binary
1599 platform = get_current_platform()
1600 version = None
1601 if isinstance(app, str):
1602 version = get_version(app)
1604 if not profile_scenario:
1605 profile_scenario = "settled"
1606 try:
1607 cond_prof_target_dir = get_profile(
1608 temp_download_dir,
1609 platform,
1610 profile_scenario,
1611 repo="mozilla-central",
1612 version=version,
1613 retries=2,
1615 except Exception:
1616 if version is None:
1617 # any other error is a showstopper
1618 self.log.critical("Could not get the conditioned profile")
1619 traceback.print_exc()
1620 raise
1621 version = None
1622 try:
1623 self.log.info("Retrying a profile with no version specified")
1624 cond_prof_target_dir = get_profile(
1625 temp_download_dir,
1626 platform,
1627 profile_scenario,
1628 repo="mozilla-central",
1629 version=version,
1631 except Exception:
1632 self.log.critical("Could not get the conditioned profile")
1633 traceback.print_exc()
1634 raise
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):
1641 self.log.critical(
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
1647 raise OSError
1649 self.log.info(
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):
1657 import unittest
1659 import selftest
1661 this = 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)
1673 try:
1674 suite = unittest.TestLoader().loadTestsFromTestCase(XPCShellTestsTests)
1675 return unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful()
1676 finally:
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):
1683 Run xpcshell tests.
1685 global gotSIGINT
1687 # Number of times to repeat test(s) in --verify mode
1688 VERIFY_REPEAT = 10
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"])
1711 else:
1712 self.log.error("No failures were found to re-run.")
1713 sys.exit(1)
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
1718 # normalize that.
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)
1773 self.testCount = 0
1774 self.passCount = 0
1775 self.failCount = 0
1776 self.todoCount = 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")
1787 self.setAbsPath()
1789 eprefs = options.get("extraPrefs") or []
1790 # enable fission by default
1791 if options.get("disableFission"):
1792 eprefs.append("fission.autostart=false")
1793 else:
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):
1803 return False
1805 self.log.info(
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():
1814 return False
1816 if (
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
1838 # test harness.
1839 appDirKey = None
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
1844 # http/2 server.
1845 self.trySetupNode()
1847 self.startHttp3Server()
1849 pStdout, pStderr = self.getPipes()
1851 self.buildTestList(
1852 options.get("test_tags"), options.get("testPaths"), options.get("verify")
1854 if self.singleFile:
1855 self.sequential = True
1857 if options.get("shuffle"):
1858 random.shuffle(self.alltests)
1860 self.cleanup_dir_list = []
1862 kwargs = {
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,
1886 "pStdout": pStdout,
1887 "pStderr": pStderr,
1888 "keep_going": self.keepGoing,
1889 "log": self.log,
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,
1903 if self.sequential:
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.
1918 self.log.info(
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:
1928 self.log.error(
1929 "Error: --jsdebugger can only be used with a single test!"
1931 return False
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 = []
1945 status = None
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
1954 # are re-run.
1956 path = test_object["path"]
1958 if self.singleFile and not path.endswith(self.singleFile):
1959 continue
1961 # if we have --repeat, duplicate the tests as needed
1962 for i in range(0, options.get("repeat", 0) + 1):
1963 self.testCount += 1
1965 test = testClass(
1966 test_object,
1967 verbose=self.verbose or test_object.get("verbose") == "true",
1968 usingTSan=usingTSan,
1969 usingCrashReporter=usingCrashReporter,
1970 mobileArgs=mobileArgs,
1971 **kwargs,
1973 if "run-sequentially" in test_object or self.sequential:
1974 sequential_tests.append(test)
1975 else:
1976 tests_queue.append(test)
1978 status = self.runTestList(
1979 tests_queue, sequential_tests, testClass, mobileArgs, **kwargs
1981 else:
1983 # Test verification: Run each test many times, in various configurations,
1984 # in hopes of finding intermittent failures.
1987 def step1():
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):
1993 self.testCount += 1
1994 test = testClass(
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
2001 return status
2003 def step2():
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):
2010 self.testCount += 1
2011 test = testClass(
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
2019 return status
2021 steps = [
2022 ("1. Run each test %d times, sequentially." % VERIFY_REPEAT, step1),
2024 "2. Run each test %d times, sequentially, in chaos mode."
2025 % VERIFY_REPEAT,
2026 step2,
2029 startTime = datetime.now()
2030 maxTime = timedelta(seconds=options["verifyMaxTime"])
2031 for test_object in self.alltests:
2032 stepResults = {}
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:
2038 self.log.info(
2039 "::: Test verification is taking too long: Giving up!"
2041 self.log.info(
2042 "::: So far, all checks passed, but not "
2043 "all checks were run."
2045 break
2046 self.log.info(":::")
2047 self.log.info('::: Running test verification step "%s"...' % descr)
2048 self.log.info(":::")
2049 status = step()
2050 if status is not True:
2051 stepResults[descr] = "FAIL"
2052 finalResult = "FAILED!"
2053 break
2054 stepResults[descr] = "Pass"
2055 self.log.info(":::")
2056 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(":::")
2066 self.shutdownNode()
2067 self.shutdownHttp3Server()
2069 return status
2071 def start_test(self, test):
2072 test.start()
2074 def test_ended(self, test):
2075 pass
2077 def runTestList(
2078 self, tests_queue, sequential_tests, testClass, mobileArgs, **kwargs
2080 if self.sequential:
2081 self.log.info("Running tests sequentially.")
2082 else:
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()
2088 keep_going = True
2089 infra_abort = False
2090 exceptions = []
2091 tracebacks = []
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
2108 # are done, stop
2109 if not keep_going and not running_tests:
2110 break
2112 # if there's room to run more tests, start running them
2113 while (
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
2124 self.event.wait(1)
2125 self.event.clear()
2127 # find what tests are done (might be more than 1)
2128 done_tests = set()
2129 for test in running_tests:
2130 if test.done:
2131 self.test_ended(test)
2132 done_tests.add(test)
2133 test.join(
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)
2141 continue
2142 # did the test encounter any exception?
2143 if test.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
2148 keep_going = False
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)
2156 if infra_abort:
2157 return TBPL_RETRY # terminate early
2159 if keep_going:
2160 # run the other tests sequentially
2161 for test in sequential_tests:
2162 if not keep_going:
2163 self.log.error(
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)"
2168 break
2169 self.start_test(test)
2170 test.join()
2171 self.test_ended(test)
2172 if (test.failCount > 0 or test.passCount <= 0) and os.environ.get(
2173 "MOZ_AUTOMATION", 0
2174 ) != 0:
2175 self.try_again_list.append(test.test_object)
2176 continue
2177 self.addTestResults(test)
2178 # did the test encounter any exception?
2179 if test.exception:
2180 exceptions.append(test.exception)
2181 tracebacks.append(test.traceback)
2182 break
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:
2189 test = testClass(
2190 test_object,
2191 retry=False,
2192 verbose=self.verbose,
2193 mobileArgs=mobileArgs,
2194 **kwargs,
2196 self.start_test(test)
2197 test.join()
2198 self.test_ended(test)
2199 self.addTestResults(test)
2200 # did the test encounter any exception?
2201 if test.exception:
2202 exceptions.append(test.exception)
2203 tracebacks.append(test.traceback)
2204 break
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:
2215 try:
2216 shutil.rmtree(directory)
2217 except Exception:
2218 self.log.info("%s could not be cleaned up." % directory)
2220 if exceptions:
2221 self.log.info("Following exceptions were raised:")
2222 for t in tracebacks:
2223 self.log.error(t)
2224 raise exceptions[0]
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?")
2228 self.failCount = 1
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:
2244 self.log.error(
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)"
2249 return False
2251 self.log.suite_end()
2252 return self.runFailures or self.failCount == 0
2255 def main():
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:
2262 log.error(
2263 "Must provide path to xpcshell using --xpcshell or Firefox using --app-binary"
2265 sys.exit(1)
2267 if options.xpcshell is not None and options.app_binary is not None:
2268 log.error(
2269 "Cannot provide --xpcshell and --app-binary - they are mutually exclusive options. Choose one."
2271 sys.exit(1)
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!")
2277 sys.exit(1)
2279 result = xpcsh.runTests(options)
2280 if result == TBPL_RETRY:
2281 sys.exit(4)
2283 if not result:
2284 sys.exit(1)
2287 if __name__ == "__main__":
2288 main()