runner.py 29 KB

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