Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | #!/usr/bin/env python |
michael@0 | 2 | # |
michael@0 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 5 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
michael@0 | 6 | |
michael@0 | 7 | import copy |
michael@0 | 8 | import json |
michael@0 | 9 | import math |
michael@0 | 10 | import os |
michael@0 | 11 | import os.path |
michael@0 | 12 | import random |
michael@0 | 13 | import re |
michael@0 | 14 | import shutil |
michael@0 | 15 | import signal |
michael@0 | 16 | import socket |
michael@0 | 17 | import sys |
michael@0 | 18 | import time |
michael@0 | 19 | import traceback |
michael@0 | 20 | import xml.dom.minidom |
michael@0 | 21 | from collections import deque |
michael@0 | 22 | from distutils import dir_util |
michael@0 | 23 | from multiprocessing import cpu_count |
michael@0 | 24 | from optparse import OptionParser |
michael@0 | 25 | from subprocess import Popen, PIPE, STDOUT |
michael@0 | 26 | from tempfile import mkdtemp, gettempdir |
michael@0 | 27 | from threading import Timer, Thread, Event, RLock |
michael@0 | 28 | |
michael@0 | 29 | try: |
michael@0 | 30 | import psutil |
michael@0 | 31 | HAVE_PSUTIL = True |
michael@0 | 32 | except ImportError: |
michael@0 | 33 | HAVE_PSUTIL = False |
michael@0 | 34 | |
michael@0 | 35 | from automation import Automation, getGlobalLog, resetGlobalLog |
michael@0 | 36 | from automationutils import * |
michael@0 | 37 | |
michael@0 | 38 | # Printing buffered output in case of a failure or verbose mode will result |
michael@0 | 39 | # in buffered output interleaved with other threads' output. |
michael@0 | 40 | # To prevent his, each call to the logger as well as any blocks of output that |
michael@0 | 41 | # are intended to be continuous are protected by the same lock. |
michael@0 | 42 | LOG_MUTEX = RLock() |
michael@0 | 43 | |
michael@0 | 44 | HARNESS_TIMEOUT = 5 * 60 |
michael@0 | 45 | |
michael@0 | 46 | # benchmarking on tbpl revealed that this works best for now |
michael@0 | 47 | NUM_THREADS = int(cpu_count() * 4) |
michael@0 | 48 | |
michael@0 | 49 | FAILURE_ACTIONS = set(['test_unexpected_fail', |
michael@0 | 50 | 'test_unexpected_pass', |
michael@0 | 51 | 'javascript_error']) |
michael@0 | 52 | ACTION_STRINGS = { |
michael@0 | 53 | "test_unexpected_fail": "TEST-UNEXPECTED-FAIL", |
michael@0 | 54 | "test_known_fail": "TEST-KNOWN-FAIL", |
michael@0 | 55 | "test_unexpected_pass": "TEST-UNEXPECTED-PASS", |
michael@0 | 56 | "javascript_error": "TEST-UNEXPECTED-FAIL", |
michael@0 | 57 | "test_pass": "TEST-PASS", |
michael@0 | 58 | "test_info": "TEST-INFO" |
michael@0 | 59 | } |
michael@0 | 60 | |
michael@0 | 61 | # -------------------------------------------------------------- |
michael@0 | 62 | # TODO: this is a hack for mozbase without virtualenv, remove with bug 849900 |
michael@0 | 63 | # |
michael@0 | 64 | here = os.path.dirname(__file__) |
michael@0 | 65 | mozbase = os.path.realpath(os.path.join(os.path.dirname(here), 'mozbase')) |
michael@0 | 66 | |
michael@0 | 67 | if os.path.isdir(mozbase): |
michael@0 | 68 | for package in os.listdir(mozbase): |
michael@0 | 69 | sys.path.append(os.path.join(mozbase, package)) |
michael@0 | 70 | |
michael@0 | 71 | import manifestparser |
michael@0 | 72 | import mozcrash |
michael@0 | 73 | import mozinfo |
michael@0 | 74 | |
michael@0 | 75 | # -------------------------------------------------------------- |
michael@0 | 76 | |
michael@0 | 77 | # TODO: perhaps this should be in a more generally shared location? |
michael@0 | 78 | # This regex matches all of the C0 and C1 control characters |
michael@0 | 79 | # (U+0000 through U+001F; U+007F; U+0080 through U+009F), |
michael@0 | 80 | # except TAB (U+0009), CR (U+000D), LF (U+000A) and backslash (U+005C). |
michael@0 | 81 | # A raw string is deliberately not used. |
michael@0 | 82 | _cleanup_encoding_re = re.compile(u'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\\\\]') |
michael@0 | 83 | def _cleanup_encoding_repl(m): |
michael@0 | 84 | c = m.group(0) |
michael@0 | 85 | return '\\\\' if c == '\\' else '\\x{0:02X}'.format(ord(c)) |
michael@0 | 86 | def cleanup_encoding(s): |
michael@0 | 87 | """S is either a byte or unicode string. Either way it may |
michael@0 | 88 | contain control characters, unpaired surrogates, reserved code |
michael@0 | 89 | points, etc. If it is a byte string, it is assumed to be |
michael@0 | 90 | UTF-8, but it may not be *correct* UTF-8. Produce a byte |
michael@0 | 91 | string that can safely be dumped into a (generally UTF-8-coded) |
michael@0 | 92 | logfile.""" |
michael@0 | 93 | if not isinstance(s, unicode): |
michael@0 | 94 | s = s.decode('utf-8', 'replace') |
michael@0 | 95 | if s.endswith('\n'): |
michael@0 | 96 | # A new line is always added by head.js to delimit messages, |
michael@0 | 97 | # however consumers will want to supply their own. |
michael@0 | 98 | s = s[:-1] |
michael@0 | 99 | # Replace all C0 and C1 control characters with \xNN escapes. |
michael@0 | 100 | s = _cleanup_encoding_re.sub(_cleanup_encoding_repl, s) |
michael@0 | 101 | return s.encode('utf-8', 'backslashreplace') |
michael@0 | 102 | |
michael@0 | 103 | """ Control-C handling """ |
michael@0 | 104 | gotSIGINT = False |
michael@0 | 105 | def markGotSIGINT(signum, stackFrame): |
michael@0 | 106 | global gotSIGINT |
michael@0 | 107 | gotSIGINT = True |
michael@0 | 108 | |
michael@0 | 109 | class XPCShellTestThread(Thread): |
michael@0 | 110 | def __init__(self, test_object, event, cleanup_dir_list, retry=True, |
michael@0 | 111 | tests_root_dir=None, app_dir_key=None, interactive=False, |
michael@0 | 112 | verbose=False, pStdout=None, pStderr=None, keep_going=False, |
michael@0 | 113 | log=None, **kwargs): |
michael@0 | 114 | Thread.__init__(self) |
michael@0 | 115 | self.daemon = True |
michael@0 | 116 | |
michael@0 | 117 | self.test_object = test_object |
michael@0 | 118 | self.cleanup_dir_list = cleanup_dir_list |
michael@0 | 119 | self.retry = retry |
michael@0 | 120 | |
michael@0 | 121 | self.appPath = kwargs.get('appPath') |
michael@0 | 122 | self.xrePath = kwargs.get('xrePath') |
michael@0 | 123 | self.testingModulesDir = kwargs.get('testingModulesDir') |
michael@0 | 124 | self.debuggerInfo = kwargs.get('debuggerInfo') |
michael@0 | 125 | self.pluginsPath = kwargs.get('pluginsPath') |
michael@0 | 126 | self.httpdManifest = kwargs.get('httpdManifest') |
michael@0 | 127 | self.httpdJSPath = kwargs.get('httpdJSPath') |
michael@0 | 128 | self.headJSPath = kwargs.get('headJSPath') |
michael@0 | 129 | self.testharnessdir = kwargs.get('testharnessdir') |
michael@0 | 130 | self.profileName = kwargs.get('profileName') |
michael@0 | 131 | self.singleFile = kwargs.get('singleFile') |
michael@0 | 132 | self.env = copy.deepcopy(kwargs.get('env')) |
michael@0 | 133 | self.symbolsPath = kwargs.get('symbolsPath') |
michael@0 | 134 | self.logfiles = kwargs.get('logfiles') |
michael@0 | 135 | self.xpcshell = kwargs.get('xpcshell') |
michael@0 | 136 | self.xpcsRunArgs = kwargs.get('xpcsRunArgs') |
michael@0 | 137 | self.failureManifest = kwargs.get('failureManifest') |
michael@0 | 138 | self.on_message = kwargs.get('on_message') |
michael@0 | 139 | |
michael@0 | 140 | self.tests_root_dir = tests_root_dir |
michael@0 | 141 | self.app_dir_key = app_dir_key |
michael@0 | 142 | self.interactive = interactive |
michael@0 | 143 | self.verbose = verbose |
michael@0 | 144 | self.pStdout = pStdout |
michael@0 | 145 | self.pStderr = pStderr |
michael@0 | 146 | self.keep_going = keep_going |
michael@0 | 147 | self.log = log |
michael@0 | 148 | |
michael@0 | 149 | # only one of these will be set to 1. adding them to the totals in |
michael@0 | 150 | # the harness |
michael@0 | 151 | self.passCount = 0 |
michael@0 | 152 | self.todoCount = 0 |
michael@0 | 153 | self.failCount = 0 |
michael@0 | 154 | |
michael@0 | 155 | self.output_lines = [] |
michael@0 | 156 | self.has_failure_output = False |
michael@0 | 157 | self.saw_proc_start = False |
michael@0 | 158 | self.saw_proc_end = False |
michael@0 | 159 | |
michael@0 | 160 | # event from main thread to signal work done |
michael@0 | 161 | self.event = event |
michael@0 | 162 | self.done = False # explicitly set flag so we don't rely on thread.isAlive |
michael@0 | 163 | |
michael@0 | 164 | def run(self): |
michael@0 | 165 | try: |
michael@0 | 166 | self.run_test() |
michael@0 | 167 | except Exception as e: |
michael@0 | 168 | self.exception = e |
michael@0 | 169 | self.traceback = traceback.format_exc() |
michael@0 | 170 | else: |
michael@0 | 171 | self.exception = None |
michael@0 | 172 | self.traceback = None |
michael@0 | 173 | if self.retry: |
michael@0 | 174 | self.log.info("TEST-INFO | %s | Test failed or timed out, will retry." |
michael@0 | 175 | % self.test_object['name']) |
michael@0 | 176 | self.done = True |
michael@0 | 177 | self.event.set() |
michael@0 | 178 | |
michael@0 | 179 | def kill(self, proc): |
michael@0 | 180 | """ |
michael@0 | 181 | Simple wrapper to kill a process. |
michael@0 | 182 | On a remote system, this is overloaded to handle remote process communication. |
michael@0 | 183 | """ |
michael@0 | 184 | return proc.kill() |
michael@0 | 185 | |
michael@0 | 186 | def removeDir(self, dirname): |
michael@0 | 187 | """ |
michael@0 | 188 | Simple wrapper to remove (recursively) a given directory. |
michael@0 | 189 | On a remote system, we need to overload this to work on the remote filesystem. |
michael@0 | 190 | """ |
michael@0 | 191 | shutil.rmtree(dirname) |
michael@0 | 192 | |
michael@0 | 193 | def poll(self, proc): |
michael@0 | 194 | """ |
michael@0 | 195 | Simple wrapper to check if a process has terminated. |
michael@0 | 196 | On a remote system, this is overloaded to handle remote process communication. |
michael@0 | 197 | """ |
michael@0 | 198 | return proc.poll() |
michael@0 | 199 | |
michael@0 | 200 | def createLogFile(self, test_file, stdout): |
michael@0 | 201 | """ |
michael@0 | 202 | For a given test file and stdout buffer, create a log file. |
michael@0 | 203 | On a remote system we have to fix the test name since it can contain directories. |
michael@0 | 204 | """ |
michael@0 | 205 | with open(test_file + ".log", "w") as f: |
michael@0 | 206 | f.write(stdout) |
michael@0 | 207 | |
michael@0 | 208 | def getReturnCode(self, proc): |
michael@0 | 209 | """ |
michael@0 | 210 | Simple wrapper to get the return code for a given process. |
michael@0 | 211 | On a remote system we overload this to work with the remote process management. |
michael@0 | 212 | """ |
michael@0 | 213 | return proc.returncode |
michael@0 | 214 | |
michael@0 | 215 | def communicate(self, proc): |
michael@0 | 216 | """ |
michael@0 | 217 | Simple wrapper to communicate with a process. |
michael@0 | 218 | On a remote system, this is overloaded to handle remote process communication. |
michael@0 | 219 | """ |
michael@0 | 220 | # Processing of incremental output put here to |
michael@0 | 221 | # sidestep issues on remote platforms, where what we know |
michael@0 | 222 | # as proc is a file pulled off of a device. |
michael@0 | 223 | if proc.stdout: |
michael@0 | 224 | while True: |
michael@0 | 225 | line = proc.stdout.readline() |
michael@0 | 226 | if not line: |
michael@0 | 227 | break |
michael@0 | 228 | self.process_line(line) |
michael@0 | 229 | |
michael@0 | 230 | if self.saw_proc_start and not self.saw_proc_end: |
michael@0 | 231 | self.has_failure_output = True |
michael@0 | 232 | |
michael@0 | 233 | return proc.communicate() |
michael@0 | 234 | |
michael@0 | 235 | def launchProcess(self, cmd, stdout, stderr, env, cwd): |
michael@0 | 236 | """ |
michael@0 | 237 | Simple wrapper to launch a process. |
michael@0 | 238 | On a remote system, this is more complex and we need to overload this function. |
michael@0 | 239 | """ |
michael@0 | 240 | if HAVE_PSUTIL: |
michael@0 | 241 | popen_func = psutil.Popen |
michael@0 | 242 | else: |
michael@0 | 243 | popen_func = Popen |
michael@0 | 244 | proc = popen_func(cmd, stdout=stdout, stderr=stderr, |
michael@0 | 245 | env=env, cwd=cwd) |
michael@0 | 246 | return proc |
michael@0 | 247 | |
michael@0 | 248 | def checkForCrashes(self, |
michael@0 | 249 | dump_directory, |
michael@0 | 250 | symbols_path, |
michael@0 | 251 | test_name=None): |
michael@0 | 252 | """ |
michael@0 | 253 | Simple wrapper to check for crashes. |
michael@0 | 254 | On a remote system, this is more complex and we need to overload this function. |
michael@0 | 255 | """ |
michael@0 | 256 | return mozcrash.check_for_crashes(dump_directory, symbols_path, test_name=test_name) |
michael@0 | 257 | |
michael@0 | 258 | def logCommand(self, name, completeCmd, testdir): |
michael@0 | 259 | self.log.info("TEST-INFO | %s | full command: %r" % (name, completeCmd)) |
michael@0 | 260 | self.log.info("TEST-INFO | %s | current directory: %r" % (name, testdir)) |
michael@0 | 261 | # Show only those environment variables that are changed from |
michael@0 | 262 | # the ambient environment. |
michael@0 | 263 | changedEnv = (set("%s=%s" % i for i in self.env.iteritems()) |
michael@0 | 264 | - set("%s=%s" % i for i in os.environ.iteritems())) |
michael@0 | 265 | self.log.info("TEST-INFO | %s | environment: %s" % (name, list(changedEnv))) |
michael@0 | 266 | |
michael@0 | 267 | def testTimeout(self, test_file, proc): |
michael@0 | 268 | if not self.retry: |
michael@0 | 269 | self.log.error("TEST-UNEXPECTED-FAIL | %s | Test timed out" % test_file) |
michael@0 | 270 | self.done = True |
michael@0 | 271 | Automation().killAndGetStackNoScreenshot(proc.pid, self.appPath, self.debuggerInfo) |
michael@0 | 272 | |
michael@0 | 273 | def buildCmdTestFile(self, name): |
michael@0 | 274 | """ |
michael@0 | 275 | Build the command line arguments for the test file. |
michael@0 | 276 | On a remote system, this may be overloaded to use a remote path structure. |
michael@0 | 277 | """ |
michael@0 | 278 | return ['-e', 'const _TEST_FILE = ["%s"];' % |
michael@0 | 279 | replaceBackSlashes(name)] |
michael@0 | 280 | |
michael@0 | 281 | def setupTempDir(self): |
michael@0 | 282 | tempDir = mkdtemp() |
michael@0 | 283 | self.env["XPCSHELL_TEST_TEMP_DIR"] = tempDir |
michael@0 | 284 | if self.interactive: |
michael@0 | 285 | self.log.info("TEST-INFO | temp dir is %s" % tempDir) |
michael@0 | 286 | return tempDir |
michael@0 | 287 | |
michael@0 | 288 | def setupPluginsDir(self): |
michael@0 | 289 | if not os.path.isdir(self.pluginsPath): |
michael@0 | 290 | return None |
michael@0 | 291 | |
michael@0 | 292 | pluginsDir = mkdtemp() |
michael@0 | 293 | # shutil.copytree requires dst to not exist. Deleting the tempdir |
michael@0 | 294 | # would make a race condition possible in a concurrent environment, |
michael@0 | 295 | # so we are using dir_utils.copy_tree which accepts an existing dst |
michael@0 | 296 | dir_util.copy_tree(self.pluginsPath, pluginsDir) |
michael@0 | 297 | if self.interactive: |
michael@0 | 298 | self.log.info("TEST-INFO | plugins dir is %s" % pluginsDir) |
michael@0 | 299 | return pluginsDir |
michael@0 | 300 | |
michael@0 | 301 | def setupProfileDir(self): |
michael@0 | 302 | """ |
michael@0 | 303 | Create a temporary folder for the profile and set appropriate environment variables. |
michael@0 | 304 | When running check-interactive and check-one, the directory is well-defined and |
michael@0 | 305 | retained for inspection once the tests complete. |
michael@0 | 306 | |
michael@0 | 307 | On a remote system, this may be overloaded to use a remote path structure. |
michael@0 | 308 | """ |
michael@0 | 309 | if self.interactive or self.singleFile: |
michael@0 | 310 | profileDir = os.path.join(gettempdir(), self.profileName, "xpcshellprofile") |
michael@0 | 311 | try: |
michael@0 | 312 | # This could be left over from previous runs |
michael@0 | 313 | self.removeDir(profileDir) |
michael@0 | 314 | except: |
michael@0 | 315 | pass |
michael@0 | 316 | os.makedirs(profileDir) |
michael@0 | 317 | else: |
michael@0 | 318 | profileDir = mkdtemp() |
michael@0 | 319 | self.env["XPCSHELL_TEST_PROFILE_DIR"] = profileDir |
michael@0 | 320 | if self.interactive or self.singleFile: |
michael@0 | 321 | self.log.info("TEST-INFO | profile dir is %s" % profileDir) |
michael@0 | 322 | return profileDir |
michael@0 | 323 | |
michael@0 | 324 | def buildCmdHead(self, headfiles, tailfiles, xpcscmd): |
michael@0 | 325 | """ |
michael@0 | 326 | Build the command line arguments for the head and tail files, |
michael@0 | 327 | along with the address of the webserver which some tests require. |
michael@0 | 328 | |
michael@0 | 329 | On a remote system, this is overloaded to resolve quoting issues over a secondary command line. |
michael@0 | 330 | """ |
michael@0 | 331 | cmdH = ", ".join(['"' + replaceBackSlashes(f) + '"' |
michael@0 | 332 | for f in headfiles]) |
michael@0 | 333 | cmdT = ", ".join(['"' + replaceBackSlashes(f) + '"' |
michael@0 | 334 | for f in tailfiles]) |
michael@0 | 335 | return xpcscmd + \ |
michael@0 | 336 | ['-e', 'const _SERVER_ADDR = "localhost"', |
michael@0 | 337 | '-e', 'const _HEAD_FILES = [%s];' % cmdH, |
michael@0 | 338 | '-e', 'const _TAIL_FILES = [%s];' % cmdT] |
michael@0 | 339 | |
michael@0 | 340 | def getHeadAndTailFiles(self, test_object): |
michael@0 | 341 | """Obtain the list of head and tail files. |
michael@0 | 342 | |
michael@0 | 343 | Returns a 2-tuple. The first element is a list of head files. The second |
michael@0 | 344 | is a list of tail files. |
michael@0 | 345 | """ |
michael@0 | 346 | def sanitize_list(s, kind): |
michael@0 | 347 | for f in s.strip().split(' '): |
michael@0 | 348 | f = f.strip() |
michael@0 | 349 | if len(f) < 1: |
michael@0 | 350 | continue |
michael@0 | 351 | |
michael@0 | 352 | path = os.path.normpath(os.path.join(test_object['here'], f)) |
michael@0 | 353 | if not os.path.exists(path): |
michael@0 | 354 | raise Exception('%s file does not exist: %s' % (kind, path)) |
michael@0 | 355 | |
michael@0 | 356 | if not os.path.isfile(path): |
michael@0 | 357 | raise Exception('%s file is not a file: %s' % (kind, path)) |
michael@0 | 358 | |
michael@0 | 359 | yield path |
michael@0 | 360 | |
michael@0 | 361 | return (list(sanitize_list(test_object['head'], 'head')), |
michael@0 | 362 | list(sanitize_list(test_object['tail'], 'tail'))) |
michael@0 | 363 | |
michael@0 | 364 | def buildXpcsCmd(self, testdir): |
michael@0 | 365 | """ |
michael@0 | 366 | Load the root head.js file as the first file in our test path, before other head, test, and tail files. |
michael@0 | 367 | On a remote system, we overload this to add additional command line arguments, so this gets overloaded. |
michael@0 | 368 | """ |
michael@0 | 369 | # - NOTE: if you rename/add any of the constants set here, update |
michael@0 | 370 | # do_load_child_test_harness() in head.js |
michael@0 | 371 | if not self.appPath: |
michael@0 | 372 | self.appPath = self.xrePath |
michael@0 | 373 | |
michael@0 | 374 | self.xpcsCmd = [ |
michael@0 | 375 | self.xpcshell, |
michael@0 | 376 | '-g', self.xrePath, |
michael@0 | 377 | '-a', self.appPath, |
michael@0 | 378 | '-r', self.httpdManifest, |
michael@0 | 379 | '-m', |
michael@0 | 380 | '-s', |
michael@0 | 381 | '-e', 'const _HTTPD_JS_PATH = "%s";' % self.httpdJSPath, |
michael@0 | 382 | '-e', 'const _HEAD_JS_PATH = "%s";' % self.headJSPath |
michael@0 | 383 | ] |
michael@0 | 384 | |
michael@0 | 385 | if self.testingModulesDir: |
michael@0 | 386 | # Escape backslashes in string literal. |
michael@0 | 387 | sanitized = self.testingModulesDir.replace('\\', '\\\\') |
michael@0 | 388 | self.xpcsCmd.extend([ |
michael@0 | 389 | '-e', |
michael@0 | 390 | 'const _TESTING_MODULES_DIR = "%s";' % sanitized |
michael@0 | 391 | ]) |
michael@0 | 392 | |
michael@0 | 393 | self.xpcsCmd.extend(['-f', os.path.join(self.testharnessdir, 'head.js')]) |
michael@0 | 394 | |
michael@0 | 395 | if self.debuggerInfo: |
michael@0 | 396 | self.xpcsCmd = [self.debuggerInfo["path"]] + self.debuggerInfo["args"] + self.xpcsCmd |
michael@0 | 397 | |
michael@0 | 398 | # Automation doesn't specify a pluginsPath and xpcshell defaults to |
michael@0 | 399 | # $APPDIR/plugins. We do the same here so we can carry on with |
michael@0 | 400 | # setting up every test with its own plugins directory. |
michael@0 | 401 | if not self.pluginsPath: |
michael@0 | 402 | self.pluginsPath = os.path.join(self.appPath, 'plugins') |
michael@0 | 403 | |
michael@0 | 404 | self.pluginsDir = self.setupPluginsDir() |
michael@0 | 405 | if self.pluginsDir: |
michael@0 | 406 | self.xpcsCmd.extend(['-p', self.pluginsDir]) |
michael@0 | 407 | |
michael@0 | 408 | def cleanupDir(self, directory, name, xunit_result): |
michael@0 | 409 | if not os.path.exists(directory): |
michael@0 | 410 | return |
michael@0 | 411 | |
michael@0 | 412 | TRY_LIMIT = 25 # up to TRY_LIMIT attempts (one every second), because |
michael@0 | 413 | # the Windows filesystem is slow to react to the changes |
michael@0 | 414 | try_count = 0 |
michael@0 | 415 | while try_count < TRY_LIMIT: |
michael@0 | 416 | try: |
michael@0 | 417 | self.removeDir(directory) |
michael@0 | 418 | except OSError: |
michael@0 | 419 | self.log.info("TEST-INFO | Failed to remove directory: %s. Waiting." % directory) |
michael@0 | 420 | # We suspect the filesystem may still be making changes. Wait a |
michael@0 | 421 | # little bit and try again. |
michael@0 | 422 | time.sleep(1) |
michael@0 | 423 | try_count += 1 |
michael@0 | 424 | else: |
michael@0 | 425 | # removed fine |
michael@0 | 426 | return |
michael@0 | 427 | |
michael@0 | 428 | # we try cleaning up again later at the end of the run |
michael@0 | 429 | self.cleanup_dir_list.append(directory) |
michael@0 | 430 | |
michael@0 | 431 | def clean_temp_dirs(self, name, stdout): |
michael@0 | 432 | # We don't want to delete the profile when running check-interactive |
michael@0 | 433 | # or check-one. |
michael@0 | 434 | if self.profileDir and not self.interactive and not self.singleFile: |
michael@0 | 435 | self.cleanupDir(self.profileDir, name, self.xunit_result) |
michael@0 | 436 | |
michael@0 | 437 | self.cleanupDir(self.tempDir, name, self.xunit_result) |
michael@0 | 438 | |
michael@0 | 439 | if self.pluginsDir: |
michael@0 | 440 | self.cleanupDir(self.pluginsDir, name, self.xunit_result) |
michael@0 | 441 | |
michael@0 | 442 | def message_from_line(self, line): |
michael@0 | 443 | """ Given a line of raw output, convert to a string message. """ |
michael@0 | 444 | if isinstance(line, basestring): |
michael@0 | 445 | # This function has received unstructured output. |
michael@0 | 446 | if line: |
michael@0 | 447 | if 'TEST-UNEXPECTED-' in line: |
michael@0 | 448 | self.has_failure_output = True |
michael@0 | 449 | return line |
michael@0 | 450 | |
michael@0 | 451 | msg = ['%s: ' % line['process'] if 'process' in line else ''] |
michael@0 | 452 | |
michael@0 | 453 | # Each call to the logger in head.js either specified '_message' |
michael@0 | 454 | # or both 'source_file' and 'diagnostic'. If either of these are |
michael@0 | 455 | # missing, they ended up being undefined as a result of the way |
michael@0 | 456 | # the test was run. |
michael@0 | 457 | if '_message' in line: |
michael@0 | 458 | msg.append(line['_message']) |
michael@0 | 459 | if 'diagnostic' in line: |
michael@0 | 460 | msg.append('\nDiagnostic: %s' % line['diagnostic']) |
michael@0 | 461 | else: |
michael@0 | 462 | msg.append('%s | %s | %s' % (ACTION_STRINGS[line['action']], |
michael@0 | 463 | line.get('source_file', 'undefined'), |
michael@0 | 464 | line.get('diagnostic', 'undefined'))) |
michael@0 | 465 | |
michael@0 | 466 | msg.append('\n%s' % line['stack'] if 'stack' in line else '') |
michael@0 | 467 | return ''.join(msg) |
michael@0 | 468 | |
michael@0 | 469 | def parse_output(self, output): |
michael@0 | 470 | """Parses process output for structured messages and saves output as it is |
michael@0 | 471 | read. Sets self.has_failure_output in case of evidence of a failure""" |
michael@0 | 472 | for line_string in output.splitlines(): |
michael@0 | 473 | self.process_line(line_string) |
michael@0 | 474 | |
michael@0 | 475 | if self.saw_proc_start and not self.saw_proc_end: |
michael@0 | 476 | self.has_failure_output = True |
michael@0 | 477 | |
michael@0 | 478 | def report_message(self, line): |
michael@0 | 479 | """ Reports a message to a consumer, both as a strucutured and |
michael@0 | 480 | human-readable log message. """ |
michael@0 | 481 | |
michael@0 | 482 | message = cleanup_encoding(self.message_from_line(line)) |
michael@0 | 483 | if message.endswith('\n'): |
michael@0 | 484 | # A new line is always added by head.js to delimit messages, |
michael@0 | 485 | # however consumers will want to supply their own. |
michael@0 | 486 | message = message[:-1] |
michael@0 | 487 | |
michael@0 | 488 | if self.on_message: |
michael@0 | 489 | self.on_message(line, message) |
michael@0 | 490 | else: |
michael@0 | 491 | self.output_lines.append(message) |
michael@0 | 492 | |
michael@0 | 493 | def process_line(self, line_string): |
michael@0 | 494 | """ Parses a single line of output, determining its significance and |
michael@0 | 495 | reporting a message. |
michael@0 | 496 | """ |
michael@0 | 497 | try: |
michael@0 | 498 | line_object = json.loads(line_string) |
michael@0 | 499 | if not isinstance(line_object, dict): |
michael@0 | 500 | self.report_message(line_string) |
michael@0 | 501 | return |
michael@0 | 502 | except ValueError: |
michael@0 | 503 | self.report_message(line_string) |
michael@0 | 504 | return |
michael@0 | 505 | |
michael@0 | 506 | if 'action' not in line_object: |
michael@0 | 507 | # In case a test outputs something that happens to be valid |
michael@0 | 508 | # JSON. |
michael@0 | 509 | self.report_message(line_string) |
michael@0 | 510 | return |
michael@0 | 511 | |
michael@0 | 512 | action = line_object['action'] |
michael@0 | 513 | self.report_message(line_object) |
michael@0 | 514 | |
michael@0 | 515 | if action in FAILURE_ACTIONS: |
michael@0 | 516 | self.has_failure_output = True |
michael@0 | 517 | elif action == 'child_test_start': |
michael@0 | 518 | self.saw_proc_start = True |
michael@0 | 519 | elif action == 'child_test_end': |
michael@0 | 520 | self.saw_proc_end = True |
michael@0 | 521 | |
michael@0 | 522 | def log_output(self, output): |
michael@0 | 523 | """Prints given output line-by-line to avoid overflowing buffers.""" |
michael@0 | 524 | self.log.info(">>>>>>>") |
michael@0 | 525 | if output: |
michael@0 | 526 | if isinstance(output, basestring): |
michael@0 | 527 | output = output.splitlines() |
michael@0 | 528 | for part in output: |
michael@0 | 529 | # For multi-line output, such as a stack trace |
michael@0 | 530 | for line in part.splitlines(): |
michael@0 | 531 | try: |
michael@0 | 532 | line = line.decode('utf-8') |
michael@0 | 533 | except UnicodeDecodeError: |
michael@0 | 534 | self.log.info("TEST-INFO | %s | Detected non UTF-8 output."\ |
michael@0 | 535 | " Please modify the test to only print UTF-8." % |
michael@0 | 536 | self.test_object['name']) |
michael@0 | 537 | # add '?' instead of funky bytes |
michael@0 | 538 | line = line.decode('utf-8', 'replace') |
michael@0 | 539 | self.log.info(line) |
michael@0 | 540 | self.log.info("<<<<<<<") |
michael@0 | 541 | |
michael@0 | 542 | def run_test(self): |
michael@0 | 543 | """Run an individual xpcshell test.""" |
michael@0 | 544 | global gotSIGINT |
michael@0 | 545 | |
michael@0 | 546 | name = self.test_object['path'] |
michael@0 | 547 | |
michael@0 | 548 | self.xunit_result = {'name': self.test_object['name'], 'classname': 'xpcshell'} |
michael@0 | 549 | |
michael@0 | 550 | # The xUnit package is defined as the path component between the root |
michael@0 | 551 | # dir and the test with path characters replaced with '.' (using Java |
michael@0 | 552 | # class notation). |
michael@0 | 553 | if self.tests_root_dir is not None: |
michael@0 | 554 | self.tests_root_dir = os.path.normpath(self.tests_root_dir) |
michael@0 | 555 | if os.path.normpath(self.test_object['here']).find(self.tests_root_dir) != 0: |
michael@0 | 556 | raise Exception('tests_root_dir is not a parent path of %s' % |
michael@0 | 557 | self.test_object['here']) |
michael@0 | 558 | relpath = self.test_object['here'][len(self.tests_root_dir):].lstrip('/\\') |
michael@0 | 559 | self.xunit_result['classname'] = relpath.replace('/', '.').replace('\\', '.') |
michael@0 | 560 | |
michael@0 | 561 | # Check for skipped tests |
michael@0 | 562 | if 'disabled' in self.test_object: |
michael@0 | 563 | self.log.info('TEST-INFO | skipping %s | %s' % |
michael@0 | 564 | (name, self.test_object['disabled'])) |
michael@0 | 565 | |
michael@0 | 566 | self.xunit_result['skipped'] = True |
michael@0 | 567 | self.retry = False |
michael@0 | 568 | |
michael@0 | 569 | self.keep_going = True |
michael@0 | 570 | return |
michael@0 | 571 | |
michael@0 | 572 | # Check for known-fail tests |
michael@0 | 573 | expected = self.test_object['expected'] == 'pass' |
michael@0 | 574 | |
michael@0 | 575 | # By default self.appPath will equal the gre dir. If specified in the |
michael@0 | 576 | # xpcshell.ini file, set a different app dir for this test. |
michael@0 | 577 | if self.app_dir_key and self.app_dir_key in self.test_object: |
michael@0 | 578 | rel_app_dir = self.test_object[self.app_dir_key] |
michael@0 | 579 | rel_app_dir = os.path.join(self.xrePath, rel_app_dir) |
michael@0 | 580 | self.appPath = os.path.abspath(rel_app_dir) |
michael@0 | 581 | else: |
michael@0 | 582 | self.appPath = None |
michael@0 | 583 | |
michael@0 | 584 | test_dir = os.path.dirname(name) |
michael@0 | 585 | self.buildXpcsCmd(test_dir) |
michael@0 | 586 | head_files, tail_files = self.getHeadAndTailFiles(self.test_object) |
michael@0 | 587 | cmdH = self.buildCmdHead(head_files, tail_files, self.xpcsCmd) |
michael@0 | 588 | |
michael@0 | 589 | # Create a profile and a temp dir that the JS harness can stick |
michael@0 | 590 | # a profile and temporary data in |
michael@0 | 591 | self.profileDir = self.setupProfileDir() |
michael@0 | 592 | self.tempDir = self.setupTempDir() |
michael@0 | 593 | |
michael@0 | 594 | # The test file will have to be loaded after the head files. |
michael@0 | 595 | cmdT = self.buildCmdTestFile(name) |
michael@0 | 596 | |
michael@0 | 597 | args = self.xpcsRunArgs[:] |
michael@0 | 598 | if 'debug' in self.test_object: |
michael@0 | 599 | args.insert(0, '-d') |
michael@0 | 600 | |
michael@0 | 601 | completeCmd = cmdH + cmdT + args |
michael@0 | 602 | |
michael@0 | 603 | testTimeoutInterval = HARNESS_TIMEOUT |
michael@0 | 604 | # Allow a test to request a multiple of the timeout if it is expected to take long |
michael@0 | 605 | if 'requesttimeoutfactor' in self.test_object: |
michael@0 | 606 | testTimeoutInterval *= int(self.test_object['requesttimeoutfactor']) |
michael@0 | 607 | |
michael@0 | 608 | testTimer = None |
michael@0 | 609 | if not self.interactive and not self.debuggerInfo: |
michael@0 | 610 | testTimer = Timer(testTimeoutInterval, lambda: self.testTimeout(name, proc)) |
michael@0 | 611 | testTimer.start() |
michael@0 | 612 | |
michael@0 | 613 | proc = None |
michael@0 | 614 | stdout = None |
michael@0 | 615 | stderr = None |
michael@0 | 616 | |
michael@0 | 617 | try: |
michael@0 | 618 | self.log.info("TEST-INFO | %s | running test ..." % name) |
michael@0 | 619 | if self.verbose: |
michael@0 | 620 | self.logCommand(name, completeCmd, test_dir) |
michael@0 | 621 | |
michael@0 | 622 | startTime = time.time() |
michael@0 | 623 | proc = self.launchProcess(completeCmd, |
michael@0 | 624 | stdout=self.pStdout, stderr=self.pStderr, env=self.env, cwd=test_dir) |
michael@0 | 625 | |
michael@0 | 626 | if self.interactive: |
michael@0 | 627 | self.log.info("TEST-INFO | %s | Process ID: %d" % (name, proc.pid)) |
michael@0 | 628 | |
michael@0 | 629 | stdout, stderr = self.communicate(proc) |
michael@0 | 630 | |
michael@0 | 631 | if self.interactive: |
michael@0 | 632 | # Not sure what else to do here... |
michael@0 | 633 | self.keep_going = True |
michael@0 | 634 | return |
michael@0 | 635 | |
michael@0 | 636 | if testTimer: |
michael@0 | 637 | testTimer.cancel() |
michael@0 | 638 | |
michael@0 | 639 | if stdout: |
michael@0 | 640 | self.parse_output(stdout) |
michael@0 | 641 | result = not (self.has_failure_output or |
michael@0 | 642 | (self.getReturnCode(proc) != 0)) |
michael@0 | 643 | |
michael@0 | 644 | if result != expected: |
michael@0 | 645 | if self.retry: |
michael@0 | 646 | self.clean_temp_dirs(name, stdout) |
michael@0 | 647 | return |
michael@0 | 648 | |
michael@0 | 649 | failureType = "TEST-UNEXPECTED-%s" % ("FAIL" if expected else "PASS") |
michael@0 | 650 | message = "%s | %s | test failed (with xpcshell return code: %d)" % ( |
michael@0 | 651 | failureType, name, self.getReturnCode(proc)) |
michael@0 | 652 | if self.output_lines: |
michael@0 | 653 | message += ", see following log:" |
michael@0 | 654 | |
michael@0 | 655 | with LOG_MUTEX: |
michael@0 | 656 | self.log.error(message) |
michael@0 | 657 | self.log_output(self.output_lines) |
michael@0 | 658 | |
michael@0 | 659 | self.failCount += 1 |
michael@0 | 660 | self.xunit_result["passed"] = False |
michael@0 | 661 | |
michael@0 | 662 | self.xunit_result["failure"] = { |
michael@0 | 663 | "type": failureType, |
michael@0 | 664 | "message": message, |
michael@0 | 665 | "text": stdout |
michael@0 | 666 | } |
michael@0 | 667 | |
michael@0 | 668 | if self.failureManifest: |
michael@0 | 669 | with open(self.failureManifest, 'a') as f: |
michael@0 | 670 | f.write('[%s]\n' % self.test_object['path']) |
michael@0 | 671 | for k, v in self.test_object.items(): |
michael@0 | 672 | f.write('%s = %s\n' % (k, v)) |
michael@0 | 673 | |
michael@0 | 674 | else: |
michael@0 | 675 | now = time.time() |
michael@0 | 676 | timeTaken = (now - startTime) * 1000 |
michael@0 | 677 | self.xunit_result["time"] = now - startTime |
michael@0 | 678 | |
michael@0 | 679 | with LOG_MUTEX: |
michael@0 | 680 | self.log.info("TEST-%s | %s | test passed (time: %.3fms)" % ("PASS" if expected else "KNOWN-FAIL", name, timeTaken)) |
michael@0 | 681 | if self.verbose: |
michael@0 | 682 | self.log_output(self.output_lines) |
michael@0 | 683 | |
michael@0 | 684 | self.xunit_result["passed"] = True |
michael@0 | 685 | self.retry = False |
michael@0 | 686 | |
michael@0 | 687 | if expected: |
michael@0 | 688 | self.passCount = 1 |
michael@0 | 689 | else: |
michael@0 | 690 | self.todoCount = 1 |
michael@0 | 691 | self.xunit_result["todo"] = True |
michael@0 | 692 | |
michael@0 | 693 | if self.checkForCrashes(self.tempDir, self.symbolsPath, test_name=name): |
michael@0 | 694 | if self.retry: |
michael@0 | 695 | self.clean_temp_dirs(name, stdout) |
michael@0 | 696 | return |
michael@0 | 697 | |
michael@0 | 698 | message = "PROCESS-CRASH | %s | application crashed" % name |
michael@0 | 699 | self.failCount = 1 |
michael@0 | 700 | self.xunit_result["passed"] = False |
michael@0 | 701 | self.xunit_result["failure"] = { |
michael@0 | 702 | "type": "PROCESS-CRASH", |
michael@0 | 703 | "message": message, |
michael@0 | 704 | "text": stdout |
michael@0 | 705 | } |
michael@0 | 706 | |
michael@0 | 707 | if self.logfiles and stdout: |
michael@0 | 708 | self.createLogFile(name, stdout) |
michael@0 | 709 | |
michael@0 | 710 | finally: |
michael@0 | 711 | # We can sometimes get here before the process has terminated, which would |
michael@0 | 712 | # cause removeDir() to fail - so check for the process & kill it it needed. |
michael@0 | 713 | if proc and self.poll(proc) is None: |
michael@0 | 714 | self.kill(proc) |
michael@0 | 715 | |
michael@0 | 716 | if self.retry: |
michael@0 | 717 | self.clean_temp_dirs(name, stdout) |
michael@0 | 718 | return |
michael@0 | 719 | |
michael@0 | 720 | with LOG_MUTEX: |
michael@0 | 721 | message = "TEST-UNEXPECTED-FAIL | %s | Process still running after test!" % name |
michael@0 | 722 | self.log.error(message) |
michael@0 | 723 | self.log_output(self.output_lines) |
michael@0 | 724 | |
michael@0 | 725 | self.failCount = 1 |
michael@0 | 726 | self.xunit_result["passed"] = False |
michael@0 | 727 | self.xunit_result["failure"] = { |
michael@0 | 728 | "type": "TEST-UNEXPECTED-FAIL", |
michael@0 | 729 | "message": message, |
michael@0 | 730 | "text": stdout |
michael@0 | 731 | } |
michael@0 | 732 | |
michael@0 | 733 | self.clean_temp_dirs(name, stdout) |
michael@0 | 734 | |
michael@0 | 735 | if gotSIGINT: |
michael@0 | 736 | self.xunit_result["passed"] = False |
michael@0 | 737 | self.xunit_result["time"] = "0.0" |
michael@0 | 738 | self.xunit_result["failure"] = { |
michael@0 | 739 | "type": "SIGINT", |
michael@0 | 740 | "message": "Received SIGINT", |
michael@0 | 741 | "text": "Received SIGINT (control-C) during test execution." |
michael@0 | 742 | } |
michael@0 | 743 | |
michael@0 | 744 | self.log.error("TEST-UNEXPECTED-FAIL | Received SIGINT (control-C) during test execution") |
michael@0 | 745 | if self.keep_going: |
michael@0 | 746 | gotSIGINT = False |
michael@0 | 747 | else: |
michael@0 | 748 | self.keep_going = False |
michael@0 | 749 | return |
michael@0 | 750 | |
michael@0 | 751 | self.keep_going = True |
michael@0 | 752 | |
michael@0 | 753 | class XPCShellTests(object): |
michael@0 | 754 | |
michael@0 | 755 | log = getGlobalLog() |
michael@0 | 756 | oldcwd = os.getcwd() |
michael@0 | 757 | |
michael@0 | 758 | def __init__(self, log=None): |
michael@0 | 759 | """ Init logging and node status """ |
michael@0 | 760 | if log: |
michael@0 | 761 | resetGlobalLog(log) |
michael@0 | 762 | |
michael@0 | 763 | # Each method of the underlying logger must acquire the log |
michael@0 | 764 | # mutex before writing to stdout. |
michael@0 | 765 | log_funs = ['debug', 'info', 'warning', 'error', 'critical', 'log'] |
michael@0 | 766 | for fun_name in log_funs: |
michael@0 | 767 | unwrapped = getattr(self.log, fun_name, None) |
michael@0 | 768 | if unwrapped: |
michael@0 | 769 | def wrap(fn): |
michael@0 | 770 | def wrapped(*args, **kwargs): |
michael@0 | 771 | with LOG_MUTEX: |
michael@0 | 772 | fn(*args, **kwargs) |
michael@0 | 773 | return wrapped |
michael@0 | 774 | setattr(self.log, fun_name, wrap(unwrapped)) |
michael@0 | 775 | |
michael@0 | 776 | self.nodeProc = {} |
michael@0 | 777 | |
michael@0 | 778 | def buildTestList(self): |
michael@0 | 779 | """ |
michael@0 | 780 | read the xpcshell.ini manifest and set self.alltests to be |
michael@0 | 781 | an array of test objects. |
michael@0 | 782 | |
michael@0 | 783 | if we are chunking tests, it will be done here as well |
michael@0 | 784 | """ |
michael@0 | 785 | if isinstance(self.manifest, manifestparser.TestManifest): |
michael@0 | 786 | mp = self.manifest |
michael@0 | 787 | else: |
michael@0 | 788 | mp = manifestparser.TestManifest(strict=False) |
michael@0 | 789 | if self.manifest is None: |
michael@0 | 790 | for testdir in self.testdirs: |
michael@0 | 791 | if testdir: |
michael@0 | 792 | mp.read(os.path.join(testdir, 'xpcshell.ini')) |
michael@0 | 793 | else: |
michael@0 | 794 | mp.read(self.manifest) |
michael@0 | 795 | |
michael@0 | 796 | self.buildTestPath() |
michael@0 | 797 | |
michael@0 | 798 | try: |
michael@0 | 799 | self.alltests = mp.active_tests(**mozinfo.info) |
michael@0 | 800 | except TypeError: |
michael@0 | 801 | sys.stderr.write("*** offending mozinfo.info: %s\n" % repr(mozinfo.info)) |
michael@0 | 802 | raise |
michael@0 | 803 | |
michael@0 | 804 | if self.singleFile is None and self.totalChunks > 1: |
michael@0 | 805 | self.chunkTests() |
michael@0 | 806 | |
michael@0 | 807 | def chunkTests(self): |
michael@0 | 808 | """ |
michael@0 | 809 | Split the list of tests up into [totalChunks] pieces and filter the |
michael@0 | 810 | self.alltests based on thisChunk, so we only run a subset. |
michael@0 | 811 | """ |
michael@0 | 812 | totalTests = len(self.alltests) |
michael@0 | 813 | testsPerChunk = math.ceil(totalTests / float(self.totalChunks)) |
michael@0 | 814 | start = int(round((self.thisChunk-1) * testsPerChunk)) |
michael@0 | 815 | end = int(start + testsPerChunk) |
michael@0 | 816 | if end > totalTests: |
michael@0 | 817 | end = totalTests |
michael@0 | 818 | self.log.info("Running tests %d-%d/%d", start+1, end, totalTests) |
michael@0 | 819 | self.alltests = self.alltests[start:end] |
michael@0 | 820 | |
michael@0 | 821 | def setAbsPath(self): |
michael@0 | 822 | """ |
michael@0 | 823 | Set the absolute path for xpcshell, httpdjspath and xrepath. |
michael@0 | 824 | These 3 variables depend on input from the command line and we need to allow for absolute paths. |
michael@0 | 825 | This function is overloaded for a remote solution as os.path* won't work remotely. |
michael@0 | 826 | """ |
michael@0 | 827 | self.testharnessdir = os.path.dirname(os.path.abspath(__file__)) |
michael@0 | 828 | self.headJSPath = self.testharnessdir.replace("\\", "/") + "/head.js" |
michael@0 | 829 | self.xpcshell = os.path.abspath(self.xpcshell) |
michael@0 | 830 | |
michael@0 | 831 | # we assume that httpd.js lives in components/ relative to xpcshell |
michael@0 | 832 | self.httpdJSPath = os.path.join(os.path.dirname(self.xpcshell), 'components', 'httpd.js') |
michael@0 | 833 | self.httpdJSPath = replaceBackSlashes(self.httpdJSPath) |
michael@0 | 834 | |
michael@0 | 835 | self.httpdManifest = os.path.join(os.path.dirname(self.xpcshell), 'components', 'httpd.manifest') |
michael@0 | 836 | self.httpdManifest = replaceBackSlashes(self.httpdManifest) |
michael@0 | 837 | |
michael@0 | 838 | if self.xrePath is None: |
michael@0 | 839 | self.xrePath = os.path.dirname(self.xpcshell) |
michael@0 | 840 | else: |
michael@0 | 841 | self.xrePath = os.path.abspath(self.xrePath) |
michael@0 | 842 | |
michael@0 | 843 | if self.mozInfo is None: |
michael@0 | 844 | self.mozInfo = os.path.join(self.testharnessdir, "mozinfo.json") |
michael@0 | 845 | |
michael@0 | 846 | def buildCoreEnvironment(self): |
michael@0 | 847 | """ |
michael@0 | 848 | Add environment variables likely to be used across all platforms, including remote systems. |
michael@0 | 849 | """ |
michael@0 | 850 | # Make assertions fatal |
michael@0 | 851 | self.env["XPCOM_DEBUG_BREAK"] = "stack-and-abort" |
michael@0 | 852 | # Crash reporting interferes with debugging |
michael@0 | 853 | if not self.debuggerInfo: |
michael@0 | 854 | self.env["MOZ_CRASHREPORTER"] = "1" |
michael@0 | 855 | # Don't launch the crash reporter client |
michael@0 | 856 | self.env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" |
michael@0 | 857 | # Capturing backtraces is very slow on some platforms, and it's |
michael@0 | 858 | # disabled by automation.py too |
michael@0 | 859 | self.env["NS_TRACE_MALLOC_DISABLE_STACKS"] = "1" |
michael@0 | 860 | # Don't permit remote connections. |
michael@0 | 861 | self.env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1" |
michael@0 | 862 | |
michael@0 | 863 | def buildEnvironment(self): |
michael@0 | 864 | """ |
michael@0 | 865 | Create and returns a dictionary of self.env to include all the appropriate env variables and values. |
michael@0 | 866 | On a remote system, we overload this to set different values and are missing things like os.environ and PATH. |
michael@0 | 867 | """ |
michael@0 | 868 | self.env = dict(os.environ) |
michael@0 | 869 | self.buildCoreEnvironment() |
michael@0 | 870 | if sys.platform == 'win32': |
michael@0 | 871 | self.env["PATH"] = self.env["PATH"] + ";" + self.xrePath |
michael@0 | 872 | elif sys.platform in ('os2emx', 'os2knix'): |
michael@0 | 873 | os.environ["BEGINLIBPATH"] = self.xrePath + ";" + self.env["BEGINLIBPATH"] |
michael@0 | 874 | os.environ["LIBPATHSTRICT"] = "T" |
michael@0 | 875 | elif sys.platform == 'osx' or sys.platform == "darwin": |
michael@0 | 876 | self.env["DYLD_LIBRARY_PATH"] = self.xrePath |
michael@0 | 877 | else: # unix or linux? |
michael@0 | 878 | if not "LD_LIBRARY_PATH" in self.env or self.env["LD_LIBRARY_PATH"] is None: |
michael@0 | 879 | self.env["LD_LIBRARY_PATH"] = self.xrePath |
michael@0 | 880 | else: |
michael@0 | 881 | self.env["LD_LIBRARY_PATH"] = ":".join([self.xrePath, self.env["LD_LIBRARY_PATH"]]) |
michael@0 | 882 | |
michael@0 | 883 | if "asan" in self.mozInfo and self.mozInfo["asan"]: |
michael@0 | 884 | # ASan symbolizer support |
michael@0 | 885 | llvmsym = os.path.join(self.xrePath, "llvm-symbolizer") |
michael@0 | 886 | if os.path.isfile(llvmsym): |
michael@0 | 887 | self.env["ASAN_SYMBOLIZER_PATH"] = llvmsym |
michael@0 | 888 | self.log.info("INFO | runxpcshelltests.py | ASan using symbolizer at %s", llvmsym) |
michael@0 | 889 | else: |
michael@0 | 890 | self.log.info("INFO | runxpcshelltests.py | ASan symbolizer binary not found: %s", llvmsym) |
michael@0 | 891 | |
michael@0 | 892 | return self.env |
michael@0 | 893 | |
michael@0 | 894 | def getPipes(self): |
michael@0 | 895 | """ |
michael@0 | 896 | Determine the value of the stdout and stderr for the test. |
michael@0 | 897 | Return value is a list (pStdout, pStderr). |
michael@0 | 898 | """ |
michael@0 | 899 | if self.interactive: |
michael@0 | 900 | pStdout = None |
michael@0 | 901 | pStderr = None |
michael@0 | 902 | else: |
michael@0 | 903 | if (self.debuggerInfo and self.debuggerInfo["interactive"]): |
michael@0 | 904 | pStdout = None |
michael@0 | 905 | pStderr = None |
michael@0 | 906 | else: |
michael@0 | 907 | if sys.platform == 'os2emx': |
michael@0 | 908 | pStdout = None |
michael@0 | 909 | else: |
michael@0 | 910 | pStdout = PIPE |
michael@0 | 911 | pStderr = STDOUT |
michael@0 | 912 | return pStdout, pStderr |
michael@0 | 913 | |
michael@0 | 914 | def buildTestPath(self): |
michael@0 | 915 | """ |
michael@0 | 916 | If we specifiy a testpath, set the self.testPath variable to be the given directory or file. |
michael@0 | 917 | |
michael@0 | 918 | |testPath| will be the optional path only, or |None|. |
michael@0 | 919 | |singleFile| will be the optional test only, or |None|. |
michael@0 | 920 | """ |
michael@0 | 921 | self.singleFile = None |
michael@0 | 922 | if self.testPath is not None: |
michael@0 | 923 | if self.testPath.endswith('.js'): |
michael@0 | 924 | # Split into path and file. |
michael@0 | 925 | if self.testPath.find('/') == -1: |
michael@0 | 926 | # Test only. |
michael@0 | 927 | self.singleFile = self.testPath |
michael@0 | 928 | else: |
michael@0 | 929 | # Both path and test. |
michael@0 | 930 | # Reuse |testPath| temporarily. |
michael@0 | 931 | self.testPath = self.testPath.rsplit('/', 1) |
michael@0 | 932 | self.singleFile = self.testPath[1] |
michael@0 | 933 | self.testPath = self.testPath[0] |
michael@0 | 934 | else: |
michael@0 | 935 | # Path only. |
michael@0 | 936 | # Simply remove optional ending separator. |
michael@0 | 937 | self.testPath = self.testPath.rstrip("/") |
michael@0 | 938 | |
michael@0 | 939 | def verifyDirPath(self, dirname): |
michael@0 | 940 | """ |
michael@0 | 941 | Simple wrapper to get the absolute path for a given directory name. |
michael@0 | 942 | On a remote system, we need to overload this to work on the remote filesystem. |
michael@0 | 943 | """ |
michael@0 | 944 | return os.path.abspath(dirname) |
michael@0 | 945 | |
michael@0 | 946 | def trySetupNode(self): |
michael@0 | 947 | """ |
michael@0 | 948 | Run node for SPDY tests, if available, and updates mozinfo as appropriate. |
michael@0 | 949 | """ |
michael@0 | 950 | nodeMozInfo = {'hasNode': False} # Assume the worst |
michael@0 | 951 | nodeBin = None |
michael@0 | 952 | |
michael@0 | 953 | # We try to find the node executable in the path given to us by the user in |
michael@0 | 954 | # the MOZ_NODE_PATH environment variable |
michael@0 | 955 | localPath = os.getenv('MOZ_NODE_PATH', None) |
michael@0 | 956 | if localPath and os.path.exists(localPath) and os.path.isfile(localPath): |
michael@0 | 957 | nodeBin = localPath |
michael@0 | 958 | |
michael@0 | 959 | if nodeBin: |
michael@0 | 960 | self.log.info('Found node at %s' % (nodeBin,)) |
michael@0 | 961 | |
michael@0 | 962 | def startServer(name, serverJs): |
michael@0 | 963 | if os.path.exists(serverJs): |
michael@0 | 964 | # OK, we found our SPDY server, let's try to get it running |
michael@0 | 965 | self.log.info('Found %s at %s' % (name, serverJs)) |
michael@0 | 966 | try: |
michael@0 | 967 | # We pipe stdin to node because the spdy server will exit when its |
michael@0 | 968 | # stdin reaches EOF |
michael@0 | 969 | process = Popen([nodeBin, serverJs], stdin=PIPE, stdout=PIPE, |
michael@0 | 970 | stderr=STDOUT, env=self.env, cwd=os.getcwd()) |
michael@0 | 971 | self.nodeProc[name] = process |
michael@0 | 972 | |
michael@0 | 973 | # Check to make sure the server starts properly by waiting for it to |
michael@0 | 974 | # tell us it's started |
michael@0 | 975 | msg = process.stdout.readline() |
michael@0 | 976 | if 'server listening' in msg: |
michael@0 | 977 | nodeMozInfo['hasNode'] = True |
michael@0 | 978 | except OSError, e: |
michael@0 | 979 | # This occurs if the subprocess couldn't be started |
michael@0 | 980 | self.log.error('Could not run %s server: %s' % (name, str(e))) |
michael@0 | 981 | |
michael@0 | 982 | myDir = os.path.split(os.path.abspath(__file__))[0] |
michael@0 | 983 | startServer('moz-spdy', os.path.join(myDir, 'moz-spdy', 'moz-spdy.js')) |
michael@0 | 984 | startServer('moz-http2', os.path.join(myDir, 'moz-http2', 'moz-http2.js')) |
michael@0 | 985 | |
michael@0 | 986 | mozinfo.update(nodeMozInfo) |
michael@0 | 987 | |
michael@0 | 988 | def shutdownNode(self): |
michael@0 | 989 | """ |
michael@0 | 990 | Shut down our node process, if it exists |
michael@0 | 991 | """ |
michael@0 | 992 | for name, proc in self.nodeProc.iteritems(): |
michael@0 | 993 | self.log.info('Node %s server shutting down ...' % name) |
michael@0 | 994 | proc.terminate() |
michael@0 | 995 | |
michael@0 | 996 | def writeXunitResults(self, results, name=None, filename=None, fh=None): |
michael@0 | 997 | """ |
michael@0 | 998 | Write Xunit XML from results. |
michael@0 | 999 | |
michael@0 | 1000 | The function receives an iterable of results dicts. Each dict must have |
michael@0 | 1001 | the following keys: |
michael@0 | 1002 | |
michael@0 | 1003 | classname - The "class" name of the test. |
michael@0 | 1004 | name - The simple name of the test. |
michael@0 | 1005 | |
michael@0 | 1006 | In addition, it must have one of the following saying how the test |
michael@0 | 1007 | executed: |
michael@0 | 1008 | |
michael@0 | 1009 | passed - Boolean indicating whether the test passed. False if it |
michael@0 | 1010 | failed. |
michael@0 | 1011 | skipped - True if the test was skipped. |
michael@0 | 1012 | |
michael@0 | 1013 | The following keys are optional: |
michael@0 | 1014 | |
michael@0 | 1015 | time - Execution time of the test in decimal seconds. |
michael@0 | 1016 | failure - Dict describing test failure. Requires keys: |
michael@0 | 1017 | type - String type of failure. |
michael@0 | 1018 | message - String describing basic failure. |
michael@0 | 1019 | text - Verbose string describing failure. |
michael@0 | 1020 | |
michael@0 | 1021 | Arguments: |
michael@0 | 1022 | |
michael@0 | 1023 | |name|, Name of the test suite. Many tools expect Java class dot notation |
michael@0 | 1024 | e.g. dom.simple.foo. A directory with '/' converted to '.' is a good |
michael@0 | 1025 | choice. |
michael@0 | 1026 | |fh|, File handle to write XML to. |
michael@0 | 1027 | |filename|, File name to write XML to. |
michael@0 | 1028 | |results|, Iterable of tuples describing the results. |
michael@0 | 1029 | """ |
michael@0 | 1030 | if filename is None and fh is None: |
michael@0 | 1031 | raise Exception("One of filename or fh must be defined.") |
michael@0 | 1032 | |
michael@0 | 1033 | if name is None: |
michael@0 | 1034 | name = "xpcshell" |
michael@0 | 1035 | else: |
michael@0 | 1036 | assert isinstance(name, basestring) |
michael@0 | 1037 | |
michael@0 | 1038 | if filename is not None: |
michael@0 | 1039 | fh = open(filename, 'wb') |
michael@0 | 1040 | |
michael@0 | 1041 | doc = xml.dom.minidom.Document() |
michael@0 | 1042 | testsuite = doc.createElement("testsuite") |
michael@0 | 1043 | testsuite.setAttribute("name", name) |
michael@0 | 1044 | doc.appendChild(testsuite) |
michael@0 | 1045 | |
michael@0 | 1046 | total = 0 |
michael@0 | 1047 | passed = 0 |
michael@0 | 1048 | failed = 0 |
michael@0 | 1049 | skipped = 0 |
michael@0 | 1050 | |
michael@0 | 1051 | for result in results: |
michael@0 | 1052 | total += 1 |
michael@0 | 1053 | |
michael@0 | 1054 | if result.get("skipped", None): |
michael@0 | 1055 | skipped += 1 |
michael@0 | 1056 | elif result["passed"]: |
michael@0 | 1057 | passed += 1 |
michael@0 | 1058 | else: |
michael@0 | 1059 | failed += 1 |
michael@0 | 1060 | |
michael@0 | 1061 | testcase = doc.createElement("testcase") |
michael@0 | 1062 | testcase.setAttribute("classname", result["classname"]) |
michael@0 | 1063 | testcase.setAttribute("name", result["name"]) |
michael@0 | 1064 | |
michael@0 | 1065 | if "time" in result: |
michael@0 | 1066 | testcase.setAttribute("time", str(result["time"])) |
michael@0 | 1067 | else: |
michael@0 | 1068 | # It appears most tools expect the time attribute to be present. |
michael@0 | 1069 | testcase.setAttribute("time", "0") |
michael@0 | 1070 | |
michael@0 | 1071 | if "failure" in result: |
michael@0 | 1072 | failure = doc.createElement("failure") |
michael@0 | 1073 | failure.setAttribute("type", str(result["failure"]["type"])) |
michael@0 | 1074 | failure.setAttribute("message", result["failure"]["message"]) |
michael@0 | 1075 | |
michael@0 | 1076 | # Lossy translation but required to not break CDATA. Also, text could |
michael@0 | 1077 | # be None and Python 2.5's minidom doesn't accept None. Later versions |
michael@0 | 1078 | # do, however. |
michael@0 | 1079 | cdata = result["failure"]["text"] |
michael@0 | 1080 | if not isinstance(cdata, str): |
michael@0 | 1081 | cdata = "" |
michael@0 | 1082 | |
michael@0 | 1083 | cdata = cdata.replace("]]>", "]] >") |
michael@0 | 1084 | text = doc.createCDATASection(cdata) |
michael@0 | 1085 | failure.appendChild(text) |
michael@0 | 1086 | testcase.appendChild(failure) |
michael@0 | 1087 | |
michael@0 | 1088 | if result.get("skipped", None): |
michael@0 | 1089 | e = doc.createElement("skipped") |
michael@0 | 1090 | testcase.appendChild(e) |
michael@0 | 1091 | |
michael@0 | 1092 | testsuite.appendChild(testcase) |
michael@0 | 1093 | |
michael@0 | 1094 | testsuite.setAttribute("tests", str(total)) |
michael@0 | 1095 | testsuite.setAttribute("failures", str(failed)) |
michael@0 | 1096 | testsuite.setAttribute("skip", str(skipped)) |
michael@0 | 1097 | |
michael@0 | 1098 | doc.writexml(fh, addindent=" ", newl="\n", encoding="utf-8") |
michael@0 | 1099 | |
michael@0 | 1100 | def post_to_autolog(self, results, name): |
michael@0 | 1101 | from moztest.results import TestContext, TestResult, TestResultCollection |
michael@0 | 1102 | from moztest.output.autolog import AutologOutput |
michael@0 | 1103 | |
michael@0 | 1104 | context = TestContext( |
michael@0 | 1105 | testgroup='b2g xpcshell testsuite', |
michael@0 | 1106 | operating_system='android', |
michael@0 | 1107 | arch='emulator', |
michael@0 | 1108 | harness='xpcshell', |
michael@0 | 1109 | hostname=socket.gethostname(), |
michael@0 | 1110 | tree='b2g', |
michael@0 | 1111 | buildtype='opt', |
michael@0 | 1112 | ) |
michael@0 | 1113 | |
michael@0 | 1114 | collection = TestResultCollection('b2g emulator testsuite') |
michael@0 | 1115 | |
michael@0 | 1116 | for result in results: |
michael@0 | 1117 | duration = result.get('time', 0) |
michael@0 | 1118 | |
michael@0 | 1119 | if 'skipped' in result: |
michael@0 | 1120 | outcome = 'SKIPPED' |
michael@0 | 1121 | elif 'todo' in result: |
michael@0 | 1122 | outcome = 'KNOWN-FAIL' |
michael@0 | 1123 | elif result['passed']: |
michael@0 | 1124 | outcome = 'PASS' |
michael@0 | 1125 | else: |
michael@0 | 1126 | outcome = 'UNEXPECTED-FAIL' |
michael@0 | 1127 | |
michael@0 | 1128 | output = None |
michael@0 | 1129 | if 'failure' in result: |
michael@0 | 1130 | output = result['failure']['text'] |
michael@0 | 1131 | |
michael@0 | 1132 | t = TestResult(name=result['name'], test_class=name, |
michael@0 | 1133 | time_start=0, context=context) |
michael@0 | 1134 | t.finish(result=outcome, time_end=duration, output=output) |
michael@0 | 1135 | |
michael@0 | 1136 | collection.append(t) |
michael@0 | 1137 | collection.time_taken += duration |
michael@0 | 1138 | |
michael@0 | 1139 | out = AutologOutput() |
michael@0 | 1140 | out.post(out.make_testgroups(collection)) |
michael@0 | 1141 | |
michael@0 | 1142 | def buildXpcsRunArgs(self): |
michael@0 | 1143 | """ |
michael@0 | 1144 | Add arguments to run the test or make it interactive. |
michael@0 | 1145 | """ |
michael@0 | 1146 | if self.interactive: |
michael@0 | 1147 | self.xpcsRunArgs = [ |
michael@0 | 1148 | '-e', 'print("To start the test, type |_execute_test();|.");', |
michael@0 | 1149 | '-i'] |
michael@0 | 1150 | else: |
michael@0 | 1151 | self.xpcsRunArgs = ['-e', '_execute_test(); quit(0);'] |
michael@0 | 1152 | |
michael@0 | 1153 | def addTestResults(self, test): |
michael@0 | 1154 | self.passCount += test.passCount |
michael@0 | 1155 | self.failCount += test.failCount |
michael@0 | 1156 | self.todoCount += test.todoCount |
michael@0 | 1157 | self.xunitResults.append(test.xunit_result) |
michael@0 | 1158 | |
michael@0 | 1159 | def runTests(self, xpcshell, xrePath=None, appPath=None, symbolsPath=None, |
michael@0 | 1160 | manifest=None, testdirs=None, testPath=None, mobileArgs=None, |
michael@0 | 1161 | interactive=False, verbose=False, keepGoing=False, logfiles=True, |
michael@0 | 1162 | thisChunk=1, totalChunks=1, debugger=None, |
michael@0 | 1163 | debuggerArgs=None, debuggerInteractive=False, |
michael@0 | 1164 | profileName=None, mozInfo=None, sequential=False, shuffle=False, |
michael@0 | 1165 | testsRootDir=None, xunitFilename=None, xunitName=None, |
michael@0 | 1166 | testingModulesDir=None, autolog=False, pluginsPath=None, |
michael@0 | 1167 | testClass=XPCShellTestThread, failureManifest=None, |
michael@0 | 1168 | on_message=None, **otherOptions): |
michael@0 | 1169 | """Run xpcshell tests. |
michael@0 | 1170 | |
michael@0 | 1171 | |xpcshell|, is the xpcshell executable to use to run the tests. |
michael@0 | 1172 | |xrePath|, if provided, is the path to the XRE to use. |
michael@0 | 1173 | |appPath|, if provided, is the path to an application directory. |
michael@0 | 1174 | |symbolsPath|, if provided is the path to a directory containing |
michael@0 | 1175 | breakpad symbols for processing crashes in tests. |
michael@0 | 1176 | |manifest|, if provided, is a file containing a list of |
michael@0 | 1177 | test directories to run. |
michael@0 | 1178 | |testdirs|, if provided, is a list of absolute paths of test directories. |
michael@0 | 1179 | No-manifest only option. |
michael@0 | 1180 | |testPath|, if provided, indicates a single path and/or test to run. |
michael@0 | 1181 | |pluginsPath|, if provided, custom plugins directory to be returned from |
michael@0 | 1182 | the xpcshell dir svc provider for NS_APP_PLUGINS_DIR_LIST. |
michael@0 | 1183 | |interactive|, if set to True, indicates to provide an xpcshell prompt |
michael@0 | 1184 | instead of automatically executing the test. |
michael@0 | 1185 | |verbose|, if set to True, will cause stdout/stderr from tests to |
michael@0 | 1186 | be printed always |
michael@0 | 1187 | |logfiles|, if set to False, indicates not to save output to log files. |
michael@0 | 1188 | Non-interactive only option. |
michael@0 | 1189 | |debuggerInfo|, if set, specifies the debugger and debugger arguments |
michael@0 | 1190 | that will be used to launch xpcshell. |
michael@0 | 1191 | |profileName|, if set, specifies the name of the application for the profile |
michael@0 | 1192 | directory if running only a subset of tests. |
michael@0 | 1193 | |mozInfo|, if set, specifies specifies build configuration information, either as a filename containing JSON, or a dict. |
michael@0 | 1194 | |shuffle|, if True, execute tests in random order. |
michael@0 | 1195 | |testsRootDir|, absolute path to root directory of all tests. This is used |
michael@0 | 1196 | by xUnit generation to determine the package name of the tests. |
michael@0 | 1197 | |xunitFilename|, if set, specifies the filename to which to write xUnit XML |
michael@0 | 1198 | results. |
michael@0 | 1199 | |xunitName|, if outputting an xUnit XML file, the str value to use for the |
michael@0 | 1200 | testsuite name. |
michael@0 | 1201 | |testingModulesDir|, if provided, specifies where JS modules reside. |
michael@0 | 1202 | xpcshell will register a resource handler mapping this path. |
michael@0 | 1203 | |otherOptions| may be present for the convenience of subclasses |
michael@0 | 1204 | """ |
michael@0 | 1205 | |
michael@0 | 1206 | global gotSIGINT |
michael@0 | 1207 | |
michael@0 | 1208 | if testdirs is None: |
michael@0 | 1209 | testdirs = [] |
michael@0 | 1210 | |
michael@0 | 1211 | if xunitFilename is not None or xunitName is not None: |
michael@0 | 1212 | if not isinstance(testsRootDir, basestring): |
michael@0 | 1213 | raise Exception("testsRootDir must be a str when outputting xUnit.") |
michael@0 | 1214 | |
michael@0 | 1215 | if not os.path.isabs(testsRootDir): |
michael@0 | 1216 | testsRootDir = os.path.abspath(testsRootDir) |
michael@0 | 1217 | |
michael@0 | 1218 | if not os.path.exists(testsRootDir): |
michael@0 | 1219 | raise Exception("testsRootDir path does not exists: %s" % |
michael@0 | 1220 | testsRootDir) |
michael@0 | 1221 | |
michael@0 | 1222 | # Try to guess modules directory. |
michael@0 | 1223 | # This somewhat grotesque hack allows the buildbot machines to find the |
michael@0 | 1224 | # modules directory without having to configure the buildbot hosts. This |
michael@0 | 1225 | # code path should never be executed in local runs because the build system |
michael@0 | 1226 | # should always set this argument. |
michael@0 | 1227 | if not testingModulesDir: |
michael@0 | 1228 | ourDir = os.path.dirname(__file__) |
michael@0 | 1229 | possible = os.path.join(ourDir, os.path.pardir, 'modules') |
michael@0 | 1230 | |
michael@0 | 1231 | if os.path.isdir(possible): |
michael@0 | 1232 | testingModulesDir = possible |
michael@0 | 1233 | |
michael@0 | 1234 | if testingModulesDir: |
michael@0 | 1235 | # The resource loader expects native paths. Depending on how we were |
michael@0 | 1236 | # invoked, a UNIX style path may sneak in on Windows. We try to |
michael@0 | 1237 | # normalize that. |
michael@0 | 1238 | testingModulesDir = os.path.normpath(testingModulesDir) |
michael@0 | 1239 | |
michael@0 | 1240 | if not os.path.isabs(testingModulesDir): |
michael@0 | 1241 | testingModulesDir = os.path.abspath(testingModulesDir) |
michael@0 | 1242 | |
michael@0 | 1243 | if not testingModulesDir.endswith(os.path.sep): |
michael@0 | 1244 | testingModulesDir += os.path.sep |
michael@0 | 1245 | |
michael@0 | 1246 | self.xpcshell = xpcshell |
michael@0 | 1247 | self.xrePath = xrePath |
michael@0 | 1248 | self.appPath = appPath |
michael@0 | 1249 | self.symbolsPath = symbolsPath |
michael@0 | 1250 | self.manifest = manifest |
michael@0 | 1251 | self.testdirs = testdirs |
michael@0 | 1252 | self.testPath = testPath |
michael@0 | 1253 | self.interactive = interactive |
michael@0 | 1254 | self.verbose = verbose |
michael@0 | 1255 | self.keepGoing = keepGoing |
michael@0 | 1256 | self.logfiles = logfiles |
michael@0 | 1257 | self.on_message = on_message |
michael@0 | 1258 | self.totalChunks = totalChunks |
michael@0 | 1259 | self.thisChunk = thisChunk |
michael@0 | 1260 | self.debuggerInfo = getDebuggerInfo(self.oldcwd, debugger, debuggerArgs, debuggerInteractive) |
michael@0 | 1261 | self.profileName = profileName or "xpcshell" |
michael@0 | 1262 | self.mozInfo = mozInfo |
michael@0 | 1263 | self.testingModulesDir = testingModulesDir |
michael@0 | 1264 | self.pluginsPath = pluginsPath |
michael@0 | 1265 | self.sequential = sequential |
michael@0 | 1266 | |
michael@0 | 1267 | if not testdirs and not manifest: |
michael@0 | 1268 | # nothing to test! |
michael@0 | 1269 | self.log.error("Error: No test dirs or test manifest specified!") |
michael@0 | 1270 | return False |
michael@0 | 1271 | |
michael@0 | 1272 | self.testCount = 0 |
michael@0 | 1273 | self.passCount = 0 |
michael@0 | 1274 | self.failCount = 0 |
michael@0 | 1275 | self.todoCount = 0 |
michael@0 | 1276 | |
michael@0 | 1277 | self.setAbsPath() |
michael@0 | 1278 | self.buildXpcsRunArgs() |
michael@0 | 1279 | |
michael@0 | 1280 | self.event = Event() |
michael@0 | 1281 | |
michael@0 | 1282 | # Handle filenames in mozInfo |
michael@0 | 1283 | if not isinstance(self.mozInfo, dict): |
michael@0 | 1284 | mozInfoFile = self.mozInfo |
michael@0 | 1285 | if not os.path.isfile(mozInfoFile): |
michael@0 | 1286 | self.log.error("Error: couldn't find mozinfo.json at '%s'. Perhaps you need to use --build-info-json?" % mozInfoFile) |
michael@0 | 1287 | return False |
michael@0 | 1288 | self.mozInfo = json.load(open(mozInfoFile)) |
michael@0 | 1289 | |
michael@0 | 1290 | # mozinfo.info is used as kwargs. Some builds are done with |
michael@0 | 1291 | # an older Python that can't handle Unicode keys in kwargs. |
michael@0 | 1292 | # All of the keys in question should be ASCII. |
michael@0 | 1293 | fixedInfo = {} |
michael@0 | 1294 | for k, v in self.mozInfo.items(): |
michael@0 | 1295 | if isinstance(k, unicode): |
michael@0 | 1296 | k = k.encode('ascii') |
michael@0 | 1297 | fixedInfo[k] = v |
michael@0 | 1298 | self.mozInfo = fixedInfo |
michael@0 | 1299 | |
michael@0 | 1300 | mozinfo.update(self.mozInfo) |
michael@0 | 1301 | |
michael@0 | 1302 | # buildEnvironment() needs mozInfo, so we call it after mozInfo is initialized. |
michael@0 | 1303 | self.buildEnvironment() |
michael@0 | 1304 | |
michael@0 | 1305 | # The appDirKey is a optional entry in either the default or individual test |
michael@0 | 1306 | # sections that defines a relative application directory for test runs. If |
michael@0 | 1307 | # defined we pass 'grePath/$appDirKey' for the -a parameter of the xpcshell |
michael@0 | 1308 | # test harness. |
michael@0 | 1309 | appDirKey = None |
michael@0 | 1310 | if "appname" in self.mozInfo: |
michael@0 | 1311 | appDirKey = self.mozInfo["appname"] + "-appdir" |
michael@0 | 1312 | |
michael@0 | 1313 | # We have to do this before we build the test list so we know whether or |
michael@0 | 1314 | # not to run tests that depend on having the node spdy server |
michael@0 | 1315 | self.trySetupNode() |
michael@0 | 1316 | |
michael@0 | 1317 | pStdout, pStderr = self.getPipes() |
michael@0 | 1318 | |
michael@0 | 1319 | self.buildTestList() |
michael@0 | 1320 | if self.singleFile: |
michael@0 | 1321 | self.sequential = True |
michael@0 | 1322 | |
michael@0 | 1323 | if shuffle: |
michael@0 | 1324 | random.shuffle(self.alltests) |
michael@0 | 1325 | |
michael@0 | 1326 | self.xunitResults = [] |
michael@0 | 1327 | self.cleanup_dir_list = [] |
michael@0 | 1328 | self.try_again_list = [] |
michael@0 | 1329 | |
michael@0 | 1330 | kwargs = { |
michael@0 | 1331 | 'appPath': self.appPath, |
michael@0 | 1332 | 'xrePath': self.xrePath, |
michael@0 | 1333 | 'testingModulesDir': self.testingModulesDir, |
michael@0 | 1334 | 'debuggerInfo': self.debuggerInfo, |
michael@0 | 1335 | 'pluginsPath': self.pluginsPath, |
michael@0 | 1336 | 'httpdManifest': self.httpdManifest, |
michael@0 | 1337 | 'httpdJSPath': self.httpdJSPath, |
michael@0 | 1338 | 'headJSPath': self.headJSPath, |
michael@0 | 1339 | 'testharnessdir': self.testharnessdir, |
michael@0 | 1340 | 'profileName': self.profileName, |
michael@0 | 1341 | 'singleFile': self.singleFile, |
michael@0 | 1342 | 'env': self.env, # making a copy of this in the testthreads |
michael@0 | 1343 | 'symbolsPath': self.symbolsPath, |
michael@0 | 1344 | 'logfiles': self.logfiles, |
michael@0 | 1345 | 'xpcshell': self.xpcshell, |
michael@0 | 1346 | 'xpcsRunArgs': self.xpcsRunArgs, |
michael@0 | 1347 | 'failureManifest': failureManifest, |
michael@0 | 1348 | 'on_message': self.on_message, |
michael@0 | 1349 | } |
michael@0 | 1350 | |
michael@0 | 1351 | if self.sequential: |
michael@0 | 1352 | # Allow user to kill hung xpcshell subprocess with SIGINT |
michael@0 | 1353 | # when we are only running tests sequentially. |
michael@0 | 1354 | signal.signal(signal.SIGINT, markGotSIGINT) |
michael@0 | 1355 | |
michael@0 | 1356 | if self.debuggerInfo: |
michael@0 | 1357 | # Force a sequential run |
michael@0 | 1358 | self.sequential = True |
michael@0 | 1359 | |
michael@0 | 1360 | # If we have an interactive debugger, disable SIGINT entirely. |
michael@0 | 1361 | if self.debuggerInfo["interactive"]: |
michael@0 | 1362 | signal.signal(signal.SIGINT, lambda signum, frame: None) |
michael@0 | 1363 | |
michael@0 | 1364 | # create a queue of all tests that will run |
michael@0 | 1365 | tests_queue = deque() |
michael@0 | 1366 | # also a list for the tests that need to be run sequentially |
michael@0 | 1367 | sequential_tests = [] |
michael@0 | 1368 | for test_object in self.alltests: |
michael@0 | 1369 | name = test_object['path'] |
michael@0 | 1370 | if self.singleFile and not name.endswith(self.singleFile): |
michael@0 | 1371 | continue |
michael@0 | 1372 | |
michael@0 | 1373 | if self.testPath and name.find(self.testPath) == -1: |
michael@0 | 1374 | continue |
michael@0 | 1375 | |
michael@0 | 1376 | self.testCount += 1 |
michael@0 | 1377 | |
michael@0 | 1378 | test = testClass(test_object, self.event, self.cleanup_dir_list, |
michael@0 | 1379 | tests_root_dir=testsRootDir, app_dir_key=appDirKey, |
michael@0 | 1380 | interactive=interactive, verbose=verbose, pStdout=pStdout, |
michael@0 | 1381 | pStderr=pStderr, keep_going=keepGoing, log=self.log, |
michael@0 | 1382 | mobileArgs=mobileArgs, **kwargs) |
michael@0 | 1383 | if 'run-sequentially' in test_object or self.sequential: |
michael@0 | 1384 | sequential_tests.append(test) |
michael@0 | 1385 | else: |
michael@0 | 1386 | tests_queue.append(test) |
michael@0 | 1387 | |
michael@0 | 1388 | if self.sequential: |
michael@0 | 1389 | self.log.info("INFO | Running tests sequentially.") |
michael@0 | 1390 | else: |
michael@0 | 1391 | self.log.info("INFO | Using at most %d threads." % NUM_THREADS) |
michael@0 | 1392 | |
michael@0 | 1393 | # keep a set of NUM_THREADS running tests and start running the |
michael@0 | 1394 | # tests in the queue at most NUM_THREADS at a time |
michael@0 | 1395 | running_tests = set() |
michael@0 | 1396 | keep_going = True |
michael@0 | 1397 | exceptions = [] |
michael@0 | 1398 | tracebacks = [] |
michael@0 | 1399 | while tests_queue or running_tests: |
michael@0 | 1400 | # if we're not supposed to continue and all of the running tests |
michael@0 | 1401 | # are done, stop |
michael@0 | 1402 | if not keep_going and not running_tests: |
michael@0 | 1403 | break |
michael@0 | 1404 | |
michael@0 | 1405 | # if there's room to run more tests, start running them |
michael@0 | 1406 | while keep_going and tests_queue and (len(running_tests) < NUM_THREADS): |
michael@0 | 1407 | test = tests_queue.popleft() |
michael@0 | 1408 | running_tests.add(test) |
michael@0 | 1409 | test.start() |
michael@0 | 1410 | |
michael@0 | 1411 | # queue is full (for now) or no more new tests, |
michael@0 | 1412 | # process the finished tests so far |
michael@0 | 1413 | |
michael@0 | 1414 | # wait for at least one of the tests to finish |
michael@0 | 1415 | self.event.wait(1) |
michael@0 | 1416 | self.event.clear() |
michael@0 | 1417 | |
michael@0 | 1418 | # find what tests are done (might be more than 1) |
michael@0 | 1419 | done_tests = set() |
michael@0 | 1420 | for test in running_tests: |
michael@0 | 1421 | if test.done: |
michael@0 | 1422 | done_tests.add(test) |
michael@0 | 1423 | test.join(1) # join with timeout so we don't hang on blocked threads |
michael@0 | 1424 | # if the test had trouble, we will try running it again |
michael@0 | 1425 | # at the end of the run |
michael@0 | 1426 | if test.retry or test.is_alive(): |
michael@0 | 1427 | # if the join call timed out, test.is_alive => True |
michael@0 | 1428 | self.try_again_list.append(test.test_object) |
michael@0 | 1429 | continue |
michael@0 | 1430 | # did the test encounter any exception? |
michael@0 | 1431 | if test.exception: |
michael@0 | 1432 | exceptions.append(test.exception) |
michael@0 | 1433 | tracebacks.append(test.traceback) |
michael@0 | 1434 | # we won't add any more tests, will just wait for |
michael@0 | 1435 | # the currently running ones to finish |
michael@0 | 1436 | keep_going = False |
michael@0 | 1437 | keep_going = keep_going and test.keep_going |
michael@0 | 1438 | self.addTestResults(test) |
michael@0 | 1439 | |
michael@0 | 1440 | # make room for new tests to run |
michael@0 | 1441 | running_tests.difference_update(done_tests) |
michael@0 | 1442 | |
michael@0 | 1443 | if keep_going: |
michael@0 | 1444 | # run the other tests sequentially |
michael@0 | 1445 | for test in sequential_tests: |
michael@0 | 1446 | if not keep_going: |
michael@0 | 1447 | self.log.error("TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so stopped run. " \ |
michael@0 | 1448 | "(Use --keep-going to keep running tests after killing one with SIGINT)") |
michael@0 | 1449 | break |
michael@0 | 1450 | # we don't want to retry these tests |
michael@0 | 1451 | test.retry = False |
michael@0 | 1452 | test.start() |
michael@0 | 1453 | test.join() |
michael@0 | 1454 | self.addTestResults(test) |
michael@0 | 1455 | # did the test encounter any exception? |
michael@0 | 1456 | if test.exception: |
michael@0 | 1457 | exceptions.append(test.exception) |
michael@0 | 1458 | tracebacks.append(test.traceback) |
michael@0 | 1459 | break |
michael@0 | 1460 | keep_going = test.keep_going |
michael@0 | 1461 | |
michael@0 | 1462 | # retry tests that failed when run in parallel |
michael@0 | 1463 | if self.try_again_list: |
michael@0 | 1464 | self.log.info("Retrying tests that failed when run in parallel.") |
michael@0 | 1465 | for test_object in self.try_again_list: |
michael@0 | 1466 | test = testClass(test_object, self.event, self.cleanup_dir_list, |
michael@0 | 1467 | retry=False, tests_root_dir=testsRootDir, |
michael@0 | 1468 | app_dir_key=appDirKey, interactive=interactive, |
michael@0 | 1469 | verbose=verbose, pStdout=pStdout, pStderr=pStderr, |
michael@0 | 1470 | keep_going=keepGoing, log=self.log, mobileArgs=mobileArgs, |
michael@0 | 1471 | **kwargs) |
michael@0 | 1472 | test.start() |
michael@0 | 1473 | test.join() |
michael@0 | 1474 | self.addTestResults(test) |
michael@0 | 1475 | # did the test encounter any exception? |
michael@0 | 1476 | if test.exception: |
michael@0 | 1477 | exceptions.append(test.exception) |
michael@0 | 1478 | tracebacks.append(test.traceback) |
michael@0 | 1479 | break |
michael@0 | 1480 | keep_going = test.keep_going |
michael@0 | 1481 | |
michael@0 | 1482 | # restore default SIGINT behaviour |
michael@0 | 1483 | signal.signal(signal.SIGINT, signal.SIG_DFL) |
michael@0 | 1484 | |
michael@0 | 1485 | self.shutdownNode() |
michael@0 | 1486 | # Clean up any slacker directories that might be lying around |
michael@0 | 1487 | # Some might fail because of windows taking too long to unlock them. |
michael@0 | 1488 | # We don't do anything if this fails because the test slaves will have |
michael@0 | 1489 | # their $TEMP dirs cleaned up on reboot anyway. |
michael@0 | 1490 | for directory in self.cleanup_dir_list: |
michael@0 | 1491 | try: |
michael@0 | 1492 | shutil.rmtree(directory) |
michael@0 | 1493 | except: |
michael@0 | 1494 | self.log.info("INFO | %s could not be cleaned up." % directory) |
michael@0 | 1495 | |
michael@0 | 1496 | if exceptions: |
michael@0 | 1497 | self.log.info("INFO | Following exceptions were raised:") |
michael@0 | 1498 | for t in tracebacks: |
michael@0 | 1499 | self.log.error(t) |
michael@0 | 1500 | raise exceptions[0] |
michael@0 | 1501 | |
michael@0 | 1502 | if self.testCount == 0: |
michael@0 | 1503 | self.log.error("TEST-UNEXPECTED-FAIL | runxpcshelltests.py | No tests run. Did you pass an invalid --test-path?") |
michael@0 | 1504 | self.failCount = 1 |
michael@0 | 1505 | |
michael@0 | 1506 | self.log.info("INFO | Result summary:") |
michael@0 | 1507 | self.log.info("INFO | Passed: %d" % self.passCount) |
michael@0 | 1508 | self.log.info("INFO | Failed: %d" % self.failCount) |
michael@0 | 1509 | self.log.info("INFO | Todo: %d" % self.todoCount) |
michael@0 | 1510 | self.log.info("INFO | Retried: %d" % len(self.try_again_list)) |
michael@0 | 1511 | |
michael@0 | 1512 | if autolog: |
michael@0 | 1513 | self.post_to_autolog(self.xunitResults, xunitName) |
michael@0 | 1514 | |
michael@0 | 1515 | if xunitFilename is not None: |
michael@0 | 1516 | self.writeXunitResults(filename=xunitFilename, results=self.xunitResults, |
michael@0 | 1517 | name=xunitName) |
michael@0 | 1518 | |
michael@0 | 1519 | if gotSIGINT and not keepGoing: |
michael@0 | 1520 | self.log.error("TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so stopped run. " \ |
michael@0 | 1521 | "(Use --keep-going to keep running tests after killing one with SIGINT)") |
michael@0 | 1522 | return False |
michael@0 | 1523 | |
michael@0 | 1524 | return self.failCount == 0 |
michael@0 | 1525 | |
michael@0 | 1526 | class XPCShellOptions(OptionParser): |
michael@0 | 1527 | def __init__(self): |
michael@0 | 1528 | """Process command line arguments and call runTests() to do the real work.""" |
michael@0 | 1529 | OptionParser.__init__(self) |
michael@0 | 1530 | |
michael@0 | 1531 | addCommonOptions(self) |
michael@0 | 1532 | self.add_option("--app-path", |
michael@0 | 1533 | type="string", dest="appPath", default=None, |
michael@0 | 1534 | help="application directory (as opposed to XRE directory)") |
michael@0 | 1535 | self.add_option("--autolog", |
michael@0 | 1536 | action="store_true", dest="autolog", default=False, |
michael@0 | 1537 | help="post to autolog") |
michael@0 | 1538 | self.add_option("--interactive", |
michael@0 | 1539 | action="store_true", dest="interactive", default=False, |
michael@0 | 1540 | help="don't automatically run tests, drop to an xpcshell prompt") |
michael@0 | 1541 | self.add_option("--verbose", |
michael@0 | 1542 | action="store_true", dest="verbose", default=False, |
michael@0 | 1543 | help="always print stdout and stderr from tests") |
michael@0 | 1544 | self.add_option("--keep-going", |
michael@0 | 1545 | action="store_true", dest="keepGoing", default=False, |
michael@0 | 1546 | help="continue running tests after test killed with control-C (SIGINT)") |
michael@0 | 1547 | self.add_option("--logfiles", |
michael@0 | 1548 | action="store_true", dest="logfiles", default=True, |
michael@0 | 1549 | help="create log files (default, only used to override --no-logfiles)") |
michael@0 | 1550 | self.add_option("--manifest", |
michael@0 | 1551 | type="string", dest="manifest", default=None, |
michael@0 | 1552 | help="Manifest of test directories to use") |
michael@0 | 1553 | self.add_option("--no-logfiles", |
michael@0 | 1554 | action="store_false", dest="logfiles", |
michael@0 | 1555 | help="don't create log files") |
michael@0 | 1556 | self.add_option("--sequential", |
michael@0 | 1557 | action="store_true", dest="sequential", default=False, |
michael@0 | 1558 | help="Run all tests sequentially") |
michael@0 | 1559 | self.add_option("--test-path", |
michael@0 | 1560 | type="string", dest="testPath", default=None, |
michael@0 | 1561 | help="single path and/or test filename to test") |
michael@0 | 1562 | self.add_option("--tests-root-dir", |
michael@0 | 1563 | type="string", dest="testsRootDir", default=None, |
michael@0 | 1564 | help="absolute path to directory where all tests are located. this is typically $(objdir)/_tests") |
michael@0 | 1565 | self.add_option("--testing-modules-dir", |
michael@0 | 1566 | dest="testingModulesDir", default=None, |
michael@0 | 1567 | help="Directory where testing modules are located.") |
michael@0 | 1568 | self.add_option("--test-plugin-path", |
michael@0 | 1569 | type="string", dest="pluginsPath", default=None, |
michael@0 | 1570 | help="Path to the location of a plugins directory containing the test plugin or plugins required for tests. " |
michael@0 | 1571 | "By default xpcshell's dir svc provider returns gre/plugins. Use test-plugin-path to add a directory " |
michael@0 | 1572 | "to return for NS_APP_PLUGINS_DIR_LIST when queried.") |
michael@0 | 1573 | self.add_option("--total-chunks", |
michael@0 | 1574 | type = "int", dest = "totalChunks", default=1, |
michael@0 | 1575 | help = "how many chunks to split the tests up into") |
michael@0 | 1576 | self.add_option("--this-chunk", |
michael@0 | 1577 | type = "int", dest = "thisChunk", default=1, |
michael@0 | 1578 | help = "which chunk to run between 1 and --total-chunks") |
michael@0 | 1579 | self.add_option("--profile-name", |
michael@0 | 1580 | type = "string", dest="profileName", default=None, |
michael@0 | 1581 | help="name of application profile being tested") |
michael@0 | 1582 | self.add_option("--build-info-json", |
michael@0 | 1583 | type = "string", dest="mozInfo", default=None, |
michael@0 | 1584 | help="path to a mozinfo.json including information about the build configuration. defaults to looking for mozinfo.json next to the script.") |
michael@0 | 1585 | self.add_option("--shuffle", |
michael@0 | 1586 | action="store_true", dest="shuffle", default=False, |
michael@0 | 1587 | help="Execute tests in random order") |
michael@0 | 1588 | self.add_option("--xunit-file", dest="xunitFilename", |
michael@0 | 1589 | help="path to file where xUnit results will be written.") |
michael@0 | 1590 | self.add_option("--xunit-suite-name", dest="xunitName", |
michael@0 | 1591 | help="name to record for this xUnit test suite. Many " |
michael@0 | 1592 | "tools expect Java class notation, e.g. " |
michael@0 | 1593 | "dom.basic.foo") |
michael@0 | 1594 | self.add_option("--failure-manifest", dest="failureManifest", |
michael@0 | 1595 | action="store", |
michael@0 | 1596 | help="path to file where failure manifest will be written.") |
michael@0 | 1597 | |
michael@0 | 1598 | def main(): |
michael@0 | 1599 | parser = XPCShellOptions() |
michael@0 | 1600 | options, args = parser.parse_args() |
michael@0 | 1601 | |
michael@0 | 1602 | if len(args) < 2 and options.manifest is None or \ |
michael@0 | 1603 | (len(args) < 1 and options.manifest is not None): |
michael@0 | 1604 | print >>sys.stderr, """Usage: %s <path to xpcshell> <test dirs> |
michael@0 | 1605 | or: %s --manifest=test.manifest <path to xpcshell>""" % (sys.argv[0], |
michael@0 | 1606 | sys.argv[0]) |
michael@0 | 1607 | sys.exit(1) |
michael@0 | 1608 | |
michael@0 | 1609 | xpcsh = XPCShellTests() |
michael@0 | 1610 | |
michael@0 | 1611 | if options.interactive and not options.testPath: |
michael@0 | 1612 | print >>sys.stderr, "Error: You must specify a test filename in interactive mode!" |
michael@0 | 1613 | sys.exit(1) |
michael@0 | 1614 | |
michael@0 | 1615 | if not xpcsh.runTests(args[0], testdirs=args[1:], **options.__dict__): |
michael@0 | 1616 | sys.exit(1) |
michael@0 | 1617 | |
michael@0 | 1618 | if __name__ == '__main__': |
michael@0 | 1619 | main() |