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