|
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 import os |
|
6 import sys |
|
7 import time |
|
8 import tempfile |
|
9 import atexit |
|
10 import shlex |
|
11 import subprocess |
|
12 import re |
|
13 import shutil |
|
14 |
|
15 import mozrunner |
|
16 from cuddlefish.prefs import DEFAULT_COMMON_PREFS |
|
17 from cuddlefish.prefs import DEFAULT_FIREFOX_PREFS |
|
18 from cuddlefish.prefs import DEFAULT_THUNDERBIRD_PREFS |
|
19 from cuddlefish.prefs import DEFAULT_FENNEC_PREFS |
|
20 |
|
21 # Used to remove noise from ADB output |
|
22 CLEANUP_ADB = re.compile(r'^(I|E)/(stdout|stderr|GeckoConsole)\s*\(\s*\d+\):\s*(.*)$') |
|
23 # Used to filter only messages send by `console` module |
|
24 FILTER_ONLY_CONSOLE_FROM_ADB = re.compile(r'^I/(stdout|stderr)\s*\(\s*\d+\):\s*((info|warning|error|debug): .*)$') |
|
25 |
|
26 # Used to detect the currently running test |
|
27 PARSEABLE_TEST_NAME = re.compile(r'TEST-START \| ([^\n]+)\n') |
|
28 |
|
29 # Maximum time we'll wait for tests to finish, in seconds. |
|
30 # The purpose of this timeout is to recover from infinite loops. It should be |
|
31 # longer than the amount of time any test run takes, including those on slow |
|
32 # machines running slow (debug) versions of Firefox. |
|
33 RUN_TIMEOUT = 1.5 * 60 * 60 # 1.5 Hour |
|
34 |
|
35 # Maximum time we'll wait for tests to emit output, in seconds. |
|
36 # The purpose of this timeout is to recover from hangs. It should be longer |
|
37 # than the amount of time any test takes to report results. |
|
38 OUTPUT_TIMEOUT = 60 * 5 # five minutes |
|
39 |
|
40 def follow_file(filename): |
|
41 """ |
|
42 Generator that yields the latest unread content from the given |
|
43 file, or None if no new content is available. |
|
44 |
|
45 For example: |
|
46 |
|
47 >>> f = open('temp.txt', 'w') |
|
48 >>> f.write('hello') |
|
49 >>> f.flush() |
|
50 >>> tail = follow_file('temp.txt') |
|
51 >>> tail.next() |
|
52 'hello' |
|
53 >>> tail.next() is None |
|
54 True |
|
55 >>> f.write('there') |
|
56 >>> f.flush() |
|
57 >>> tail.next() |
|
58 'there' |
|
59 >>> f.close() |
|
60 >>> os.remove('temp.txt') |
|
61 """ |
|
62 |
|
63 last_pos = 0 |
|
64 last_size = 0 |
|
65 while True: |
|
66 newstuff = None |
|
67 if os.path.exists(filename): |
|
68 size = os.stat(filename).st_size |
|
69 if size > last_size: |
|
70 last_size = size |
|
71 f = open(filename, 'r') |
|
72 f.seek(last_pos) |
|
73 newstuff = f.read() |
|
74 last_pos = f.tell() |
|
75 f.close() |
|
76 yield newstuff |
|
77 |
|
78 # subprocess.check_output only appeared in python2.7, so this code is taken |
|
79 # from python source code for compatibility with py2.5/2.6 |
|
80 class CalledProcessError(Exception): |
|
81 def __init__(self, returncode, cmd, output=None): |
|
82 self.returncode = returncode |
|
83 self.cmd = cmd |
|
84 self.output = output |
|
85 def __str__(self): |
|
86 return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) |
|
87 |
|
88 def check_output(*popenargs, **kwargs): |
|
89 if 'stdout' in kwargs: |
|
90 raise ValueError('stdout argument not allowed, it will be overridden.') |
|
91 process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) |
|
92 output, unused_err = process.communicate() |
|
93 retcode = process.poll() |
|
94 if retcode: |
|
95 cmd = kwargs.get("args") |
|
96 if cmd is None: |
|
97 cmd = popenargs[0] |
|
98 raise CalledProcessError(retcode, cmd, output=output) |
|
99 return output |
|
100 |
|
101 |
|
102 class FennecProfile(mozrunner.Profile): |
|
103 preferences = {} |
|
104 names = ['fennec'] |
|
105 |
|
106 class FennecRunner(mozrunner.Runner): |
|
107 profile_class = FennecProfile |
|
108 |
|
109 names = ['fennec'] |
|
110 |
|
111 __DARWIN_PATH = '/Applications/Fennec.app/Contents/MacOS/fennec' |
|
112 |
|
113 def __init__(self, binary=None, **kwargs): |
|
114 if sys.platform == 'darwin' and binary and binary.endswith('.app'): |
|
115 # Assume it's a Fennec app dir. |
|
116 binary = os.path.join(binary, 'Contents/MacOS/fennec') |
|
117 |
|
118 self.__real_binary = binary |
|
119 |
|
120 mozrunner.Runner.__init__(self, **kwargs) |
|
121 |
|
122 def find_binary(self): |
|
123 if not self.__real_binary: |
|
124 if sys.platform == 'darwin': |
|
125 if os.path.exists(self.__DARWIN_PATH): |
|
126 return self.__DARWIN_PATH |
|
127 self.__real_binary = mozrunner.Runner.find_binary(self) |
|
128 return self.__real_binary |
|
129 |
|
130 FENNEC_REMOTE_PATH = '/mnt/sdcard/jetpack-profile' |
|
131 |
|
132 class RemoteFennecRunner(mozrunner.Runner): |
|
133 profile_class = FennecProfile |
|
134 |
|
135 names = ['fennec'] |
|
136 |
|
137 _INTENT_PREFIX = 'org.mozilla.' |
|
138 |
|
139 _adb_path = None |
|
140 |
|
141 def __init__(self, binary=None, **kwargs): |
|
142 # Check that we have a binary set |
|
143 if not binary: |
|
144 raise ValueError("You have to define `--binary` option set to the " |
|
145 "path to your ADB executable.") |
|
146 # Ensure that binary refer to a valid ADB executable |
|
147 output = subprocess.Popen([binary], stdout=subprocess.PIPE, |
|
148 stderr=subprocess.PIPE).communicate() |
|
149 output = "".join(output) |
|
150 if not ("Android Debug Bridge" in output): |
|
151 raise ValueError("`--binary` option should be the path to your " |
|
152 "ADB executable.") |
|
153 self.binary = binary |
|
154 |
|
155 mobile_app_name = kwargs['cmdargs'][0] |
|
156 self.profile = kwargs['profile'] |
|
157 self._adb_path = binary |
|
158 |
|
159 # This pref has to be set to `false` otherwise, we do not receive |
|
160 # output of adb commands! |
|
161 subprocess.call([self._adb_path, "shell", |
|
162 "setprop log.redirect-stdio false"]) |
|
163 |
|
164 # Android apps are launched by their "intent" name, |
|
165 # Automatically detect already installed firefox by using `pm` program |
|
166 # or use name given as cfx `--mobile-app` argument. |
|
167 intents = self.getIntentNames() |
|
168 if not intents: |
|
169 raise ValueError("Unable to found any Firefox " |
|
170 "application on your device.") |
|
171 elif mobile_app_name: |
|
172 if not mobile_app_name in intents: |
|
173 raise ValueError("Unable to found Firefox application " |
|
174 "with intent name '%s'\n" |
|
175 "Available ones are: %s" % |
|
176 (mobile_app_name, ", ".join(intents))) |
|
177 self._intent_name = self._INTENT_PREFIX + mobile_app_name |
|
178 else: |
|
179 if "firefox" in intents: |
|
180 self._intent_name = self._INTENT_PREFIX + "firefox" |
|
181 elif "firefox_beta" in intents: |
|
182 self._intent_name = self._INTENT_PREFIX + "firefox_beta" |
|
183 elif "firefox_nightly" in intents: |
|
184 self._intent_name = self._INTENT_PREFIX + "firefox_nightly" |
|
185 else: |
|
186 self._intent_name = self._INTENT_PREFIX + intents[0] |
|
187 |
|
188 print "Launching mobile application with intent name " + self._intent_name |
|
189 |
|
190 # First try to kill firefox if it is already running |
|
191 pid = self.getProcessPID(self._intent_name) |
|
192 if pid != None: |
|
193 print "Killing running Firefox instance ..." |
|
194 subprocess.call([self._adb_path, "shell", |
|
195 "am force-stop " + self._intent_name]) |
|
196 time.sleep(7) |
|
197 # It appears recently that the PID still exists even after |
|
198 # Fennec closes, so removing this error still allows the tests |
|
199 # to pass as the new Fennec instance is able to start. |
|
200 # Leaving error in but commented out for now. |
|
201 # |
|
202 #if self.getProcessPID(self._intent_name) != None: |
|
203 # raise Exception("Unable to automatically kill running Firefox" + |
|
204 # " instance. Please close it manually before " + |
|
205 # "executing cfx.") |
|
206 |
|
207 print "Pushing the addon to your device" |
|
208 |
|
209 # Create a clean empty profile on the sd card |
|
210 subprocess.call([self._adb_path, "shell", "rm -r " + FENNEC_REMOTE_PATH]) |
|
211 subprocess.call([self._adb_path, "shell", "mkdir " + FENNEC_REMOTE_PATH]) |
|
212 |
|
213 # Push the profile folder created by mozrunner to the device |
|
214 # (we can't simply use `adb push` as it doesn't copy empty folders) |
|
215 localDir = self.profile.profile |
|
216 remoteDir = FENNEC_REMOTE_PATH |
|
217 for root, dirs, files in os.walk(localDir, followlinks='true'): |
|
218 relRoot = os.path.relpath(root, localDir) |
|
219 # Note about os.path usage below: |
|
220 # Local files may be using Windows `\` separators but |
|
221 # remote are always `/`, so we need to convert local ones to `/` |
|
222 for file in files: |
|
223 localFile = os.path.join(root, file) |
|
224 remoteFile = remoteDir.replace("/", os.sep) |
|
225 if relRoot != ".": |
|
226 remoteFile = os.path.join(remoteFile, relRoot) |
|
227 remoteFile = os.path.join(remoteFile, file) |
|
228 remoteFile = "/".join(remoteFile.split(os.sep)) |
|
229 subprocess.Popen([self._adb_path, "push", localFile, remoteFile], |
|
230 stderr=subprocess.PIPE).wait() |
|
231 for dir in dirs: |
|
232 targetDir = remoteDir.replace("/", os.sep) |
|
233 if relRoot != ".": |
|
234 targetDir = os.path.join(targetDir, relRoot) |
|
235 targetDir = os.path.join(targetDir, dir) |
|
236 targetDir = "/".join(targetDir.split(os.sep)) |
|
237 # `-p` option is not supported on all devices! |
|
238 subprocess.call([self._adb_path, "shell", "mkdir " + targetDir]) |
|
239 |
|
240 @property |
|
241 def command(self): |
|
242 """Returns the command list to run.""" |
|
243 return [self._adb_path, |
|
244 "shell", |
|
245 "am start " + |
|
246 "-a android.activity.MAIN " + |
|
247 "-n " + self._intent_name + "/" + self._intent_name + ".App " + |
|
248 "--es args \"-profile " + FENNEC_REMOTE_PATH + "\"" |
|
249 ] |
|
250 |
|
251 def start(self): |
|
252 subprocess.call(self.command) |
|
253 |
|
254 def getProcessPID(self, processName): |
|
255 p = subprocess.Popen([self._adb_path, "shell", "ps"], |
|
256 stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
257 line = p.stdout.readline() |
|
258 while line: |
|
259 columns = line.split() |
|
260 pid = columns[1] |
|
261 name = columns[-1] |
|
262 line = p.stdout.readline() |
|
263 if processName in name: |
|
264 return pid |
|
265 return None |
|
266 |
|
267 def getIntentNames(self): |
|
268 p = subprocess.Popen([self._adb_path, "shell", "pm list packages"], |
|
269 stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
270 names = [] |
|
271 for line in p.stdout.readlines(): |
|
272 line = re.sub("(^package:)|\s", "", line) |
|
273 if self._INTENT_PREFIX in line: |
|
274 names.append(line.replace(self._INTENT_PREFIX, "")) |
|
275 return names |
|
276 |
|
277 |
|
278 class XulrunnerAppProfile(mozrunner.Profile): |
|
279 preferences = {} |
|
280 names = [] |
|
281 |
|
282 class XulrunnerAppRunner(mozrunner.Runner): |
|
283 """ |
|
284 Runner for any XULRunner app. Can use a Firefox binary in XULRunner |
|
285 mode to execute the app, or can use XULRunner itself. Expects the |
|
286 app's application.ini to be passed in as one of the items in |
|
287 'cmdargs' in the constructor. |
|
288 |
|
289 This class relies a lot on the particulars of mozrunner.Runner's |
|
290 implementation, and does some unfortunate acrobatics to get around |
|
291 some of the class' limitations/assumptions. |
|
292 """ |
|
293 |
|
294 profile_class = XulrunnerAppProfile |
|
295 |
|
296 # This is a default, and will be overridden in the instance if |
|
297 # Firefox is used in XULRunner mode. |
|
298 names = ['xulrunner'] |
|
299 |
|
300 # Default location of XULRunner on OS X. |
|
301 __DARWIN_PATH = "/Library/Frameworks/XUL.framework/xulrunner-bin" |
|
302 __LINUX_PATH = "/usr/bin/xulrunner" |
|
303 |
|
304 # What our application.ini's path looks like if it's part of |
|
305 # an "installed" XULRunner app on OS X. |
|
306 __DARWIN_APP_INI_SUFFIX = '.app/Contents/Resources/application.ini' |
|
307 |
|
308 def __init__(self, binary=None, **kwargs): |
|
309 if sys.platform == 'darwin' and binary and binary.endswith('.app'): |
|
310 # Assume it's a Firefox app dir. |
|
311 binary = os.path.join(binary, 'Contents/MacOS/firefox-bin') |
|
312 |
|
313 self.__app_ini = None |
|
314 self.__real_binary = binary |
|
315 |
|
316 mozrunner.Runner.__init__(self, **kwargs) |
|
317 |
|
318 # See if we're using a genuine xulrunner-bin from the XULRunner SDK, |
|
319 # or if we're being asked to use Firefox in XULRunner mode. |
|
320 self.__is_xulrunner_sdk = 'xulrunner' in self.binary |
|
321 |
|
322 if sys.platform == 'linux2' and not self.env.get('LD_LIBRARY_PATH'): |
|
323 self.env['LD_LIBRARY_PATH'] = os.path.dirname(self.binary) |
|
324 |
|
325 newargs = [] |
|
326 for item in self.cmdargs: |
|
327 if 'application.ini' in item: |
|
328 self.__app_ini = item |
|
329 else: |
|
330 newargs.append(item) |
|
331 self.cmdargs = newargs |
|
332 |
|
333 if not self.__app_ini: |
|
334 raise ValueError('application.ini not found in cmdargs') |
|
335 if not os.path.exists(self.__app_ini): |
|
336 raise ValueError("file does not exist: '%s'" % self.__app_ini) |
|
337 |
|
338 if (sys.platform == 'darwin' and |
|
339 self.binary == self.__DARWIN_PATH and |
|
340 self.__app_ini.endswith(self.__DARWIN_APP_INI_SUFFIX)): |
|
341 # If the application.ini is in an app bundle, then |
|
342 # it could be inside an "installed" XULRunner app. |
|
343 # If this is the case, use the app's actual |
|
344 # binary instead of the XUL framework's, so we get |
|
345 # a proper app icon, etc. |
|
346 new_binary = '/'.join(self.__app_ini.split('/')[:-2] + |
|
347 ['MacOS', 'xulrunner']) |
|
348 if os.path.exists(new_binary): |
|
349 self.binary = new_binary |
|
350 |
|
351 @property |
|
352 def command(self): |
|
353 """Returns the command list to run.""" |
|
354 |
|
355 if self.__is_xulrunner_sdk: |
|
356 return [self.binary, self.__app_ini, '-profile', |
|
357 self.profile.profile] |
|
358 else: |
|
359 return [self.binary, '-app', self.__app_ini, '-profile', |
|
360 self.profile.profile] |
|
361 |
|
362 def __find_xulrunner_binary(self): |
|
363 if sys.platform == 'darwin': |
|
364 if os.path.exists(self.__DARWIN_PATH): |
|
365 return self.__DARWIN_PATH |
|
366 if sys.platform == 'linux2': |
|
367 if os.path.exists(self.__LINUX_PATH): |
|
368 return self.__LINUX_PATH |
|
369 return None |
|
370 |
|
371 def find_binary(self): |
|
372 # This gets called by the superclass constructor. It will |
|
373 # always get called, even if a binary was passed into the |
|
374 # constructor, because we want to have full control over |
|
375 # what the exact setting of self.binary is. |
|
376 |
|
377 if not self.__real_binary: |
|
378 self.__real_binary = self.__find_xulrunner_binary() |
|
379 if not self.__real_binary: |
|
380 dummy_profile = {} |
|
381 runner = mozrunner.FirefoxRunner(profile=dummy_profile) |
|
382 self.__real_binary = runner.find_binary() |
|
383 self.names = runner.names |
|
384 return self.__real_binary |
|
385 |
|
386 def set_overloaded_modules(env_root, app_type, addon_id, preferences, overloads): |
|
387 # win32 file scheme needs 3 slashes |
|
388 desktop_file_scheme = "file://" |
|
389 if not env_root.startswith("/"): |
|
390 desktop_file_scheme = desktop_file_scheme + "/" |
|
391 |
|
392 pref_prefix = "extensions.modules." + addon_id + ".path" |
|
393 |
|
394 # Set preferences that will map require prefix to a given path |
|
395 for name, path in overloads.items(): |
|
396 if len(name) == 0: |
|
397 prefName = pref_prefix |
|
398 else: |
|
399 prefName = pref_prefix + "." + name |
|
400 if app_type == "fennec-on-device": |
|
401 # For testing on device, we have to copy overloaded files from fs |
|
402 # to the device and use device path instead of local fs path. |
|
403 # Actual copy of files if done after the call to Profile constructor |
|
404 preferences[prefName] = "file://" + \ |
|
405 FENNEC_REMOTE_PATH + "/overloads/" + name |
|
406 else: |
|
407 preferences[prefName] = desktop_file_scheme + \ |
|
408 path.replace("\\", "/") + "/" |
|
409 |
|
410 def run_app(harness_root_dir, manifest_rdf, harness_options, |
|
411 app_type, binary=None, profiledir=None, verbose=False, |
|
412 parseable=False, enforce_timeouts=False, |
|
413 logfile=None, addons=None, args=None, extra_environment={}, |
|
414 norun=None, |
|
415 used_files=None, enable_mobile=False, |
|
416 mobile_app_name=None, |
|
417 env_root=None, |
|
418 is_running_tests=False, |
|
419 overload_modules=False, |
|
420 bundle_sdk=True, |
|
421 pkgdir=""): |
|
422 if binary: |
|
423 binary = os.path.expanduser(binary) |
|
424 |
|
425 if addons is None: |
|
426 addons = [] |
|
427 else: |
|
428 addons = list(addons) |
|
429 |
|
430 cmdargs = [] |
|
431 preferences = dict(DEFAULT_COMMON_PREFS) |
|
432 |
|
433 # For now, only allow running on Mobile with --force-mobile argument |
|
434 if app_type in ["fennec", "fennec-on-device"] and not enable_mobile: |
|
435 print """ |
|
436 WARNING: Firefox Mobile support is still experimental. |
|
437 If you would like to run an addon on this platform, use --force-mobile flag: |
|
438 |
|
439 cfx --force-mobile""" |
|
440 return 0 |
|
441 |
|
442 if app_type == "fennec-on-device": |
|
443 profile_class = FennecProfile |
|
444 preferences.update(DEFAULT_FENNEC_PREFS) |
|
445 runner_class = RemoteFennecRunner |
|
446 # We pass the intent name through command arguments |
|
447 cmdargs.append(mobile_app_name) |
|
448 elif enable_mobile or app_type == "fennec": |
|
449 profile_class = FennecProfile |
|
450 preferences.update(DEFAULT_FENNEC_PREFS) |
|
451 runner_class = FennecRunner |
|
452 elif app_type == "xulrunner": |
|
453 profile_class = XulrunnerAppProfile |
|
454 runner_class = XulrunnerAppRunner |
|
455 cmdargs.append(os.path.join(harness_root_dir, 'application.ini')) |
|
456 elif app_type == "firefox": |
|
457 profile_class = mozrunner.FirefoxProfile |
|
458 preferences.update(DEFAULT_FIREFOX_PREFS) |
|
459 runner_class = mozrunner.FirefoxRunner |
|
460 elif app_type == "thunderbird": |
|
461 profile_class = mozrunner.ThunderbirdProfile |
|
462 preferences.update(DEFAULT_THUNDERBIRD_PREFS) |
|
463 runner_class = mozrunner.ThunderbirdRunner |
|
464 else: |
|
465 raise ValueError("Unknown app: %s" % app_type) |
|
466 if sys.platform == 'darwin' and app_type != 'xulrunner': |
|
467 cmdargs.append('-foreground') |
|
468 |
|
469 if args: |
|
470 cmdargs.extend(shlex.split(args)) |
|
471 |
|
472 # TODO: handle logs on remote device |
|
473 if app_type != "fennec-on-device": |
|
474 # tempfile.gettempdir() was constant, preventing two simultaneous "cfx |
|
475 # run"/"cfx test" on the same host. On unix it points at /tmp (which is |
|
476 # world-writeable), enabling a symlink attack (e.g. imagine some bad guy |
|
477 # does 'ln -s ~/.ssh/id_rsa /tmp/harness_result'). NamedTemporaryFile |
|
478 # gives us a unique filename that fixes both problems. We leave the |
|
479 # (0-byte) file in place until the browser-side code starts writing to |
|
480 # it, otherwise the symlink attack becomes possible again. |
|
481 fileno,resultfile = tempfile.mkstemp(prefix="harness-result-") |
|
482 os.close(fileno) |
|
483 harness_options['resultFile'] = resultfile |
|
484 |
|
485 def maybe_remove_logfile(): |
|
486 if os.path.exists(logfile): |
|
487 os.remove(logfile) |
|
488 |
|
489 logfile_tail = None |
|
490 |
|
491 # We always buffer output through a logfile for two reasons: |
|
492 # 1. On Windows, it's the only way to print console output to stdout/err. |
|
493 # 2. It enables us to keep track of the last time output was emitted, |
|
494 # so we can raise an exception if the test runner hangs. |
|
495 if not logfile: |
|
496 fileno,logfile = tempfile.mkstemp(prefix="harness-log-") |
|
497 os.close(fileno) |
|
498 logfile_tail = follow_file(logfile) |
|
499 atexit.register(maybe_remove_logfile) |
|
500 |
|
501 logfile = os.path.abspath(os.path.expanduser(logfile)) |
|
502 maybe_remove_logfile() |
|
503 |
|
504 env = {} |
|
505 env.update(os.environ) |
|
506 env['MOZ_NO_REMOTE'] = '1' |
|
507 env['XPCOM_DEBUG_BREAK'] = 'stack' |
|
508 env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1' |
|
509 env.update(extra_environment) |
|
510 if norun: |
|
511 cmdargs.append("-no-remote") |
|
512 |
|
513 # Create the addon XPI so mozrunner will copy it to the profile it creates. |
|
514 # We delete it below after getting mozrunner to create the profile. |
|
515 from cuddlefish.xpi import build_xpi |
|
516 xpi_path = tempfile.mktemp(suffix='cfx-tmp.xpi') |
|
517 build_xpi(template_root_dir=harness_root_dir, |
|
518 manifest=manifest_rdf, |
|
519 xpi_path=xpi_path, |
|
520 harness_options=harness_options, |
|
521 limit_to=used_files, |
|
522 bundle_sdk=bundle_sdk, |
|
523 pkgdir=pkgdir) |
|
524 addons.append(xpi_path) |
|
525 |
|
526 starttime = last_output_time = time.time() |
|
527 |
|
528 # Redirect runner output to a file so we can catch output not generated |
|
529 # by us. |
|
530 # In theory, we could do this using simple redirection on all platforms |
|
531 # other than Windows, but this way we only have a single codepath to |
|
532 # maintain. |
|
533 fileno,outfile = tempfile.mkstemp(prefix="harness-stdout-") |
|
534 os.close(fileno) |
|
535 outfile_tail = follow_file(outfile) |
|
536 def maybe_remove_outfile(): |
|
537 if os.path.exists(outfile): |
|
538 os.remove(outfile) |
|
539 atexit.register(maybe_remove_outfile) |
|
540 outf = open(outfile, "w") |
|
541 popen_kwargs = { 'stdout': outf, 'stderr': outf} |
|
542 |
|
543 profile = None |
|
544 |
|
545 if app_type == "fennec-on-device": |
|
546 # Install a special addon when we run firefox on mobile device |
|
547 # in order to be able to kill it |
|
548 mydir = os.path.dirname(os.path.abspath(__file__)) |
|
549 addon_dir = os.path.join(mydir, "mobile-utils") |
|
550 addons.append(addon_dir) |
|
551 |
|
552 # Overload addon-specific commonjs modules path with lib/ folder |
|
553 overloads = dict() |
|
554 if overload_modules: |
|
555 overloads[""] = os.path.join(env_root, "lib") |
|
556 |
|
557 # Overload tests/ mapping with test/ folder, only when running test |
|
558 if is_running_tests: |
|
559 overloads["tests"] = os.path.join(env_root, "test") |
|
560 |
|
561 set_overloaded_modules(env_root, app_type, harness_options["jetpackID"], \ |
|
562 preferences, overloads) |
|
563 |
|
564 # the XPI file is copied into the profile here |
|
565 profile = profile_class(addons=addons, |
|
566 profile=profiledir, |
|
567 preferences=preferences) |
|
568 |
|
569 # Delete the temporary xpi file |
|
570 os.remove(xpi_path) |
|
571 |
|
572 # Copy overloaded files registered in set_overloaded_modules |
|
573 # For testing on device, we have to copy overloaded files from fs |
|
574 # to the device and use device path instead of local fs path. |
|
575 # (has to be done after the call to profile_class() which eventualy creates |
|
576 # profile folder) |
|
577 if app_type == "fennec-on-device": |
|
578 profile_path = profile.profile |
|
579 for name, path in overloads.items(): |
|
580 shutil.copytree(path, \ |
|
581 os.path.join(profile_path, "overloads", name)) |
|
582 |
|
583 runner = runner_class(profile=profile, |
|
584 binary=binary, |
|
585 env=env, |
|
586 cmdargs=cmdargs, |
|
587 kp_kwargs=popen_kwargs) |
|
588 |
|
589 sys.stdout.flush(); sys.stderr.flush() |
|
590 |
|
591 if app_type == "fennec-on-device": |
|
592 if not enable_mobile: |
|
593 print >>sys.stderr, """ |
|
594 WARNING: Firefox Mobile support is still experimental. |
|
595 If you would like to run an addon on this platform, use --force-mobile flag: |
|
596 |
|
597 cfx --force-mobile""" |
|
598 return 0 |
|
599 |
|
600 # In case of mobile device, we need to get stdio from `adb logcat` cmd: |
|
601 |
|
602 # First flush logs in order to avoid catching previous ones |
|
603 subprocess.call([binary, "logcat", "-c"]) |
|
604 |
|
605 # Launch adb command |
|
606 runner.start() |
|
607 |
|
608 # We can immediatly remove temporary profile folder |
|
609 # as it has been uploaded to the device |
|
610 profile.cleanup() |
|
611 # We are not going to use the output log file |
|
612 outf.close() |
|
613 |
|
614 # Then we simply display stdout of `adb logcat` |
|
615 p = subprocess.Popen([binary, "logcat", "stderr:V stdout:V GeckoConsole:V *:S"], stdout=subprocess.PIPE) |
|
616 while True: |
|
617 line = p.stdout.readline() |
|
618 if line == '': |
|
619 break |
|
620 # mobile-utils addon contains an application quit event observer |
|
621 # that will print this string: |
|
622 if "APPLICATION-QUIT" in line: |
|
623 break |
|
624 |
|
625 if verbose: |
|
626 # if --verbose is given, we display everything: |
|
627 # All JS Console messages, stdout and stderr. |
|
628 m = CLEANUP_ADB.match(line) |
|
629 if not m: |
|
630 print line.rstrip() |
|
631 continue |
|
632 print m.group(3) |
|
633 else: |
|
634 # Otherwise, display addons messages dispatched through |
|
635 # console.[info, log, debug, warning, error](msg) |
|
636 m = FILTER_ONLY_CONSOLE_FROM_ADB.match(line) |
|
637 if m: |
|
638 print m.group(2) |
|
639 |
|
640 print >>sys.stderr, "Program terminated successfully." |
|
641 return 0 |
|
642 |
|
643 |
|
644 print >>sys.stderr, "Using binary at '%s'." % runner.binary |
|
645 |
|
646 # Ensure cfx is being used with Firefox 4.0+. |
|
647 # TODO: instead of dying when Firefox is < 4, warn when Firefox is outside |
|
648 # the minVersion/maxVersion boundaries. |
|
649 version_output = check_output(runner.command + ["-v"]) |
|
650 # Note: this regex doesn't handle all valid versions in the Toolkit Version |
|
651 # Format <https://developer.mozilla.org/en/Toolkit_version_format>, just the |
|
652 # common subset that we expect Mozilla apps to use. |
|
653 mo = re.search(r"Mozilla (Firefox|Iceweasel|Fennec)\b[^ ]* ((\d+)\.\S*)", |
|
654 version_output) |
|
655 if not mo: |
|
656 # cfx may be used with Thunderbird, SeaMonkey or an exotic Firefox |
|
657 # version. |
|
658 print """ |
|
659 WARNING: cannot determine Firefox version; please ensure you are running |
|
660 a Mozilla application equivalent to Firefox 4.0 or greater. |
|
661 """ |
|
662 elif mo.group(1) == "Fennec": |
|
663 # For now, only allow running on Mobile with --force-mobile argument |
|
664 if not enable_mobile: |
|
665 print """ |
|
666 WARNING: Firefox Mobile support is still experimental. |
|
667 If you would like to run an addon on this platform, use --force-mobile flag: |
|
668 |
|
669 cfx --force-mobile""" |
|
670 return |
|
671 else: |
|
672 version = mo.group(3) |
|
673 if int(version) < 4: |
|
674 print """ |
|
675 cfx requires Firefox 4 or greater and is unable to find a compatible |
|
676 binary. Please install a newer version of Firefox or provide the path to |
|
677 your existing compatible version with the --binary flag: |
|
678 |
|
679 cfx --binary=PATH_TO_FIREFOX_BINARY""" |
|
680 return |
|
681 |
|
682 # Set the appropriate extensions.checkCompatibility preference to false, |
|
683 # so the tests run even if the SDK is not marked as compatible with the |
|
684 # version of Firefox on which they are running, and we don't have to |
|
685 # ensure we update the maxVersion before the version of Firefox changes |
|
686 # every six weeks. |
|
687 # |
|
688 # The regex we use here is effectively the same as BRANCH_REGEX from |
|
689 # /toolkit/mozapps/extensions/content/extensions.js, which toolkit apps |
|
690 # use to determine whether or not to load an incompatible addon. |
|
691 # |
|
692 br = re.search(r"^([^\.]+\.[0-9]+[a-z]*).*", mo.group(2), re.I) |
|
693 if br: |
|
694 prefname = 'extensions.checkCompatibility.' + br.group(1) |
|
695 profile.preferences[prefname] = False |
|
696 # Calling profile.set_preferences here duplicates the list of prefs |
|
697 # in prefs.js, since the profile calls self.set_preferences in its |
|
698 # constructor, but that is ok, because it doesn't change the set of |
|
699 # preferences that are ultimately registered in Firefox. |
|
700 profile.set_preferences(profile.preferences) |
|
701 |
|
702 print >>sys.stderr, "Using profile at '%s'." % profile.profile |
|
703 sys.stderr.flush() |
|
704 |
|
705 if norun: |
|
706 print "To launch the application, enter the following command:" |
|
707 print " ".join(runner.command) + " " + (" ".join(runner.cmdargs)) |
|
708 return 0 |
|
709 |
|
710 runner.start() |
|
711 |
|
712 done = False |
|
713 result = None |
|
714 test_name = "unknown" |
|
715 |
|
716 def Timeout(message, test_name, parseable): |
|
717 if parseable: |
|
718 sys.stderr.write("TEST-UNEXPECTED-FAIL | %s | %s\n" % (test_name, message)) |
|
719 sys.stderr.flush() |
|
720 return Exception(message) |
|
721 |
|
722 try: |
|
723 while not done: |
|
724 time.sleep(0.05) |
|
725 for tail in (logfile_tail, outfile_tail): |
|
726 if tail: |
|
727 new_chars = tail.next() |
|
728 if new_chars: |
|
729 last_output_time = time.time() |
|
730 sys.stderr.write(new_chars) |
|
731 sys.stderr.flush() |
|
732 if is_running_tests and parseable: |
|
733 match = PARSEABLE_TEST_NAME.search(new_chars) |
|
734 if match: |
|
735 test_name = match.group(1) |
|
736 if os.path.exists(resultfile): |
|
737 result = open(resultfile).read() |
|
738 if result: |
|
739 if result in ['OK', 'FAIL']: |
|
740 done = True |
|
741 else: |
|
742 sys.stderr.write("Hrm, resultfile (%s) contained something weird (%d bytes)\n" % (resultfile, len(result))) |
|
743 sys.stderr.write("'"+result+"'\n") |
|
744 if enforce_timeouts: |
|
745 if time.time() - last_output_time > OUTPUT_TIMEOUT: |
|
746 raise Timeout("Test output exceeded timeout (%ds)." % |
|
747 OUTPUT_TIMEOUT, test_name, parseable) |
|
748 if time.time() - starttime > RUN_TIMEOUT: |
|
749 raise Timeout("Test run exceeded timeout (%ds)." % |
|
750 RUN_TIMEOUT, test_name, parseable) |
|
751 except: |
|
752 runner.stop() |
|
753 raise |
|
754 else: |
|
755 runner.wait(10) |
|
756 finally: |
|
757 outf.close() |
|
758 if profile: |
|
759 profile.cleanup() |
|
760 |
|
761 print >>sys.stderr, "Total time: %f seconds" % (time.time() - starttime) |
|
762 |
|
763 if result == 'OK': |
|
764 print >>sys.stderr, "Program terminated successfully." |
|
765 return 0 |
|
766 else: |
|
767 print >>sys.stderr, "Program terminated unsuccessfully." |
|
768 return -1 |