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