testing/mochitest/runtests.py

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:026554cc84b0
1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5 """
6 Runs the Mochitest test harness.
7 """
8
9 from __future__ import with_statement
10 import os
11 import sys
12 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
13 sys.path.insert(0, SCRIPT_DIR);
14
15 import glob
16 import json
17 import mozcrash
18 import mozinfo
19 import mozprocess
20 import mozrunner
21 import optparse
22 import re
23 import shutil
24 import signal
25 import subprocess
26 import tempfile
27 import time
28 import traceback
29 import urllib2
30 import zipfile
31
32 from automationutils import environment, getDebuggerInfo, isURL, KeyValueParseError, parseKeyValue, processLeakLog, systemMemory, dumpScreen, ShutdownLeaks, printstatus
33 from datetime import datetime
34 from manifestparser import TestManifest
35 from mochitest_options import MochitestOptions
36 from mozprofile import Profile, Preferences
37 from mozprofile.permissions import ServerLocations
38 from urllib import quote_plus as encodeURIComponent
39
40 # This should use the `which` module already in tree, but it is
41 # not yet present in the mozharness environment
42 from mozrunner.utils import findInPath as which
43
44 # set up logging handler a la automation.py.in for compatability
45 import logging
46 log = logging.getLogger()
47 def resetGlobalLog():
48 while log.handlers:
49 log.removeHandler(log.handlers[0])
50 handler = logging.StreamHandler(sys.stdout)
51 log.setLevel(logging.INFO)
52 log.addHandler(handler)
53 resetGlobalLog()
54
55 ###########################
56 # Option for NSPR logging #
57 ###########################
58
59 # Set the desired log modules you want an NSPR log be produced by a try run for, or leave blank to disable the feature.
60 # This will be passed to NSPR_LOG_MODULES environment variable. Try run will then put a download link for the log file
61 # on tbpl.mozilla.org.
62
63 NSPR_LOG_MODULES = ""
64
65 ####################
66 # PROCESS HANDLING #
67 ####################
68
69 def call(*args, **kwargs):
70 """front-end function to mozprocess.ProcessHandler"""
71 # TODO: upstream -> mozprocess
72 # https://bugzilla.mozilla.org/show_bug.cgi?id=791383
73 process = mozprocess.ProcessHandler(*args, **kwargs)
74 process.run()
75 return process.wait()
76
77 def killPid(pid):
78 # see also https://bugzilla.mozilla.org/show_bug.cgi?id=911249#c58
79 try:
80 os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
81 except Exception, e:
82 log.info("Failed to kill process %d: %s", pid, str(e))
83
84 if mozinfo.isWin:
85 import ctypes, ctypes.wintypes, time, msvcrt
86
87 def isPidAlive(pid):
88 STILL_ACTIVE = 259
89 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
90 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
91 if not pHandle:
92 return False
93 pExitCode = ctypes.wintypes.DWORD()
94 ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
95 ctypes.windll.kernel32.CloseHandle(pHandle)
96 return pExitCode.value == STILL_ACTIVE
97
98 else:
99 import errno
100
101 def isPidAlive(pid):
102 try:
103 # kill(pid, 0) checks for a valid PID without actually sending a signal
104 # The method throws OSError if the PID is invalid, which we catch below.
105 os.kill(pid, 0)
106
107 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
108 # the process terminates before we get to this point.
109 wpid, wstatus = os.waitpid(pid, os.WNOHANG)
110 return wpid == 0
111 except OSError, err:
112 # Catch the errors we might expect from os.kill/os.waitpid,
113 # and re-raise any others
114 if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
115 return False
116 raise
117 # TODO: ^ upstream isPidAlive to mozprocess
118
119 #######################
120 # HTTP SERVER SUPPORT #
121 #######################
122
123 class MochitestServer(object):
124 "Web server used to serve Mochitests, for closer fidelity to the real web."
125
126 def __init__(self, options):
127 if isinstance(options, optparse.Values):
128 options = vars(options)
129 self._closeWhenDone = options['closeWhenDone']
130 self._utilityPath = options['utilityPath']
131 self._xrePath = options['xrePath']
132 self._profileDir = options['profilePath']
133 self.webServer = options['webServer']
134 self.httpPort = options['httpPort']
135 self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { "server" : self.webServer, "port" : self.httpPort }
136 self.testPrefix = "'webapprt_'" if options.get('webapprtContent') else "undefined"
137
138 if options.get('httpdPath'):
139 self._httpdPath = options['httpdPath']
140 else:
141 self._httpdPath = SCRIPT_DIR
142 self._httpdPath = os.path.abspath(self._httpdPath)
143
144 def start(self):
145 "Run the Mochitest server, returning the process ID of the server."
146
147 # get testing environment
148 env = environment(xrePath=self._xrePath)
149 env["XPCOM_DEBUG_BREAK"] = "warn"
150
151 # When running with an ASan build, our xpcshell server will also be ASan-enabled,
152 # thus consuming too much resources when running together with the browser on
153 # the test slaves. Try to limit the amount of resources by disabling certain
154 # features.
155 env["ASAN_OPTIONS"] = "quarantine_size=1:redzone=32:malloc_context_size=5"
156
157 if mozinfo.isWin:
158 env["PATH"] = env["PATH"] + ";" + str(self._xrePath)
159
160 args = ["-g", self._xrePath,
161 "-v", "170",
162 "-f", os.path.join(self._httpdPath, "httpd.js"),
163 "-e", """const _PROFILE_PATH = '%(profile)s'; const _SERVER_PORT = '%(port)s'; const _SERVER_ADDR = '%(server)s'; const _TEST_PREFIX = %(testPrefix)s; const _DISPLAY_RESULTS = %(displayResults)s;""" %
164 {"profile" : self._profileDir.replace('\\', '\\\\'), "port" : self.httpPort, "server" : self.webServer,
165 "testPrefix" : self.testPrefix, "displayResults" : str(not self._closeWhenDone).lower() },
166 "-f", os.path.join(SCRIPT_DIR, "server.js")]
167
168 xpcshell = os.path.join(self._utilityPath,
169 "xpcshell" + mozinfo.info['bin_suffix'])
170 command = [xpcshell] + args
171 self._process = mozprocess.ProcessHandler(command, cwd=SCRIPT_DIR, env=env)
172 self._process.run()
173 log.info("%s : launching %s", self.__class__.__name__, command)
174 pid = self._process.pid
175 log.info("runtests.py | Server pid: %d", pid)
176
177 def ensureReady(self, timeout):
178 assert timeout >= 0
179
180 aliveFile = os.path.join(self._profileDir, "server_alive.txt")
181 i = 0
182 while i < timeout:
183 if os.path.exists(aliveFile):
184 break
185 time.sleep(1)
186 i += 1
187 else:
188 log.error("TEST-UNEXPECTED-FAIL | runtests.py | Timed out while waiting for server startup.")
189 self.stop()
190 sys.exit(1)
191
192 def stop(self):
193 try:
194 with urllib2.urlopen(self.shutdownURL) as c:
195 c.read()
196
197 # TODO: need ProcessHandler.poll()
198 # https://bugzilla.mozilla.org/show_bug.cgi?id=912285
199 # rtncode = self._process.poll()
200 rtncode = self._process.proc.poll()
201 if rtncode is None:
202 # TODO: need ProcessHandler.terminate() and/or .send_signal()
203 # https://bugzilla.mozilla.org/show_bug.cgi?id=912285
204 # self._process.terminate()
205 self._process.proc.terminate()
206 except:
207 self._process.kill()
208
209 class WebSocketServer(object):
210 "Class which encapsulates the mod_pywebsocket server"
211
212 def __init__(self, options, scriptdir, debuggerInfo=None):
213 self.port = options.webSocketPort
214 self._scriptdir = scriptdir
215 self.debuggerInfo = debuggerInfo
216
217 def start(self):
218 # Invoke pywebsocket through a wrapper which adds special SIGINT handling.
219 #
220 # If we're in an interactive debugger, the wrapper causes the server to
221 # ignore SIGINT so the server doesn't capture a ctrl+c meant for the
222 # debugger.
223 #
224 # If we're not in an interactive debugger, the wrapper causes the server to
225 # die silently upon receiving a SIGINT.
226 scriptPath = 'pywebsocket_wrapper.py'
227 script = os.path.join(self._scriptdir, scriptPath)
228
229 cmd = [sys.executable, script]
230 if self.debuggerInfo and self.debuggerInfo['interactive']:
231 cmd += ['--interactive']
232 cmd += ['-p', str(self.port), '-w', self._scriptdir, '-l', \
233 os.path.join(self._scriptdir, "websock.log"), \
234 '--log-level=debug', '--allow-handlers-outside-root-dir']
235 # start the process
236 self._process = mozprocess.ProcessHandler(cmd, cwd=SCRIPT_DIR)
237 self._process.run()
238 pid = self._process.pid
239 log.info("runtests.py | Websocket server pid: %d", pid)
240
241 def stop(self):
242 self._process.kill()
243
244 class MochitestUtilsMixin(object):
245 """
246 Class containing some utility functions common to both local and remote
247 mochitest runners
248 """
249
250 # TODO Utility classes are a code smell. This class is temporary
251 # and should be removed when desktop mochitests are refactored
252 # on top of mozbase. Each of the functions in here should
253 # probably live somewhere in mozbase
254
255 oldcwd = os.getcwd()
256 jarDir = 'mochijar'
257
258 # Path to the test script on the server
259 TEST_PATH = "tests"
260 CHROME_PATH = "redirect.html"
261 urlOpts = []
262
263 def __init__(self):
264 self.update_mozinfo()
265 self.server = None
266 self.wsserver = None
267 self.sslTunnel = None
268 self._locations = None
269
270 def update_mozinfo(self):
271 """walk up directories to find mozinfo.json update the info"""
272 # TODO: This should go in a more generic place, e.g. mozinfo
273
274 path = SCRIPT_DIR
275 dirs = set()
276 while path != os.path.expanduser('~'):
277 if path in dirs:
278 break
279 dirs.add(path)
280 path = os.path.split(path)[0]
281
282 mozinfo.find_and_update_from_json(*dirs)
283
284 def getFullPath(self, path):
285 " Get an absolute path relative to self.oldcwd."
286 return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
287
288 def getLogFilePath(self, logFile):
289 """ return the log file path relative to the device we are testing on, in most cases
290 it will be the full path on the local system
291 """
292 return self.getFullPath(logFile)
293
294 @property
295 def locations(self):
296 if self._locations is not None:
297 return self._locations
298 locations_file = os.path.join(SCRIPT_DIR, 'server-locations.txt')
299 self._locations = ServerLocations(locations_file)
300 return self._locations
301
302 def buildURLOptions(self, options, env):
303 """ Add test control options from the command line to the url
304
305 URL parameters to test URL:
306
307 autorun -- kick off tests automatically
308 closeWhenDone -- closes the browser after the tests
309 hideResultsTable -- hides the table of individual test results
310 logFile -- logs test run to an absolute path
311 totalChunks -- how many chunks to split tests into
312 thisChunk -- which chunk to run
313 startAt -- name of test to start at
314 endAt -- name of test to end at
315 timeout -- per-test timeout in seconds
316 repeat -- How many times to repeat the test, ie: repeat=1 will run the test twice.
317 """
318
319 # allow relative paths for logFile
320 if options.logFile:
321 options.logFile = self.getLogFilePath(options.logFile)
322
323 # Note that all tests under options.subsuite need to be browser chrome tests.
324 if options.browserChrome or options.chrome or options.subsuite or \
325 options.a11y or options.webapprtChrome:
326 self.makeTestConfig(options)
327 else:
328 if options.autorun:
329 self.urlOpts.append("autorun=1")
330 if options.timeout:
331 self.urlOpts.append("timeout=%d" % options.timeout)
332 if options.closeWhenDone:
333 self.urlOpts.append("closeWhenDone=1")
334 if options.logFile:
335 self.urlOpts.append("logFile=" + encodeURIComponent(options.logFile))
336 self.urlOpts.append("fileLevel=" + encodeURIComponent(options.fileLevel))
337 if options.consoleLevel:
338 self.urlOpts.append("consoleLevel=" + encodeURIComponent(options.consoleLevel))
339 if options.totalChunks:
340 self.urlOpts.append("totalChunks=%d" % options.totalChunks)
341 self.urlOpts.append("thisChunk=%d" % options.thisChunk)
342 if options.chunkByDir:
343 self.urlOpts.append("chunkByDir=%d" % options.chunkByDir)
344 if options.startAt:
345 self.urlOpts.append("startAt=%s" % options.startAt)
346 if options.endAt:
347 self.urlOpts.append("endAt=%s" % options.endAt)
348 if options.shuffle:
349 self.urlOpts.append("shuffle=1")
350 if "MOZ_HIDE_RESULTS_TABLE" in env and env["MOZ_HIDE_RESULTS_TABLE"] == "1":
351 self.urlOpts.append("hideResultsTable=1")
352 if options.runUntilFailure:
353 self.urlOpts.append("runUntilFailure=1")
354 if options.repeat:
355 self.urlOpts.append("repeat=%d" % options.repeat)
356 if os.path.isfile(os.path.join(self.oldcwd, os.path.dirname(__file__), self.TEST_PATH, options.testPath)) and options.repeat > 0:
357 self.urlOpts.append("testname=%s" % ("/").join([self.TEST_PATH, options.testPath]))
358 if options.testManifest:
359 self.urlOpts.append("testManifest=%s" % options.testManifest)
360 if hasattr(options, 'runOnly') and options.runOnly:
361 self.urlOpts.append("runOnly=true")
362 else:
363 self.urlOpts.append("runOnly=false")
364 if options.manifestFile:
365 self.urlOpts.append("manifestFile=%s" % options.manifestFile)
366 if options.failureFile:
367 self.urlOpts.append("failureFile=%s" % self.getFullPath(options.failureFile))
368 if options.runSlower:
369 self.urlOpts.append("runSlower=true")
370 if options.debugOnFailure:
371 self.urlOpts.append("debugOnFailure=true")
372 if options.dumpOutputDirectory:
373 self.urlOpts.append("dumpOutputDirectory=%s" % encodeURIComponent(options.dumpOutputDirectory))
374 if options.dumpAboutMemoryAfterTest:
375 self.urlOpts.append("dumpAboutMemoryAfterTest=true")
376 if options.dumpDMDAfterTest:
377 self.urlOpts.append("dumpDMDAfterTest=true")
378 if options.quiet:
379 self.urlOpts.append("quiet=true")
380
381 def getTestFlavor(self, options):
382 if options.browserChrome:
383 return "browser-chrome"
384 elif options.chrome:
385 return "chrome"
386 elif options.a11y:
387 return "a11y"
388 elif options.webapprtChrome:
389 return "webapprt-chrome"
390 else:
391 return "mochitest"
392
393 # This check can be removed when bug 983867 is fixed.
394 def isTest(self, options, filename):
395 allow_js_css = False
396 if options.browserChrome:
397 allow_js_css = True
398 testPattern = re.compile(r"browser_.+\.js")
399 elif options.chrome or options.a11y:
400 testPattern = re.compile(r"(browser|test)_.+\.(xul|html|js|xhtml)")
401 elif options.webapprtContent:
402 testPattern = re.compile(r"webapprt_")
403 elif options.webapprtChrome:
404 allow_js_css = True
405 testPattern = re.compile(r"browser_")
406 else:
407 testPattern = re.compile(r"test_")
408
409 if not allow_js_css and (".js" in filename or ".css" in filename):
410 return False
411
412 pathPieces = filename.split("/")
413
414 return (testPattern.match(pathPieces[-1]) and
415 not re.search(r'\^headers\^$', filename))
416
417 def getTestPath(self, options):
418 if options.ipcplugins:
419 return "dom/plugins/test"
420 else:
421 return options.testPath
422
423 def getTestRoot(self, options):
424 if options.browserChrome:
425 if options.immersiveMode:
426 return 'metro'
427 return 'browser'
428 elif options.a11y:
429 return 'a11y'
430 elif options.webapprtChrome:
431 return 'webapprtChrome'
432 elif options.chrome:
433 return 'chrome'
434 return self.TEST_PATH
435
436 def buildTestURL(self, options):
437 testHost = "http://mochi.test:8888"
438 testPath = self.getTestPath(options)
439 testURL = "/".join([testHost, self.TEST_PATH, testPath])
440 if os.path.isfile(os.path.join(self.oldcwd, os.path.dirname(__file__), self.TEST_PATH, testPath)) and options.repeat > 0:
441 testURL = "/".join([testHost, self.TEST_PATH, os.path.dirname(testPath)])
442 if options.chrome or options.a11y:
443 testURL = "/".join([testHost, self.CHROME_PATH])
444 elif options.browserChrome:
445 testURL = "about:blank"
446 return testURL
447
448 def buildTestPath(self, options):
449 """ Build the url path to the specific test harness and test file or directory
450 Build a manifest of tests to run and write out a json file for the harness to read
451 """
452 manifest = None
453
454 testRoot = self.getTestRoot(options)
455 # testdir refers to 'mochitest' here.
456 testdir = SCRIPT_DIR.split(os.getcwd())[-1]
457 testdir = testdir.strip(os.sep)
458 testRootAbs = os.path.abspath(os.path.join(testdir, testRoot))
459 if isinstance(options.manifestFile, TestManifest):
460 manifest = options.manifestFile
461 elif options.manifestFile and os.path.isfile(options.manifestFile):
462 manifestFileAbs = os.path.abspath(options.manifestFile)
463 assert manifestFileAbs.startswith(testRootAbs)
464 manifest = TestManifest([options.manifestFile], strict=False)
465 else:
466 masterName = self.getTestFlavor(options) + '.ini'
467 masterPath = os.path.join(testdir, testRoot, masterName)
468
469 if os.path.exists(masterPath):
470 manifest = TestManifest([masterPath], strict=False)
471
472 if manifest:
473 # Python 2.6 doesn't allow unicode keys to be used for keyword
474 # arguments. This gross hack works around the problem until we
475 # rid ourselves of 2.6.
476 info = {}
477 for k, v in mozinfo.info.items():
478 if isinstance(k, unicode):
479 k = k.encode('ascii')
480 info[k] = v
481
482 # Bug 883858 - return all tests including disabled tests
483 tests = manifest.active_tests(disabled=True, options=options, **info)
484 paths = []
485 testPath = self.getTestPath(options)
486
487 for test in tests:
488 pathAbs = os.path.abspath(test['path'])
489 assert pathAbs.startswith(testRootAbs)
490 tp = pathAbs[len(testRootAbs):].replace('\\', '/').strip('/')
491
492 # Filter out tests if we are using --test-path
493 if testPath and not tp.startswith(testPath):
494 continue
495
496 if not self.isTest(options, tp):
497 print 'Warning: %s from manifest %s is not a valid test' % (test['name'], test['manifest'])
498 continue
499
500 testob = {'path': tp}
501 if test.has_key('disabled'):
502 testob['disabled'] = test['disabled']
503 paths.append(testob)
504
505 # Sort tests so they are run in a deterministic order.
506 def path_sort(ob1, ob2):
507 path1 = ob1['path'].split('/')
508 path2 = ob2['path'].split('/')
509 return cmp(path1, path2)
510
511 paths.sort(path_sort)
512
513 # Bug 883865 - add this functionality into manifestDestiny
514 with open(os.path.join(testdir, 'tests.json'), 'w') as manifestFile:
515 manifestFile.write(json.dumps({'tests': paths}))
516 options.manifestFile = 'tests.json'
517
518 return self.buildTestURL(options)
519
520 def startWebSocketServer(self, options, debuggerInfo):
521 """ Launch the websocket server """
522 self.wsserver = WebSocketServer(options, SCRIPT_DIR, debuggerInfo)
523 self.wsserver.start()
524
525 def startWebServer(self, options):
526 """Create the webserver and start it up"""
527
528 self.server = MochitestServer(options)
529 self.server.start()
530
531 if options.pidFile != "":
532 with open(options.pidFile + ".xpcshell.pid", 'w') as f:
533 f.write("%s" % self.server._process.pid)
534
535 def startServers(self, options, debuggerInfo):
536 # start servers and set ports
537 # TODO: pass these values, don't set on `self`
538 self.webServer = options.webServer
539 self.httpPort = options.httpPort
540 self.sslPort = options.sslPort
541 self.webSocketPort = options.webSocketPort
542
543 # httpd-path is specified by standard makefile targets and may be specified
544 # on the command line to select a particular version of httpd.js. If not
545 # specified, try to select the one from hostutils.zip, as required in bug 882932.
546 if not options.httpdPath:
547 options.httpdPath = os.path.join(options.utilityPath, "components")
548
549 self.startWebServer(options)
550 self.startWebSocketServer(options, debuggerInfo)
551
552 # start SSL pipe
553 self.sslTunnel = SSLTunnel(options)
554 self.sslTunnel.buildConfig(self.locations)
555 self.sslTunnel.start()
556
557 # If we're lucky, the server has fully started by now, and all paths are
558 # ready, etc. However, xpcshell cold start times suck, at least for debug
559 # builds. We'll try to connect to the server for awhile, and if we fail,
560 # we'll try to kill the server and exit with an error.
561 if self.server is not None:
562 self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
563
564 def stopServers(self):
565 """Servers are no longer needed, and perhaps more importantly, anything they
566 might spew to console might confuse things."""
567 if self.server is not None:
568 try:
569 log.info('Stopping web server')
570 self.server.stop()
571 except Exception:
572 log.exception('Exception when stopping web server')
573
574 if self.wsserver is not None:
575 try:
576 log.info('Stopping web socket server')
577 self.wsserver.stop()
578 except Exception:
579 log.exception('Exception when stopping web socket server');
580
581 if self.sslTunnel is not None:
582 try:
583 log.info('Stopping ssltunnel')
584 self.sslTunnel.stop()
585 except Exception:
586 log.exception('Exception stopping ssltunnel');
587
588 def copyExtraFilesToProfile(self, options):
589 "Copy extra files or dirs specified on the command line to the testing profile."
590 for f in options.extraProfileFiles:
591 abspath = self.getFullPath(f)
592 if os.path.isfile(abspath):
593 shutil.copy2(abspath, options.profilePath)
594 elif os.path.isdir(abspath):
595 dest = os.path.join(options.profilePath, os.path.basename(abspath))
596 shutil.copytree(abspath, dest)
597 else:
598 log.warning("runtests.py | Failed to copy %s to profile", abspath)
599
600 def installChromeJar(self, chrome, options):
601 """
602 copy mochijar directory to profile as an extension so we have chrome://mochikit for all harness code
603 """
604 # Write chrome.manifest.
605 with open(os.path.join(options.profilePath, "extensions", "staged", "mochikit@mozilla.org", "chrome.manifest"), "a") as mfile:
606 mfile.write(chrome)
607
608 def addChromeToProfile(self, options):
609 "Adds MochiKit chrome tests to the profile."
610
611 # Create (empty) chrome directory.
612 chromedir = os.path.join(options.profilePath, "chrome")
613 os.mkdir(chromedir)
614
615 # Write userChrome.css.
616 chrome = """
617 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); /* set default namespace to XUL */
618 toolbar,
619 toolbarpalette {
620 background-color: rgb(235, 235, 235) !important;
621 }
622 toolbar#nav-bar {
623 background-image: none !important;
624 }
625 """
626 with open(os.path.join(options.profilePath, "userChrome.css"), "a") as chromeFile:
627 chromeFile.write(chrome)
628
629 manifest = os.path.join(options.profilePath, "tests.manifest")
630 with open(manifest, "w") as manifestFile:
631 # Register chrome directory.
632 chrometestDir = os.path.join(os.path.abspath("."), SCRIPT_DIR) + "/"
633 if mozinfo.isWin:
634 chrometestDir = "file:///" + chrometestDir.replace("\\", "/")
635 manifestFile.write("content mochitests %s contentaccessible=yes\n" % chrometestDir)
636
637 if options.testingModulesDir is not None:
638 manifestFile.write("resource testing-common file:///%s\n" %
639 options.testingModulesDir)
640
641 # Call installChromeJar().
642 if not os.path.isdir(os.path.join(SCRIPT_DIR, self.jarDir)):
643 log.testFail("invalid setup: missing mochikit extension")
644 return None
645
646 # Support Firefox (browser), B2G (shell), SeaMonkey (navigator), and Webapp
647 # Runtime (webapp).
648 chrome = ""
649 if options.browserChrome or options.chrome or options.a11y or options.webapprtChrome:
650 chrome += """
651 overlay chrome://browser/content/browser.xul chrome://mochikit/content/browser-test-overlay.xul
652 overlay chrome://browser/content/shell.xhtml chrome://mochikit/content/browser-test-overlay.xul
653 overlay chrome://navigator/content/navigator.xul chrome://mochikit/content/browser-test-overlay.xul
654 overlay chrome://webapprt/content/webapp.xul chrome://mochikit/content/browser-test-overlay.xul
655 """
656
657 self.installChromeJar(chrome, options)
658 return manifest
659
660 def getExtensionsToInstall(self, options):
661 "Return a list of extensions to install in the profile"
662 extensions = options.extensionsToInstall or []
663 appDir = options.app[:options.app.rfind(os.sep)] if options.app else options.utilityPath
664
665 extensionDirs = [
666 # Extensions distributed with the test harness.
667 os.path.normpath(os.path.join(SCRIPT_DIR, "extensions")),
668 ]
669 if appDir:
670 # Extensions distributed with the application.
671 extensionDirs.append(os.path.join(appDir, "distribution", "extensions"))
672
673 for extensionDir in extensionDirs:
674 if os.path.isdir(extensionDir):
675 for dirEntry in os.listdir(extensionDir):
676 if dirEntry not in options.extensionsToExclude:
677 path = os.path.join(extensionDir, dirEntry)
678 if os.path.isdir(path) or (os.path.isfile(path) and path.endswith(".xpi")):
679 extensions.append(path)
680
681 # append mochikit
682 extensions.append(os.path.join(SCRIPT_DIR, self.jarDir))
683 return extensions
684
685 class SSLTunnel:
686 def __init__(self, options):
687 self.process = None
688 self.utilityPath = options.utilityPath
689 self.xrePath = options.xrePath
690 self.certPath = options.certPath
691 self.sslPort = options.sslPort
692 self.httpPort = options.httpPort
693 self.webServer = options.webServer
694 self.webSocketPort = options.webSocketPort
695
696 self.customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
697 self.clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
698 self.redirRE = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
699
700 def writeLocation(self, config, loc):
701 for option in loc.options:
702 match = self.customCertRE.match(option)
703 if match:
704 customcert = match.group("nickname");
705 config.write("listen:%s:%s:%s:%s\n" %
706 (loc.host, loc.port, self.sslPort, customcert))
707
708 match = self.clientAuthRE.match(option)
709 if match:
710 clientauth = match.group("clientauth");
711 config.write("clientauth:%s:%s:%s:%s\n" %
712 (loc.host, loc.port, self.sslPort, clientauth))
713
714 match = self.redirRE.match(option)
715 if match:
716 redirhost = match.group("redirhost")
717 config.write("redirhost:%s:%s:%s:%s\n" %
718 (loc.host, loc.port, self.sslPort, redirhost))
719
720 def buildConfig(self, locations):
721 """Create the ssltunnel configuration file"""
722 configFd, self.configFile = tempfile.mkstemp(prefix="ssltunnel", suffix=".cfg")
723 with os.fdopen(configFd, "w") as config:
724 config.write("httpproxy:1\n")
725 config.write("certdbdir:%s\n" % self.certPath)
726 config.write("forward:127.0.0.1:%s\n" % self.httpPort)
727 config.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
728 config.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
729
730 for loc in locations:
731 if loc.scheme == "https" and "nocert" not in loc.options:
732 self.writeLocation(config, loc)
733
734 def start(self):
735 """ Starts the SSL Tunnel """
736
737 # start ssltunnel to provide https:// URLs capability
738 bin_suffix = mozinfo.info.get('bin_suffix', '')
739 ssltunnel = os.path.join(self.utilityPath, "ssltunnel" + bin_suffix)
740 if not os.path.exists(ssltunnel):
741 log.error("INFO | runtests.py | expected to find ssltunnel at %s", ssltunnel)
742 exit(1)
743
744 env = environment(xrePath=self.xrePath)
745 self.process = mozprocess.ProcessHandler([ssltunnel, self.configFile],
746 env=env)
747 self.process.run()
748 log.info("INFO | runtests.py | SSL tunnel pid: %d", self.process.pid)
749
750 def stop(self):
751 """ Stops the SSL Tunnel and cleans up """
752 if self.process is not None:
753 self.process.kill()
754 if os.path.exists(self.configFile):
755 os.remove(self.configFile)
756
757 class Mochitest(MochitestUtilsMixin):
758 certdbNew = False
759 sslTunnel = None
760 vmwareHelper = None
761 DEFAULT_TIMEOUT = 60.0
762
763 # XXX use automation.py for test name to avoid breaking legacy
764 # TODO: replace this with 'runtests.py' or 'mochitest' or the like
765 test_name = 'automation.py'
766
767 def __init__(self):
768 super(Mochitest, self).__init__()
769
770 # environment function for browserEnv
771 self.environment = environment
772
773 # Max time in seconds to wait for server startup before tests will fail -- if
774 # this seems big, it's mostly for debug machines where cold startup
775 # (particularly after a build) takes forever.
776 self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get('debug') else 90
777
778 # metro browser sub process id
779 self.browserProcessId = None
780
781
782 self.haveDumpedScreen = False
783
784 def extraPrefs(self, extraPrefs):
785 """interpolate extra preferences from option strings"""
786
787 try:
788 return dict(parseKeyValue(extraPrefs, context='--setpref='))
789 except KeyValueParseError, e:
790 print str(e)
791 sys.exit(1)
792
793 def fillCertificateDB(self, options):
794 # TODO: move -> mozprofile:
795 # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c35
796
797 pwfilePath = os.path.join(options.profilePath, ".crtdbpw")
798 with open(pwfilePath, "w") as pwfile:
799 pwfile.write("\n")
800
801 # Pre-create the certification database for the profile
802 env = self.environment(xrePath=options.xrePath)
803 bin_suffix = mozinfo.info.get('bin_suffix', '')
804 certutil = os.path.join(options.utilityPath, "certutil" + bin_suffix)
805 pk12util = os.path.join(options.utilityPath, "pk12util" + bin_suffix)
806
807 if self.certdbNew:
808 # android and b2g use the new DB formats exclusively
809 certdbPath = "sql:" + options.profilePath
810 else:
811 # desktop seems to use the old
812 certdbPath = options.profilePath
813
814 status = call([certutil, "-N", "-d", certdbPath, "-f", pwfilePath], env=env)
815 if status:
816 return status
817
818 # Walk the cert directory and add custom CAs and client certs
819 files = os.listdir(options.certPath)
820 for item in files:
821 root, ext = os.path.splitext(item)
822 if ext == ".ca":
823 trustBits = "CT,,"
824 if root.endswith("-object"):
825 trustBits = "CT,,CT"
826 call([certutil, "-A", "-i", os.path.join(options.certPath, item),
827 "-d", certdbPath, "-f", pwfilePath, "-n", root, "-t", trustBits],
828 env=env)
829 elif ext == ".client":
830 call([pk12util, "-i", os.path.join(options.certPath, item),
831 "-w", pwfilePath, "-d", certdbPath],
832 env=env)
833
834 os.unlink(pwfilePath)
835 return 0
836
837 def buildProfile(self, options):
838 """ create the profile and add optional chrome bits and files if requested """
839 if options.browserChrome and options.timeout:
840 options.extraPrefs.append("testing.browserTestHarness.timeout=%d" % options.timeout)
841 options.extraPrefs.append("browser.tabs.remote=%s" % ('true' if options.e10s else 'false'))
842 options.extraPrefs.append("browser.tabs.remote.autostart=%s" % ('true' if options.e10s else 'false'))
843
844 # get extensions to install
845 extensions = self.getExtensionsToInstall(options)
846
847 # web apps
848 appsPath = os.path.join(SCRIPT_DIR, 'profile_data', 'webapps_mochitest.json')
849 if os.path.exists(appsPath):
850 with open(appsPath) as apps_file:
851 apps = json.load(apps_file)
852 else:
853 apps = None
854
855 # preferences
856 prefsPath = os.path.join(SCRIPT_DIR, 'profile_data', 'prefs_general.js')
857 prefs = dict(Preferences.read_prefs(prefsPath))
858 prefs.update(self.extraPrefs(options.extraPrefs))
859
860 # interpolate preferences
861 interpolation = {"server": "%s:%s" % (options.webServer, options.httpPort)}
862 prefs = json.loads(json.dumps(prefs) % interpolation)
863 for pref in prefs:
864 prefs[pref] = Preferences.cast(prefs[pref])
865 # TODO: make this less hacky
866 # https://bugzilla.mozilla.org/show_bug.cgi?id=913152
867
868 # proxy
869 proxy = {'remote': options.webServer,
870 'http': options.httpPort,
871 'https': options.sslPort,
872 # use SSL port for legacy compatibility; see
873 # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
874 # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
875 # - https://github.com/mozilla/mozbase/commit/43f9510e3d58bfed32790c82a57edac5f928474d
876 # 'ws': str(self.webSocketPort)
877 'ws': options.sslPort
878 }
879
880
881 # create a profile
882 self.profile = Profile(profile=options.profilePath,
883 addons=extensions,
884 locations=self.locations,
885 preferences=prefs,
886 apps=apps,
887 proxy=proxy
888 )
889
890 # Fix options.profilePath for legacy consumers.
891 options.profilePath = self.profile.profile
892
893 manifest = self.addChromeToProfile(options)
894 self.copyExtraFilesToProfile(options)
895
896 # create certificate database for the profile
897 # TODO: this should really be upstreamed somewhere, maybe mozprofile
898 certificateStatus = self.fillCertificateDB(options)
899 if certificateStatus:
900 log.info("TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed")
901 return None
902
903 return manifest
904
905 def buildBrowserEnv(self, options, debugger=False):
906 """build the environment variables for the specific test and operating system"""
907 browserEnv = self.environment(xrePath=options.xrePath, debugger=debugger,
908 dmdPath=options.dmdPath)
909
910 # These variables are necessary for correct application startup; change
911 # via the commandline at your own risk.
912 browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
913
914 # interpolate environment passed with options
915 try:
916 browserEnv.update(dict(parseKeyValue(options.environment, context='--setenv')))
917 except KeyValueParseError, e:
918 log.error(str(e))
919 return
920
921 browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
922
923 if options.fatalAssertions:
924 browserEnv["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
925
926 # Produce an NSPR log, is setup (see NSPR_LOG_MODULES global at the top of
927 # this script).
928 self.nsprLogs = NSPR_LOG_MODULES and "MOZ_UPLOAD_DIR" in os.environ
929 if self.nsprLogs:
930 browserEnv["NSPR_LOG_MODULES"] = NSPR_LOG_MODULES
931
932 browserEnv["NSPR_LOG_FILE"] = "%s/nspr.log" % tempfile.gettempdir()
933 browserEnv["GECKO_SEPARATE_NSPR_LOGS"] = "1"
934
935 if debugger and not options.slowscript:
936 browserEnv["JS_DISABLE_SLOW_SCRIPT_SIGNALS"] = "1"
937
938 return browserEnv
939
940 def cleanup(self, manifest, options):
941 """ remove temporary files and profile """
942 os.remove(manifest)
943 del self.profile
944 if options.pidFile != "":
945 try:
946 os.remove(options.pidFile)
947 if os.path.exists(options.pidFile + ".xpcshell.pid"):
948 os.remove(options.pidFile + ".xpcshell.pid")
949 except:
950 log.warn("cleaning up pidfile '%s' was unsuccessful from the test harness", options.pidFile)
951
952 def dumpScreen(self, utilityPath):
953 if self.haveDumpedScreen:
954 log.info("Not taking screenshot here: see the one that was previously logged")
955 return
956 self.haveDumpedScreen = True
957 dumpScreen(utilityPath)
958
959 def killAndGetStack(self, processPID, utilityPath, debuggerInfo, dump_screen=False):
960 """
961 Kill the process, preferrably in a way that gets us a stack trace.
962 Also attempts to obtain a screenshot before killing the process
963 if specified.
964 """
965
966 if dump_screen:
967 self.dumpScreen(utilityPath)
968
969 if mozinfo.info.get('crashreporter', True) and not debuggerInfo:
970 if mozinfo.isWin:
971 # We should have a "crashinject" program in our utility path
972 crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
973 if os.path.exists(crashinject):
974 status = subprocess.Popen([crashinject, str(processPID)]).wait()
975 printstatus(status, "crashinject")
976 if status == 0:
977 return
978 else:
979 try:
980 os.kill(processPID, signal.SIGABRT)
981 except OSError:
982 # https://bugzilla.mozilla.org/show_bug.cgi?id=921509
983 log.info("Can't trigger Breakpad, process no longer exists")
984 return
985 log.info("Can't trigger Breakpad, just killing process")
986 killPid(processPID)
987
988 def checkForZombies(self, processLog, utilityPath, debuggerInfo):
989 """Look for hung processes"""
990
991 if not os.path.exists(processLog):
992 log.info('Automation Error: PID log not found: %s', processLog)
993 # Whilst no hung process was found, the run should still display as a failure
994 return True
995
996 # scan processLog for zombies
997 log.info('INFO | zombiecheck | Reading PID log: %s', processLog)
998 processList = []
999 pidRE = re.compile(r'launched child process (\d+)$')
1000 with open(processLog) as processLogFD:
1001 for line in processLogFD:
1002 log.info(line.rstrip())
1003 m = pidRE.search(line)
1004 if m:
1005 processList.append(int(m.group(1)))
1006
1007 # kill zombies
1008 foundZombie = False
1009 for processPID in processList:
1010 log.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID)
1011 if isPidAlive(processPID):
1012 foundZombie = True
1013 log.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID)
1014 self.killAndGetStack(processPID, utilityPath, debuggerInfo, dump_screen=not debuggerInfo)
1015
1016 return foundZombie
1017
1018 def startVMwareRecording(self, options):
1019 """ starts recording inside VMware VM using the recording helper dll """
1020 assert mozinfo.isWin
1021 from ctypes import cdll
1022 self.vmwareHelper = cdll.LoadLibrary(self.vmwareHelperPath)
1023 if self.vmwareHelper is None:
1024 log.warning("runtests.py | Failed to load "
1025 "VMware recording helper")
1026 return
1027 log.info("runtests.py | Starting VMware recording.")
1028 try:
1029 self.vmwareHelper.StartRecording()
1030 except Exception, e:
1031 log.warning("runtests.py | Failed to start "
1032 "VMware recording: (%s)" % str(e))
1033 self.vmwareHelper = None
1034
1035 def stopVMwareRecording(self):
1036 """ stops recording inside VMware VM using the recording helper dll """
1037 try:
1038 assert mozinfo.isWin
1039 if self.vmwareHelper is not None:
1040 log.info("runtests.py | Stopping VMware recording.")
1041 self.vmwareHelper.StopRecording()
1042 except Exception, e:
1043 log.warning("runtests.py | Failed to stop "
1044 "VMware recording: (%s)" % str(e))
1045 log.exception('Error stopping VMWare recording')
1046
1047 self.vmwareHelper = None
1048
1049 def runApp(self,
1050 testUrl,
1051 env,
1052 app,
1053 profile,
1054 extraArgs,
1055 utilityPath,
1056 debuggerInfo=None,
1057 symbolsPath=None,
1058 timeout=-1,
1059 onLaunch=None,
1060 webapprtChrome=False,
1061 hide_subtests=False,
1062 screenshotOnFail=False):
1063 """
1064 Run the app, log the duration it took to execute, return the status code.
1065 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
1066 """
1067
1068 # debugger information
1069 interactive = False
1070 debug_args = None
1071 if debuggerInfo:
1072 interactive = debuggerInfo['interactive']
1073 debug_args = [debuggerInfo['path']] + debuggerInfo['args']
1074
1075 # fix default timeout
1076 if timeout == -1:
1077 timeout = self.DEFAULT_TIMEOUT
1078
1079 # build parameters
1080 is_test_build = mozinfo.info.get('tests_enabled', True)
1081 bin_suffix = mozinfo.info.get('bin_suffix', '')
1082
1083 # copy env so we don't munge the caller's environment
1084 env = env.copy()
1085
1086 # make sure we clean up after ourselves.
1087 try:
1088 # set process log environment variable
1089 tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
1090 os.close(tmpfd)
1091 env["MOZ_PROCESS_LOG"] = processLog
1092
1093 if interactive:
1094 # If an interactive debugger is attached,
1095 # don't use timeouts, and don't capture ctrl-c.
1096 timeout = None
1097 signal.signal(signal.SIGINT, lambda sigid, frame: None)
1098
1099 # build command line
1100 cmd = os.path.abspath(app)
1101 args = list(extraArgs)
1102 # TODO: mozrunner should use -foreground at least for mac
1103 # https://bugzilla.mozilla.org/show_bug.cgi?id=916512
1104 args.append('-foreground')
1105 if testUrl:
1106 if debuggerInfo and debuggerInfo['requiresEscapedArgs']:
1107 testUrl = testUrl.replace("&", "\\&")
1108 args.append(testUrl)
1109
1110 if mozinfo.info["debug"] and not webapprtChrome:
1111 shutdownLeaks = ShutdownLeaks(log.info)
1112 else:
1113 shutdownLeaks = None
1114
1115 # create an instance to process the output
1116 outputHandler = self.OutputHandler(harness=self,
1117 utilityPath=utilityPath,
1118 symbolsPath=symbolsPath,
1119 dump_screen_on_timeout=not debuggerInfo,
1120 dump_screen_on_fail=screenshotOnFail,
1121 hide_subtests=hide_subtests,
1122 shutdownLeaks=shutdownLeaks,
1123 )
1124
1125 def timeoutHandler():
1126 outputHandler.log_output_buffer()
1127 browserProcessId = outputHandler.browserProcessId
1128 self.handleTimeout(timeout, proc, utilityPath, debuggerInfo, browserProcessId)
1129 kp_kwargs = {'kill_on_timeout': False,
1130 'cwd': SCRIPT_DIR,
1131 'onTimeout': [timeoutHandler]}
1132 kp_kwargs['processOutputLine'] = [outputHandler]
1133
1134 # create mozrunner instance and start the system under test process
1135 self.lastTestSeen = self.test_name
1136 startTime = datetime.now()
1137
1138 # b2g desktop requires FirefoxRunner even though appname is b2g
1139 if mozinfo.info.get('appname') == 'b2g' and mozinfo.info.get('toolkit') != 'gonk':
1140 runner_cls = mozrunner.FirefoxRunner
1141 else:
1142 runner_cls = mozrunner.runners.get(mozinfo.info.get('appname', 'firefox'),
1143 mozrunner.Runner)
1144 runner = runner_cls(profile=self.profile,
1145 binary=cmd,
1146 cmdargs=args,
1147 env=env,
1148 process_class=mozprocess.ProcessHandlerMixin,
1149 kp_kwargs=kp_kwargs,
1150 )
1151
1152 # XXX work around bug 898379 until mozrunner is updated for m-c; see
1153 # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c49
1154 runner.kp_kwargs = kp_kwargs
1155
1156 # start the runner
1157 runner.start(debug_args=debug_args,
1158 interactive=interactive,
1159 outputTimeout=timeout)
1160 proc = runner.process_handler
1161 log.info("INFO | runtests.py | Application pid: %d", proc.pid)
1162
1163 if onLaunch is not None:
1164 # Allow callers to specify an onLaunch callback to be fired after the
1165 # app is launched.
1166 # We call onLaunch for b2g desktop mochitests so that we can
1167 # run a Marionette script after gecko has completed startup.
1168 onLaunch()
1169
1170 # wait until app is finished
1171 # XXX copy functionality from
1172 # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py#L61
1173 # until bug 913970 is fixed regarding mozrunner `wait` not returning status
1174 # see https://bugzilla.mozilla.org/show_bug.cgi?id=913970
1175 status = proc.wait()
1176 printstatus(status, "Main app process")
1177 runner.process_handler = None
1178
1179 if timeout is None:
1180 didTimeout = False
1181 else:
1182 didTimeout = proc.didTimeout
1183
1184 # finalize output handler
1185 outputHandler.finish(didTimeout)
1186
1187 # record post-test information
1188 if status:
1189 log.info("TEST-UNEXPECTED-FAIL | %s | application terminated with exit code %s", self.lastTestSeen, status)
1190 else:
1191 self.lastTestSeen = 'Main app process exited normally'
1192
1193 log.info("INFO | runtests.py | Application ran for: %s", str(datetime.now() - startTime))
1194
1195 # Do a final check for zombie child processes.
1196 zombieProcesses = self.checkForZombies(processLog, utilityPath, debuggerInfo)
1197
1198 # check for crashes
1199 minidump_path = os.path.join(self.profile.profile, "minidumps")
1200 crashed = mozcrash.check_for_crashes(minidump_path,
1201 symbolsPath,
1202 test_name=self.lastTestSeen)
1203
1204 if crashed or zombieProcesses:
1205 status = 1
1206
1207 finally:
1208 # cleanup
1209 if os.path.exists(processLog):
1210 os.remove(processLog)
1211
1212 return status
1213
1214 def runTests(self, options, onLaunch=None):
1215 """ Prepare, configure, run tests and cleanup """
1216
1217 # get debugger info, a dict of:
1218 # {'path': path to the debugger (string),
1219 # 'interactive': whether the debugger is interactive or not (bool)
1220 # 'args': arguments to the debugger (list)
1221 # TODO: use mozrunner.local.debugger_arguments:
1222 # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42
1223 debuggerInfo = getDebuggerInfo(self.oldcwd,
1224 options.debugger,
1225 options.debuggerArgs,
1226 options.debuggerInteractive)
1227
1228 self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log")
1229
1230 browserEnv = self.buildBrowserEnv(options, debuggerInfo is not None)
1231 if browserEnv is None:
1232 return 1
1233
1234 # buildProfile sets self.profile .
1235 # This relies on sideeffects and isn't very stateful:
1236 # https://bugzilla.mozilla.org/show_bug.cgi?id=919300
1237 manifest = self.buildProfile(options)
1238 if manifest is None:
1239 return 1
1240
1241 try:
1242 self.startServers(options, debuggerInfo)
1243
1244 testURL = self.buildTestPath(options)
1245 self.buildURLOptions(options, browserEnv)
1246 if self.urlOpts:
1247 testURL += "?" + "&".join(self.urlOpts)
1248
1249 if options.webapprtContent:
1250 options.browserArgs.extend(('-test-mode', testURL))
1251 testURL = None
1252
1253 if options.immersiveMode:
1254 options.browserArgs.extend(('-firefoxpath', options.app))
1255 options.app = self.immersiveHelperPath
1256
1257 if options.jsdebugger:
1258 options.browserArgs.extend(['-jsdebugger'])
1259
1260 # Remove the leak detection file so it can't "leak" to the tests run.
1261 # The file is not there if leak logging was not enabled in the application build.
1262 if os.path.exists(self.leak_report_file):
1263 os.remove(self.leak_report_file)
1264
1265 # then again to actually run mochitest
1266 if options.timeout:
1267 timeout = options.timeout + 30
1268 elif options.debugger or not options.autorun:
1269 timeout = None
1270 else:
1271 timeout = 330.0 # default JS harness timeout is 300 seconds
1272
1273 if options.vmwareRecording:
1274 self.startVMwareRecording(options);
1275
1276 log.info("runtests.py | Running tests: start.\n")
1277 try:
1278 status = self.runApp(testURL,
1279 browserEnv,
1280 options.app,
1281 profile=self.profile,
1282 extraArgs=options.browserArgs,
1283 utilityPath=options.utilityPath,
1284 debuggerInfo=debuggerInfo,
1285 symbolsPath=options.symbolsPath,
1286 timeout=timeout,
1287 onLaunch=onLaunch,
1288 webapprtChrome=options.webapprtChrome,
1289 hide_subtests=options.hide_subtests,
1290 screenshotOnFail=options.screenshotOnFail
1291 )
1292 except KeyboardInterrupt:
1293 log.info("runtests.py | Received keyboard interrupt.\n");
1294 status = -1
1295 except:
1296 traceback.print_exc()
1297 log.error("Automation Error: Received unexpected exception while running application\n")
1298 status = 1
1299
1300 finally:
1301 if options.vmwareRecording:
1302 self.stopVMwareRecording();
1303 self.stopServers()
1304
1305 processLeakLog(self.leak_report_file, options.leakThreshold)
1306
1307 if self.nsprLogs:
1308 with zipfile.ZipFile("%s/nsprlog.zip" % browserEnv["MOZ_UPLOAD_DIR"], "w", zipfile.ZIP_DEFLATED) as logzip:
1309 for logfile in glob.glob("%s/nspr*.log*" % tempfile.gettempdir()):
1310 logzip.write(logfile)
1311 os.remove(logfile)
1312
1313 log.info("runtests.py | Running tests: end.")
1314
1315 if manifest is not None:
1316 self.cleanup(manifest, options)
1317
1318 return status
1319
1320 def handleTimeout(self, timeout, proc, utilityPath, debuggerInfo, browserProcessId):
1321 """handle process output timeout"""
1322 # TODO: bug 913975 : _processOutput should call self.processOutputLine one more time one timeout (I think)
1323 log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
1324 browserProcessId = browserProcessId or proc.pid
1325 self.killAndGetStack(browserProcessId, utilityPath, debuggerInfo, dump_screen=not debuggerInfo)
1326
1327 ### output processing
1328
1329 class OutputHandler(object):
1330 """line output handler for mozrunner"""
1331 def __init__(self, harness, utilityPath, symbolsPath=None, dump_screen_on_timeout=True, dump_screen_on_fail=False,
1332 hide_subtests=False, shutdownLeaks=None):
1333 """
1334 harness -- harness instance
1335 dump_screen_on_timeout -- whether to dump the screen on timeout
1336 """
1337 self.harness = harness
1338 self.output_buffer = []
1339 self.running_test = False
1340 self.utilityPath = utilityPath
1341 self.symbolsPath = symbolsPath
1342 self.dump_screen_on_timeout = dump_screen_on_timeout
1343 self.dump_screen_on_fail = dump_screen_on_fail
1344 self.hide_subtests = hide_subtests
1345 self.shutdownLeaks = shutdownLeaks
1346
1347 # perl binary to use
1348 self.perl = which('perl')
1349
1350 # With metro browser runs this script launches the metro test harness which launches the browser.
1351 # The metro test harness hands back the real browser process id via log output which we need to
1352 # pick up on and parse out. This variable tracks the real browser process id if we find it.
1353 self.browserProcessId = None
1354
1355 # stack fixer function and/or process
1356 self.stackFixerFunction, self.stackFixerProcess = self.stackFixer()
1357
1358 def processOutputLine(self, line):
1359 """per line handler of output for mozprocess"""
1360 for handler in self.outputHandlers():
1361 line = handler(line)
1362 __call__ = processOutputLine
1363
1364 def outputHandlers(self):
1365 """returns ordered list of output handlers"""
1366 return [self.fix_stack,
1367 self.format,
1368 self.dumpScreenOnTimeout,
1369 self.dumpScreenOnFail,
1370 self.metro_subprocess_id,
1371 self.trackShutdownLeaks,
1372 self.check_test_failure,
1373 self.log,
1374 self.record_last_test,
1375 ]
1376
1377 def stackFixer(self):
1378 """
1379 return 2-tuple, (stackFixerFunction, StackFixerProcess),
1380 if any, to use on the output lines
1381 """
1382
1383 if not mozinfo.info.get('debug'):
1384 return None, None
1385
1386 stackFixerFunction = stackFixerProcess = None
1387
1388 def import_stackFixerModule(module_name):
1389 sys.path.insert(0, self.utilityPath)
1390 module = __import__(module_name, globals(), locals(), [])
1391 sys.path.pop(0)
1392 return module
1393
1394 if self.symbolsPath and os.path.exists(self.symbolsPath):
1395 # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
1396 # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
1397 stackFixerModule = import_stackFixerModule('fix_stack_using_bpsyms')
1398 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, self.symbolsPath)
1399
1400 elif mozinfo.isLinux and self.perl:
1401 # Run logsource through fix-linux-stack.pl (uses addr2line)
1402 # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
1403 stackFixerCommand = [self.perl, os.path.join(self.utilityPath, "fix-linux-stack.pl")]
1404 stackFixerProcess = subprocess.Popen(stackFixerCommand, stdin=subprocess.PIPE,
1405 stdout=subprocess.PIPE)
1406 def fixFunc(line):
1407 stackFixerProcess.stdin.write(line + '\n')
1408 return stackFixerProcess.stdout.readline().rstrip()
1409
1410 stackFixerFunction = fixFunc
1411
1412 return (stackFixerFunction, stackFixerProcess)
1413
1414 def finish(self, didTimeout):
1415 if self.stackFixerProcess:
1416 self.stackFixerProcess.communicate()
1417 status = self.stackFixerProcess.returncode
1418 if status and not didTimeout:
1419 log.info("TEST-UNEXPECTED-FAIL | runtests.py | Stack fixer process exited with code %d during test run", status)
1420
1421 if self.shutdownLeaks:
1422 self.shutdownLeaks.process()
1423
1424 def log_output_buffer(self):
1425 if self.output_buffer:
1426 lines = [' %s' % line for line in self.output_buffer]
1427 log.info("Buffered test output:\n%s" % '\n'.join(lines))
1428
1429 # output line handlers:
1430 # these take a line and return a line
1431
1432 def fix_stack(self, line):
1433 if self.stackFixerFunction:
1434 return self.stackFixerFunction(line)
1435 return line
1436
1437 def format(self, line):
1438 """format the line"""
1439 return line.rstrip().decode("UTF-8", "ignore")
1440
1441 def dumpScreenOnTimeout(self, line):
1442 if not self.dump_screen_on_fail and self.dump_screen_on_timeout and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
1443 self.log_output_buffer()
1444 self.harness.dumpScreen(self.utilityPath)
1445 return line
1446
1447 def dumpScreenOnFail(self, line):
1448 if self.dump_screen_on_fail and "TEST-UNEXPECTED-FAIL" in line:
1449 self.log_output_buffer()
1450 self.harness.dumpScreen(self.utilityPath)
1451 return line
1452
1453 def metro_subprocess_id(self, line):
1454 """look for metro browser subprocess id"""
1455 if "METRO_BROWSER_PROCESS" in line:
1456 index = line.find("=")
1457 if index != -1:
1458 self.browserProcessId = line[index+1:].rstrip()
1459 log.info("INFO | runtests.py | metro browser sub process id detected: %s", self.browserProcessId)
1460 return line
1461
1462 def trackShutdownLeaks(self, line):
1463 if self.shutdownLeaks:
1464 self.shutdownLeaks.log(line)
1465 return line
1466
1467 def check_test_failure(self, line):
1468 if 'TEST-END' in line:
1469 self.running_test = False
1470 if any('TEST-UNEXPECTED' in l for l in self.output_buffer):
1471 self.log_output_buffer()
1472 return line
1473
1474 def log(self, line):
1475 if self.hide_subtests and self.running_test:
1476 self.output_buffer.append(line)
1477 else:
1478 # hack to make separators align nicely, remove when we use mozlog
1479 if self.hide_subtests and 'TEST-END' in line:
1480 index = line.index('TEST-END') + len('TEST-END')
1481 line = line[:index] + ' ' * (len('TEST-START')-len('TEST-END')) + line[index:]
1482 log.info(line)
1483 return line
1484
1485 def record_last_test(self, line):
1486 """record last test on harness"""
1487 if "TEST-START" in line and "|" in line:
1488 if not line.endswith('Shutdown'):
1489 self.output_buffer = []
1490 self.running_test = True
1491 self.harness.lastTestSeen = line.split("|")[1].strip()
1492 return line
1493
1494
1495 def makeTestConfig(self, options):
1496 "Creates a test configuration file for customizing test execution."
1497 options.logFile = options.logFile.replace("\\", "\\\\")
1498 options.testPath = options.testPath.replace("\\", "\\\\")
1499 testRoot = self.getTestRoot(options)
1500
1501 if "MOZ_HIDE_RESULTS_TABLE" in os.environ and os.environ["MOZ_HIDE_RESULTS_TABLE"] == "1":
1502 options.hideResultsTable = True
1503
1504 d = dict(options.__dict__)
1505 d['testRoot'] = testRoot
1506 content = json.dumps(d)
1507
1508 with open(os.path.join(options.profilePath, "testConfig.js"), "w") as config:
1509 config.write(content)
1510
1511 def installExtensionFromPath(self, options, path, extensionID = None):
1512 """install an extension to options.profilePath"""
1513
1514 # TODO: currently extensionID is unused; see
1515 # https://bugzilla.mozilla.org/show_bug.cgi?id=914267
1516 # [mozprofile] make extensionID a parameter to install_from_path
1517 # https://github.com/mozilla/mozbase/blob/master/mozprofile/mozprofile/addons.py#L169
1518
1519 extensionPath = self.getFullPath(path)
1520
1521 log.info("runtests.py | Installing extension at %s to %s." %
1522 (extensionPath, options.profilePath))
1523
1524 addons = AddonManager(options.profilePath)
1525
1526 # XXX: del the __del__
1527 # hack can be removed when mozprofile is mirrored to m-c ; see
1528 # https://bugzilla.mozilla.org/show_bug.cgi?id=911218 :
1529 # [mozprofile] AddonManager should only cleanup on __del__ optionally:
1530 # https://github.com/mozilla/mozbase/blob/master/mozprofile/mozprofile/addons.py#L266
1531 if hasattr(addons, '__del__'):
1532 del addons.__del__
1533
1534 addons.install_from_path(path)
1535
1536 def installExtensionsToProfile(self, options):
1537 "Install special testing extensions, application distributed extensions, and specified on the command line ones to testing profile."
1538 for path in self.getExtensionsToInstall(options):
1539 self.installExtensionFromPath(options, path)
1540
1541
1542 def main():
1543
1544 # parse command line options
1545 mochitest = Mochitest()
1546 parser = MochitestOptions()
1547 options, args = parser.parse_args()
1548 options = parser.verifyOptions(options, mochitest)
1549 if options is None:
1550 # parsing error
1551 sys.exit(1)
1552
1553 options.utilityPath = mochitest.getFullPath(options.utilityPath)
1554 options.certPath = mochitest.getFullPath(options.certPath)
1555 if options.symbolsPath and not isURL(options.symbolsPath):
1556 options.symbolsPath = mochitest.getFullPath(options.symbolsPath)
1557
1558 sys.exit(mochitest.runTests(options))
1559
1560 if __name__ == "__main__":
1561 main()

mercurial