|
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 file, |
|
3 # You can obtain one at http://mozilla.org/MPL/2.0/. |
|
4 |
|
5 import json |
|
6 import os |
|
7 import posixpath |
|
8 import shutil |
|
9 import sys |
|
10 import tempfile |
|
11 import threading |
|
12 import time |
|
13 import traceback |
|
14 |
|
15 here = os.path.abspath(os.path.dirname(__file__)) |
|
16 sys.path.insert(0, here) |
|
17 |
|
18 from runtests import Mochitest |
|
19 from runtests import MochitestUtilsMixin |
|
20 from runtests import MochitestOptions |
|
21 from runtests import MochitestServer |
|
22 from mochitest_options import B2GOptions, MochitestOptions |
|
23 |
|
24 from marionette import Marionette |
|
25 |
|
26 from mozdevice import DeviceManagerADB |
|
27 from mozprofile import Profile, Preferences |
|
28 from mozrunner import B2GRunner |
|
29 import mozlog |
|
30 import mozinfo |
|
31 import moznetwork |
|
32 |
|
33 log = mozlog.getLogger('Mochitest') |
|
34 |
|
35 class B2GMochitest(MochitestUtilsMixin): |
|
36 def __init__(self, marionette, |
|
37 out_of_process=True, |
|
38 profile_data_dir=None, |
|
39 locations=os.path.join(here, 'server-locations.txt')): |
|
40 super(B2GMochitest, self).__init__() |
|
41 self.marionette = marionette |
|
42 self.out_of_process = out_of_process |
|
43 self.locations_file = locations |
|
44 self.preferences = [] |
|
45 self.webapps = None |
|
46 self.test_script = os.path.join(here, 'b2g_start_script.js') |
|
47 self.test_script_args = [self.out_of_process] |
|
48 self.product = 'b2g' |
|
49 |
|
50 if profile_data_dir: |
|
51 self.preferences = [os.path.join(profile_data_dir, f) |
|
52 for f in os.listdir(profile_data_dir) if f.startswith('pref')] |
|
53 self.webapps = [os.path.join(profile_data_dir, f) |
|
54 for f in os.listdir(profile_data_dir) if f.startswith('webapp')] |
|
55 |
|
56 # mozinfo is populated by the parent class |
|
57 if mozinfo.info['debug']: |
|
58 self.SERVER_STARTUP_TIMEOUT = 180 |
|
59 else: |
|
60 self.SERVER_STARTUP_TIMEOUT = 90 |
|
61 |
|
62 def setup_common_options(self, options): |
|
63 test_url = self.buildTestPath(options) |
|
64 if len(self.urlOpts) > 0: |
|
65 test_url += "?" + "&".join(self.urlOpts) |
|
66 self.test_script_args.append(test_url) |
|
67 |
|
68 def buildTestPath(self, options): |
|
69 # Skip over the manifest building that happens on desktop. |
|
70 return self.buildTestURL(options) |
|
71 |
|
72 def build_profile(self, options): |
|
73 # preferences |
|
74 prefs = {} |
|
75 for path in self.preferences: |
|
76 prefs.update(Preferences.read_prefs(path)) |
|
77 |
|
78 for v in options.extraPrefs: |
|
79 thispref = v.split("=", 1) |
|
80 if len(thispref) < 2: |
|
81 print "Error: syntax error in --setpref=" + v |
|
82 sys.exit(1) |
|
83 prefs[thispref[0]] = thispref[1] |
|
84 |
|
85 # interpolate the preferences |
|
86 interpolation = { "server": "%s:%s" % (options.webServer, options.httpPort), |
|
87 "OOP": "true" if self.out_of_process else "false" } |
|
88 prefs = json.loads(json.dumps(prefs) % interpolation) |
|
89 for pref in prefs: |
|
90 prefs[pref] = Preferences.cast(prefs[pref]) |
|
91 |
|
92 kwargs = { |
|
93 'addons': self.getExtensionsToInstall(options), |
|
94 'apps': self.webapps, |
|
95 'locations': self.locations_file, |
|
96 'preferences': prefs, |
|
97 'proxy': {"remote": options.webServer} |
|
98 } |
|
99 |
|
100 if options.profile: |
|
101 self.profile = Profile.clone(options.profile, **kwargs) |
|
102 else: |
|
103 self.profile = Profile(**kwargs) |
|
104 |
|
105 options.profilePath = self.profile.profile |
|
106 # TODO bug 839108 - mozprofile should probably handle this |
|
107 manifest = self.addChromeToProfile(options) |
|
108 self.copyExtraFilesToProfile(options) |
|
109 return manifest |
|
110 |
|
111 def run_tests(self, options): |
|
112 """ Prepare, configure, run tests and cleanup """ |
|
113 |
|
114 self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log") |
|
115 manifest = self.build_profile(options) |
|
116 |
|
117 self.startServers(options, None) |
|
118 self.buildURLOptions(options, {'MOZ_HIDE_RESULTS_TABLE': '1'}) |
|
119 self.test_script_args.append(not options.emulator) |
|
120 self.test_script_args.append(options.wifi) |
|
121 |
|
122 if options.debugger or not options.autorun: |
|
123 timeout = None |
|
124 else: |
|
125 if not options.timeout: |
|
126 if mozinfo.info['debug']: |
|
127 options.timeout = 420 |
|
128 else: |
|
129 options.timeout = 300 |
|
130 timeout = options.timeout + 30.0 |
|
131 |
|
132 log.info("runtestsb2g.py | Running tests: start.") |
|
133 status = 0 |
|
134 try: |
|
135 runner_args = { 'profile': self.profile, |
|
136 'devicemanager': self._dm, |
|
137 'marionette': self.marionette, |
|
138 'remote_test_root': self.remote_test_root, |
|
139 'symbols_path': options.symbolsPath, |
|
140 'test_script': self.test_script, |
|
141 'test_script_args': self.test_script_args } |
|
142 self.runner = B2GRunner(**runner_args) |
|
143 self.runner.start(outputTimeout=timeout) |
|
144 status = self.runner.wait() |
|
145 if status is None: |
|
146 # the runner has timed out |
|
147 status = 124 |
|
148 except KeyboardInterrupt: |
|
149 log.info("runtests.py | Received keyboard interrupt.\n"); |
|
150 status = -1 |
|
151 except: |
|
152 traceback.print_exc() |
|
153 log.error("Automation Error: Received unexpected exception while running application\n") |
|
154 self.runner.check_for_crashes() |
|
155 status = 1 |
|
156 |
|
157 self.stopServers() |
|
158 |
|
159 log.info("runtestsb2g.py | Running tests: end.") |
|
160 |
|
161 if manifest is not None: |
|
162 self.cleanup(manifest, options) |
|
163 return status |
|
164 |
|
165 |
|
166 class B2GDeviceMochitest(B2GMochitest): |
|
167 |
|
168 _dm = None |
|
169 |
|
170 def __init__(self, marionette, devicemanager, profile_data_dir, |
|
171 local_binary_dir, remote_test_root=None, remote_log_file=None): |
|
172 B2GMochitest.__init__(self, marionette, out_of_process=True, profile_data_dir=profile_data_dir) |
|
173 self._dm = devicemanager |
|
174 self.remote_test_root = remote_test_root or self._dm.getDeviceRoot() |
|
175 self.remote_profile = posixpath.join(self.remote_test_root, 'profile') |
|
176 self.remote_log = remote_log_file or posixpath.join(self.remote_test_root, 'log', 'mochitest.log') |
|
177 self.local_log = None |
|
178 self.local_binary_dir = local_binary_dir |
|
179 |
|
180 if not self._dm.dirExists(posixpath.dirname(self.remote_log)): |
|
181 self._dm.mkDirs(self.remote_log) |
|
182 |
|
183 def cleanup(self, manifest, options): |
|
184 if self.local_log: |
|
185 self._dm.getFile(self.remote_log, self.local_log) |
|
186 self._dm.removeFile(self.remote_log) |
|
187 |
|
188 if options.pidFile != "": |
|
189 try: |
|
190 os.remove(options.pidFile) |
|
191 os.remove(options.pidFile + ".xpcshell.pid") |
|
192 except: |
|
193 print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % options.pidFile |
|
194 |
|
195 # stop and clean up the runner |
|
196 if getattr(self, 'runner', False): |
|
197 self.runner.cleanup() |
|
198 self.runner = None |
|
199 |
|
200 def startServers(self, options, debuggerInfo): |
|
201 """ Create the servers on the host and start them up """ |
|
202 savedXre = options.xrePath |
|
203 savedUtility = options.utilityPath |
|
204 savedProfie = options.profilePath |
|
205 options.xrePath = self.local_binary_dir |
|
206 options.utilityPath = self.local_binary_dir |
|
207 options.profilePath = tempfile.mkdtemp() |
|
208 |
|
209 MochitestUtilsMixin.startServers(self, options, debuggerInfo) |
|
210 |
|
211 options.xrePath = savedXre |
|
212 options.utilityPath = savedUtility |
|
213 options.profilePath = savedProfie |
|
214 |
|
215 def buildURLOptions(self, options, env): |
|
216 self.local_log = options.logFile |
|
217 options.logFile = self.remote_log |
|
218 options.profilePath = self.profile.profile |
|
219 retVal = super(B2GDeviceMochitest, self).buildURLOptions(options, env) |
|
220 |
|
221 self.setup_common_options(options) |
|
222 |
|
223 options.profilePath = self.remote_profile |
|
224 options.logFile = self.local_log |
|
225 return retVal |
|
226 |
|
227 |
|
228 class B2GDesktopMochitest(B2GMochitest, Mochitest): |
|
229 |
|
230 def __init__(self, marionette, profile_data_dir): |
|
231 B2GMochitest.__init__(self, marionette, out_of_process=False, profile_data_dir=profile_data_dir) |
|
232 Mochitest.__init__(self) |
|
233 self.certdbNew = True |
|
234 |
|
235 def runMarionetteScript(self, marionette, test_script, test_script_args): |
|
236 assert(marionette.wait_for_port()) |
|
237 marionette.start_session() |
|
238 marionette.set_context(marionette.CONTEXT_CHROME) |
|
239 |
|
240 if os.path.isfile(test_script): |
|
241 f = open(test_script, 'r') |
|
242 test_script = f.read() |
|
243 f.close() |
|
244 self.marionette.execute_script(test_script, |
|
245 script_args=test_script_args) |
|
246 |
|
247 def startTests(self): |
|
248 # This is run in a separate thread because otherwise, the app's |
|
249 # stdout buffer gets filled (which gets drained only after this |
|
250 # function returns, by waitForFinish), which causes the app to hang. |
|
251 thread = threading.Thread(target=self.runMarionetteScript, |
|
252 args=(self.marionette, |
|
253 self.test_script, |
|
254 self.test_script_args)) |
|
255 thread.start() |
|
256 |
|
257 def buildURLOptions(self, options, env): |
|
258 retVal = super(B2GDesktopMochitest, self).buildURLOptions(options, env) |
|
259 |
|
260 self.setup_common_options(options) |
|
261 |
|
262 # Copy the extensions to the B2G bundles dir. |
|
263 extensionDir = os.path.join(options.profilePath, 'extensions', 'staged') |
|
264 bundlesDir = os.path.join(os.path.dirname(options.app), |
|
265 'distribution', 'bundles') |
|
266 |
|
267 for filename in os.listdir(extensionDir): |
|
268 shutil.rmtree(os.path.join(bundlesDir, filename), True) |
|
269 shutil.copytree(os.path.join(extensionDir, filename), |
|
270 os.path.join(bundlesDir, filename)) |
|
271 |
|
272 return retVal |
|
273 |
|
274 def buildProfile(self, options): |
|
275 return self.build_profile(options) |
|
276 |
|
277 |
|
278 def run_remote_mochitests(parser, options): |
|
279 # create our Marionette instance |
|
280 kwargs = {} |
|
281 if options.emulator: |
|
282 kwargs['emulator'] = options.emulator |
|
283 if options.noWindow: |
|
284 kwargs['noWindow'] = True |
|
285 if options.geckoPath: |
|
286 kwargs['gecko_path'] = options.geckoPath |
|
287 if options.logcat_dir: |
|
288 kwargs['logcat_dir'] = options.logcat_dir |
|
289 if options.busybox: |
|
290 kwargs['busybox'] = options.busybox |
|
291 if options.symbolsPath: |
|
292 kwargs['symbols_path'] = options.symbolsPath |
|
293 # needless to say sdcard is only valid if using an emulator |
|
294 if options.sdcard: |
|
295 kwargs['sdcard'] = options.sdcard |
|
296 if options.b2gPath: |
|
297 kwargs['homedir'] = options.b2gPath |
|
298 if options.marionette: |
|
299 host, port = options.marionette.split(':') |
|
300 kwargs['host'] = host |
|
301 kwargs['port'] = int(port) |
|
302 |
|
303 marionette = Marionette.getMarionetteOrExit(**kwargs) |
|
304 |
|
305 if options.emulator: |
|
306 dm = marionette.emulator.dm |
|
307 else: |
|
308 # create the DeviceManager |
|
309 kwargs = {'adbPath': options.adbPath, |
|
310 'deviceRoot': options.remoteTestRoot} |
|
311 if options.deviceIP: |
|
312 kwargs.update({'host': options.deviceIP, |
|
313 'port': options.devicePort}) |
|
314 dm = DeviceManagerADB(**kwargs) |
|
315 |
|
316 options = parser.verifyRemoteOptions(options) |
|
317 if (options == None): |
|
318 print "ERROR: Invalid options specified, use --help for a list of valid options" |
|
319 sys.exit(1) |
|
320 |
|
321 mochitest = B2GDeviceMochitest(marionette, dm, options.profile_data_dir, options.xrePath, |
|
322 remote_test_root=options.remoteTestRoot, |
|
323 remote_log_file=options.remoteLogFile) |
|
324 |
|
325 options = parser.verifyOptions(options, mochitest) |
|
326 if (options == None): |
|
327 sys.exit(1) |
|
328 |
|
329 retVal = 1 |
|
330 try: |
|
331 mochitest.cleanup(None, options) |
|
332 retVal = mochitest.run_tests(options) |
|
333 except: |
|
334 print "Automation Error: Exception caught while running tests" |
|
335 traceback.print_exc() |
|
336 mochitest.stopServers() |
|
337 try: |
|
338 mochitest.cleanup(None, options) |
|
339 except: |
|
340 pass |
|
341 retVal = 1 |
|
342 |
|
343 sys.exit(retVal) |
|
344 |
|
345 def run_desktop_mochitests(parser, options): |
|
346 # create our Marionette instance |
|
347 kwargs = {} |
|
348 if options.marionette: |
|
349 host, port = options.marionette.split(':') |
|
350 kwargs['host'] = host |
|
351 kwargs['port'] = int(port) |
|
352 marionette = Marionette.getMarionetteOrExit(**kwargs) |
|
353 mochitest = B2GDesktopMochitest(marionette, options.profile_data_dir) |
|
354 |
|
355 # add a -bin suffix if b2g-bin exists, but just b2g was specified |
|
356 if options.app[-4:] != '-bin': |
|
357 if os.path.isfile("%s-bin" % options.app): |
|
358 options.app = "%s-bin" % options.app |
|
359 |
|
360 options = MochitestOptions.verifyOptions(parser, options, mochitest) |
|
361 if options == None: |
|
362 sys.exit(1) |
|
363 |
|
364 if options.desktop and not options.profile: |
|
365 raise Exception("must specify --profile when specifying --desktop") |
|
366 |
|
367 options.browserArgs += ['-marionette'] |
|
368 |
|
369 sys.exit(mochitest.runTests(options, onLaunch=mochitest.startTests)) |
|
370 |
|
371 def main(): |
|
372 parser = B2GOptions() |
|
373 options, args = parser.parse_args() |
|
374 |
|
375 if options.desktop: |
|
376 run_desktop_mochitests(parser, options) |
|
377 else: |
|
378 run_remote_mochitests(parser, options) |
|
379 |
|
380 if __name__ == "__main__": |
|
381 main() |