testing/xpcshell/runxpcshelltests.py

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rwxr-xr-x

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

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()

mercurial