Bug 1927677 - part 3 - Show the navbar divider in the same layout as the navbar r...
[gecko.git] / testing / xpcshell / runxpcshelltests.py
blob77b26a39aa12ce237676ac652f679c7f6a8c985e
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_key_value, 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 get_full_group_name(test):
110 group = test["manifest"]
111 if "ancestor_manifest" in test:
112 ancestor_manifest = normsep(test["ancestor_manifest"])
113 # Only change the group id if ancestor is not the generated root manifest.
114 if "/" in ancestor_manifest:
115 group = "{}:{}".format(ancestor_manifest, group)
116 return group
119 def _cleanup_encoding_repl(m):
120 c = m.group(0)
121 return "\\\\" if c == "\\" else "\\x{0:02X}".format(ord(c))
124 def cleanup_encoding(s):
125 """S is either a byte or unicode string. Either way it may
126 contain control characters, unpaired surrogates, reserved code
127 points, etc. If it is a byte string, it is assumed to be
128 UTF-8, but it may not be *correct* UTF-8. Return a
129 sanitized unicode object."""
130 if not isinstance(s, six.string_types):
131 if isinstance(s, six.binary_type):
132 return six.ensure_str(s)
133 else:
134 return six.text_type(s)
135 if isinstance(s, six.binary_type):
136 s = s.decode("utf-8", "replace")
137 # Replace all C0 and C1 control characters with \xNN escapes.
138 return _cleanup_encoding_re.sub(_cleanup_encoding_repl, s)
141 @contextmanager
142 def popenCleanupHack():
144 Hack to work around https://bugs.python.org/issue37380
145 The basic idea is that on old versions of Python on Windows,
146 we need to clear subprocess._cleanup before we call Popen(),
147 then restore it afterwards.
149 savedCleanup = None
150 if mozinfo.isWin and sys.version_info[0] == 3 and sys.version_info < (3, 7, 5):
151 savedCleanup = subprocess._cleanup
152 subprocess._cleanup = lambda: None
153 try:
154 yield
155 finally:
156 if savedCleanup:
157 subprocess._cleanup = savedCleanup
160 """ Control-C handling """
161 gotSIGINT = False
164 def markGotSIGINT(signum, stackFrame):
165 global gotSIGINT
166 gotSIGINT = True
169 class XPCShellTestThread(Thread):
170 def __init__(
171 self,
172 test_object,
173 retry=True,
174 verbose=False,
175 usingTSan=False,
176 usingCrashReporter=False,
177 **kwargs
179 Thread.__init__(self)
180 self.daemon = True
182 self.test_object = test_object
183 self.retry = retry
184 self.verbose = verbose
185 self.usingTSan = usingTSan
186 self.usingCrashReporter = usingCrashReporter
188 self.appPath = kwargs.get("appPath")
189 self.xrePath = kwargs.get("xrePath")
190 self.utility_path = kwargs.get("utility_path")
191 self.testingModulesDir = kwargs.get("testingModulesDir")
192 self.debuggerInfo = kwargs.get("debuggerInfo")
193 self.jsDebuggerInfo = kwargs.get("jsDebuggerInfo")
194 self.headJSPath = kwargs.get("headJSPath")
195 self.testharnessdir = kwargs.get("testharnessdir")
196 self.profileName = kwargs.get("profileName")
197 self.singleFile = kwargs.get("singleFile")
198 self.env = copy.deepcopy(kwargs.get("env"))
199 self.symbolsPath = kwargs.get("symbolsPath")
200 self.logfiles = kwargs.get("logfiles")
201 self.app_binary = kwargs.get("app_binary")
202 self.xpcshell = kwargs.get("xpcshell")
203 self.xpcsRunArgs = kwargs.get("xpcsRunArgs")
204 self.failureManifest = kwargs.get("failureManifest")
205 self.jscovdir = kwargs.get("jscovdir")
206 self.stack_fixer_function = kwargs.get("stack_fixer_function")
207 self._rootTempDir = kwargs.get("tempDir")
208 self.cleanup_dir_list = kwargs.get("cleanup_dir_list")
209 self.pStdout = kwargs.get("pStdout")
210 self.pStderr = kwargs.get("pStderr")
211 self.keep_going = kwargs.get("keep_going")
212 self.log = kwargs.get("log")
213 self.app_dir_key = kwargs.get("app_dir_key")
214 self.interactive = kwargs.get("interactive")
215 self.rootPrefsFile = kwargs.get("rootPrefsFile")
216 self.extraPrefs = kwargs.get("extraPrefs")
217 self.verboseIfFails = kwargs.get("verboseIfFails")
218 self.headless = kwargs.get("headless")
219 self.runFailures = kwargs.get("runFailures")
220 self.timeoutAsPass = kwargs.get("timeoutAsPass")
221 self.crashAsPass = kwargs.get("crashAsPass")
222 self.conditionedProfileDir = kwargs.get("conditionedProfileDir")
223 if self.runFailures:
224 self.retry = False
226 # Default the test prefsFile to the rootPrefsFile.
227 self.prefsFile = self.rootPrefsFile
229 # only one of these will be set to 1. adding them to the totals in
230 # the harness
231 self.passCount = 0
232 self.todoCount = 0
233 self.failCount = 0
235 # Context for output processing
236 self.output_lines = []
237 self.has_failure_output = False
238 self.saw_crash_reporter_init = False
239 self.saw_proc_start = False
240 self.saw_proc_end = False
241 self.command = None
242 self.harness_timeout = kwargs.get("harness_timeout")
243 self.timedout = False
244 self.infra = False
246 # event from main thread to signal work done
247 self.event = kwargs.get("event")
248 self.done = False # explicitly set flag so we don't rely on thread.isAlive
250 def run(self):
251 try:
252 self.run_test()
253 except PermissionError as e:
254 self.infra = True
255 self.exception = e
256 self.traceback = traceback.format_exc()
257 except Exception as e:
258 self.exception = e
259 self.traceback = traceback.format_exc()
260 else:
261 self.exception = None
262 self.traceback = None
263 if self.retry:
264 self.log.info(
265 "%s failed or timed out, will retry." % self.test_object["id"]
267 self.done = True
268 self.event.set()
270 def kill(self, proc):
272 Simple wrapper to kill a process.
273 On a remote system, this is overloaded to handle remote process communication.
275 return proc.kill()
277 def removeDir(self, dirname):
279 Simple wrapper to remove (recursively) a given directory.
280 On a remote system, we need to overload this to work on the remote filesystem.
282 mozfile.remove(dirname)
284 def poll(self, proc):
286 Simple wrapper to check if a process has terminated.
287 On a remote system, this is overloaded to handle remote process communication.
289 return proc.poll()
291 def createLogFile(self, test_file, stdout):
293 For a given test file and stdout buffer, create a log file.
294 On a remote system we have to fix the test name since it can contain directories.
296 with open(test_file + ".log", "w") as f:
297 f.write(stdout)
299 def getReturnCode(self, proc):
301 Simple wrapper to get the return code for a given process.
302 On a remote system we overload this to work with the remote process management.
304 if proc is not None and hasattr(proc, "returncode"):
305 return proc.returncode
306 return -1
308 def communicate(self, proc):
310 Simple wrapper to communicate with a process.
311 On a remote system, this is overloaded to handle remote process communication.
313 # Processing of incremental output put here to
314 # sidestep issues on remote platforms, where what we know
315 # as proc is a file pulled off of a device.
316 if proc.stdout:
317 while True:
318 line = proc.stdout.readline()
319 if not line:
320 break
321 self.process_line(line)
323 if self.saw_proc_start and not self.saw_proc_end:
324 self.has_failure_output = True
326 return proc.communicate()
328 def launchProcess(
329 self, cmd, stdout, stderr, env, cwd, timeout=None, test_name=None
332 Simple wrapper to launch a process.
333 On a remote system, this is more complex and we need to overload this function.
335 # timeout is needed by remote xpcshell to extend the
336 # remote device timeout. It is not used in this function.
337 if six.PY3:
338 cwd = six.ensure_str(cwd)
339 for i in range(len(cmd)):
340 cmd[i] = six.ensure_str(cmd[i])
342 if HAVE_PSUTIL:
343 popen_func = psutil.Popen
344 else:
345 popen_func = Popen
347 with popenCleanupHack():
348 proc = popen_func(cmd, stdout=stdout, stderr=stderr, env=env, cwd=cwd)
350 return proc
352 def checkForCrashes(self, dump_directory, symbols_path, test_name=None):
354 Simple wrapper to check for crashes.
355 On a remote system, this is more complex and we need to overload this function.
357 quiet = False
358 if self.crashAsPass:
359 quiet = True
361 return mozcrash.log_crashes(
362 self.log, dump_directory, symbols_path, test=test_name, quiet=quiet
365 def logCommand(self, name, completeCmd, testdir):
366 self.log.info("%s | full command: %r" % (name, completeCmd))
367 self.log.info("%s | current directory: %r" % (name, testdir))
368 # Show only those environment variables that are changed from
369 # the ambient environment.
370 changedEnv = set("%s=%s" % i for i in six.iteritems(self.env)) - set(
371 "%s=%s" % i for i in six.iteritems(os.environ)
373 self.log.info("%s | environment: %s" % (name, list(changedEnv)))
374 shell_command_tokens = [
375 pipes.quote(tok) for tok in list(changedEnv) + completeCmd
377 self.log.info(
378 "%s | as shell command: (cd %s; %s)"
379 % (name, pipes.quote(testdir), " ".join(shell_command_tokens))
382 def killTimeout(self, proc):
383 if proc is not None and hasattr(proc, "pid"):
384 mozcrash.kill_and_get_minidump(
385 proc.pid, self.tempDir, utility_path=self.utility_path
387 else:
388 self.log.info("not killing -- proc or pid unknown")
390 def postCheck(self, proc):
391 """Checks for a still-running test process, kills it and fails the test if found.
392 We can sometimes get here before the process has terminated, which would
393 cause removeDir() to fail - so check for the process and kill it if needed.
395 if proc and self.poll(proc) is None:
396 if HAVE_PSUTIL:
397 try:
398 self.kill(proc)
399 except psutil.NoSuchProcess:
400 pass
401 else:
402 self.kill(proc)
403 message = "%s | Process still running after test!" % self.test_object["id"]
404 if self.retry:
405 self.log.info(message)
406 return
408 self.log.error(message)
409 self.log_full_output()
410 self.failCount = 1
412 def testTimeout(self, proc):
413 if self.test_object["expected"] == "pass":
414 expected = "PASS"
415 else:
416 expected = "FAIL"
418 if self.retry:
419 self.log.test_end(
420 self.test_object["id"],
421 "TIMEOUT",
422 expected="TIMEOUT",
423 message="Test timed out",
425 else:
426 result = "TIMEOUT"
427 if self.timeoutAsPass:
428 expected = "FAIL"
429 result = "FAIL"
430 self.failCount = 1
431 self.log.test_end(
432 self.test_object["id"],
433 result,
434 expected=expected,
435 message="Test timed out",
437 self.log_full_output()
439 self.done = True
440 self.timedout = True
441 self.killTimeout(proc)
442 self.log.info("xpcshell return code: %s" % self.getReturnCode(proc))
443 self.postCheck(proc)
444 self.clean_temp_dirs(self.test_object["path"])
446 def updateTestPrefsFile(self):
447 # If the Manifest file has some additional prefs, merge the
448 # prefs set in the user.js file stored in the _rootTempdir
449 # with the prefs from the manifest and the prefs specified
450 # in the extraPrefs option.
451 if "prefs" in self.test_object:
452 # Merge the user preferences in a fake profile dir in a
453 # local temporary dir (self.tempDir is the remoteTmpDir
454 # for the RemoteXPCShellTestThread subclass and so we
455 # can't use that tempDir here).
456 localTempDir = mkdtemp(prefix="xpc-other-", dir=self._rootTempDir)
458 filename = "user.js"
459 interpolation = {"server": "dummyserver"}
460 profile = Profile(profile=localTempDir, restore=False)
461 # _rootTempDir contains a user.js file, generated by buildPrefsFile
462 profile.merge(self._rootTempDir, interpolation=interpolation)
464 prefs = self.test_object["prefs"].strip().split()
465 name = self.test_object["id"]
466 if self.verbose:
467 self.log.info(
468 "%s: Per-test extra prefs will be set:\n {}".format(
469 "\n ".join(prefs)
471 % name
474 profile.set_preferences(parse_preferences(prefs), filename=filename)
475 # Make sure that the extra prefs form the command line are overriding
476 # any prefs inherited from the shared profile data or the manifest prefs.
477 profile.set_preferences(
478 parse_preferences(self.extraPrefs), filename=filename
480 return os.path.join(profile.profile, filename)
482 # Return the root prefsFile if there is no other prefs to merge.
483 # This is the path set by buildPrefsFile.
484 return self.rootPrefsFile
486 @property
487 def conditioned_profile_copy(self):
488 """Returns a copy of the original conditioned profile that was created."""
490 condprof_copy = os.path.join(tempfile.mkdtemp(), "profile")
491 shutil.copytree(
492 self.conditionedProfileDir,
493 condprof_copy,
494 ignore=shutil.ignore_patterns("lock"),
496 self.log.info("Created a conditioned-profile copy: %s" % condprof_copy)
497 return condprof_copy
499 def buildCmdTestFile(self, name):
501 Build the command line arguments for the test file.
502 On a remote system, this may be overloaded to use a remote path structure.
504 return ["-e", 'const _TEST_FILE = ["%s"];' % name.replace("\\", "/")]
506 def setupTempDir(self):
507 tempDir = mkdtemp(prefix="xpc-other-", dir=self._rootTempDir)
508 self.env["XPCSHELL_TEST_TEMP_DIR"] = tempDir
509 if self.interactive:
510 self.log.info("temp dir is %s" % tempDir)
511 return tempDir
513 def setupProfileDir(self):
515 Create a temporary folder for the profile and set appropriate environment variables.
516 When running check-interactive and check-one, the directory is well-defined and
517 retained for inspection once the tests complete.
519 On a remote system, this may be overloaded to use a remote path structure.
521 if self.conditionedProfileDir:
522 profileDir = self.conditioned_profile_copy
523 elif self.interactive or self.singleFile:
524 profileDir = os.path.join(gettempdir(), self.profileName, "xpcshellprofile")
525 try:
526 # This could be left over from previous runs
527 self.removeDir(profileDir)
528 except Exception:
529 pass
530 os.makedirs(profileDir)
531 else:
532 profileDir = mkdtemp(prefix="xpc-profile-", dir=self._rootTempDir)
533 self.env["XPCSHELL_TEST_PROFILE_DIR"] = profileDir
534 if self.interactive or self.singleFile:
535 self.log.info("profile dir is %s" % profileDir)
536 return profileDir
538 def setupMozinfoJS(self):
539 mozInfoJSPath = os.path.join(self.profileDir, "mozinfo.json")
540 mozInfoJSPath = mozInfoJSPath.replace("\\", "\\\\")
541 mozinfo.output_to_file(mozInfoJSPath)
542 return mozInfoJSPath
544 def buildCmdHead(self):
546 Build the command line arguments for the head files,
547 along with the address of the webserver which some tests require.
549 On a remote system, this is overloaded to resolve quoting issues over a
550 secondary command line.
552 headfiles = self.getHeadFiles(self.test_object)
553 cmdH = ", ".join(['"' + f.replace("\\", "/") + '"' for f in headfiles])
555 dbgport = 0 if self.jsDebuggerInfo is None else self.jsDebuggerInfo.port
557 return [
558 "-e",
559 "const _HEAD_FILES = [%s];" % cmdH,
560 "-e",
561 "const _JSDEBUGGER_PORT = %d;" % dbgport,
564 def getHeadFiles(self, test):
565 """Obtain lists of head- files. Returns a list of head files."""
567 def sanitize_list(s, kind):
568 for f in s.strip().split(" "):
569 f = f.strip()
570 if len(f) < 1:
571 continue
573 path = os.path.normpath(os.path.join(test["here"], f))
574 if not os.path.exists(path):
575 raise Exception("%s file does not exist: %s" % (kind, path))
577 if not os.path.isfile(path):
578 raise Exception("%s file is not a file: %s" % (kind, path))
580 yield path
582 headlist = test.get("head", "")
583 return list(sanitize_list(headlist, "head"))
585 def buildXpcsCmd(self):
587 Load the root head.js file as the first file in our test path, before other head,
588 and test files. On a remote system, we overload this to add additional command
589 line arguments, so this gets overloaded.
591 # - NOTE: if you rename/add any of the constants set here, update
592 # do_load_child_test_harness() in head.js
593 if not self.appPath:
594 self.appPath = self.xrePath
596 if self.app_binary:
597 xpcsCmd = [
598 self.app_binary,
599 "--xpcshell",
601 else:
602 xpcsCmd = [
603 self.xpcshell,
606 xpcsCmd += [
607 "-g",
608 self.xrePath,
609 "-a",
610 self.appPath,
611 "-m",
612 "-e",
613 'const _HEAD_JS_PATH = "%s";' % self.headJSPath,
614 "-e",
615 'const _MOZINFO_JS_PATH = "%s";' % self.mozInfoJSPath,
616 "-e",
617 'const _PREFS_FILE = "%s";' % self.prefsFile.replace("\\", "\\\\"),
620 if self.testingModulesDir:
621 # Escape backslashes in string literal.
622 sanitized = self.testingModulesDir.replace("\\", "\\\\")
623 xpcsCmd.extend(["-e", 'const _TESTING_MODULES_DIR = "%s";' % sanitized])
625 xpcsCmd.extend(["-f", os.path.join(self.testharnessdir, "head.js")])
627 if self.debuggerInfo:
628 xpcsCmd = [self.debuggerInfo.path] + self.debuggerInfo.args + xpcsCmd
630 return xpcsCmd
632 def cleanupDir(self, directory, name):
633 if not os.path.exists(directory):
634 return
636 # up to TRY_LIMIT attempts (one every second), because
637 # the Windows filesystem is slow to react to the changes
638 TRY_LIMIT = 25
639 try_count = 0
640 while try_count < TRY_LIMIT:
641 try:
642 self.removeDir(directory)
643 except OSError:
644 self.log.info("Failed to remove directory: %s. Waiting." % directory)
645 # We suspect the filesystem may still be making changes. Wait a
646 # little bit and try again.
647 time.sleep(1)
648 try_count += 1
649 else:
650 # removed fine
651 return
653 # we try cleaning up again later at the end of the run
654 self.cleanup_dir_list.append(directory)
656 def clean_temp_dirs(self, name):
657 # We don't want to delete the profile when running check-interactive
658 # or check-one.
659 if self.profileDir and not self.interactive and not self.singleFile:
660 self.cleanupDir(self.profileDir, name)
662 self.cleanupDir(self.tempDir, name)
664 def parse_output(self, output):
665 """Parses process output for structured messages and saves output as it is
666 read. Sets self.has_failure_output in case of evidence of a failure"""
667 for line_string in output.splitlines():
668 self.process_line(line_string)
670 if self.saw_proc_start and not self.saw_proc_end:
671 self.has_failure_output = True
673 def fix_text_output(self, line):
674 line = cleanup_encoding(line)
675 if self.stack_fixer_function is not None:
676 line = self.stack_fixer_function(line)
678 if isinstance(line, bytes):
679 line = line.decode("utf-8")
680 return line
682 def log_line(self, line):
683 """Log a line of output (either a parser json object or text output from
684 the test process"""
685 if isinstance(line, six.string_types) or isinstance(line, bytes):
686 line = self.fix_text_output(line).rstrip("\r\n")
687 self.log.process_output(self.proc_ident, line, command=self.command)
688 else:
689 if "message" in line:
690 line["message"] = self.fix_text_output(line["message"])
691 if "xpcshell_process" in line:
692 line["thread"] = " ".join(
693 [current_thread().name, line["xpcshell_process"]]
695 else:
696 line["thread"] = current_thread().name
697 self.log.log_raw(line)
699 def log_full_output(self):
700 """Logs any buffered output from the test process, and clears the buffer."""
701 if not self.output_lines:
702 return
703 self.log.info(">>>>>>>")
704 for line in self.output_lines:
705 self.log_line(line)
706 self.log.info("<<<<<<<")
707 self.output_lines = []
709 def report_message(self, message):
710 """Stores or logs a json log message in mozlog format."""
711 if self.verbose:
712 self.log_line(message)
713 else:
714 self.output_lines.append(message)
716 def process_line(self, line_string):
717 """Parses a single line of output, determining its significance and
718 reporting a message.
720 if isinstance(line_string, bytes):
721 # Transform binary to string representation
722 line_string = line_string.decode(sys.stdout.encoding, errors="replace")
724 if not line_string.strip():
725 return
727 try:
728 line_object = json.loads(line_string)
729 if not isinstance(line_object, dict):
730 self.report_message(line_string)
731 return
732 except ValueError:
733 self.report_message(line_string)
734 return
736 if (
737 "action" not in line_object
738 or line_object["action"] not in EXPECTED_LOG_ACTIONS
740 # The test process output JSON.
741 self.report_message(line_string)
742 return
744 if line_object["action"] == "crash_reporter_init":
745 self.saw_crash_reporter_init = True
746 return
748 action = line_object["action"]
750 self.has_failure_output = (
751 self.has_failure_output
752 or "expected" in line_object
753 or action == "log"
754 and line_object["level"] == "ERROR"
757 self.report_message(line_object)
759 if action == "log" and line_object["message"] == "CHILD-TEST-STARTED":
760 self.saw_proc_start = True
761 elif action == "log" and line_object["message"] == "CHILD-TEST-COMPLETED":
762 self.saw_proc_end = True
764 def run_test(self):
765 """Run an individual xpcshell test."""
766 global gotSIGINT
768 name = self.test_object["id"]
769 path = self.test_object["path"]
770 group = get_full_group_name(self.test_object)
772 # Check for skipped tests
773 if "disabled" in self.test_object:
774 message = self.test_object["disabled"]
775 if not message:
776 message = "disabled from xpcshell manifest"
777 self.log.test_start(name, group=group)
778 self.log.test_end(name, "SKIP", message=message, group=group)
780 self.retry = False
781 self.keep_going = True
782 return
784 # Check for known-fail tests
785 expect_pass = self.test_object["expected"] == "pass"
787 # By default self.appPath will equal the gre dir. If specified in the
788 # xpcshell.toml file, set a different app dir for this test.
789 if self.app_dir_key and self.app_dir_key in self.test_object:
790 rel_app_dir = self.test_object[self.app_dir_key]
791 rel_app_dir = os.path.join(self.xrePath, rel_app_dir)
792 self.appPath = os.path.abspath(rel_app_dir)
793 else:
794 self.appPath = None
796 test_dir = os.path.dirname(path)
798 # Create a profile and a temp dir that the JS harness can stick
799 # a profile and temporary data in
800 self.profileDir = self.setupProfileDir()
801 self.tempDir = self.setupTempDir()
802 self.mozInfoJSPath = self.setupMozinfoJS()
804 # Setup per-manifest prefs and write them into the tempdir.
805 self.prefsFile = self.updateTestPrefsFile()
807 # The order of the command line is important:
808 # 1) Arguments for xpcshell itself
809 self.command = self.buildXpcsCmd()
811 # 2) Arguments for the head files
812 self.command.extend(self.buildCmdHead())
814 # 3) Arguments for the test file
815 self.command.extend(self.buildCmdTestFile(path))
816 self.command.extend(["-e", 'const _TEST_NAME = "%s";' % name])
818 # 4) Arguments for code coverage
819 if self.jscovdir:
820 self.command.extend(
821 ["-e", 'const _JSCOV_DIR = "%s";' % self.jscovdir.replace("\\", "/")]
824 # 5) Runtime arguments
825 if "debug" in self.test_object:
826 self.command.append("-d")
828 self.command.extend(self.xpcsRunArgs)
830 if self.test_object.get("dmd") == "true":
831 self.env["PYTHON"] = sys.executable
832 self.env["BREAKPAD_SYMBOLS_PATH"] = self.symbolsPath
834 if self.test_object.get("snap") == "true":
835 self.env["SNAP_NAME"] = "firefox"
836 self.env["SNAP_INSTANCE_NAME"] = "firefox"
838 if self.test_object.get("subprocess") == "true":
839 self.env["PYTHON"] = sys.executable
841 if (
842 self.test_object.get("headless", "true" if self.headless else None)
843 == "true"
845 self.env["MOZ_HEADLESS"] = "1"
846 self.env["DISPLAY"] = "77" # Set a fake display.
848 testTimeoutInterval = self.harness_timeout
849 # Allow a test to request a multiple of the timeout if it is expected to take long
850 if "requesttimeoutfactor" in self.test_object:
851 testTimeoutInterval *= int(self.test_object["requesttimeoutfactor"])
853 testTimer = None
854 if not self.interactive and not self.debuggerInfo and not self.jsDebuggerInfo:
855 testTimer = Timer(testTimeoutInterval, lambda: self.testTimeout(proc))
856 testTimer.start()
858 proc = None
859 process_output = None
861 try:
862 self.log.test_start(name, group=group)
863 if self.verbose:
864 self.logCommand(name, self.command, test_dir)
866 proc = self.launchProcess(
867 self.command,
868 stdout=self.pStdout,
869 stderr=self.pStderr,
870 env=self.env,
871 cwd=test_dir,
872 timeout=testTimeoutInterval,
873 test_name=name,
876 if hasattr(proc, "pid"):
877 self.proc_ident = proc.pid
878 else:
879 # On mobile, "proc" is just a file.
880 self.proc_ident = name
882 if self.interactive:
883 self.log.info("%s | Process ID: %d" % (name, self.proc_ident))
885 # Communicate returns a tuple of (stdout, stderr), however we always
886 # redirect stderr to stdout, so the second element is ignored.
887 process_output, _ = self.communicate(proc)
889 if self.interactive:
890 # Not sure what else to do here...
891 self.keep_going = True
892 return
894 if testTimer:
895 testTimer.cancel()
897 if process_output:
898 # For the remote case, stdout is not yet depleted, so we parse
899 # it here all at once.
900 self.parse_output(process_output)
902 return_code = self.getReturnCode(proc)
904 # TSan'd processes return 66 if races are detected. This isn't
905 # good in the sense that there's no way to distinguish between
906 # a process that would normally have returned zero but has races,
907 # and a race-free process that returns 66. But I don't see how
908 # to do better. This ambiguity is at least constrained to the
909 # with-TSan case. It doesn't affect normal builds.
911 # This also assumes that the magic value 66 isn't overridden by
912 # a TSAN_OPTIONS=exitcode=<number> environment variable setting.
914 TSAN_EXIT_CODE_WITH_RACES = 66
916 return_code_ok = return_code == 0 or (
917 self.usingTSan and return_code == TSAN_EXIT_CODE_WITH_RACES
920 # Due to the limitation on the remote xpcshell test, the process
921 # return code does not represent the process crash.
922 # If crash_reporter_init log has not been seen and the return code
923 # is 0, it means the process crashed before setting up the crash
924 # reporter.
926 # NOTE: Crash reporter is not enabled on some configuration, such
927 # as ASAN and TSAN. Those configuration shouldn't be using
928 # remote xpcshell test, and the crash should be caught by
929 # the process return code.
930 # NOTE: self.saw_crash_reporter_init is False also when adb failed
931 # to launch process, and in that case the return code is
932 # not 0.
933 # (see launchProcess in remotexpcshelltests.py)
934 ended_before_crash_reporter_init = (
935 return_code_ok
936 and self.usingCrashReporter
937 and not self.saw_crash_reporter_init
940 passed = (
941 (not self.has_failure_output)
942 and not ended_before_crash_reporter_init
943 and return_code_ok
946 status = "PASS" if passed else "FAIL"
947 expected = "PASS" if expect_pass else "FAIL"
948 message = "xpcshell return code: %d" % return_code
950 if self.timedout:
951 return
953 if status != expected or ended_before_crash_reporter_init:
954 if ended_before_crash_reporter_init:
955 self.log.test_end(
956 name,
957 "CRASH",
958 expected=expected,
959 message="Test ended before setting up the crash reporter",
960 group=group,
962 elif self.retry:
963 self.log.test_end(
964 name,
965 status,
966 expected=status,
967 message="Test failed or timed out, will retry",
968 group=group,
970 self.clean_temp_dirs(path)
971 if self.verboseIfFails and not self.verbose:
972 self.log_full_output()
973 return
974 else:
975 self.log.test_end(
976 name, status, expected=expected, message=message, group=group
978 self.log_full_output()
980 self.failCount += 1
982 if self.failureManifest:
983 with open(self.failureManifest, "a") as f:
984 f.write("[%s]\n" % self.test_object["path"])
985 for k, v in self.test_object.items():
986 f.write("%s = %s\n" % (k, v))
988 else:
989 # If TSan reports a race, dump the output, else we can't
990 # diagnose what the problem was. See comments above about
991 # the significance of TSAN_EXIT_CODE_WITH_RACES.
992 if self.usingTSan and return_code == TSAN_EXIT_CODE_WITH_RACES:
993 self.log_full_output()
995 self.log.test_end(
996 name, status, expected=expected, message=message, group=group
998 if self.verbose:
999 self.log_full_output()
1001 self.retry = False
1003 if expect_pass:
1004 self.passCount = 1
1005 else:
1006 self.todoCount = 1
1008 if self.checkForCrashes(self.tempDir, self.symbolsPath, test_name=name):
1009 if self.retry:
1010 self.clean_temp_dirs(path)
1011 return
1013 # If we assert during shutdown there's a chance the test has passed
1014 # but we haven't logged full output, so do so here.
1015 self.log_full_output()
1016 self.failCount = 1
1018 if self.logfiles and process_output:
1019 self.createLogFile(name, process_output)
1021 finally:
1022 self.postCheck(proc)
1023 self.clean_temp_dirs(path)
1025 if gotSIGINT:
1026 self.log.error("Received SIGINT (control-C) during test execution")
1027 if self.keep_going:
1028 gotSIGINT = False
1029 else:
1030 self.keep_going = False
1031 return
1033 self.keep_going = True
1036 class XPCShellTests(object):
1037 def __init__(self, log=None):
1038 """Initializes node status and logger."""
1039 self.log = log
1040 self.harness_timeout = HARNESS_TIMEOUT
1041 self.nodeProc = {}
1042 self.http3Server = None
1043 self.conditioned_profile_dir = None
1045 def getTestManifest(self, manifest):
1046 if isinstance(manifest, TestManifest):
1047 return manifest
1048 elif manifest is not None:
1049 manifest = os.path.normpath(os.path.abspath(manifest))
1050 if os.path.isfile(manifest):
1051 return TestManifest([manifest], strict=True)
1052 else:
1053 toml_path = os.path.join(manifest, "xpcshell.toml")
1054 else:
1055 toml_path = os.path.join(SCRIPT_DIR, "tests", "xpcshell.toml")
1057 if os.path.exists(toml_path):
1058 return TestManifest([toml_path], strict=True)
1059 else:
1060 self.log.error(
1061 "Failed to find manifest at %s; use --manifest "
1062 "to set path explicitly." % toml_path
1064 sys.exit(1)
1066 def normalizeTest(self, root, test_object):
1067 path = test_object.get("file_relpath", test_object["relpath"])
1068 if "dupe-manifest" in test_object and "ancestor_manifest" in test_object:
1069 test_object["id"] = "%s:%s" % (
1070 os.path.basename(test_object["ancestor_manifest"]),
1071 path,
1073 else:
1074 test_object["id"] = path
1076 if root:
1077 test_object["manifest"] = os.path.relpath(test_object["manifest"], root)
1079 if os.sep != "/":
1080 for key in ("id", "manifest"):
1081 test_object[key] = test_object[key].replace(os.sep, "/")
1083 return test_object
1085 def buildTestList(self, test_tags=None, test_paths=None, verify=False):
1086 """Reads the xpcshell.toml manifest and set self.alltests to an array.
1088 Given the parameters, this method compiles a list of tests to be run
1089 that matches the criteria set by parameters.
1091 If any chunking of tests are to occur, it is also done in this method.
1093 If no tests are added to the list of tests to be run, an error
1094 is logged. A sys.exit() signal is sent to the caller.
1096 Args:
1097 test_tags (list, optional): list of strings.
1098 test_paths (list, optional): list of strings derived from the command
1099 line argument provided by user, specifying
1100 tests to be run.
1101 verify (bool, optional): boolean value.
1103 if test_paths is None:
1104 test_paths = []
1106 mp = self.getTestManifest(self.manifest)
1108 root = mp.rootdir
1109 if build and not root:
1110 root = build.topsrcdir
1111 normalize = partial(self.normalizeTest, root)
1113 filters = []
1114 if test_tags:
1115 filters.extend([tags(x) for x in test_tags])
1117 path_filter = None
1118 if test_paths:
1119 path_filter = pathprefix(test_paths)
1120 filters.append(path_filter)
1122 noDefaultFilters = False
1123 if self.runFailures:
1124 filters.append(failures(self.runFailures))
1125 noDefaultFilters = True
1127 if self.totalChunks > 1:
1128 filters.append(chunk_by_slice(self.thisChunk, self.totalChunks))
1129 try:
1130 self.alltests = list(
1131 map(
1132 normalize,
1133 mp.active_tests(
1134 filters=filters,
1135 noDefaultFilters=noDefaultFilters,
1136 strictExpressions=True,
1137 **mozinfo.info,
1141 except TypeError:
1142 sys.stderr.write("*** offending mozinfo.info: %s\n" % repr(mozinfo.info))
1143 raise
1145 if path_filter and path_filter.missing:
1146 self.log.warning(
1147 "The following path(s) didn't resolve any tests:\n {}".format(
1148 " \n".join(sorted(path_filter.missing))
1152 if len(self.alltests) == 0:
1153 if (
1154 test_paths
1155 and path_filter.missing == set(test_paths)
1156 and os.environ.get("MOZ_AUTOMATION") == "1"
1158 # This can happen in CI when a manifest doesn't exist due to a
1159 # build config variable in moz.build traversal. Don't generate
1160 # an error in this case. Adding a todo count avoids mozharness
1161 # raising an error.
1162 self.todoCount += len(path_filter.missing)
1163 else:
1164 self.log.error(
1165 "no tests to run using specified "
1166 "combination of filters: {}".format(mp.fmt_filters())
1168 sys.exit(1)
1170 if len(self.alltests) == 1 and not verify:
1171 self.singleFile = os.path.basename(self.alltests[0]["path"])
1172 else:
1173 self.singleFile = None
1175 if self.dump_tests:
1176 self.dump_tests = os.path.expanduser(self.dump_tests)
1177 assert os.path.exists(os.path.dirname(self.dump_tests))
1178 with open(self.dump_tests, "w") as dumpFile:
1179 dumpFile.write(json.dumps({"active_tests": self.alltests}))
1181 self.log.info("Dumping active_tests to %s file." % self.dump_tests)
1182 sys.exit()
1184 def setAbsPath(self):
1186 Set the absolute path for xpcshell and xrepath. These 3 variables
1187 depend on input from the command line and we need to allow for absolute paths.
1188 This function is overloaded for a remote solution as os.path* won't work remotely.
1190 self.testharnessdir = os.path.dirname(os.path.abspath(__file__))
1191 self.headJSPath = self.testharnessdir.replace("\\", "/") + "/head.js"
1192 if self.xpcshell is not None:
1193 self.xpcshell = os.path.abspath(self.xpcshell)
1195 if self.app_binary is not None:
1196 self.app_binary = os.path.abspath(self.app_binary)
1198 if self.xrePath is None:
1199 binary_path = self.app_binary or self.xpcshell
1200 self.xrePath = os.path.dirname(binary_path)
1201 if mozinfo.isMac:
1202 # Check if we're run from an OSX app bundle and override
1203 # self.xrePath if we are.
1204 appBundlePath = os.path.join(
1205 os.path.dirname(os.path.dirname(self.xpcshell)), "Resources"
1207 if os.path.exists(os.path.join(appBundlePath, "application.ini")):
1208 self.xrePath = appBundlePath
1209 else:
1210 self.xrePath = os.path.abspath(self.xrePath)
1212 if self.mozInfo is None:
1213 self.mozInfo = os.path.join(self.testharnessdir, "mozinfo.json")
1215 def buildPrefsFile(self, extraPrefs):
1216 # Create the prefs.js file
1218 # In test packages used in CI, the profile_data directory is installed
1219 # in the SCRIPT_DIR.
1220 profile_data_dir = os.path.join(SCRIPT_DIR, "profile_data")
1221 # If possible, read profile data from topsrcdir. This prevents us from
1222 # requiring a re-build to pick up newly added extensions in the
1223 # <profile>/extensions directory.
1224 if build:
1225 path = os.path.join(build.topsrcdir, "testing", "profiles")
1226 if os.path.isdir(path):
1227 profile_data_dir = path
1228 # Still not found? Look for testing/profiles relative to testing/xpcshell.
1229 if not os.path.isdir(profile_data_dir):
1230 path = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "profiles"))
1231 if os.path.isdir(path):
1232 profile_data_dir = path
1234 with open(os.path.join(profile_data_dir, "profiles.json"), "r") as fh:
1235 base_profiles = json.load(fh)["xpcshell"]
1237 # values to use when interpolating preferences
1238 interpolation = {
1239 "server": "dummyserver",
1242 profile = Profile(profile=self.tempDir, restore=False)
1243 prefsFile = os.path.join(profile.profile, "user.js")
1245 # Empty the user.js file in case the file existed before.
1246 with open(prefsFile, "w"):
1247 pass
1249 for name in base_profiles:
1250 path = os.path.join(profile_data_dir, name)
1251 profile.merge(path, interpolation=interpolation)
1253 # add command line prefs
1254 prefs = parse_preferences(extraPrefs)
1255 profile.set_preferences(prefs)
1257 self.prefsFile = prefsFile
1258 return prefs
1260 def buildCoreEnvironment(self):
1262 Add environment variables likely to be used across all platforms, including
1263 remote systems.
1265 # Make assertions fatal
1266 self.env["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
1267 # Crash reporting interferes with debugging
1268 if not self.debuggerInfo:
1269 self.env["MOZ_CRASHREPORTER"] = "1"
1270 # Don't launch the crash reporter client
1271 self.env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
1272 # Don't permit remote connections by default.
1273 # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
1274 # enable non-local connections for the purposes of local testing.
1275 # Don't override the user's choice here. See bug 1049688.
1276 self.env.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1")
1277 if self.mozInfo.get("topsrcdir") is not None:
1278 self.env["MOZ_DEVELOPER_REPO_DIR"] = self.mozInfo["topsrcdir"]
1279 if self.mozInfo.get("topobjdir") is not None:
1280 self.env["MOZ_DEVELOPER_OBJ_DIR"] = self.mozInfo["topobjdir"]
1282 # Disable the content process sandbox for the xpcshell tests. They
1283 # currently attempt to do things like bind() sockets, which is not
1284 # compatible with the sandbox.
1285 self.env["MOZ_DISABLE_CONTENT_SANDBOX"] = "1"
1286 if os.getenv("MOZ_FETCHES_DIR", None):
1287 self.env["MOZ_FETCHES_DIR"] = os.getenv("MOZ_FETCHES_DIR", None)
1289 if self.mozInfo.get("socketprocess_networking"):
1290 self.env["MOZ_FORCE_USE_SOCKET_PROCESS"] = "1"
1291 else:
1292 self.env["MOZ_DISABLE_SOCKET_PROCESS"] = "1"
1294 def buildEnvironment(self):
1296 Create and returns a dictionary of self.env to include all the appropriate env
1297 variables and values. On a remote system, we overload this to set different
1298 values and are missing things like os.environ and PATH.
1300 self.env = dict(os.environ)
1301 self.buildCoreEnvironment()
1302 if sys.platform == "win32":
1303 self.env["PATH"] = self.env["PATH"] + ";" + self.xrePath
1304 elif sys.platform in ("os2emx", "os2knix"):
1305 os.environ["BEGINLIBPATH"] = self.xrePath + ";" + self.env["BEGINLIBPATH"]
1306 os.environ["LIBPATHSTRICT"] = "T"
1307 elif sys.platform == "osx" or sys.platform == "darwin":
1308 self.env["DYLD_LIBRARY_PATH"] = os.path.join(
1309 os.path.dirname(self.xrePath), "MacOS"
1311 else: # unix or linux?
1312 if "LD_LIBRARY_PATH" not in self.env or self.env["LD_LIBRARY_PATH"] is None:
1313 self.env["LD_LIBRARY_PATH"] = self.xrePath
1314 else:
1315 self.env["LD_LIBRARY_PATH"] = ":".join(
1316 [self.xrePath, self.env["LD_LIBRARY_PATH"]]
1319 usingASan = "asan" in self.mozInfo and self.mozInfo["asan"]
1320 usingTSan = "tsan" in self.mozInfo and self.mozInfo["tsan"]
1321 if usingASan or usingTSan:
1322 # symbolizer support
1323 if "ASAN_SYMBOLIZER_PATH" in self.env and os.path.isfile(
1324 self.env["ASAN_SYMBOLIZER_PATH"]
1326 llvmsym = self.env["ASAN_SYMBOLIZER_PATH"]
1327 else:
1328 llvmsym = os.path.join(
1329 self.xrePath, "llvm-symbolizer" + self.mozInfo["bin_suffix"]
1331 if os.path.isfile(llvmsym):
1332 if usingASan:
1333 self.env["ASAN_SYMBOLIZER_PATH"] = llvmsym
1334 else:
1335 oldTSanOptions = self.env.get("TSAN_OPTIONS", "")
1336 self.env["TSAN_OPTIONS"] = "external_symbolizer_path={} {}".format(
1337 llvmsym, oldTSanOptions
1339 self.log.info("runxpcshelltests.py | using symbolizer at %s" % llvmsym)
1340 else:
1341 self.log.error(
1342 "TEST-UNEXPECTED-FAIL | runxpcshelltests.py | "
1343 "Failed to find symbolizer at %s" % llvmsym
1346 return self.env
1348 def getPipes(self):
1350 Determine the value of the stdout and stderr for the test.
1351 Return value is a list (pStdout, pStderr).
1353 if self.interactive:
1354 pStdout = None
1355 pStderr = None
1356 else:
1357 if self.debuggerInfo and self.debuggerInfo.interactive:
1358 pStdout = None
1359 pStderr = None
1360 else:
1361 if sys.platform == "os2emx":
1362 pStdout = None
1363 else:
1364 pStdout = PIPE
1365 pStderr = STDOUT
1366 return pStdout, pStderr
1368 def verifyDirPath(self, dirname):
1370 Simple wrapper to get the absolute path for a given directory name.
1371 On a remote system, we need to overload this to work on the remote filesystem.
1373 return os.path.abspath(dirname)
1375 def trySetupNode(self):
1377 Run node for HTTP/2 tests, if available, and updates mozinfo as appropriate.
1379 if os.getenv("MOZ_ASSUME_NODE_RUNNING", None):
1380 self.log.info("Assuming required node servers are already running")
1381 if not os.getenv("MOZHTTP2_PORT", None):
1382 self.log.warning(
1383 "MOZHTTP2_PORT environment variable not set. "
1384 "Tests requiring http/2 will fail."
1386 return
1388 # We try to find the node executable in the path given to us by the user in
1389 # the MOZ_NODE_PATH environment variable
1390 nodeBin = os.getenv("MOZ_NODE_PATH", None)
1391 if not nodeBin and build:
1392 nodeBin = build.substs.get("NODEJS")
1393 if not nodeBin:
1394 self.log.warning(
1395 "MOZ_NODE_PATH environment variable not set. "
1396 "Tests requiring http/2 will fail."
1398 return
1400 if not os.path.exists(nodeBin) or not os.path.isfile(nodeBin):
1401 error = "node not found at MOZ_NODE_PATH %s" % (nodeBin)
1402 self.log.error(error)
1403 raise IOError(error)
1405 self.log.info("Found node at %s" % (nodeBin,))
1407 def read_streams(name, proc, pipe):
1408 output = "stdout" if pipe == proc.stdout else "stderr"
1409 for line in iter(pipe.readline, ""):
1410 self.log.info("node %s [%s] %s" % (name, output, line))
1412 def startServer(name, serverJs):
1413 if not os.path.exists(serverJs):
1414 error = "%s not found at %s" % (name, serverJs)
1415 self.log.error(error)
1416 raise IOError(error)
1418 # OK, we found our server, let's try to get it running
1419 self.log.info("Found %s at %s" % (name, serverJs))
1420 try:
1421 # We pipe stdin to node because the server will exit when its
1422 # stdin reaches EOF
1423 with popenCleanupHack():
1424 process = Popen(
1425 [nodeBin, serverJs],
1426 stdin=PIPE,
1427 stdout=PIPE,
1428 stderr=PIPE,
1429 env=self.env,
1430 cwd=os.getcwd(),
1431 universal_newlines=True,
1432 start_new_session=True,
1434 self.nodeProc[name] = process
1436 # Check to make sure the server starts properly by waiting for it to
1437 # tell us it's started
1438 msg = process.stdout.readline()
1439 if "server listening" in msg:
1440 searchObj = re.search(
1441 r"HTTP2 server listening on ports ([0-9]+),([0-9]+)", msg, 0
1443 if searchObj:
1444 self.env["MOZHTTP2_PORT"] = searchObj.group(1)
1445 self.env["MOZNODE_EXEC_PORT"] = searchObj.group(2)
1446 t1 = Thread(
1447 target=read_streams,
1448 args=(name, process, process.stdout),
1449 daemon=True,
1451 t1.start()
1452 t2 = Thread(
1453 target=read_streams,
1454 args=(name, process, process.stderr),
1455 daemon=True,
1457 t2.start()
1458 except OSError as e:
1459 # This occurs if the subprocess couldn't be started
1460 self.log.error("Could not run %s server: %s" % (name, str(e)))
1461 raise
1463 myDir = os.path.split(os.path.abspath(__file__))[0]
1464 startServer("moz-http2", os.path.join(myDir, "moz-http2", "moz-http2.js"))
1466 def shutdownNode(self):
1468 Shut down our node process, if it exists
1470 for name, proc in six.iteritems(self.nodeProc):
1471 self.log.info("Node %s server shutting down ..." % name)
1472 if proc.poll() is not None:
1473 self.log.info("Node server %s already dead %s" % (name, proc.poll()))
1474 elif sys.platform != "win32":
1475 # Kill process and all its spawned children.
1476 os.killpg(proc.pid, signal.SIGTERM)
1477 else:
1478 proc.terminate()
1480 self.nodeProc = {}
1482 def startHttp3Server(self):
1484 Start a Http3 test server.
1486 binSuffix = ""
1487 if sys.platform == "win32":
1488 binSuffix = ".exe"
1489 http3ServerPath = self.http3ServerPath
1490 serverEnv = self.env.copy()
1491 if not http3ServerPath:
1492 if self.mozInfo["buildapp"] == "mobile/android":
1493 # For android, use binary from host utilities.
1494 http3ServerPath = os.path.join(self.xrePath, "http3server" + binSuffix)
1495 serverEnv["LD_LIBRARY_PATH"] = self.xrePath
1496 elif build:
1497 http3ServerPath = os.path.join(
1498 build.topobjdir, "dist", "bin", "http3server" + binSuffix
1500 else:
1501 http3ServerPath = os.path.join(
1502 SCRIPT_DIR, "http3server", "http3server" + binSuffix
1504 dbPath = os.path.join(SCRIPT_DIR, "http3server", "http3serverDB")
1505 if build:
1506 dbPath = os.path.join(build.topsrcdir, "netwerk", "test", "http3serverDB")
1507 options = {}
1508 options["http3ServerPath"] = http3ServerPath
1509 options["profilePath"] = dbPath
1510 options["isMochitest"] = False
1511 options["isWin"] = sys.platform == "win32"
1512 serverLog = self.env.get("MOZHTTP3_SERVER_LOG")
1513 if serverLog is not None:
1514 serverEnv["RUST_LOG"] = serverLog
1515 self.http3Server = Http3Server(options, serverEnv, self.log)
1516 self.http3Server.start()
1517 for key, value in self.http3Server.ports().items():
1518 self.env[key] = value
1519 self.env["MOZHTTP3_ECH"] = self.http3Server.echConfig()
1520 self.env["MOZ_HTTP3_SERVER_PATH"] = http3ServerPath
1521 self.env["MOZ_HTTP3_CERT_DB_PATH"] = dbPath
1523 def shutdownHttp3Server(self):
1524 if self.http3Server is None:
1525 return
1526 self.http3Server.stop()
1527 self.http3Server = None
1529 def buildXpcsRunArgs(self):
1531 Add arguments to run the test or make it interactive.
1533 if self.interactive:
1534 self.xpcsRunArgs = [
1535 "-e",
1536 'print("To start the test, type |_execute_test();|.");',
1537 "-i",
1539 else:
1540 self.xpcsRunArgs = ["-e", "_execute_test(); quit(0);"]
1542 def addTestResults(self, test):
1543 self.passCount += test.passCount
1544 self.failCount += test.failCount
1545 self.todoCount += test.todoCount
1547 def updateMozinfo(self, prefs, options):
1548 # Handle filenames in mozInfo
1549 if not isinstance(self.mozInfo, dict):
1550 mozInfoFile = self.mozInfo
1551 if not os.path.isfile(mozInfoFile):
1552 self.log.error(
1553 "Error: couldn't find mozinfo.json at '%s'. Perhaps you "
1554 "need to use --build-info-json?" % mozInfoFile
1556 return False
1557 self.mozInfo = json.load(open(mozInfoFile))
1559 # mozinfo.info is used as kwargs. Some builds are done with
1560 # an older Python that can't handle Unicode keys in kwargs.
1561 # All of the keys in question should be ASCII.
1562 fixedInfo = {}
1563 for k, v in self.mozInfo.items():
1564 if isinstance(k, bytes):
1565 k = k.decode("utf-8")
1566 fixedInfo[k] = v
1567 self.mozInfo = fixedInfo
1569 self.mozInfo["fission"] = prefs.get("fission.autostart", True)
1570 self.mozInfo["sessionHistoryInParent"] = self.mozInfo[
1571 "fission"
1572 ] or not prefs.get("fission.disableSessionHistoryInParent", False)
1574 self.mozInfo["verify"] = options.get("verify", False)
1576 self.mozInfo["socketprocess_networking"] = prefs.get(
1577 "network.http.network_access_on_socket_process.enabled", False
1580 self.mozInfo["condprof"] = options.get("conditionedProfile", False)
1581 self.mozInfo["msix"] = options.get("variant", "") == "msix"
1583 self.mozInfo["is_ubuntu"] = "Ubuntu" in platform.version()
1585 # TODO: remove this when crashreporter is fixed on mac via bug 1910777
1586 if self.mozInfo["os"] == "mac":
1587 (release, versioninfo, machine) = platform.mac_ver()
1588 versionNums = release.split(".")[:2]
1589 os_version = "%s.%s" % (versionNums[0], versionNums[1].ljust(2, "0"))
1590 if os_version.split(".")[0] == "14":
1591 self.mozInfo["crashreporter"] = False
1593 # we default to false for e10s on xpcshell
1594 self.mozInfo["e10s"] = self.mozInfo.get("e10s", False)
1596 mozinfo.update(self.mozInfo)
1597 return True
1599 @property
1600 def conditioned_profile_copy(self):
1601 """Returns a copy of the original conditioned profile that was created."""
1602 condprof_copy = os.path.join(tempfile.mkdtemp(), "profile")
1603 shutil.copytree(
1604 self.conditioned_profile_dir,
1605 condprof_copy,
1606 ignore=shutil.ignore_patterns("lock"),
1608 self.log.info("Created a conditioned-profile copy: %s" % condprof_copy)
1609 return condprof_copy
1611 def downloadConditionedProfile(self, profile_scenario, app):
1612 from condprof.client import get_profile
1613 from condprof.util import get_current_platform, get_version
1615 if self.conditioned_profile_dir:
1616 # We already have a directory, so provide a copy that
1617 # will get deleted after it's done with
1618 return self.conditioned_profile_dir
1620 # create a temp file to help ensure uniqueness
1621 temp_download_dir = tempfile.mkdtemp()
1622 self.log.info(
1623 "Making temp_download_dir from inside get_conditioned_profile {}".format(
1624 temp_download_dir
1627 # call condprof's client API to yield our platform-specific
1628 # conditioned-profile binary
1629 platform = get_current_platform()
1630 version = None
1631 if isinstance(app, str):
1632 version = get_version(app)
1634 if not profile_scenario:
1635 profile_scenario = "settled"
1636 try:
1637 cond_prof_target_dir = get_profile(
1638 temp_download_dir,
1639 platform,
1640 profile_scenario,
1641 repo="mozilla-central",
1642 version=version,
1643 retries=2,
1645 except Exception:
1646 if version is None:
1647 # any other error is a showstopper
1648 self.log.critical("Could not get the conditioned profile")
1649 traceback.print_exc()
1650 raise
1651 version = None
1652 try:
1653 self.log.info("Retrying a profile with no version specified")
1654 cond_prof_target_dir = get_profile(
1655 temp_download_dir,
1656 platform,
1657 profile_scenario,
1658 repo="mozilla-central",
1659 version=version,
1661 except Exception:
1662 self.log.critical("Could not get the conditioned profile")
1663 traceback.print_exc()
1664 raise
1666 # now get the full directory path to our fetched conditioned profile
1667 self.conditioned_profile_dir = os.path.join(
1668 temp_download_dir, cond_prof_target_dir
1670 if not os.path.exists(cond_prof_target_dir):
1671 self.log.critical(
1672 "Can't find target_dir {}, from get_profile()"
1673 "temp_download_dir {}, platform {}, scenario {}".format(
1674 cond_prof_target_dir, temp_download_dir, platform, profile_scenario
1677 raise OSError
1679 self.log.info(
1680 "Original self.conditioned_profile_dir is now set: {}".format(
1681 self.conditioned_profile_dir
1684 return self.conditioned_profile_copy
1686 def runSelfTest(self):
1687 import unittest
1689 import selftest
1691 this = self
1693 class XPCShellTestsTests(selftest.XPCShellTestsTests):
1694 def __init__(self, name):
1695 unittest.TestCase.__init__(self, name)
1696 self.testing_modules = this.testingModulesDir
1697 self.xpcshellBin = this.xpcshell
1698 self.app_binary = this.app_binary
1699 self.utility_path = this.utility_path
1700 self.symbols_path = this.symbolsPath
1702 old_info = dict(mozinfo.info)
1703 try:
1704 suite = unittest.TestLoader().loadTestsFromTestCase(XPCShellTestsTests)
1705 return unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful()
1706 finally:
1707 # The self tests modify mozinfo, so we need to reset it.
1708 mozinfo.info.clear()
1709 mozinfo.update(old_info)
1711 def runTests(self, options, testClass=XPCShellTestThread, mobileArgs=None):
1713 Run xpcshell tests.
1715 global gotSIGINT
1717 # Number of times to repeat test(s) in --verify mode
1718 VERIFY_REPEAT = 10
1720 if isinstance(options, Namespace):
1721 options = vars(options)
1723 # Try to guess modules directory.
1724 # This somewhat grotesque hack allows the buildbot machines to find the
1725 # modules directory without having to configure the buildbot hosts. This
1726 # code path should never be executed in local runs because the build system
1727 # should always set this argument.
1728 if not options.get("testingModulesDir"):
1729 possible = os.path.join(here, os.path.pardir, "modules")
1731 if os.path.isdir(possible):
1732 testingModulesDir = possible
1734 if options.get("rerun_failures"):
1735 if os.path.exists(options.get("failure_manifest")):
1736 rerun_manifest = os.path.join(
1737 os.path.dirname(options["failure_manifest"]), "rerun.toml"
1739 shutil.copyfile(options["failure_manifest"], rerun_manifest)
1740 os.remove(options["failure_manifest"])
1741 else:
1742 self.log.error("No failures were found to re-run.")
1743 sys.exit(1)
1745 if options.get("testingModulesDir"):
1746 # The resource loader expects native paths. Depending on how we were
1747 # invoked, a UNIX style path may sneak in on Windows. We try to
1748 # normalize that.
1749 testingModulesDir = os.path.normpath(options["testingModulesDir"])
1751 if not os.path.isabs(testingModulesDir):
1752 testingModulesDir = os.path.abspath(testingModulesDir)
1754 if not testingModulesDir.endswith(os.path.sep):
1755 testingModulesDir += os.path.sep
1757 self.debuggerInfo = None
1759 if options.get("debugger"):
1760 self.debuggerInfo = mozdebug.get_debugger_info(
1761 options.get("debugger"),
1762 options.get("debuggerArgs"),
1763 options.get("debuggerInteractive"),
1766 self.jsDebuggerInfo = None
1767 if options.get("jsDebugger"):
1768 # A namedtuple let's us keep .port instead of ['port']
1769 JSDebuggerInfo = namedtuple("JSDebuggerInfo", ["port"])
1770 self.jsDebuggerInfo = JSDebuggerInfo(port=options["jsDebuggerPort"])
1772 self.app_binary = options.get("app_binary")
1773 self.xpcshell = options.get("xpcshell")
1774 self.http3ServerPath = options.get("http3server")
1775 self.xrePath = options.get("xrePath")
1776 self.utility_path = options.get("utility_path")
1777 self.appPath = options.get("appPath")
1778 self.symbolsPath = options.get("symbolsPath")
1779 self.tempDir = os.path.normpath(options.get("tempDir") or tempfile.gettempdir())
1780 self.manifest = options.get("manifest")
1781 self.dump_tests = options.get("dump_tests")
1782 self.interactive = options.get("interactive")
1783 self.verbose = options.get("verbose")
1784 self.verboseIfFails = options.get("verboseIfFails")
1785 self.keepGoing = options.get("keepGoing")
1786 self.logfiles = options.get("logfiles")
1787 self.totalChunks = options.get("totalChunks", 1)
1788 self.thisChunk = options.get("thisChunk")
1789 self.profileName = options.get("profileName") or "xpcshell"
1790 self.mozInfo = options.get("mozInfo")
1791 self.testingModulesDir = testingModulesDir
1792 self.sequential = options.get("sequential")
1793 self.failure_manifest = options.get("failure_manifest")
1794 self.threadCount = options.get("threadCount") or NUM_THREADS
1795 self.jscovdir = options.get("jscovdir")
1796 self.headless = options.get("headless")
1797 self.runFailures = options.get("runFailures")
1798 self.timeoutAsPass = options.get("timeoutAsPass")
1799 self.crashAsPass = options.get("crashAsPass")
1800 self.conditionedProfile = options.get("conditionedProfile")
1801 self.repeat = options.get("repeat", 0)
1802 self.variant = options.get("variant", "")
1804 if self.variant == "msix":
1805 self.appPath = options.get("msixAppPath")
1806 self.xrePath = options.get("msixXrePath")
1807 self.app_binary = options.get("msix_app_binary")
1808 self.threadCount = 2
1809 self.xpcshell = None
1811 self.testCount = 0
1812 self.passCount = 0
1813 self.failCount = 0
1814 self.todoCount = 0
1816 if self.conditionedProfile:
1817 self.conditioned_profile_dir = self.downloadConditionedProfile(
1818 "full", self.appPath
1820 options["self_test"] = False
1822 self.setAbsPath()
1824 eprefs = options.get("extraPrefs") or []
1825 # enable fission by default
1826 if options.get("disableFission"):
1827 eprefs.append("fission.autostart=false")
1828 else:
1829 # should be by default, just in case
1830 eprefs.append("fission.autostart=true")
1832 prefs = self.buildPrefsFile(eprefs)
1833 self.buildXpcsRunArgs()
1835 self.event = Event()
1837 if not self.updateMozinfo(prefs, options):
1838 return False
1840 self.log.info(
1841 "These variables are available in the mozinfo environment and "
1842 "can be used to skip tests conditionally:"
1844 for info in sorted(self.mozInfo.items(), key=lambda item: item[0]):
1845 self.log.info(" {key}: {value}".format(key=info[0], value=info[1]))
1847 if options.get("self_test"):
1848 if not self.runSelfTest():
1849 return False
1851 if (
1852 ("tsan" in self.mozInfo and self.mozInfo["tsan"])
1853 or ("asan" in self.mozInfo and self.mozInfo["asan"])
1854 ) and not options.get("threadCount"):
1855 # TSan/ASan require significantly more memory, so reduce the amount of parallel
1856 # tests we run to avoid OOMs and timeouts. We always keep a minimum of 2 for
1857 # non-sequential execution.
1858 # pylint --py3k W1619
1859 self.threadCount = max(self.threadCount / 2, 2)
1861 self.stack_fixer_function = None
1862 if self.utility_path and os.path.exists(self.utility_path):
1863 self.stack_fixer_function = get_stack_fixer_function(
1864 self.utility_path, self.symbolsPath
1867 # buildEnvironment() needs mozInfo, so we call it after mozInfo is initialized.
1868 self.buildEnvironment()
1869 extraEnv = parse_key_value(options.get("extraEnv") or [], context="--setenv")
1870 for k, v in extraEnv:
1871 if k in self.env:
1872 self.log.info(
1873 "Using environment variable %s instead of %s." % (v, self.env[k])
1875 self.env[k] = v
1877 # The appDirKey is a optional entry in either the default or individual test
1878 # sections that defines a relative application directory for test runs. If
1879 # defined we pass 'grePath/$appDirKey' for the -a parameter of the xpcshell
1880 # test harness.
1881 appDirKey = None
1882 if "appname" in self.mozInfo:
1883 appDirKey = self.mozInfo["appname"] + "-appdir"
1885 # We have to do this before we run tests that depend on having the node
1886 # http/2 server.
1887 self.trySetupNode()
1889 self.startHttp3Server()
1891 pStdout, pStderr = self.getPipes()
1893 self.buildTestList(
1894 options.get("test_tags"), options.get("testPaths"), options.get("verify")
1896 if self.singleFile:
1897 self.sequential = True
1899 if options.get("shuffle"):
1900 random.shuffle(self.alltests)
1902 self.cleanup_dir_list = []
1904 kwargs = {
1905 "appPath": self.appPath,
1906 "xrePath": self.xrePath,
1907 "utility_path": self.utility_path,
1908 "testingModulesDir": self.testingModulesDir,
1909 "debuggerInfo": self.debuggerInfo,
1910 "jsDebuggerInfo": self.jsDebuggerInfo,
1911 "headJSPath": self.headJSPath,
1912 "tempDir": self.tempDir,
1913 "testharnessdir": self.testharnessdir,
1914 "profileName": self.profileName,
1915 "singleFile": self.singleFile,
1916 "env": self.env, # making a copy of this in the testthreads
1917 "symbolsPath": self.symbolsPath,
1918 "logfiles": self.logfiles,
1919 "app_binary": self.app_binary,
1920 "xpcshell": self.xpcshell,
1921 "xpcsRunArgs": self.xpcsRunArgs,
1922 "failureManifest": self.failure_manifest,
1923 "jscovdir": self.jscovdir,
1924 "harness_timeout": self.harness_timeout,
1925 "stack_fixer_function": self.stack_fixer_function,
1926 "event": self.event,
1927 "cleanup_dir_list": self.cleanup_dir_list,
1928 "pStdout": pStdout,
1929 "pStderr": pStderr,
1930 "keep_going": self.keepGoing,
1931 "log": self.log,
1932 "interactive": self.interactive,
1933 "app_dir_key": appDirKey,
1934 "rootPrefsFile": self.prefsFile,
1935 "extraPrefs": options.get("extraPrefs") or [],
1936 "verboseIfFails": self.verboseIfFails,
1937 "headless": self.headless,
1938 "runFailures": self.runFailures,
1939 "timeoutAsPass": self.timeoutAsPass,
1940 "crashAsPass": self.crashAsPass,
1941 "conditionedProfileDir": self.conditioned_profile_dir,
1942 "repeat": self.repeat,
1945 if self.sequential:
1946 # Allow user to kill hung xpcshell subprocess with SIGINT
1947 # when we are only running tests sequentially.
1948 signal.signal(signal.SIGINT, markGotSIGINT)
1950 if self.debuggerInfo:
1951 # Force a sequential run
1952 self.sequential = True
1954 # If we have an interactive debugger, disable SIGINT entirely.
1955 if self.debuggerInfo.interactive:
1956 signal.signal(signal.SIGINT, lambda signum, frame: None)
1958 if "lldb" in self.debuggerInfo.path:
1959 # Ask people to start debugging using 'process launch', see bug 952211.
1960 self.log.info(
1961 "It appears that you're using LLDB to debug this test. "
1962 + "Please use the 'process launch' command instead of "
1963 "the 'run' command to start xpcshell."
1966 if self.jsDebuggerInfo:
1967 # The js debugger magic needs more work to do the right thing
1968 # if debugging multiple files.
1969 if len(self.alltests) != 1:
1970 self.log.error(
1971 "Error: --jsdebugger can only be used with a single test!"
1973 return False
1975 # The test itself needs to know whether it is a tsan build, since
1976 # that has an effect on interpretation of the process return value.
1977 usingTSan = "tsan" in self.mozInfo and self.mozInfo["tsan"]
1979 usingCrashReporter = (
1980 "crashreporter" in self.mozInfo and self.mozInfo["crashreporter"]
1983 # create a queue of all tests that will run
1984 tests_queue = deque()
1985 # also a list for the tests that need to be run sequentially
1986 sequential_tests = []
1987 status = None
1989 if options.get("repeat", 0) > 0:
1990 self.sequential = True
1992 if not options.get("verify"):
1993 for test_object in self.alltests:
1994 # Test identifiers are provided for the convenience of logging. These
1995 # start as path names but are rewritten in case tests from the same path
1996 # are re-run.
1998 path = test_object["path"]
2000 if self.singleFile and not path.endswith(self.singleFile):
2001 continue
2003 # if we have --repeat, duplicate the tests as needed
2004 for i in range(0, options.get("repeat", 0) + 1):
2005 self.testCount += 1
2007 test = testClass(
2008 test_object,
2009 verbose=self.verbose or test_object.get("verbose") == "true",
2010 usingTSan=usingTSan,
2011 usingCrashReporter=usingCrashReporter,
2012 mobileArgs=mobileArgs,
2013 **kwargs,
2015 if "run-sequentially" in test_object or self.sequential:
2016 sequential_tests.append(test)
2017 else:
2018 tests_queue.append(test)
2020 status = self.runTestList(
2021 tests_queue, sequential_tests, testClass, mobileArgs, **kwargs
2023 else:
2025 # Test verification: Run each test many times, in various configurations,
2026 # in hopes of finding intermittent failures.
2029 def step1():
2030 # Run tests sequentially. Parallel mode would also work, except that
2031 # the logging system gets confused when 2 or more tests with the same
2032 # name run at the same time.
2033 sequential_tests = []
2034 for i in range(VERIFY_REPEAT):
2035 self.testCount += 1
2036 test = testClass(
2037 test_object, retry=False, mobileArgs=mobileArgs, **kwargs
2039 sequential_tests.append(test)
2040 status = self.runTestList(
2041 tests_queue, sequential_tests, testClass, mobileArgs, **kwargs
2043 return status
2045 def step2():
2046 # Run tests sequentially, with MOZ_CHAOSMODE enabled.
2047 sequential_tests = []
2048 self.env["MOZ_CHAOSMODE"] = "0xfb"
2050 # for android, adjust flags to avoid slow down
2051 if self.env.get("MOZ_ANDROID_DATA_DIR", ""):
2052 self.env["MOZ_CHAOSMODE"] = "0x3b"
2054 # chaosmode runs really slow, allow tests extra time to pass
2055 kwargs["harness_timeout"] = self.harness_timeout * 2
2056 for i in range(VERIFY_REPEAT):
2057 self.testCount += 1
2058 test = testClass(
2059 test_object, retry=False, mobileArgs=mobileArgs, **kwargs
2061 sequential_tests.append(test)
2062 status = self.runTestList(
2063 tests_queue, sequential_tests, testClass, mobileArgs, **kwargs
2065 kwargs["harness_timeout"] = self.harness_timeout
2066 return status
2068 steps = [
2069 ("1. Run each test %d times, sequentially." % VERIFY_REPEAT, step1),
2071 "2. Run each test %d times, sequentially, in chaos mode."
2072 % VERIFY_REPEAT,
2073 step2,
2076 startTime = datetime.now()
2077 maxTime = timedelta(seconds=options["verifyMaxTime"])
2078 for test_object in self.alltests:
2079 stepResults = {}
2080 for descr, step in steps:
2081 stepResults[descr] = "not run / incomplete"
2082 finalResult = "PASSED"
2083 for descr, step in steps:
2084 if (datetime.now() - startTime) > maxTime:
2085 self.log.info(
2086 "::: Test verification is taking too long: Giving up!"
2088 self.log.info(
2089 "::: So far, all checks passed, but not "
2090 "all checks were run."
2092 break
2093 self.log.info(":::")
2094 self.log.info('::: Running test verification step "%s"...' % descr)
2095 self.log.info(":::")
2096 status = step()
2097 if status is not True:
2098 stepResults[descr] = "FAIL"
2099 finalResult = "FAILED!"
2100 break
2101 stepResults[descr] = "Pass"
2102 self.log.info(":::")
2103 self.log.info(
2104 "::: Test verification summary for: %s" % test_object["path"]
2106 self.log.info(":::")
2107 for descr in sorted(stepResults.keys()):
2108 self.log.info("::: %s : %s" % (descr, stepResults[descr]))
2109 self.log.info(":::")
2110 self.log.info("::: Test verification %s" % finalResult)
2111 self.log.info(":::")
2113 self.shutdownNode()
2114 self.shutdownHttp3Server()
2116 return status
2118 def start_test(self, test):
2119 test.start()
2121 def test_ended(self, test):
2122 pass
2124 def runTestList(
2125 self, tests_queue, sequential_tests, testClass, mobileArgs, **kwargs
2127 if self.sequential:
2128 self.log.info("Running tests sequentially.")
2129 else:
2130 self.log.info("Using at most %d threads." % self.threadCount)
2132 # keep a set of threadCount running tests and start running the
2133 # tests in the queue at most threadCount at a time
2134 running_tests = set()
2135 keep_going = True
2136 infra_abort = False
2137 exceptions = []
2138 tracebacks = []
2139 self.try_again_list = []
2141 tests_by_manifest = defaultdict(list)
2142 for test in self.alltests:
2143 group = get_full_group_name(test)
2144 tests_by_manifest[group].append(test["id"])
2146 self.log.suite_start(tests_by_manifest, name="xpcshell")
2148 while tests_queue or running_tests:
2149 # if we're not supposed to continue and all of the running tests
2150 # are done, stop
2151 if not keep_going and not running_tests:
2152 break
2154 # if there's room to run more tests, start running them
2155 while (
2156 keep_going and tests_queue and (len(running_tests) < self.threadCount)
2158 test = tests_queue.popleft()
2159 running_tests.add(test)
2160 self.start_test(test)
2162 # queue is full (for now) or no more new tests,
2163 # process the finished tests so far
2165 # wait for at least one of the tests to finish
2166 self.event.wait(1)
2167 self.event.clear()
2169 # find what tests are done (might be more than 1)
2170 done_tests = set()
2171 for test in running_tests:
2172 if test.done:
2173 self.test_ended(test)
2174 done_tests.add(test)
2175 test.join(
2177 ) # join with timeout so we don't hang on blocked threads
2178 # if the test had trouble, we will try running it again
2179 # at the end of the run
2180 if test.retry or test.is_alive():
2181 # if the join call timed out, test.is_alive => True
2182 self.try_again_list.append(test.test_object)
2183 continue
2184 # did the test encounter any exception?
2185 if test.exception:
2186 exceptions.append(test.exception)
2187 tracebacks.append(test.traceback)
2188 # we won't add any more tests, will just wait for
2189 # the currently running ones to finish
2190 keep_going = False
2191 infra_abort = infra_abort and test.infra
2192 keep_going = keep_going and test.keep_going
2193 self.addTestResults(test)
2195 # make room for new tests to run
2196 running_tests.difference_update(done_tests)
2198 if infra_abort:
2199 return TBPL_RETRY # terminate early
2201 if keep_going:
2202 # run the other tests sequentially
2203 for test in sequential_tests:
2204 if not keep_going:
2205 self.log.error(
2206 "TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so "
2207 "stopped run. (Use --keep-going to keep running tests "
2208 "after killing one with SIGINT)"
2210 break
2211 self.start_test(test)
2212 test.join()
2213 self.test_ended(test)
2214 if (test.failCount > 0 or test.passCount <= 0) and os.environ.get(
2215 "MOZ_AUTOMATION", 0
2216 ) != 0:
2217 self.try_again_list.append(test.test_object)
2218 continue
2219 self.addTestResults(test)
2220 # did the test encounter any exception?
2221 if test.exception:
2222 exceptions.append(test.exception)
2223 tracebacks.append(test.traceback)
2224 break
2225 keep_going = test.keep_going
2227 # retry tests that failed when run in parallel
2228 if self.try_again_list:
2229 self.log.info("Retrying tests that failed when run in parallel.")
2230 for test_object in self.try_again_list:
2231 test = testClass(
2232 test_object,
2233 retry=False,
2234 verbose=self.verbose,
2235 mobileArgs=mobileArgs,
2236 **kwargs,
2238 self.start_test(test)
2239 test.join()
2240 self.test_ended(test)
2241 self.addTestResults(test)
2242 # did the test encounter any exception?
2243 if test.exception:
2244 exceptions.append(test.exception)
2245 tracebacks.append(test.traceback)
2246 break
2247 keep_going = test.keep_going
2249 # restore default SIGINT behaviour
2250 signal.signal(signal.SIGINT, signal.SIG_DFL)
2252 # Clean up any slacker directories that might be lying around
2253 # Some might fail because of windows taking too long to unlock them.
2254 # We don't do anything if this fails because the test machines will have
2255 # their $TEMP dirs cleaned up on reboot anyway.
2256 for directory in self.cleanup_dir_list:
2257 try:
2258 shutil.rmtree(directory)
2259 except Exception:
2260 self.log.info("%s could not be cleaned up." % directory)
2262 if exceptions:
2263 self.log.info("Following exceptions were raised:")
2264 for t in tracebacks:
2265 self.log.error(t)
2266 raise exceptions[0]
2268 if self.testCount == 0 and os.environ.get("MOZ_AUTOMATION") != "1":
2269 self.log.error("No tests run. Did you pass an invalid --test-path?")
2270 self.failCount = 1
2272 # doing this allows us to pass the mozharness parsers that
2273 # report an orange job for failCount>0
2274 if self.runFailures:
2275 passed = self.passCount
2276 self.passCount = self.failCount
2277 self.failCount = passed
2279 self.log.info("INFO | Result summary:")
2280 self.log.info("INFO | Passed: %d" % self.passCount)
2281 self.log.info("INFO | Failed: %d" % self.failCount)
2282 self.log.info("INFO | Todo: %d" % self.todoCount)
2283 self.log.info("INFO | Retried: %d" % len(self.try_again_list))
2285 if gotSIGINT and not keep_going:
2286 self.log.error(
2287 "TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so stopped run. "
2288 "(Use --keep-going to keep running tests after "
2289 "killing one with SIGINT)"
2291 return False
2293 self.log.suite_end()
2294 return self.runFailures or self.failCount == 0
2297 def main():
2298 parser = parser_desktop()
2299 options = parser.parse_args()
2301 log = commandline.setup_logging("XPCShell", options, {"tbpl": sys.stdout})
2303 if options.xpcshell is None and options.app_binary is None:
2304 log.error(
2305 "Must provide path to xpcshell using --xpcshell or Firefox using --app-binary"
2307 sys.exit(1)
2309 if options.xpcshell is not None and options.app_binary is not None:
2310 log.error(
2311 "Cannot provide --xpcshell and --app-binary - they are mutually exclusive options. Choose one."
2313 sys.exit(1)
2315 xpcsh = XPCShellTests(log)
2317 if options.interactive and not options.testPath:
2318 log.error("Error: You must specify a test filename in interactive mode!")
2319 sys.exit(1)
2321 result = xpcsh.runTests(options)
2322 if result == TBPL_RETRY:
2323 sys.exit(4)
2325 if not result:
2326 sys.exit(1)
2329 if __name__ == "__main__":
2330 main()