author | Andrew Halberstadt <ahalberstadt@mozilla.com> |
Thu, 19 Jun 2014 14:17:26 -0400 | |
changeset 189627 | f5d1163268aee3ae8d4b9b54fddfc59fb8940030 |
parent 189626 | fefe4c4ffe939468dad5203c470046c8a9839073 |
child 189628 | 93be174c58237596c5fe303c81a15de8b4520b78 |
push id | 26992 |
push user | kwierso@gmail.com |
push date | Fri, 20 Jun 2014 01:07:53 +0000 |
treeherder | mozilla-central@bdac18bd6c74 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | wlach, mdas, jgriffin |
bugs | 997244 |
milestone | 33.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/build/mobile/b2gautomation.py +++ b/build/mobile/b2gautomation.py @@ -189,17 +189,17 @@ class B2GRemoteAutomation(Automation): def restartB2G(self): # TODO hangs in subprocess.Popen without this delay time.sleep(5) self._devicemanager._checkCmd(['shell', 'stop', 'b2g']) # Wait for a bit to make sure B2G has completely shut down. time.sleep(10) self._devicemanager._checkCmd(['shell', 'start', 'b2g']) if self._is_emulator: - self.marionette.emulator.wait_for_port() + self.marionette.emulator.wait_for_port(self.marionette.port) def rebootDevice(self): # find device's current status and serial number serial, status = self.getDeviceStatus() # reboot! self._devicemanager._runCmd(['shell', '/system/bin/reboot']) @@ -257,17 +257,17 @@ class B2GRemoteAutomation(Automation): # Set up port forwarding again for Marionette, since any that # existed previously got wiped out by the reboot. if not self._is_emulator: self._devicemanager._checkCmd(['forward', 'tcp:%s' % self.marionette.port, 'tcp:%s' % self.marionette.port]) if self._is_emulator: - self.marionette.emulator.wait_for_port() + self.marionette.emulator.wait_for_port(self.marionette.port) else: time.sleep(5) # start a marionette session session = self.marionette.start_session() if 'b2g' not in session: raise Exception("bad session value %s returned by start_session" % session)
--- a/build/valgrind/mach_commands.py +++ b/build/valgrind/mach_commands.py @@ -126,17 +126,17 @@ class MachCommands(MachCommandBase): valgrind_args.append('--suppressions=' + supps_file2) exitcode = None try: runner = FirefoxRunner(profile=profile, binary=self.get_binary_path(), cmdargs=firefox_args, env=env, - kp_kwargs=kp_kwargs) + process_args=kp_kwargs) runner.start(debug_args=valgrind_args) exitcode = runner.wait() finally: errs = outputHandler.error_count supps = outputHandler.suppression_count if errs != supps: status = 1 # turns the TBPL job orange
--- a/layout/tools/reftest/b2g_desktop.py +++ b/layout/tools/reftest/b2g_desktop.py @@ -17,26 +17,29 @@ from marionette import Marionette from mozprocess import ProcessHandler from mozrunner import FirefoxRunner import mozinfo import mozlog log = mozlog.getLogger('REFTEST') class B2GDesktopReftest(RefTest): - def __init__(self, marionette): + marionette = None + + def __init__(self, marionette_args): RefTest.__init__(self) self.last_test = os.path.basename(__file__) - self.marionette = marionette + self.marionette_args = marionette_args self.profile = None self.runner = None self.test_script = os.path.join(here, 'b2g_start_script.js') self.timeout = None def run_marionette_script(self): + self.marionette = Marionette(**self.marionette_args) assert(self.marionette.wait_for_port()) self.marionette.start_session() self.marionette.set_context(self.marionette.CONTEXT_CHROME) if os.path.isfile(self.test_script): f = open(self.test_script, 'r') self.test_script = f.read() f.close() @@ -66,18 +69,18 @@ class B2GDesktopReftest(RefTest): cmd, args = self.build_command_line(options.app, ignore_window_size=options.ignoreWindowSize, browser_arg=options.browser_arg) self.runner = FirefoxRunner(profile=self.profile, binary=cmd, cmdargs=args, env=env, process_class=ProcessHandler, - symbols_path=options.symbolsPath, - kp_kwargs=kp_kwargs) + process_args=kp_kwargs, + symbols_path=options.symbolsPath) status = 0 try: self.runner.start(outputTimeout=self.timeout) log.info("%s | Application pid: %d", os.path.basename(__file__), self.runner.process_handler.pid) @@ -146,24 +149,23 @@ class B2GDesktopReftest(RefTest): msg = "%s | application timed out after %s seconds with no output" log.testFail(msg % (self.last_test, self.timeout)) # kill process to get a stack self.runner.stop(sig=signal.SIGABRT) def run_desktop_reftests(parser, options, args): - kwargs = {} + marionette_args = {} if options.marionette: host, port = options.marionette.split(':') - kwargs['host'] = host - kwargs['port'] = int(port) - marionette = Marionette.getMarionetteOrExit(**kwargs) + marionette_args['host'] = host + marionette_args['port'] = int(port) - reftest = B2GDesktopReftest(marionette) + reftest = B2GDesktopReftest(marionette_args) options = ReftestOptions.verifyCommonOptions(parser, options, reftest) if options == None: sys.exit(1) # add a -bin suffix if b2g-bin exists, but just b2g was specified if options.app[-4:] != '-bin': if os.path.isfile("%s-bin" % options.app):
--- a/layout/tools/reftest/mach_commands.py +++ b/layout/tools/reftest/mach_commands.py @@ -196,17 +196,17 @@ class ReftestRunner(MozbuildObject): try: which.which('adb') except which.WhichError: # TODO Find adb automatically if it isn't on the path raise Exception(ADB_NOT_FOUND % ('%s-remote' % suite, b2g_home)) options.b2gPath = b2g_home - options.logcat_dir = self.reftest_dir + options.logdir = self.reftest_dir options.httpdPath = os.path.join(self.topsrcdir, 'netwerk', 'test', 'httpserver') options.xrePath = xre_path options.ignoreWindowSize = True # Don't enable oop for crashtest until they run oop in automation if suite == 'reftest': options.oop = True @@ -330,19 +330,19 @@ def ReftestCommand(func): def B2GCommand(func): """Decorator that adds shared command arguments to b2g mochitest commands.""" busybox = CommandArgument('--busybox', default=None, help='Path to busybox binary to install on device') func = busybox(func) - logcatdir = CommandArgument('--logcat-dir', default=None, - help='directory to store logcat dump files') - func = logcatdir(func) + logdir = CommandArgument('--logdir', default=None, + help='directory to store log files') + func = logdir(func) sdcard = CommandArgument('--sdcard', default="10MB", help='Define size of sdcard: 1MB, 50MB...etc') func = sdcard(func) emulator_res = CommandArgument('--emulator-res', default='800x1000', help='Emulator resolution of the format \'<width>x<height>\'') func = emulator_res(func)
--- a/layout/tools/reftest/runreftestb2g.py +++ b/layout/tools/reftest/runreftestb2g.py @@ -53,19 +53,19 @@ class B2GOptions(ReftestOptions): defaults["emulator_res"] = None self.add_option("--no-window", action="store_true", dest = "noWindow", help = "Pass --no-window to the emulator") defaults["noWindow"] = False self.add_option("--adbpath", action="store", - type = "string", dest = "adbPath", + type = "string", dest = "adb_path", help = "path to adb") - defaults["adbPath"] = "adb" + defaults["adb_path"] = "adb" self.add_option("--deviceIP", action="store", type = "string", dest = "deviceIP", help = "ip address of remote device to test") defaults["deviceIP"] = None self.add_option("--devicePort", action="store", type = "string", dest = "devicePort", @@ -96,20 +96,20 @@ class B2GOptions(ReftestOptions): type = "string", dest = "pidFile", help = "name of the pidfile to generate") defaults["pidFile"] = "" self.add_option("--gecko-path", action="store", type="string", dest="geckoPath", help="the path to a gecko distribution that should " "be installed on the emulator prior to test") defaults["geckoPath"] = None - self.add_option("--logcat-dir", action="store", - type="string", dest="logcat_dir", - help="directory to store logcat dump files") - defaults["logcat_dir"] = None + self.add_option("--logdir", action="store", + type="string", dest="logdir", + help="directory to store log files") + defaults["logdir"] = None self.add_option('--busybox', action='store', type='string', dest='busybox', help="Path to busybox binary to install on device") defaults['busybox'] = None self.add_option("--httpd-path", action = "store", type = "string", dest = "httpdPath", help = "path to the httpd.js file") defaults["httpdPath"] = None @@ -161,18 +161,18 @@ class B2GOptions(ReftestOptions): options.httpPort = auto.DEFAULT_HTTP_PORT if not options.sslPort: options.sslPort = auto.DEFAULT_SSL_PORT if options.geckoPath and not options.emulator: self.error("You must specify --emulator if you specify --gecko-path") - if options.logcat_dir and not options.emulator: - self.error("You must specify --emulator if you specify --logcat-dir") + if options.logdir and not options.emulator: + self.error("You must specify --emulator if you specify --logdir") #if not options.emulator and not options.deviceIP: # print "ERROR: you must provide a device IP" # return None if options.remoteLogFile == None: options.remoteLogFile = "reftest.log" @@ -492,43 +492,45 @@ def run_remote_reftests(parser, options, kwargs = {} if options.emulator: kwargs['emulator'] = options.emulator auto.setEmulator(True) if options.noWindow: kwargs['noWindow'] = True if options.geckoPath: kwargs['gecko_path'] = options.geckoPath - if options.logcat_dir: - kwargs['logcat_dir'] = options.logcat_dir + if options.logdir: + kwargs['logdir'] = options.logdir if options.busybox: kwargs['busybox'] = options.busybox if options.symbolsPath: kwargs['symbols_path'] = options.symbolsPath if options.emulator_res: kwargs['emulator_res'] = options.emulator_res if options.b2gPath: kwargs['homedir'] = options.b2gPath if options.marionette: host,port = options.marionette.split(':') kwargs['host'] = host kwargs['port'] = int(port) - marionette = Marionette.getMarionetteOrExit(**kwargs) + if options.adb_path: + kwargs['adb_path'] = options.adb_path + marionette = Marionette(**kwargs) auto.marionette = marionette if options.emulator: dm = marionette.emulator.dm else: # create the DeviceManager - kwargs = {'adbPath': options.adbPath, + kwargs = {'adbPath': options.adb_path, 'deviceRoot': options.remoteTestRoot} if options.deviceIP: kwargs.update({'host': options.deviceIP, 'port': options.devicePort}) - dm = DeviagerADB(**kwargs) + dm = DeviceManagerADB(**kwargs) auto.setDeviceManager(dm) options = parser.verifyRemoteOptions(options, auto) if (options == None): print "ERROR: Invalid options specified, use --help for a list of valid options" sys.exit(1)
--- a/testing/config/mozharness/b2g_emulator_config.py +++ b/testing/config/mozharness/b2g_emulator_config.py @@ -1,50 +1,50 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. config = { "jsreftest_options": [ "--adbpath=%(adbpath)s", "--b2gpath=%(b2gpath)s", "--emulator=%(emulator)s", - "--emulator-res=800x1000", "--logcat-dir=%(logcat_dir)s", + "--emulator-res=800x1000", "--logdir=%(logcat_dir)s", "--remote-webserver=%(remote_webserver)s", "--ignore-window-size", "--xre-path=%(xre_path)s", "--symbols-path=%(symbols_path)s", "--busybox=%(busybox)s", "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s", "--extra-profile-file=jsreftest/tests/user.js", "%(test_manifest)s", ], "mochitest_options": [ "--adbpath=%(adbpath)s", "--b2gpath=%(b2gpath)s", "--console-level=INFO", - "--emulator=%(emulator)s", "--logcat-dir=%(logcat_dir)s", + "--emulator=%(emulator)s", "--logdir=%(logcat_dir)s", "--remote-webserver=%(remote_webserver)s", "%(test_manifest)s", "--xre-path=%(xre_path)s", "--symbols-path=%(symbols_path)s", "--busybox=%(busybox)s", "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s", "--quiet", "--certificate-path=%(certificate_path)s", "--test-path=%(test_path)s", ], "reftest_options": [ "--adbpath=%(adbpath)s", "--b2gpath=%(b2gpath)s", "--emulator=%(emulator)s", - "--emulator-res=800x1000", "--logcat-dir=%(logcat_dir)s", + "--emulator-res=800x1000", "--logdir=%(logcat_dir)s", "--remote-webserver=%(remote_webserver)s", "--ignore-window-size", "--xre-path=%(xre_path)s", "--symbols-path=%(symbols_path)s", "--busybox=%(busybox)s", "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s", "--enable-oop", "%(test_manifest)s", ], "crashtest_options": [ "--adbpath=%(adbpath)s", "--b2gpath=%(b2gpath)s", "--emulator=%(emulator)s", - "--emulator-res=800x1000", "--logcat-dir=%(logcat_dir)s", + "--emulator-res=800x1000", "--logdir=%(logcat_dir)s", "--remote-webserver=%(remote_webserver)s", "--ignore-window-size", "--xre-path=%(xre_path)s", "--symbols-path=%(symbols_path)s", "--busybox=%(busybox)s", "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s", "%(test_manifest)s", ], "xpcshell_options": [ "--adbpath=%(adbpath)s", "--b2gpath=%(b2gpath)s", "--emulator=%(emulator)s", - "--logcat-dir=%(logcat_dir)s", "--manifest=%(test_manifest)s", "--use-device-libs", + "--logdir=%(logcat_dir)s", "--manifest=%(test_manifest)s", "--use-device-libs", "--testing-modules-dir=%(modules_dir)s", "--symbols-path=%(symbols_path)s", "--busybox=%(busybox)s", "--total-chunks=%(total_chunks)s", "--this-chunk=%(this_chunk)s", ], }
--- a/testing/marionette/client/marionette/__init__.py +++ b/testing/marionette/client/marionette/__init__.py @@ -1,26 +1,54 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from gestures import smooth_scroll, pinch from by import By from marionette import Marionette, HTMLElement, Actions, MultiActions from marionette_test import MarionetteTestCase, MarionetteJSTestCase, CommonTestCase, expectedFailure, skip, SkipTest -from emulator import Emulator from errors import ( - ErrorCodes, MarionetteException, InstallGeckoError, TimeoutException, InvalidResponseException, - JavascriptException, NoSuchElementException, XPathLookupException, NoSuchWindowException, - StaleElementException, ScriptTimeoutException, ElementNotVisibleException, - NoSuchFrameException, InvalidElementStateException, NoAlertPresentException, - InvalidCookieDomainException, UnableToSetCookieException, InvalidSelectorException, - MoveTargetOutOfBoundsException, FrameSendNotInitializedError, FrameSendFailureError - ) + ElementNotVisibleException, + ErrorCodes, + FrameSendFailureError, + FrameSendNotInitializedError, + InvalidCookieDomainException, + InvalidElementStateException, + InvalidResponseException, + InvalidSelectorException, + JavascriptException, + MarionetteException, + MoveTargetOutOfBoundsException, + NoAlertPresentException, + NoSuchElementException, + NoSuchFrameException, + NoSuchWindowException, + ScriptTimeoutException, + StaleElementException, + TimeoutException, + UnableToSetCookieException, + XPathLookupException, +) from runner import ( - B2GTestCaseMixin, B2GTestResultMixin, BaseMarionetteOptions, BaseMarionetteTestRunner, EnduranceOptionsMixin, - EnduranceTestCaseMixin, HTMLReportingOptionsMixin, HTMLReportingTestResultMixin, HTMLReportingTestRunnerMixin, - Marionette, MarionetteTest, MarionetteTestResult, MarionetteTextTestRunner, MemoryEnduranceTestCaseMixin, - MozHttpd, OptionParser, TestManifest, TestResult, TestResultCollection - ) + B2GTestCaseMixin, + B2GTestResultMixin, + BaseMarionetteOptions, + BaseMarionetteTestRunner, + EnduranceOptionsMixin, + EnduranceTestCaseMixin, + HTMLReportingOptionsMixin, + HTMLReportingTestResultMixin, + HTMLReportingTestRunnerMixin, + Marionette, + MarionetteTest, + MarionetteTestResult, + MarionetteTextTestRunner, + MemoryEnduranceTestCaseMixin, + MozHttpd, + OptionParser, + TestManifest, + TestResult, + TestResultCollection +) from wait import Wait from date_time_value import DateTimeValue import decorators
deleted file mode 100644 --- a/testing/marionette/client/marionette/b2gbuild.py +++ /dev/null @@ -1,98 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -import os -import platform -import subprocess -import sys - - -class B2GBuild(object): - - @classmethod - def find_b2g_dir(cls): - for env_var in ('B2G_DIR', 'B2G_HOME'): - if env_var in os.environ: - env_dir = os.environ[env_var] - env_dir = cls.check_b2g_dir(env_dir) - if env_dir: - return env_dir - - cwd = os.getcwd() - cwd = cls.check_b2g_dir(cwd) - if cwd: - return cwd - - return None - - @classmethod - def check_adb(cls, homedir): - if 'ADB' in os.environ: - env_adb = os.environ['ADB'] - if os.path.exists(env_adb): - return env_adb - - return cls.check_host_binary(homedir, 'adb') - - @classmethod - def check_b2g_dir(cls, dir): - if os.path.isfile(os.path.join(dir, 'load-config.sh')): - return dir - - oldstyle_dir = os.path.join(dir, 'glue', 'gonk-ics') - if os.access(oldstyle_dir, os.F_OK): - return oldstyle_dir - - return None - - @classmethod - def check_fastboot(cls, homedir): - return cls.check_host_binary(homedir, 'fastboot') - - @classmethod - def check_host_binary(cls, homedir, binary): - host_dir = "linux-x86" - if platform.system() == "Darwin": - host_dir = "darwin-x86" - binary_path = subprocess.Popen(['which', binary], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - if binary_path.wait() == 0: - return binary_path.stdout.read().strip() # remove trailing newline - binary_paths = [os.path.join(homedir, 'glue', 'gonk', 'out', 'host', - host_dir, 'bin', binary), - os.path.join(homedir, 'out', 'host', host_dir, - 'bin', binary), - os.path.join(homedir, 'bin', binary)] - for option in binary_paths: - if os.path.exists(option): - return option - raise Exception('%s not found!' % binary) - - def __init__(self, homedir=None, emulator=False): - if not homedir: - homedir = self.find_b2g_dir() - else: - homedir = self.check_b2g_dir(homedir) - - if not homedir: - raise EnvironmentError('Must define B2G_HOME or pass the homedir parameter') - - self.homedir = homedir - self.adb_path = self.check_adb(self.homedir) - self.update_tools = os.path.join(self.homedir, 'tools', 'update-tools') - self.fastboot_path = None if emulator else self.check_fastboot(self.homedir) - - def import_update_tools(self): - """Import the update_tools package from B2G""" - sys.path.append(self.update_tools) - import update_tools - sys.path.pop() - return update_tools - - def check_file(self, filePath): - if not os.access(filePath, os.F_OK): - raise Exception(('File not found: %s; did you pass the B2G home ' - 'directory as the homedir parameter, or set ' - 'B2G_HOME correctly?') % filePath)
deleted file mode 100644 --- a/testing/marionette/client/marionette/b2ginstance.py +++ /dev/null @@ -1,70 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -from ConfigParser import ConfigParser -import posixpath -import shutil -import tempfile -import traceback - -from b2gbuild import B2GBuild -from mozdevice import DeviceManagerADB -import mozcrash - - -class B2GInstance(B2GBuild): - - def __init__(self, devicemanager=None, symbols_path=None, **kwargs): - B2GBuild.__init__(self, **kwargs) - - self._dm = devicemanager - self._remote_profiles = None - self.symbols_path = symbols_path - - @property - def dm(self): - if not self._dm: - self._dm = DeviceManagerADB(adbPath=self.adb_path) - return self._dm - - @property - def remote_profiles(self): - if not self._remote_profiles: - self.check_remote_profiles() - return self._remote_profiles - - def check_remote_profiles(self, remote_profiles_ini='/data/b2g/mozilla/profiles.ini'): - if not self.dm.fileExists(remote_profiles_ini): - raise Exception("Remote file '%s' not found" % remote_profiles_ini) - - local_profiles_ini = tempfile.NamedTemporaryFile() - self.dm.getFile(remote_profiles_ini, local_profiles_ini.name) - cfg = ConfigParser() - cfg.read(local_profiles_ini.name) - - remote_profiles = [] - for section in cfg.sections(): - if cfg.has_option(section, 'Path'): - if cfg.has_option(section, 'IsRelative') and cfg.getint(section, 'IsRelative'): - remote_profiles.append(posixpath.join(posixpath.dirname(remote_profiles_ini), cfg.get(section, 'Path'))) - else: - remote_profiles.append(cfg.get(section, 'Path')) - self._remote_profiles = remote_profiles - return remote_profiles - - def check_for_crashes(self): - remote_dump_dirs = [posixpath.join(p, 'minidumps') for p in self.remote_profiles] - crashed = False - for remote_dump_dir in remote_dump_dirs: - local_dump_dir = tempfile.mkdtemp() - self.dm.getDirectory(remote_dump_dir, local_dump_dir) - try: - if mozcrash.check_for_crashes(local_dump_dir, self.symbols_path): - crashed = True - except: - traceback.print_exc() - finally: - shutil.rmtree(local_dump_dir) - self.dm.removeDir(remote_dump_dir) - return crashed
deleted file mode 100644 --- a/testing/marionette/client/marionette/emulator.py +++ /dev/null @@ -1,539 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -from b2ginstance import B2GInstance -import datetime -from mozdevice import devicemanagerADB, DMError -from mozprocess import ProcessHandlerMixin -import os -import re -import platform -import shutil -import socket -import subprocess -from telnetlib import Telnet -import tempfile -import time -import traceback - -from emulator_battery import EmulatorBattery -from emulator_geo import EmulatorGeo -from emulator_screen import EmulatorScreen -from decorators import uses_marionette - -from errors import ( - InstallGeckoError, - InvalidResponseException, - MarionetteException, - ScriptTimeoutException, - TimeoutException -) - - -class LogOutputProc(ProcessHandlerMixin): - """ - Process handler for processes which save all output to a logfile. - If no logfile is specified, output will still be consumed to prevent - the output pipe's from overflowing. - """ - - def __init__(self, cmd, logfile=None, **kwargs): - self.logfile = logfile - kwargs.setdefault('processOutputLine', []).append(self.log_output) - ProcessHandlerMixin.__init__(self, cmd, **kwargs) - - def log_output(self, line): - if not self.logfile: - return - - f = open(self.logfile, 'a') - f.write(line + "\n") - f.flush() - - -class Emulator(object): - - deviceRe = re.compile(r"^emulator-(\d+)(\s*)(.*)$") - _default_res = '320x480' - prefs = {'app.update.enabled': False, - 'app.update.staging.enabled': False, - 'app.update.service.enabled': False} - env = {'MOZ_CRASHREPORTER': '1', - 'MOZ_CRASHREPORTER_NO_REPORT': '1', - 'MOZ_CRASHREPORTER_SHUTDOWN': '1'} - - def __init__(self, homedir=None, noWindow=False, logcat_dir=None, - arch="x86", emulatorBinary=None, res=None, sdcard=None, - symbols_path=None, userdata=None): - self.port = None - self.dm = None - self._emulator_launched = False - self.proc = None - self.marionette_port = None - self.telnet = None - self._tmp_sdcard = None - self._tmp_userdata = None - self._adb_started = False - self.logcat_dir = logcat_dir - self.logcat_proc = None - self.arch = arch - self.binary = emulatorBinary - self.res = res or self._default_res - self.battery = EmulatorBattery(self) - self.geo = EmulatorGeo(self) - self.screen = EmulatorScreen(self) - self.homedir = homedir - self.sdcard = sdcard - self.symbols_path = symbols_path - self.noWindow = noWindow - if self.homedir is not None: - self.homedir = os.path.expanduser(homedir) - self.dataImg = userdata - self.copy_userdata = self.dataImg is None - - def _check_for_b2g(self): - self.b2g = B2GInstance(homedir=self.homedir, emulator=True, - symbols_path=self.symbols_path) - self.adb = self.b2g.adb_path - self.homedir = self.b2g.homedir - - if self.arch not in ("x86", "arm"): - raise Exception("Emulator architecture must be one of x86, arm, got: %s" % - self.arch) - - host_dir = "linux-x86" - if platform.system() == "Darwin": - host_dir = "darwin-x86" - - host_bin_dir = os.path.join("out", "host", host_dir, "bin") - - if self.arch == "x86": - binary = os.path.join(host_bin_dir, "emulator-x86") - kernel = "prebuilts/qemu-kernel/x86/kernel-qemu" - sysdir = "out/target/product/generic_x86" - self.tail_args = [] - else: - binary = os.path.join(host_bin_dir, "emulator") - kernel = "prebuilts/qemu-kernel/arm/kernel-qemu-armv7" - sysdir = "out/target/product/generic" - self.tail_args = ["-cpu", "cortex-a8"] - - if(self.sdcard): - self.mksdcard = os.path.join(self.homedir, host_bin_dir, "mksdcard") - self.create_sdcard(self.sdcard) - - if not self.binary: - self.binary = os.path.join(self.homedir, binary) - - self.b2g.check_file(self.binary) - - self.kernelImg = os.path.join(self.homedir, kernel) - self.b2g.check_file(self.kernelImg) - - self.sysDir = os.path.join(self.homedir, sysdir) - self.b2g.check_file(self.sysDir) - - if not self.dataImg: - self.dataImg = os.path.join(self.sysDir, 'userdata.img') - self.b2g.check_file(self.dataImg) - - def __del__(self): - if self.telnet: - self.telnet.write('exit\n') - self.telnet.read_all() - - @property - def args(self): - qemuArgs = [self.binary, - '-kernel', self.kernelImg, - '-sysdir', self.sysDir, - '-data', self.dataImg] - if self._tmp_sdcard: - qemuArgs.extend(['-sdcard', self._tmp_sdcard]) - if self.noWindow: - qemuArgs.append('-no-window') - qemuArgs.extend(['-memory', '512', - '-partition-size', '512', - '-verbose', - '-skin', self.res, - '-gpu', 'on', - '-qemu'] + self.tail_args) - return qemuArgs - - @property - def is_running(self): - if self._emulator_launched: - return self.proc is not None and self.proc.poll() is None - else: - return self.port is not None - - def check_for_crash(self): - """ - Checks if the emulator has crashed or not. Always returns False if - we've connected to an already-running emulator, since we can't track - the emulator's pid in that case. Otherwise, returns True iff - self.proc is not None (meaning the emulator hasn't been explicitly - closed), and self.proc.poll() is also not None (meaning the emulator - process has terminated). - """ - return self._emulator_launched and self.proc is not None \ - and self.proc.poll() is not None - - def check_for_minidumps(self): - return self.b2g.check_for_crashes() - - def create_sdcard(self, sdcard): - self._tmp_sdcard = tempfile.mktemp(prefix='sdcard') - sdargs = [self.mksdcard, "-l", "mySdCard", sdcard, self._tmp_sdcard] - sd = subprocess.Popen(sdargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - retcode = sd.wait() - if retcode: - raise Exception('unable to create sdcard : exit code %d: %s' - % (retcode, sd.stdout.read())) - return None - - def _run_adb(self, args): - args.insert(0, self.adb) - if self.port: - args.insert(1, '-s') - args.insert(2, 'emulator-%d' % self.port) - adb = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - retcode = adb.wait() - if retcode: - raise Exception('adb terminated with exit code %d: %s' - % (retcode, adb.stdout.read())) - return adb.stdout.read() - - def _get_telnet_response(self, command=None): - output = [] - assert(self.telnet) - if command is not None: - self.telnet.write('%s\n' % command) - while True: - line = self.telnet.read_until('\n') - output.append(line.rstrip()) - if line.startswith('OK'): - return output - elif line.startswith('KO:'): - raise Exception('bad telnet response: %s' % line) - - def _run_telnet(self, command): - if not self.telnet: - self.telnet = Telnet('localhost', self.port) - self._get_telnet_response() - return self._get_telnet_response(command) - - def _run_shell(self, args): - args.insert(0, 'shell') - return self._run_adb(args).split('\r\n') - - def close(self): - if self.is_running and self._emulator_launched: - self.proc.kill() - if self._adb_started: - self._run_adb(['kill-server']) - self._adb_started = False - if self.proc: - retcode = self.proc.poll() - self.proc = None - if self._tmp_userdata: - os.remove(self._tmp_userdata) - self._tmp_userdata = None - if self._tmp_sdcard: - os.remove(self._tmp_sdcard) - self._tmp_sdcard = None - return retcode - if self.logcat_proc and self.logcat_proc.proc.poll() is None: - self.logcat_proc.kill() - return 0 - - def _get_adb_devices(self): - offline = set() - online = set() - output = self._run_adb(['devices']) - for line in output.split('\n'): - m = self.deviceRe.match(line) - if m: - if m.group(3) == 'offline': - offline.add(m.group(1)) - else: - online.add(m.group(1)) - return (online, offline) - - def start_adb(self): - result = self._run_adb(['start-server']) - # We keep track of whether we've started adb or not, so we know - # if we need to kill it. - if 'daemon started successfully' in result: - self._adb_started = True - else: - self._adb_started = False - - @uses_marionette - def wait_for_system_message(self, marionette): - marionette.set_script_timeout(45000) - # Telephony API's won't be available immediately upon emulator - # boot; we have to wait for the syste-message-listener-ready - # message before we'll be able to use them successfully. See - # bug 792647. - print 'waiting for system-message-listener-ready...' - try: - marionette.execute_async_script(""" -waitFor( - function() { marionetteScriptFinished(true); }, - function() { return isSystemMessageListenerReady(); } -); - """) - except ScriptTimeoutException: - print 'timed out' - # We silently ignore the timeout if it occurs, since - # isSystemMessageListenerReady() isn't available on - # older emulators. 45s *should* be enough of a delay - # to allow telephony API's to work. - pass - except (InvalidResponseException, IOError): - self.check_for_minidumps() - raise - print '...done' - - def connect(self): - self.adb = B2GInstance.check_adb(self.homedir, emulator=True) - self.start_adb() - - online, offline = self._get_adb_devices() - now = datetime.datetime.now() - while online == set([]): - time.sleep(1) - if datetime.datetime.now() - now > datetime.timedelta(seconds=60): - raise Exception('timed out waiting for emulator to be available') - online, offline = self._get_adb_devices() - self.port = int(list(online)[0]) - - self.dm = devicemanagerADB.DeviceManagerADB(adbPath=self.adb, - deviceSerial='emulator-%d' % self.port) - - def start(self): - self._check_for_b2g() - self.start_adb() - - qemu_args = self.args[:] - if self.copy_userdata: - # Make a copy of the userdata.img for this instance of the emulator to use. - self._tmp_userdata = tempfile.mktemp(prefix='marionette') - shutil.copyfile(self.dataImg, self._tmp_userdata) - qemu_args[qemu_args.index('-data') + 1] = self._tmp_userdata - - original_online, original_offline = self._get_adb_devices() - - filename = None - if self.logcat_dir: - filename = os.path.join(self.logcat_dir, 'qemu.log') - if os.path.isfile(filename): - self.rotate_log(filename) - - self.proc = LogOutputProc(qemu_args, filename) - self.proc.run() - - online, offline = self._get_adb_devices() - now = datetime.datetime.now() - while online - original_online == set([]): - time.sleep(1) - if datetime.datetime.now() - now > datetime.timedelta(seconds=60): - raise TimeoutException('timed out waiting for emulator to start') - online, offline = self._get_adb_devices() - self.port = int(list(online - original_online)[0]) - self._emulator_launched = True - - self.dm = devicemanagerADB.DeviceManagerADB(adbPath=self.adb, - deviceSerial='emulator-%d' % self.port) - - # bug 802877 - time.sleep(10) - self.geo.set_default_location() - self.screen.initialize() - - if self.logcat_dir: - self.save_logcat() - - # setup DNS fix for networking - self._run_adb(['shell', 'setprop', 'net.dns1', '10.0.2.3']) - - @uses_marionette - def wait_for_homescreen(self, marionette): - print 'waiting for homescreen...' - - marionette.set_context(marionette.CONTEXT_CONTENT) - marionette.execute_async_script(""" -log('waiting for mozbrowserloadend'); -window.addEventListener('mozbrowserloadend', function loaded(aEvent) { - log('received mozbrowserloadend for ' + aEvent.target.src); - if (aEvent.target.src.indexOf('ftu') != -1 || aEvent.target.src.indexOf('homescreen') != -1) { - window.removeEventListener('mozbrowserloadend', loaded); - marionetteScriptFinished(); - } -});""", script_timeout=120000) - print '...done' - - def setup(self, marionette, gecko_path=None, busybox=None): - self.set_environment(marionette) - if busybox: - self.install_busybox(busybox) - - if gecko_path: - self.install_gecko(gecko_path, marionette) - - self.wait_for_system_message(marionette) - self.set_prefs(marionette) - - @uses_marionette - def set_environment(self, marionette): - for k, v in self.env.iteritems(): - marionette.execute_script(""" - let env = Cc["@mozilla.org/process/environment;1"]. - getService(Ci.nsIEnvironment); - env.set("%s", "%s"); - """ % (k, v)) - - @uses_marionette - def set_prefs(self, marionette): - for pref in self.prefs: - marionette.execute_script(""" - Components.utils.import("resource://gre/modules/Services.jsm"); - let argtype = typeof(arguments[1]); - switch(argtype) { - case 'boolean': - Services.prefs.setBoolPref(arguments[0], arguments[1]); - break; - case 'number': - Services.prefs.setIntPref(arguments[0], arguments[1]); - break; - default: - Services.prefs.setCharPref(arguments[0], arguments[1]); - } - """, [pref, self.prefs[pref]]) - - def restart_b2g(self): - print 'restarting B2G' - self.dm.shellCheckOutput(['stop', 'b2g']) - time.sleep(10) - self.dm.shellCheckOutput(['start', 'b2g']) - - if not self.wait_for_port(): - raise TimeoutException("Timeout waiting for marionette on port '%s'" % self.marionette_port) - - def install_gecko(self, gecko_path, marionette): - """ - Install gecko into the emulator using adb push. Restart b2g after the - installation. - """ - # See bug 800102. We use this particular method of installing - # gecko in order to avoid an adb bug in which adb will sometimes - # hang indefinitely while copying large files to the system - # partition. - print 'installing gecko binaries...' - - # see bug 809437 for the path that lead to this madness - try: - # need to remount so we can write to /system/b2g - self._run_adb(['remount']) - self.dm.removeDir('/data/local/b2g') - self.dm.mkDir('/data/local/b2g') - self.dm.pushDir(gecko_path, '/data/local/b2g', retryLimit=10) - - self.dm.shellCheckOutput(['stop', 'b2g']) - - for root, dirs, files in os.walk(gecko_path): - for filename in files: - rel_path = os.path.relpath(os.path.join(root, filename), gecko_path) - data_local_file = os.path.join('/data/local/b2g', rel_path) - system_b2g_file = os.path.join('/system/b2g', rel_path) - - print 'copying', data_local_file, 'to', system_b2g_file - self.dm.shellCheckOutput(['dd', - 'if=%s' % data_local_file, - 'of=%s' % system_b2g_file]) - self.restart_b2g() - - except (DMError, MarionetteException): - # Bug 812395 - raise a single exception type for these so we can - # explicitly catch them elsewhere. - - # print exception, but hide from mozharness error detection - exc = traceback.format_exc() - exc = exc.replace('Traceback', '_traceback') - print exc - - raise InstallGeckoError("unable to restart B2G after installing gecko") - - def install_busybox(self, busybox): - self._run_adb(['remount']) - - remote_file = "/system/bin/busybox" - print 'pushing %s' % remote_file - self.dm.pushFile(busybox, remote_file, retryLimit=10) - self._run_adb(['shell', 'cd /system/bin; chmod 555 busybox; for x in `./busybox --list`; do ln -s ./busybox $x; done']) - self.dm._verifyZip() - - def rotate_log(self, srclog, index=1): - """ Rotate a logfile, by recursively rotating logs further in the sequence, - deleting the last file if necessary. - """ - basename = os.path.basename(srclog) - basename = basename[:-len('.log')] - if index > 1: - basename = basename[:-len('.1')] - basename = '%s.%d.log' % (basename, index) - - destlog = os.path.join(self.logcat_dir, basename) - if os.path.isfile(destlog): - if index == 3: - os.remove(destlog) - else: - self.rotate_log(destlog, index+1) - shutil.move(srclog, destlog) - - def save_logcat(self): - """ Save the output of logcat to a file. - """ - filename = os.path.join(self.logcat_dir, "emulator-%d.log" % self.port) - if os.path.isfile(filename): - self.rotate_log(filename) - cmd = [self.adb, '-s', 'emulator-%d' % self.port, 'logcat', '-v', 'threadtime'] - - self.logcat_proc = LogOutputProc(cmd, filename) - self.logcat_proc.run() - - def setup_port_forwarding(self, remote_port): - """ Set up TCP port forwarding to the specified port on the device, - using any availble local port, and return the local port. - """ - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(("",0)) - local_port = s.getsockname()[1] - s.close() - - self._run_adb(['forward', - 'tcp:%d' % local_port, - 'tcp:%d' % remote_port]) - - self.marionette_port = local_port - - return local_port - - def wait_for_port(self, timeout=300): - assert(self.marionette_port) - starttime = datetime.datetime.now() - while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout): - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect(('localhost', self.marionette_port)) - data = sock.recv(16) - sock.close() - if ':' in data: - return True - except: - import traceback - print traceback.format_exc() - time.sleep(1) - return False
deleted file mode 100644 --- a/testing/marionette/client/marionette/emulator_battery.py +++ /dev/null @@ -1,53 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -class EmulatorBattery(object): - - def __init__(self, emulator): - self.emulator = emulator - - def get_state(self): - status = {} - state = {} - - response = self.emulator._run_telnet('power display') - for line in response: - if ':' in line: - field, value = line.split(':') - value = value.strip() - if value == 'true': - value = True - elif value == 'false': - value = False - elif field == 'capacity': - value = float(value) - status[field] = value - - state['level'] = status.get('capacity', 0.0) / 100 - if status.get('AC') == 'online': - state['charging'] = True - else: - state['charging'] = False - - return state - - def get_charging(self): - return self.get_state()['charging'] - - def get_level(self): - return self.get_state()['level'] - - def set_level(self, level): - self.emulator._run_telnet('power capacity %d' % (level * 100)) - - def set_charging(self, charging): - if charging: - cmd = 'power ac on' - else: - cmd = 'power ac off' - self.emulator._run_telnet(cmd) - - charging = property(get_charging, set_charging) - level = property(get_level, set_level) -
deleted file mode 100644 --- a/testing/marionette/client/marionette/emulator_geo.py +++ /dev/null @@ -1,17 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -class EmulatorGeo(object): - - def __init__(self, emulator): - self.emulator = emulator - - def set_default_location(self): - self.lon = -122.08769 - self.lat = 37.41857 - self.set_location(self.lon, self.lat) - - def set_location(self, lon, lat): - self.emulator._run_telnet('geo fix %0.5f %0.5f' % (self.lon, self.lat)) -
deleted file mode 100644 --- a/testing/marionette/client/marionette/emulator_screen.py +++ /dev/null @@ -1,78 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -class EmulatorScreen(object): - """Class for screen related emulator commands.""" - - SO_PORTRAIT_PRIMARY = 'portrait-primary' - SO_PORTRAIT_SECONDARY = 'portrait-secondary' - SO_LANDSCAPE_PRIMARY = 'landscape-primary' - SO_LANDSCAPE_SECONDARY = 'landscape-secondary' - - def __init__(self, emulator): - self.emulator = emulator - - def initialize(self): - self.orientation = self.SO_PORTRAIT_PRIMARY - - def _get_raw_orientation(self): - """Get the raw value of the current device orientation.""" - response = self.emulator._run_telnet('sensor get orientation') - - return response[0].split('=')[1].strip() - - def _set_raw_orientation(self, data): - """Set the raw value of the specified device orientation.""" - self.emulator._run_telnet('sensor set orientation %s' % data) - - def get_orientation(self): - """Get the current device orientation. - - Returns; - orientation -- Orientation of the device. One of: - SO_PORTRAIT_PRIMARY - system buttons at the bottom - SO_PORTRIAT_SECONDARY - system buttons at the top - SO_LANDSCAPE_PRIMARY - system buttons at the right - SO_LANDSCAPE_SECONDARY - system buttons at the left - - """ - data = self._get_raw_orientation() - - if data == '0:-90:0': - orientation = self.SO_PORTRAIT_PRIMARY - elif data == '0:90:0': - orientation = self.SO_PORTRAIT_SECONDARY - elif data == '0:0:90': - orientation = self.SO_LANDSCAPE_PRIMARY - elif data == '0:0:-90': - orientation = self.SO_LANDSCAPE_SECONDARY - else: - raise ValueError('Unknown orientation sensor value: %s.' % data) - - return orientation - - def set_orientation(self, orientation): - """Set the specified device orientation. - - Args - orientation -- Orientation of the device. One of: - SO_PORTRAIT_PRIMARY - system buttons at the bottom - SO_PORTRIAT_SECONDARY - system buttons at the top - SO_LANDSCAPE_PRIMARY - system buttons at the right - SO_LANDSCAPE_SECONDARY - system buttons at the left - """ - if orientation == self.SO_PORTRAIT_PRIMARY: - data = '0:-90:0' - elif orientation == self.SO_PORTRAIT_SECONDARY: - data = '0:90:0' - elif orientation == self.SO_LANDSCAPE_PRIMARY: - data = '0:0:90' - elif orientation == self.SO_LANDSCAPE_SECONDARY: - data = '0:0:-90' - else: - raise ValueError('Invalid orientation: %s' % orientation) - - self._set_raw_orientation(data) - - orientation = property(get_orientation, set_orientation)
--- a/testing/marionette/client/marionette/geckoinstance.py +++ b/testing/marionette/client/marionette/geckoinstance.py @@ -20,31 +20,30 @@ class GeckoInstance(object): "browser.sessionstore.resume_from_crash": False, "browser.warnOnQuit": False} def __init__(self, host, port, bin, profile, app_args=None, symbols_path=None, gecko_log=None): self.marionette_host = host self.marionette_port = port self.bin = bin - self.profile = profile + self.profile_path = profile self.app_args = app_args or [] self.runner = None self.symbols_path = symbols_path self.gecko_log = gecko_log def start(self): - profile_path = self.profile profile_args = {"preferences": self.required_prefs} - if not profile_path: - runner_class = Runner + if not self.profile_path: profile_args["restore"] = False + profile = Profile(**profile_args) else: - runner_class = CloneRunner - profile_args["path_from"] = profile_path + profile_args["path_from"] = self.profile_path + profile = Profile.clone(**profile_args) if self.gecko_log is None: self.gecko_log = 'gecko.log' elif os.path.isdir(self.gecko_log): fname = "gecko-%d.log" % time.time() self.gecko_log = os.path.join(self.gecko_log, fname) self.gecko_log = os.path.realpath(self.gecko_log) @@ -52,44 +51,39 @@ class GeckoInstance(object): os.remove(self.gecko_log) env = os.environ.copy() # environment variables needed for crashreporting # https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting env.update({ 'MOZ_CRASHREPORTER': '1', 'MOZ_CRASHREPORTER_NO_REPORT': '1', }) - self.runner = runner_class.create( + self.runner = Runner( binary=self.bin, - profile_args=profile_args, + profile=profile, cmdargs=['-no-remote', '-marionette'] + self.app_args, env=env, symbols_path=self.symbols_path, - kp_kwargs={ + process_args={ 'processOutputLine': [NullOutput()], 'logfile': self.gecko_log}) self.runner.start() def check_for_crashes(self): return self.runner.check_for_crashes() def close(self): - self.runner.stop() - self.runner.cleanup() + if self.runner: + self.runner.stop() + self.runner.cleanup() class B2GDesktopInstance(GeckoInstance): + required_prefs = {"focusmanager.testmode": True} - required_prefs = {"focusmanager.testmode": True} + +class NullOutput(object): + def __call__(self, line): + pass + apps = {'b2g': B2GDesktopInstance, 'b2gdesktop': B2GDesktopInstance} - - -class CloneRunner(Runner): - - profile_class = Profile.clone - - -class NullOutput(object): - - def __call__(self, line): - pass
--- a/testing/marionette/client/marionette/marionette.py +++ b/testing/marionette/client/marionette/marionette.py @@ -1,37 +1,30 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import ConfigParser import datetime import os import socket -import sys +import StringIO import time import traceback import base64 from application_cache import ApplicationCache from decorators import do_crash_check -from emulator import Emulator -from emulator_screen import EmulatorScreen -from errors import ( - ErrorCodes, MarionetteException, InstallGeckoError, TimeoutException, InvalidResponseException, - JavascriptException, NoSuchElementException, XPathLookupException, NoSuchWindowException, - StaleElementException, ScriptTimeoutException, ElementNotVisibleException, - NoSuchFrameException, InvalidElementStateException, NoAlertPresentException, - InvalidCookieDomainException, UnableToSetCookieException, InvalidSelectorException, - MoveTargetOutOfBoundsException, FrameSendNotInitializedError, FrameSendFailureError - ) from keys import Keys from marionette_transport import MarionetteTransport +from mozrunner import B2GEmulatorRunner + import geckoinstance +import errors class HTMLElement(object): """ Represents a DOM Element. """ CLASS = "class name" SELECTOR = "css selector" @@ -446,51 +439,43 @@ class Marionette(object): Represents a Marionette connection to a browser or device. """ CONTEXT_CHROME = 'chrome' # non-browser content: windows, dialogs, etc. CONTEXT_CONTENT = 'content' # browser content: iframes, divs, etc. TIMEOUT_SEARCH = 'implicit' TIMEOUT_SCRIPT = 'script' TIMEOUT_PAGE = 'page load' - SCREEN_ORIENTATIONS = {"portrait": EmulatorScreen.SO_PORTRAIT_PRIMARY, - "landscape": EmulatorScreen.SO_LANDSCAPE_PRIMARY, - "portrait-primary": EmulatorScreen.SO_PORTRAIT_PRIMARY, - "landscape-primary": EmulatorScreen.SO_LANDSCAPE_PRIMARY, - "portrait-secondary": EmulatorScreen.SO_PORTRAIT_SECONDARY, - "landscape-secondary": EmulatorScreen.SO_LANDSCAPE_SECONDARY} def __init__(self, host='localhost', port=2828, app=None, app_args=None, bin=None, - profile=None, emulator=None, sdcard=None, emulatorBinary=None, - emulatorImg=None, emulator_res=None, gecko_path=None, - connectToRunningEmulator=False, homedir=None, baseurl=None, - noWindow=False, logcat_dir=None, busybox=None, symbols_path=None, - timeout=None, device_serial=None, gecko_log=None): + profile=None, emulator=None, sdcard=None, emulator_img=None, + emulator_binary=None, emulator_res=None, connect_to_running_emulator=False, + gecko_log=None, homedir=None, baseurl=None, no_window=False, logdir=None, + busybox=None, symbols_path=None, timeout=None, device_serial=None, + adb_path=None): self.host = host self.port = self.local_port = port self.bin = bin self.instance = None - self.profile = profile self.session = None self.window = None + self.runner = None self.emulator = None self.extra_emulators = [] - self.homedir = homedir self.baseurl = baseurl - self.noWindow = noWindow - self.logcat_dir = logcat_dir + self.no_window = no_window self._test_name = None self.timeout = timeout self.device_serial = device_serial if bin: port = int(self.port) if not Marionette.is_port_available(port, host=self.host): ex_msg = "%s:%d is unavailable." % (self.host, port) - raise MarionetteException(message=ex_msg) + raise errors.MarionetteException(message=ex_msg) if app: # select instance class for the given app try: instance_class = geckoinstance.apps[app] except KeyError: msg = 'Application "%s" unknown (should be one of %s)' raise NotImplementedError(msg % (app, geckoinstance.apps.keys())) else: @@ -499,62 +484,66 @@ class Marionette(object): config.read(os.path.join(os.path.dirname(bin), 'application.ini')) app = config.get('App', 'Name') instance_class = geckoinstance.apps[app.lower()] except (ConfigParser.NoOptionError, ConfigParser.NoSectionError, KeyError): instance_class = geckoinstance.GeckoInstance self.instance = instance_class(host=self.host, port=self.port, - bin=self.bin, profile=self.profile, + bin=self.bin, profile=profile, app_args=app_args, symbols_path=symbols_path, gecko_log=gecko_log) self.instance.start() assert(self.wait_for_port()), "Timed out waiting for port!" if emulator: - self.emulator = Emulator(homedir=homedir, - noWindow=self.noWindow, - logcat_dir=self.logcat_dir, - arch=emulator, - sdcard=sdcard, - symbols_path=symbols_path, - emulatorBinary=emulatorBinary, - userdata=emulatorImg, - res=emulator_res) + self.runner = B2GEmulatorRunner(b2g_home=homedir, + no_window=self.no_window, + logdir=logdir, + arch=emulator, + sdcard=sdcard, + symbols_path=symbols_path, + binary=emulator_binary, + userdata=emulator_img, + resolution=emulator_res, + profile=profile, + adb_path=adb_path) + self.emulator = self.runner.device self.emulator.start() self.port = self.emulator.setup_port_forwarding(self.port) - assert(self.emulator.wait_for_port()), "Timed out waiting for port!" + assert(self.emulator.wait_for_port(self.port)), "Timed out waiting for port!" - if connectToRunningEmulator: - self.emulator = Emulator(homedir=homedir, - logcat_dir=self.logcat_dir) + if connect_to_running_emulator: + self.runner = B2GEmulatorRunner(b2g_home=homedir, + logdir=logdir) + self.emulator = self.runner.device self.emulator.connect() self.port = self.emulator.setup_port_forwarding(self.port) - assert(self.emulator.wait_for_port()), "Timed out waiting for port!" + assert(self.emulator.wait_for_port(self.port)), "Timed out waiting for port!" self.client = MarionetteTransport(self.host, self.port) if emulator: - self.emulator.setup(self, - gecko_path=gecko_path, - busybox=busybox) + if busybox: + self.emulator.install_busybox(busybox=busybox) + self.emulator.wait_for_system_message(self) def cleanup(self): if self.session: try: self.delete_session() - except (MarionetteException, socket.error, IOError): + except (errors.MarionetteException, socket.error, IOError): # These exceptions get thrown if the Marionette server # hit an exception/died or the connection died. We can # do no further server-side cleanup in this case. pass self.session = None - if self.emulator: - self.emulator.close() + if self.runner: + self.runner.cleanup() if self.instance: self.instance.close() for qemu in self.extra_emulators: qemu.emulator.close() def __del__(self): self.cleanup() @@ -565,36 +554,16 @@ class Marionette(object): try: s.bind((host, port)) return True except socket.error: return False finally: s.close() - @classmethod - def getMarionetteOrExit(cls, *args, **kwargs): - try: - m = cls(*args, **kwargs) - return m - except InstallGeckoError: - # Bug 812395 - the process of installing gecko into the emulator - # and then restarting B2G tickles some bug in the emulator/b2g - # that intermittently causes B2G to fail to restart. To work - # around this in TBPL runs, we will fail gracefully from this - # error so that the mozharness script can try the run again. - - # This string will get caught by mozharness and will cause it - # to retry the tests. - print "Error installing gecko!" - - # Exit without a normal exception to prevent mozharness from - # flagging the error. - sys.exit() - def wait_for_port(self, timeout=60): starttime = datetime.datetime.now() while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((self.host, self.port)) data = sock.recv(16) sock.close() @@ -604,32 +573,32 @@ class Marionette(object): except socket.error: pass time.sleep(1) return False @do_crash_check def _send_message(self, command, response_key="ok", **kwargs): if not self.session and command != "newSession": - raise MarionetteException("Please start a session") + raise errors.MarionetteException("Please start a session") message = {"name": command} if self.session: message["sessionId"] = self.session if kwargs: message["parameters"] = kwargs try: response = self.client.send(message) except socket.timeout: self.session = None self.window = None self.client.close() - raise TimeoutException( - "Connection timed out", status=ErrorCodes.TIMEOUT) + raise errors.TimeoutException( + "Connection timed out", status=errors.ErrorCodes.TIMEOUT) # Process any emulator commands that are sent from a script # while it's executing. while True: if response.get("emulator_cmd"): response = self._handle_emulator_cmd(response) continue; @@ -641,97 +610,97 @@ class Marionette(object): if response_key in response: return response[response_key] self._handle_error(response) def _handle_emulator_cmd(self, response): cmd = response.get("emulator_cmd") if not cmd or not self.emulator: - raise MarionetteException( + raise errors.MarionetteException( "No emulator in this test to run command against") cmd = cmd.encode("ascii") result = self.emulator._run_telnet(cmd) return self.client.send({"name": "emulatorCmdResult", "id": response.get("id"), "result": result}) def _handle_emulator_shell(self, response): args = response.get("emulator_shell") if not isinstance(args, list) or not self.emulator: - raise MarionetteException( + raise errors.MarionetteException( "No emulator in this test to run shell command against") - result = self.emulator._run_shell(args) + buf = StringIO.StringIO() + self.emulator.dm.shell(args, buf) + result = str(buf.getvalue()[0:-1]).rstrip().splitlines() + buf.close() return self.client.send({"name": "emulatorCmdResult", "id": response.get("id"), "result": result}) def _handle_error(self, response): if 'error' in response and isinstance(response['error'], dict): status = response['error'].get('status', 500) message = response['error'].get('message') stacktrace = response['error'].get('stacktrace') # status numbers come from # http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes - if status == ErrorCodes.NO_SUCH_ELEMENT: - raise NoSuchElementException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.NO_SUCH_FRAME: - raise NoSuchFrameException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.STALE_ELEMENT_REFERENCE: - raise StaleElementException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.ELEMENT_NOT_VISIBLE: - raise ElementNotVisibleException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.INVALID_ELEMENT_STATE: - raise InvalidElementStateException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.UNKNOWN_ERROR: - raise MarionetteException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.ELEMENT_IS_NOT_SELECTABLE: - raise ElementNotSelectableException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.JAVASCRIPT_ERROR: - raise JavascriptException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.XPATH_LOOKUP_ERROR: - raise XPathLookupException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.TIMEOUT: - raise TimeoutException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.NO_SUCH_WINDOW: - raise NoSuchWindowException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.INVALID_COOKIE_DOMAIN: - raise InvalidCookieDomainException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.UNABLE_TO_SET_COOKIE: - raise UnableToSetCookieException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.NO_ALERT_OPEN: - raise NoAlertPresentException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.SCRIPT_TIMEOUT: - raise ScriptTimeoutException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.INVALID_SELECTOR \ - or status == ErrorCodes.INVALID_XPATH_SELECTOR \ - or status == ErrorCodes.INVALID_XPATH_SELECTOR_RETURN_TYPER: - raise InvalidSelectorException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.MOVE_TARGET_OUT_OF_BOUNDS: - raise MoveTargetOutOfBoundsException(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.FRAME_SEND_NOT_INITIALIZED_ERROR: - raise FrameSendNotInitializedError(message=message, status=status, stacktrace=stacktrace) - elif status == ErrorCodes.FRAME_SEND_FAILURE_ERROR: - raise FrameSendFailureError(message=message, status=status, stacktrace=stacktrace) + if status == errors.ErrorCodes.NO_SUCH_ELEMENT: + raise errors.NoSuchElementException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.NO_SUCH_FRAME: + raise errors.NoSuchFrameException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.STALE_ELEMENT_REFERENCE: + raise errors.StaleElementException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.ELEMENT_NOT_VISIBLE: + raise errors.ElementNotVisibleException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.INVALID_ELEMENT_STATE: + raise errors.InvalidElementStateException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.UNKNOWN_ERROR: + raise errors.MarionetteException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.ELEMENT_IS_NOT_SELECTABLE: + raise errors.ElementNotSelectableException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.JAVASCRIPT_ERROR: + raise errors.JavascriptException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.XPATH_LOOKUP_ERROR: + raise errors.XPathLookupException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.TIMEOUT: + raise errors.TimeoutException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.NO_SUCH_WINDOW: + raise errors.NoSuchWindowException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.INVALID_COOKIE_DOMAIN: + raise errors.InvalidCookieDomainException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.UNABLE_TO_SET_COOKIE: + raise errors.UnableToSetCookieException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.NO_ALERT_OPEN: + raise errors.NoAlertPresentException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.SCRIPT_TIMEOUT: + raise errors.ScriptTimeoutException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.INVALID_SELECTOR \ + or status == errors.ErrorCodes.INVALID_XPATH_SELECTOR \ + or status == errors.ErrorCodes.INVALID_XPATH_SELECTOR_RETURN_TYPER: + raise errors.InvalidSelectorException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.MOVE_TARGET_OUT_OF_BOUNDS: + raise errors.MoveTargetOutOfBoundsException(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.FRAME_SEND_NOT_INITIALIZED_ERROR: + raise errors.FrameSendNotInitializedError(message=message, status=status, stacktrace=stacktrace) + elif status == errors.ErrorCodes.FRAME_SEND_FAILURE_ERROR: + raise errors.FrameSendFailureError(message=message, status=status, stacktrace=stacktrace) else: - raise MarionetteException(message=message, status=status, stacktrace=stacktrace) - raise MarionetteException(message=response, status=500) + raise errors.MarionetteException(message=message, status=status, stacktrace=stacktrace) + raise errors.MarionetteException(message=response, status=500) def check_for_crash(self): returncode = None name = None crashed = False - if self.emulator: - if self.emulator.check_for_crash(): + if self.runner: + if self.runner.check_for_crashes(): returncode = self.emulator.proc.returncode name = 'emulator' crashed = True - - if self.emulator.check_for_minidumps(): - crashed = True elif self.instance: if self.instance.check_for_crashes(): crashed = True if returncode is not None: print ('PROCESS-CRASH | %s | abnormal termination with exit code %d' % (name, returncode)) return crashed @@ -1454,9 +1423,9 @@ class Marionette(object): respectively, and "portrait-secondary" as well as "landscape-secondary". :param orientation: The orientation to lock the screen in. """ self._send_message("setScreenOrientation", "ok", orientation=orientation) if self.emulator: - self.emulator.screen.orientation = self.SCREEN_ORIENTATIONS[orientation.lower()] + self.emulator.screen.orientation = orientation.lower()
--- a/testing/marionette/client/marionette/runner/base.py +++ b/testing/marionette/client/marionette/runner/base.py @@ -362,39 +362,39 @@ class BaseMarionetteOptions(OptionParser dest='emulator', choices=['x86', 'arm'], help='if no --address is given, then the harness will launch a B2G emulator on which to run ' 'emulator tests. if --address is given, then the harness assumes you are running an ' 'emulator already, and will run the emulator tests using that emulator. you need to ' 'specify which architecture to emulate for both cases') self.add_option('--emulator-binary', action='store', - dest='emulatorBinary', + dest='emulator_binary', help='launch a specific emulator binary rather than launching from the B2G built emulator') self.add_option('--emulator-img', action='store', - dest='emulatorImg', + dest='emulator_img', help='use a specific image file instead of a fresh one') self.add_option('--emulator-res', action='store', dest='emulator_res', type='str', help='set a custom resolution for the emulator' 'Example: "480x800"') self.add_option('--sdcard', action='store', dest='sdcard', help='size of sdcard to create for the emulator') self.add_option('--no-window', action='store_true', - dest='noWindow', + dest='no_window', default=False, help='when Marionette launches an emulator, start it with the -no-window argument') self.add_option('--logcat-dir', - dest='logcat_dir', + dest='logdir', action='store', help='directory to store logcat dump files') self.add_option('--address', dest='address', action='store', help='host:port of running Gecko instance to connect to') self.add_option('--device', dest='device_serial', @@ -436,20 +436,16 @@ class BaseMarionetteOptions(OptionParser action='store', type=int, default=0, help='number of times to repeat the test(s)') self.add_option('-x', '--xml-output', action='store', dest='xml_output', help='xml output') - self.add_option('--gecko-path', - dest='gecko_path', - action='store', - help='path to b2g gecko binaries that should be installed on the device or emulator') self.add_option('--testvars', dest='testvars', action='store', help='path to a json file with any test data required') self.add_option('--tree', dest='tree', action='store', default='b2g', @@ -520,18 +516,18 @@ class BaseMarionetteOptions(OptionParser print 'can\'t specify both --emulator and --binary' sys.exit(1) if not options.es_servers: options.es_servers = ['elasticsearch-zlb.dev.vlan81.phx.mozilla.com:9200', 'elasticsearch-zlb.webapp.scl3.mozilla.com:9200'] # default to storing logcat output for emulator runs - if options.emulator and not options.logcat_dir: - options.logcat_dir = 'logcat' + if options.emulator and not options.logdir: + options.logdir = 'logcat' # check for valid resolution string, strip whitespaces try: if options.emulator_res: dims = options.emulator_res.split('x') assert len(dims) == 2 width = str(int(dims[0])) height = str(int(dims[1])) @@ -557,48 +553,47 @@ class BaseMarionetteOptions(OptionParser return (options, tests) class BaseMarionetteTestRunner(object): textrunnerclass = MarionetteTextTestRunner - def __init__(self, address=None, emulator=None, emulatorBinary=None, - emulatorImg=None, emulator_res='480x800', homedir=None, + def __init__(self, address=None, emulator=None, emulator_binary=None, + emulator_img=None, emulator_res='480x800', homedir=None, app=None, app_args=None, bin=None, profile=None, autolog=False, - revision=None, logger=None, testgroup="marionette", noWindow=False, - logcat_dir=None, xml_output=None, repeat=0, gecko_path=None, + revision=None, logger=None, testgroup="marionette", no_window=False, + logdir=None, xml_output=None, repeat=0, testvars=None, tree=None, type=None, device_serial=None, symbols_path=None, timeout=None, es_servers=None, shuffle=False, shuffle_seed=random.randint(0, sys.maxint), sdcard=None, this_chunk=1, total_chunks=1, sources=None, server_root=None, gecko_log=None, **kwargs): self.address = address self.emulator = emulator - self.emulatorBinary = emulatorBinary - self.emulatorImg = emulatorImg + self.emulator_binary = emulator_binary + self.emulator_img = emulator_img self.emulator_res = emulator_res self.homedir = homedir self.app = app self.app_args = app_args or [] self.bin = bin self.profile = profile self.autolog = autolog self.testgroup = testgroup self.revision = revision self.logger = logger - self.noWindow = noWindow + self.no_window = no_window self.httpd = None self.marionette = None - self.logcat_dir = logcat_dir + self.logdir = logdir self.xml_output = xml_output self.repeat = repeat - self.gecko_path = gecko_path self.testvars = {} self.test_kwargs = kwargs self.tree = tree self.type = type self.device_serial = device_serial self.symbols_path = symbols_path self.timeout = timeout self._device = None @@ -635,19 +630,19 @@ class BaseMarionetteTestRunner(object): self.reset_test_stats() if self.logger is None: self.logger = logging.getLogger('Marionette') self.logger.setLevel(logging.INFO) self.logger.addHandler(logging.StreamHandler()) - if self.logcat_dir: - if not os.access(self.logcat_dir, os.F_OK): - os.mkdir(self.logcat_dir) + if self.logdir: + if not os.access(self.logdir, os.F_OK): + os.mkdir(self.logdir) # for XML output self.testvars['xml_output'] = self.xml_output self.results = [] @property def capabilities(self): if self._capabilities: @@ -712,18 +707,17 @@ class BaseMarionetteTestRunner(object): 'bin': self.bin, 'profile': self.profile, 'gecko_log': self.gecko_log, }) if self.emulator: kwargs.update({ 'homedir': self.homedir, - 'logcat_dir': self.logcat_dir, - 'gecko_path': self.gecko_path, + 'logdir': self.logdir, }) if self.address: host, port = self.address.split(':') kwargs.update({ 'host': host, 'port': int(port), }) @@ -736,33 +730,33 @@ class BaseMarionetteTestRunner(object): connection = socket.socket(socket.AF_INET,socket.SOCK_STREAM) connection.connect((host,int(port))) connection.close() except Exception, e: raise Exception("Connection attempt to %s:%s failed with error: %s" %(host,port,e)) elif self.emulator: kwargs.update({ 'emulator': self.emulator, - 'emulatorBinary': self.emulatorBinary, - 'emulatorImg': self.emulatorImg, + 'emulator_binary': self.emulator_binary, + 'emulator_img': self.emulator_img, 'emulator_res': self.emulator_res, - 'noWindow': self.noWindow, + 'no_window': self.no_window, 'sdcard': self.sdcard, }) return kwargs def start_marionette(self): self.marionette = Marionette(**self._build_kwargs()) def post_to_autolog(self, elapsedtime): self.logger.info('posting results to autolog') logfile = None if self.emulator: - filename = os.path.join(os.path.abspath(self.logcat_dir), + filename = os.path.join(os.path.abspath(self.logdir), "emulator-%d.log" % self.marionette.emulator.port) if os.access(filename, os.F_OK): logfile = filename for es_server in self.es_servers: # This is all autolog stuff. # See: https://wiki.mozilla.org/Auto-tools/Projects/Autolog
--- a/testing/marionette/client/marionette/runner/mixins/b2g.py +++ b/testing/marionette/client/marionette/runner/mixins/b2g.py @@ -4,24 +4,20 @@ import mozdevice import os import re def get_dm(marionette=None,**kwargs): dm_type = os.environ.get('DM_TRANS', 'adb') - if marionette and marionette.emulator: - adb_path = marionette.emulator.b2g.adb_path - return mozdevice.DeviceManagerADB(adbPath=adb_path, - deviceSerial='emulator-%d' % marionette.emulator.port, - **kwargs) + if marionette and hasattr(marionette.runner, 'device'): + return marionette.runner.app_ctx.dm elif marionette and marionette.device_serial and dm_type == 'adb': - return mozdevice.DeviceManagerADB(deviceSerial=marionette.device_serial, - **kwargs) + return mozdevice.DeviceManagerADB(deviceSerial=marionette.device_serial, **kwargs) else: if dm_type == 'adb': return mozdevice.DeviceManagerADB(**kwargs) elif dm_type == 'sut': host = os.environ.get('TEST_DEVICE') if not host: raise Exception('Must specify host with SUT!') return mozdevice.DeviceManagerSUT(host=host)
--- a/testing/marionette/client/marionette/tests/unit/test_clearing.py +++ b/testing/marionette/client/marionette/tests/unit/test_clearing.py @@ -1,14 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase -from marionette import InvalidElementStateException +from errors import InvalidElementStateException class TestClear(MarionetteTestCase): def testWriteableTextInputShouldClear(self): test_html = self.marionette.absolute_url("test_clearing.html") self.marionette.navigate(test_html) element = self.marionette.find_element("id", "writableTextInput") element.clear() self.assertEqual("", element.get_attribute("value"))
--- a/testing/marionette/client/marionette/tests/unit/test_element_touch.py +++ b/testing/marionette/client/marionette/tests/unit/test_element_touch.py @@ -1,14 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase -from marionette import MarionetteException +from errors import MarionetteException class testElementTouch(MarionetteTestCase): def test_touch(self): testAction = self.marionette.absolute_url("testAction.html") self.marionette.navigate(testAction) button = self.marionette.find_element("id", "button1") button.tap() expected = "button1-touchstart-touchend-mousemove-mousedown-mouseup-click"
--- a/testing/marionette/client/marionette/tests/unit/test_emulator.py +++ b/testing/marionette/client/marionette/tests/unit/test_emulator.py @@ -1,30 +1,30 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase -from marionette import JavascriptException, MarionetteException +from errors import JavascriptException, MarionetteException class TestEmulatorContent(MarionetteTestCase): def test_emulator_cmd(self): self.marionette.set_script_timeout(10000) expected = ["<build>", "OK"] result = self.marionette.execute_async_script(""" runEmulatorCmd("avd name", marionetteScriptFinished) """); self.assertEqual(result, expected) def test_emulator_shell(self): self.marionette.set_script_timeout(10000) - expected = ["Hello World!", ""] + expected = ["Hello World!"] result = self.marionette.execute_async_script(""" runEmulatorShell(["echo", "Hello World!"], marionetteScriptFinished) """); self.assertEqual(result, expected) def test_emulator_order(self): self.marionette.set_script_timeout(10000) self.assertRaises(MarionetteException,
--- a/testing/marionette/client/marionette/tests/unit/test_execute_async_script.py +++ b/testing/marionette/client/marionette/tests/unit/test_execute_async_script.py @@ -1,14 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase -from marionette import JavascriptException, MarionetteException, ScriptTimeoutException +from errors import JavascriptException, MarionetteException, ScriptTimeoutException import time class TestExecuteAsyncContent(MarionetteTestCase): def setUp(self): super(TestExecuteAsyncContent, self).setUp() self.marionette.set_script_timeout(1000)
--- a/testing/marionette/client/marionette/tests/unit/test_execute_isolate.py +++ b/testing/marionette/client/marionette/tests/unit/test_execute_isolate.py @@ -1,14 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase, skip_if_b2g -from marionette import JavascriptException, MarionetteException, ScriptTimeoutException +from errors import JavascriptException, MarionetteException, ScriptTimeoutException class TestExecuteIsolationContent(MarionetteTestCase): def setUp(self): super(TestExecuteIsolationContent, self).setUp() self.content = True def test_execute_async_isolate(self): # Results from one execute call that has timed out should not
--- a/testing/marionette/client/marionette/tests/unit/test_execute_script.py +++ b/testing/marionette/client/marionette/tests/unit/test_execute_script.py @@ -1,16 +1,16 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import urllib from by import By -from marionette import JavascriptException, MarionetteException +from errors import JavascriptException, MarionetteException from marionette_test import MarionetteTestCase def inline(doc): return "data:text/html;charset=utf-8,%s" % urllib.quote(doc) elements = inline("<p>foo</p> <p>bar</p>") class TestExecuteContent(MarionetteTestCase):
--- a/testing/marionette/client/marionette/tests/unit/test_findelement.py +++ b/testing/marionette/client/marionette/tests/unit/test_findelement.py @@ -1,16 +1,16 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase from marionette import HTMLElement from by import By -from marionette import NoSuchElementException +from errors import NoSuchElementException class TestElements(MarionetteTestCase): def test_id(self): test_html = self.marionette.absolute_url("test.html") self.marionette.navigate(test_html) el = self.marionette.execute_script("return window.document.getElementById('mozLink');") found_el = self.marionette.find_element(By.ID, "mozLink")
--- a/testing/marionette/client/marionette/tests/unit/test_findelement_chrome.py +++ b/testing/marionette/client/marionette/tests/unit/test_findelement_chrome.py @@ -1,16 +1,16 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase from marionette import HTMLElement from by import By -from marionette import NoSuchElementException +from errors import NoSuchElementException class TestElementsChrome(MarionetteTestCase): def setUp(self): MarionetteTestCase.setUp(self) self.marionette.set_context("chrome") self.win = self.marionette.current_window_handle self.marionette.execute_script("window.open('chrome://marionette/content/test.xul', 'foo', 'chrome,centerscreen');")
--- a/testing/marionette/client/marionette/tests/unit/test_implicit_waits.py +++ b/testing/marionette/client/marionette/tests/unit/test_implicit_waits.py @@ -1,14 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase -from marionette import NoSuchElementException +from errors import NoSuchElementException class TestImplicitWaits(MarionetteTestCase): def testShouldImplicitlyWaitForASingleElement(self): test_html = self.marionette.absolute_url("test_dynamic.html") self.marionette.navigate(test_html) add = self.marionette.find_element("id", "adder") self.marionette.set_search_timeout("3000") add.click()
--- a/testing/marionette/client/marionette/tests/unit/test_navigation.py +++ b/testing/marionette/client/marionette/tests/unit/test_navigation.py @@ -1,15 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase -from marionette import MarionetteException -from marionette import TimeoutException +from errors import MarionetteException, TimeoutException class TestNavigate(MarionetteTestCase): def test_navigate(self): self.assertTrue(self.marionette.execute_script("window.location.href = 'about:blank'; return true;")) self.assertEqual("about:blank", self.marionette.execute_script("return window.location.href;")) test_html = self.marionette.absolute_url("test.html") self.marionette.navigate(test_html) self.assertNotEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
--- a/testing/marionette/client/marionette/tests/unit/test_screen_orientation.py +++ b/testing/marionette/client/marionette/tests/unit/test_screen_orientation.py @@ -1,17 +1,17 @@ # -*- fill-column: 100; comment-column: 100; -*- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -from emulator_screen import EmulatorScreen -from marionette import MarionetteException +from errors import MarionetteException from marionette_test import MarionetteTestCase +from mozrunner.devices.emulator_screen import EmulatorScreen default_orientation = "portrait-primary" unknown_orientation = "Unknown screen orientation: %s" class TestScreenOrientation(MarionetteTestCase): def tearDown(self): self.marionette.set_orientation(default_orientation) self.assertEqual(self.marionette.orientation, default_orientation, "invalid state")
--- a/testing/marionette/client/marionette/tests/unit/test_simpletest_sanity.py +++ b/testing/marionette/client/marionette/tests/unit/test_simpletest_sanity.py @@ -1,14 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase -from marionette import JavascriptException, MarionetteException, ScriptTimeoutException +from errors import JavascriptException, MarionetteException, ScriptTimeoutException class SimpletestSanityTest(MarionetteTestCase): callFinish = "return finish();" def test_is(self): def runtests(): sentFail1 = "is(true, false, 'isTest1', TEST_UNEXPECTED_FAIL, TEST_PASS);" + self.callFinish
--- a/testing/marionette/client/marionette/tests/unit/test_single_finger.py +++ b/testing/marionette/client/marionette/tests/unit/test_single_finger.py @@ -1,15 +1,15 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase from marionette import Actions -from marionette import MarionetteException +from errors import MarionetteException #add this directory to the path import os import sys sys.path.append(os.path.dirname(__file__)) from single_finger_functions import ( chain, chain_flick, context_menu, double_tap, long_press_action, long_press_on_xy_action, move_element, move_element_offset, press_release, single_tap, wait,
--- a/testing/marionette/client/marionette/tests/unit/test_single_finger_desktop.py +++ b/testing/marionette/client/marionette/tests/unit/test_single_finger_desktop.py @@ -1,11 +1,11 @@ from marionette_test import MarionetteTestCase from marionette import Actions -from marionette import MarionetteException +from errors import MarionetteException #add this directory to the path import os import sys sys.path.append(os.path.dirname(__file__)) from single_finger_functions import ( chain, chain_flick, context_menu, double_tap, long_press_action, long_press_on_xy_action, move_element, move_element_offset, press_release, single_tap, wait,
--- a/testing/marionette/client/marionette/tests/unit/test_specialpowers.py +++ b/testing/marionette/client/marionette/tests/unit/test_specialpowers.py @@ -1,14 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase -from marionette import JavascriptException, MarionetteException +from errors import JavascriptException, MarionetteException class TestSpecialPowersContent(MarionetteTestCase): testpref = "testing.marionette.contentcharpref" testvalue = "blabla" def test_prefs(self): result = self.marionette.execute_script("""
--- a/testing/marionette/client/marionette/tests/unit/test_switch_frame.py +++ b/testing/marionette/client/marionette/tests/unit/test_switch_frame.py @@ -1,14 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase -from marionette import JavascriptException +from errors import JavascriptException class TestSwitchFrame(MarionetteTestCase): def test_switch_simple(self): start_url = "test_iframe.html" verify_title = "Marionette IFrame Test" verify_url = "test.html" test_html = self.marionette.absolute_url(start_url)
--- a/testing/marionette/client/marionette/tests/unit/test_switch_frame_chrome.py +++ b/testing/marionette/client/marionette/tests/unit/test_switch_frame_chrome.py @@ -1,14 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase -from marionette import JavascriptException +from errors import JavascriptException class TestSwitchFrameChrome(MarionetteTestCase): def setUp(self): MarionetteTestCase.setUp(self) self.marionette.set_context("chrome") self.win = self.marionette.current_window_handle self.marionette.execute_script("window.open('chrome://marionette/content/test.xul', 'foo', 'chrome,centerscreen');") self.marionette.switch_to_window('foo')
--- a/testing/marionette/client/marionette/tests/unit/test_timeouts.py +++ b/testing/marionette/client/marionette/tests/unit/test_timeouts.py @@ -1,16 +1,16 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. import os from marionette_test import MarionetteTestCase from marionette import HTMLElement -from marionette import NoSuchElementException, JavascriptException, MarionetteException, ScriptTimeoutException +from errors import NoSuchElementException, JavascriptException, MarionetteException, ScriptTimeoutException class TestTimeouts(MarionetteTestCase): def test_pagetimeout_notdefinetimeout_pass(self): test_html = self.marionette.absolute_url("test.html") self.marionette.navigate(test_html) def test_pagetimeout_fail(self): self.marionette.timeouts("page load", 0)
--- a/testing/marionette/client/marionette/tests/unit/test_typing.py +++ b/testing/marionette/client/marionette/tests/unit/test_typing.py @@ -1,15 +1,15 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_test import MarionetteTestCase from keys import Keys -from marionette import ElementNotVisibleException +from errors import ElementNotVisibleException class TestTyping(MarionetteTestCase): def testShouldFireKeyPressEvents(self): test_html = self.marionette.absolute_url("javascriptPage.html") self.marionette.navigate(test_html) keyReporter = self.marionette.find_element("id", "keyReporter")
--- a/testing/marionette/client/requirements.txt +++ b/testing/marionette/client/requirements.txt @@ -1,12 +1,12 @@ marionette-transport == 0.2 manifestparser mozhttpd >= 0.5 mozinfo >= 0.7 mozprocess >= 0.9 -mozrunner >= 5.15 -mozdevice >= 0.22 +mozrunner >= 6.0 +mozdevice >= 0.37 moznetwork >= 0.21 mozcrash >= 0.5 mozprofile >= 0.7 moztest >= 0.1 mozversion >= 0.2
--- a/testing/mochitest/mach_commands.py +++ b/testing/mochitest/mach_commands.py @@ -173,17 +173,17 @@ class MochitestRunner(MozbuildObject): try: which.which('adb') except which.WhichError: # TODO Find adb automatically if it isn't on the path print(ADB_NOT_FOUND % ('mochitest-remote', b2g_home)) return 1 options.b2gPath = b2g_home - options.logcat_dir = self.mochitest_dir + options.logdir = self.mochitest_dir options.httpdPath = self.mochitest_dir options.xrePath = xre_path return mochitest.run_remote_mochitests(parser, options) def run_desktop_test(self, context, suite=None, test_paths=None, debugger=None, debugger_args=None, slowscript=False, screenshot_on_fail = False, shuffle=False, keep_open=False, rerun_failures=False, no_autorun=False, repeat=0, run_until_failure=False, slow=False, chunk_by_dir=0, total_chunks=None, this_chunk=None, @@ -550,19 +550,19 @@ def MochitestCommand(func): def B2GCommand(func): """Decorator that adds shared command arguments to b2g mochitest commands.""" busybox = CommandArgument('--busybox', default=None, help='Path to busybox binary to install on device') func = busybox(func) - logcatdir = CommandArgument('--logcat-dir', default=None, - help='directory to store logcat dump files') - func = logcatdir(func) + logdir = CommandArgument('--logdir', default=None, + help='directory to store log files') + func = logdir(func) profile = CommandArgument('--profile', default=None, help='for desktop testing, the path to the \ gaia profile to use') func = profile(func) geckopath = CommandArgument('--gecko-path', default=None, help='the path to a gecko distribution that should \
--- a/testing/mochitest/mochitest_options.py +++ b/testing/mochitest/mochitest_options.py @@ -700,21 +700,21 @@ class B2GOptions(MochitestOptions): [["--profile"], { "action": "store", "type": "string", "dest": "profile", "help": "for desktop testing, the path to the \ gaia profile to use", "default": None, }], - [["--logcat-dir"], + [["--logdir"], { "action": "store", "type": "string", - "dest": "logcat_dir", - "help": "directory to store logcat dump files", + "dest": "logdir", + "help": "directory to store log files", "default": None, }], [['--busybox'], { "action": 'store', "type": 'string', "dest": 'busybox', "help": "Path to busybox binary to install on device", "default": None, @@ -733,17 +733,16 @@ class B2GOptions(MochitestOptions): MochitestOptions.__init__(self) for option in self.b2g_options: self.add_option(*option[0], **option[1]) defaults = {} defaults["httpPort"] = DEFAULT_PORTS['http'] defaults["sslPort"] = DEFAULT_PORTS['https'] - defaults["remoteTestRoot"] = "/data/local/tests" defaults["logFile"] = "mochitest.log" defaults["autorun"] = True defaults["closeWhenDone"] = True defaults["testPath"] = "" defaults["extensionsToExclude"] = ["specialpowers"] self.set_defaults(**defaults) def verifyRemoteOptions(self, options): @@ -752,18 +751,18 @@ class B2GOptions(MochitestOptions): options.remoteWebServer = moznetwork.get_ip() else: self.error("You must specify a --remote-webserver=<ip address>") options.webServer = options.remoteWebServer if options.geckoPath and not options.emulator: self.error("You must specify --emulator if you specify --gecko-path") - if options.logcat_dir and not options.emulator: - self.error("You must specify --emulator if you specify --logcat-dir") + if options.logdir and not options.emulator: + self.error("You must specify --emulator if you specify --logdir") if not os.path.isdir(options.xrePath): self.error("--xre-path '%s' is not a directory" % options.xrePath) xpcshell = os.path.join(options.xrePath, 'xpcshell') if not os.access(xpcshell, os.F_OK): self.error('xpcshell not found at %s' % xpcshell) if self.elf_arm(xpcshell): self.error('--xre-path points to an ARM version of xpcshell; it '
--- a/testing/mochitest/runtests.py +++ b/testing/mochitest/runtests.py @@ -1253,33 +1253,28 @@ class Mochitest(MochitestUtilsMixin): 'cwd': SCRIPT_DIR, 'onTimeout': [timeoutHandler]} kp_kwargs['processOutputLine'] = [outputHandler] # create mozrunner instance and start the system under test process self.lastTestSeen = self.test_name startTime = datetime.now() - # b2g desktop requires FirefoxRunner even though appname is b2g + # b2g desktop requires Runner even though appname is b2g if mozinfo.info.get('appname') == 'b2g' and mozinfo.info.get('toolkit') != 'gonk': - runner_cls = mozrunner.FirefoxRunner + runner_cls = mozrunner.Runner else: runner_cls = mozrunner.runners.get(mozinfo.info.get('appname', 'firefox'), mozrunner.Runner) runner = runner_cls(profile=self.profile, binary=cmd, cmdargs=args, env=env, process_class=mozprocess.ProcessHandlerMixin, - kp_kwargs=kp_kwargs, - ) - - # XXX work around bug 898379 until mozrunner is updated for m-c; see - # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c49 - runner.kp_kwargs = kp_kwargs + process_args=kp_kwargs) # start the runner runner.start(debug_args=debug_args, interactive=interactive, outputTimeout=timeout) proc = runner.process_handler log.info("INFO | runtests.py | Application pid: %d", proc.pid)
--- a/testing/mochitest/runtestsb2g.py +++ b/testing/mochitest/runtestsb2g.py @@ -4,46 +4,44 @@ import json import os import posixpath import shutil import sys import tempfile import threading -import time import traceback here = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, here) from runtests import Mochitest from runtests import MochitestUtilsMixin -from runtests import MochitestOptions from runtests import MochitestServer from mochitest_options import B2GOptions, MochitestOptions from marionette import Marionette from mozdevice import DeviceManagerADB from mozprofile import Profile, Preferences -from mozrunner import B2GRunner import mozlog import mozinfo -import moznetwork log = mozlog.getLogger('Mochitest') class B2GMochitest(MochitestUtilsMixin): - def __init__(self, marionette, + marionette = None + + def __init__(self, marionette_args, out_of_process=True, profile_data_dir=None, locations=os.path.join(here, 'server-locations.txt')): super(B2GMochitest, self).__init__() - self.marionette = marionette + self.marionette_args = marionette_args self.out_of_process = out_of_process self.locations_file = locations self.preferences = [] self.webapps = None self.test_script = os.path.join(here, 'b2g_start_script.js') self.test_script_args = [self.out_of_process] self.product = 'b2g' @@ -116,97 +114,117 @@ class B2GMochitest(MochitestUtilsMixin): return manifest def run_tests(self, options): """ Prepare, configure, run tests and cleanup """ self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log") manifest = self.build_profile(options) - self.startServers(options, None) - self.buildURLOptions(options, {'MOZ_HIDE_RESULTS_TABLE': '1'}) - self.test_script_args.append(not options.emulator) - self.test_script_args.append(options.wifi) - if options.debugger or not options.autorun: timeout = None else: if not options.timeout: if mozinfo.info['debug']: options.timeout = 420 else: options.timeout = 300 timeout = options.timeout + 30.0 log.info("runtestsb2g.py | Running tests: start.") status = 0 try: - runner_args = { 'profile': self.profile, - 'devicemanager': self._dm, - 'marionette': self.marionette, - 'remote_test_root': self.remote_test_root, - 'symbols_path': options.symbolsPath, - 'test_script': self.test_script, - 'test_script_args': self.test_script_args } - self.runner = B2GRunner(**runner_args) + self.marionette_args['profile'] = self.profile + self.marionette = Marionette(**self.marionette_args) + self.runner = self.marionette.runner + self.app_ctx = self.runner.app_ctx + + self.remote_log = posixpath.join(self.app_ctx.remote_test_root, + 'log', 'mochitest.log') + if not self.app_ctx.dm.dirExists(posixpath.dirname(self.remote_log)): + self.app_ctx.dm.mkDirs(self.remote_log) + + self.startServers(options, None) + self.buildURLOptions(options, {'MOZ_HIDE_RESULTS_TABLE': '1'}) + self.test_script_args.append(not options.emulator) + self.test_script_args.append(options.wifi) + + self.runner.start(outputTimeout=timeout) + + self.marionette.wait_for_port() + self.marionette.start_session() + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + # Disable offline status management (bug 777145), otherwise the network + # will be 'offline' when the mochitests start. Presumably, the network + # won't be offline on a real device, so we only do this for emulators. + self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/Services.jsm"); + Services.io.manageOfflineStatus = false; + Services.io.offline = false; + """) + + if os.path.isfile(self.test_script): + with open(self.test_script, 'r') as script: + self.marionette.execute_script(script.read(), + script_args=self.test_script_args) + else: + self.marionette.execute_script(self.test_script, + script_args=self.test_script_args) status = self.runner.wait() if status is None: # the runner has timed out status = 124 except KeyboardInterrupt: log.info("runtests.py | Received keyboard interrupt.\n"); status = -1 except: traceback.print_exc() log.error("Automation Error: Received unexpected exception while running application\n") - self.runner.check_for_crashes() + if hasattr(self, 'runner'): + self.runner.check_for_crashes() status = 1 self.stopServers() log.info("runtestsb2g.py | Running tests: end.") if manifest is not None: self.cleanup(manifest, options) return status class B2GDeviceMochitest(B2GMochitest, Mochitest): + remote_log = None - _dm = None - - def __init__(self, marionette, devicemanager, profile_data_dir, + def __init__(self, marionette_args, profile_data_dir, local_binary_dir, remote_test_root=None, remote_log_file=None): - B2GMochitest.__init__(self, marionette, out_of_process=True, profile_data_dir=profile_data_dir) - Mochitest.__init__(self) - self._dm = devicemanager - self.remote_test_root = remote_test_root or self._dm.getDeviceRoot() - self.remote_profile = posixpath.join(self.remote_test_root, 'profile') - self.remote_log = remote_log_file or posixpath.join(self.remote_test_root, 'log', 'mochitest.log') + B2GMochitest.__init__(self, marionette_args, out_of_process=True, profile_data_dir=profile_data_dir) self.local_log = None self.local_binary_dir = local_binary_dir - if not self._dm.dirExists(posixpath.dirname(self.remote_log)): - self._dm.mkDirs(self.remote_log) - def cleanup(self, manifest, options): if self.local_log: - self._dm.getFile(self.remote_log, self.local_log) - self._dm.removeFile(self.remote_log) + self.app_ctx.dm.getFile(self.remote_log, self.local_log) + self.app_ctx.dm.removeFile(self.remote_log) if options.pidFile != "": try: os.remove(options.pidFile) os.remove(options.pidFile + ".xpcshell.pid") except: print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % options.pidFile # stop and clean up the runner if getattr(self, 'runner', False): + if self.local_log: + self.app_ctx.dm.getFile(self.remote_log, self.local_log) + self.app_ctx.dm.removeFile(self.remote_log) + self.runner.cleanup() self.runner = None def startServers(self, options, debuggerInfo): """ Create the servers on the host and start them up """ savedXre = options.xrePath savedUtility = options.utilityPath savedProfie = options.profilePath @@ -223,24 +241,24 @@ class B2GDeviceMochitest(B2GMochitest, M def buildURLOptions(self, options, env): self.local_log = options.logFile options.logFile = self.remote_log options.profilePath = self.profile.profile super(B2GDeviceMochitest, self).buildURLOptions(options, env) self.setup_common_options(options) - options.profilePath = self.remote_profile + options.profilePath = self.app_ctx.remote_profile options.logFile = self.local_log class B2GDesktopMochitest(B2GMochitest, Mochitest): - def __init__(self, marionette, profile_data_dir): - B2GMochitest.__init__(self, marionette, out_of_process=False, profile_data_dir=profile_data_dir) + def __init__(self, marionette_args, profile_data_dir): + B2GMochitest.__init__(self, marionette_args, out_of_process=False, profile_data_dir=profile_data_dir) Mochitest.__init__(self) self.certdbNew = True def runMarionetteScript(self, marionette, test_script, test_script_args): assert(marionette.wait_for_port()) marionette.start_session() marionette.set_context(marionette.CONTEXT_CHROME) @@ -250,16 +268,17 @@ class B2GDesktopMochitest(B2GMochitest, f.close() self.marionette.execute_script(test_script, script_args=test_script_args) def startTests(self): # This is run in a separate thread because otherwise, the app's # stdout buffer gets filled (which gets drained only after this # function returns, by waitForFinish), which causes the app to hang. + self.marionette = Marionette(**self.marionette_args) thread = threading.Thread(target=self.runMarionetteScript, args=(self.marionette, self.test_script, self.test_script_args)) thread.start() def buildURLOptions(self, options, env): super(B2GDesktopMochitest, self).buildURLOptions(options, env) @@ -277,59 +296,37 @@ class B2GDesktopMochitest(B2GMochitest, os.path.join(bundlesDir, filename)) def buildProfile(self, options): return self.build_profile(options) def run_remote_mochitests(parser, options): # create our Marionette instance - kwargs = {} - if options.emulator: - kwargs['emulator'] = options.emulator - if options.noWindow: - kwargs['noWindow'] = True - if options.geckoPath: - kwargs['gecko_path'] = options.geckoPath - if options.logcat_dir: - kwargs['logcat_dir'] = options.logcat_dir - if options.busybox: - kwargs['busybox'] = options.busybox - if options.symbolsPath: - kwargs['symbols_path'] = options.symbolsPath - # needless to say sdcard is only valid if using an emulator - if options.sdcard: - kwargs['sdcard'] = options.sdcard - if options.b2gPath: - kwargs['homedir'] = options.b2gPath + marionette_args = { + 'adb_path': options.adbPath, + 'emulator': options.emulator, + 'no_window': options.noWindow, + 'logdir': options.logdir, + 'busybox': options.busybox, + 'symbols_path': options.symbolsPath, + 'sdcard': options.sdcard, + 'homedir': options.b2gPath, + } if options.marionette: host, port = options.marionette.split(':') - kwargs['host'] = host - kwargs['port'] = int(port) - - marionette = Marionette.getMarionetteOrExit(**kwargs) - - if options.emulator: - dm = marionette.emulator.dm - else: - # create the DeviceManager - kwargs = {'adbPath': options.adbPath, - 'deviceRoot': options.remoteTestRoot} - if options.deviceIP: - kwargs.update({'host': options.deviceIP, - 'port': options.devicePort}) - dm = DeviceManagerADB(**kwargs) + marionette_args['host'] = host + marionette_args['port'] = int(port) options = parser.verifyRemoteOptions(options) if (options == None): print "ERROR: Invalid options specified, use --help for a list of valid options" sys.exit(1) - mochitest = B2GDeviceMochitest(marionette, dm, options.profile_data_dir, options.xrePath, - remote_test_root=options.remoteTestRoot, + mochitest = B2GDeviceMochitest(marionette_args, options.profile_data_dir, options.xrePath, remote_log_file=options.remoteLogFile) options = parser.verifyOptions(options, mochitest) if (options == None): sys.exit(1) retVal = 1 try: @@ -344,23 +341,22 @@ def run_remote_mochitests(parser, option except: pass retVal = 1 sys.exit(retVal) def run_desktop_mochitests(parser, options): # create our Marionette instance - kwargs = {} + marionette_args = {} if options.marionette: host, port = options.marionette.split(':') - kwargs['host'] = host - kwargs['port'] = int(port) - marionette = Marionette.getMarionetteOrExit(**kwargs) - mochitest = B2GDesktopMochitest(marionette, options.profile_data_dir) + marionette_args['host'] = host + marionette_args['port'] = int(port) + mochitest = B2GDesktopMochitest(marionette_args, options.profile_data_dir) # add a -bin suffix if b2g-bin exists, but just b2g was specified if options.app[-4:] != '-bin': if os.path.isfile("%s-bin" % options.app): options.app = "%s-bin" % options.app options = MochitestOptions.verifyOptions(parser, options, mochitest) if options == None:
new file mode 100644 --- /dev/null +++ b/testing/mozbase/docs/mozrunner.rst @@ -0,0 +1,177 @@ +:mod:`mozrunner` --- Manage remote and local gecko processes +============================================================ + +Mozrunner provides an API to manage a gecko-based application with an +arbitrary configuration profile. It currently supports local desktop +binaries such as Firefox and Thunderbird, as well as Firefox OS on +mobile devices and emulators. + + +Basic usage +----------- + +The simplest way to use mozrunner, is to instantiate a runner, start it +and then wait for it to finish: + +.. code-block:: python + + from mozrunner import FirefoxRunner + binary = 'path/to/firefox/binary' + runner = FirefoxRunner(binary=binary) + runner.start() + runner.wait() + +This automatically creates and uses a default mozprofile object. If you +wish to use a specialized or pre-existing profile, you can create a +:doc:`mozprofile <mozprofile>` object and pass it in: + +.. code-block:: python + + from mozprofile import Profile + from mozrunner import FirefoxRunner + import os + + binary = 'path/to/firefox/binary' + profile_path = 'path/to/profile' + if os.path.exists(profile_path): + profile = Profile.clone(path_from=profile_path) + else: + profile = Profile(profile=profile_path) + runner = FirefoxRunner(binary=binary, profile=profile) + runner.start() + runner.wait() + + +Handling output +--------------- + +By default, mozrunner dumps the output of the gecko process to standard output. +It is possible to add arbitrary output handlers by passing them in via the +`process_args` argument. Be careful, passing in a handler overrides the default +behaviour. So if you want to use a handler in addition to dumping to stdout, you +need to specify that explicitly. For example: + +.. code-block:: python + + from mozrunner import FirefoxRunner + + def handle_output_line(line): + do_something(line) + + binary = 'path/to/firefox/binary' + process_args = { 'stream': sys.stdout, + 'processOutputLine': [handle_output_line] } + runner = FirefoxRunner(binary=binary, process_args=process_args) + +Mozrunner uses :doc:`mozprocess <mozprocess>` to manage the underlying gecko +process and handle output. See the :doc:`mozprocess documentation <mozprocess>` +for all available arguments accepted by `process_args`. + + +Handling timeouts +----------------- + +Sometimes gecko can hang, or maybe it is just taking too long. To handle this case you +may want to set a timeout. Mozrunner has two kinds of timeouts, the +traditional `timeout`, and the `outputTimeout`. These get passed into the +`runner.start()` method. Setting `timeout` will cause gecko to be killed after +the specified number of seconds, no matter what. Setting `outputTimeout` will cause +gecko to be killed after the specified number of seconds with no output. In both +cases the process handler's `onTimeout` callbacks will be triggered. + +.. code-block:: python + + from mozrunner import FirefoxRunner + + def on_timeout(): + print('timed out after 10 seconds with no output!') + + binary = 'path/to/firefox/binary' + process_args = { 'onTimeout': on_timeout } + runner = FirefoxRunner(binary=binary, process_args=process_args) + runner.start(outputTimeout=10) + runner.wait() + +The `runner.wait()` method also accepts a timeout argument. But unlike the arguments +to `runner.start()`, this one simply returns from the wait call and does not kill the +gecko process. + +.. code-block:: python + + runner.start(timeout=100) + + waiting = 0 + while runner.wait(timeout=1) is None: + waiting += 1 + print("Been waiting for %d seconds so far.." % waiting) + assert waiting <= 100 + + +Using a device runner +--------------------- + +The previous examples used a GeckoRuntimeRunner. If you want to control a +gecko process on a remote device, you need to use a DeviceRunner. The api is +nearly identical except you don't pass in a binary, instead you create a device +object. For example, for B2G (Firefox OS) emulators you might do: + +.. code-block:: python + + from mozrunner import B2GEmulatorRunner + + b2g_home = 'path/to/B2G' + runner = B2GEmulatorRunner(arch='arm', b2g_home=b2g_home) + runner.start() + runner.wait() + +Device runners have a `device` object. Remember that the gecko process runs on +the device. In the case of the emulator, it is possible to start the +device independently of the gecko process. + +.. code-block:: python + + runner.device.start() # launches the emulator (which also launches gecko) + runner.start() # stops the gecko process, installs the profile, restarts the gecko process + + +Runner API Documentation +------------------------ + +Application Runners +~~~~~~~~~~~~~~~~~~~ +.. automodule:: mozrunner.runners + :members: + +BaseRunner +~~~~~~~~~~ +.. autoclass:: mozrunner.base.BaseRunner + :members: + +GeckoRuntimeRunner +~~~~~~~~~~~~~~~~~~ +.. autoclass:: mozrunner.base.GeckoRuntimeRunner + :show-inheritance: + :members: + +DeviceRunner +~~~~~~~~~~~~ +.. autoclass:: mozrunner.base.DeviceRunner + :show-inheritance: + :members: + +Device API Documentation +------------------------ + +Generally using the device classes directly shouldn't be required, but in some +cases it may be desirable. + +Device +~~~~~~ +.. autoclass:: mozrunner.devices.Device + :members: + +Emulator +~~~~~~~~ +.. autoclass:: mozrunner.devices.Emulator + :show-inheritance: + :members:
--- a/testing/mozbase/docs/setuprunning.rst +++ b/testing/mozbase/docs/setuprunning.rst @@ -7,9 +7,10 @@ controlled environment such that it can correctly handling the case where the system crashes. .. toctree:: :maxdepth: 2 mozfile mozprofile mozprocess + mozrunner mozcrash
--- a/testing/mozbase/mozdevice/mozdevice/devicemanager.py +++ b/testing/mozbase/mozdevice/mozdevice/devicemanager.py @@ -1,15 +1,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. import hashlib import mozlog -import socket import os import posixpath import re import struct import StringIO import zlib from Zeroconf import Zeroconf, ServiceBrowser @@ -373,23 +372,25 @@ class DeviceManager(object): :param env: Environment to pass to exec command :param cwd: Directory to execute command from :param timeout: specified in seconds, defaults to 'default_timeout' :param root: Specifies whether command requires root privileges """ def shellCheckOutput(self, cmd, env=None, cwd=None, timeout=None, root=False): """ - Executes shell command on device and returns output as a string. + Executes shell command on device and returns output as a string. Raises if + the return code is non-zero. :param cmd: Commandline list to execute :param env: Environment to pass to exec command :param cwd: Directory to execute command from :param timeout: specified in seconds, defaults to 'default_timeout' :param root: Specifies whether command requires root privileges + :raises: DMError """ buf = StringIO.StringIO() retval = self.shell(cmd, buf, env=env, cwd=cwd, timeout=timeout, root=root) output = str(buf.getvalue()[0:-1]).rstrip() buf.close() if retval != 0: raise DMError("Non-zero return code for command: %s (output: '%s', retval: '%s')" % (cmd, output, retval)) return output
--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py +++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py @@ -24,21 +24,22 @@ class DeviceManagerADB(DeviceManager): _haveRootShell = False _haveSu = False _useZip = False _logcatNeedsRoot = False _pollingInterval = 0.01 _packageName = None _tempDir = None + connected = False default_timeout = 300 def __init__(self, host=None, port=5555, retryLimit=5, packageName='fennec', adbPath='adb', deviceSerial=None, deviceRoot=None, - logLevel=mozlog.ERROR, **kwargs): + logLevel=mozlog.ERROR, autoconnect=True, **kwargs): DeviceManager.__init__(self, logLevel) self.host = host self.port = port self.retryLimit = retryLimit self.deviceRoot = deviceRoot # the path to adb, or 'adb' to assume that it's on the PATH self._adbPath = adbPath @@ -53,38 +54,43 @@ class DeviceManagerADB(DeviceManager): else: self._packageName = 'org.mozilla.fennec_' elif packageName: self._packageName = packageName # verify that we can run the adb command. can't continue otherwise self._verifyADB() - # try to connect to the device over tcp/ip if we have a hostname - if self.host: - self._connectRemoteADB() + if autoconnect: + self.connect() - # verify that we can connect to the device. can't continue - self._verifyDevice() + def connect(self): + if not self.connected: + # try to connect to the device over tcp/ip if we have a hostname + if self.host: + self._connectRemoteADB() - # set up device root - self._setupDeviceRoot() + # verify that we can connect to the device. can't continue + self._verifyDevice() - # Some commands require root to work properly, even with ADB (e.g. - # grabbing APKs out of /data). For these cases, we check whether - # we're running as root. If that isn't true, check for the - # existence of an su binary - self._checkForRoot() + # set up device root + self._setupDeviceRoot() - # can we use zip to speed up some file operations? (currently not - # required) - try: - self._verifyZip() - except DMError: - pass + # Some commands require root to work properly, even with ADB (e.g. + # grabbing APKs out of /data). For these cases, we check whether + # we're running as root. If that isn't true, check for the + # existence of an su binary + self._checkForRoot() + + # can we use zip to speed up some file operations? (currently not + # required) + try: + self._verifyZip() + except DMError: + pass def __del__(self): if self.host: self._disconnectRemoteADB() def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False): # FIXME: this function buffers all output of the command into memory, # always. :(
--- a/testing/mozbase/mozdevice/setup.py +++ b/testing/mozbase/mozdevice/setup.py @@ -1,16 +1,16 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. from setuptools import setup PACKAGE_NAME = 'mozdevice' -PACKAGE_VERSION = '0.36' +PACKAGE_VERSION = '0.37' deps = ['mozfile >= 1.0', 'mozlog', 'moznetwork >= 0.24' ] setup(name=PACKAGE_NAME, version=PACKAGE_VERSION,
--- a/testing/mozbase/mozprocess/mozprocess/processhandler.py +++ b/testing/mozbase/mozprocess/mozprocess/processhandler.py @@ -688,17 +688,17 @@ falling back to not using job objects fo self.proc.kill(sig=sig) # When we kill the the managed process we also have to wait for the # outThread to be finished. Otherwise consumers would have to assume # that it still has not completely shutdown. return self.wait() except AttributeError: # Try to print a relevant error message. - if not self.proc: + if not hasattr(self, 'proc'): print >> sys.stderr, "Unable to kill Process because call to ProcessHandler constructor failed." else: raise def readWithTimeout(self, f, timeout): """ Try to read a line of output from the file object *f*.
--- a/testing/mozbase/mozrunner/mozrunner/__init__.py +++ b/testing/mozbase/mozrunner/mozrunner/__init__.py @@ -1,11 +1,10 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. - +from .cli import * from .errors import * -from .local import * -from .local import LocalRunner as Runner -from .remote import * +from .runners import * -runners = local_runners -runners.update(remote_runners) +import base +import devices +import utils
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/application.py @@ -0,0 +1,129 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from distutils.spawn import find_executable +import glob +import os +import posixpath +import sys + +from mozdevice import DeviceManagerADB +from mozprofile import ( + Profile, + FirefoxProfile, + MetroFirefoxProfile, + ThunderbirdProfile +) + +here = os.path.abspath(os.path.dirname(__file__)) + +def get_app_context(appname): + context_map = { 'default': DefaultContext, + 'b2g': B2GContext, + 'firefox': FirefoxContext, + 'thunderbird': ThunderbirdContext, + 'metro': MetroContext } + if appname not in context_map: + raise KeyError("Application '%s' not supported!" % appname) + return context_map[appname] + + +class DefaultContext(object): + profile_class = Profile + + +class B2GContext(object): + _bindir = None + _dm = None + _remote_profile = None + profile_class = Profile + + def __init__(self, b2g_home=None, adb_path=None): + self.homedir = b2g_home or os.environ.get('B2G_HOME') + if not self.homedir: + raise EnvironmentError('Must define B2G_HOME or pass the b2g_home parameter') + + if not os.path.isdir(self.homedir): + raise OSError('Homedir \'%s\' does not exist!' % self.homedir) + + self._adb = adb_path + self.update_tools = os.path.join(self.homedir, 'tools', 'update-tools') + self.fastboot = self.which('fastboot') + + self.remote_binary = '/system/bin/b2g.sh' + self.remote_process = '/system/b2g/b2g' + self.remote_bundles_dir = '/system/b2g/distribution/bundles' + self.remote_busybox = '/system/bin/busybox' + self.remote_profiles_ini = '/data/b2g/mozilla/profiles.ini' + self.remote_test_root = '/data/local/tests' + + @property + def adb(self): + if not self._adb: + paths = [os.environ.get('ADB'), + os.environ.get('ADB_PATH'), + self.which('adb')] + paths = [p for p in paths if p is not None if os.path.isfile(p)] + if not paths: + raise OSError('Could not find the adb binary, make sure it is on your' \ + 'path or set the $ADB_PATH environment variable.') + self._adb = paths[0] + return self._adb + + @property + def bindir(self): + if not self._bindir: + # TODO get this via build configuration + path = os.path.join(self.homedir, 'out', 'host', '*', 'bin') + self._bindir = glob.glob(path)[0] + return self._bindir + + @property + def dm(self): + if not self._dm: + self._dm = DeviceManagerADB(adbPath=self.adb, autoconnect=False, deviceRoot=self.remote_test_root) + return self._dm + + @property + def remote_profile(self): + if not self._remote_profile: + self._remote_profile = posixpath.join(self.remote_test_root, 'profile') + return self._remote_profile + + + def which(self, binary): + if self.bindir not in sys.path: + sys.path.insert(0, self.bindir) + + return find_executable(binary, os.pathsep.join(sys.path)) + + def stop_application(self): + self.dm.shellCheckOutput(['stop', 'b2g']) + + # For some reason user.js in the profile doesn't get picked up. + # Manually copy it over to prefs.js. See bug 1009730 for more details. + self.dm.moveTree(posixpath.join(self.remote_profile, 'user.js'), + posixpath.join(self.remote_profile, 'prefs.js')) + + +class FirefoxContext(object): + profile_class = FirefoxProfile + + +class ThunderbirdContext(object): + profile_class = ThunderbirdProfile + + +class MetroContext(object): + profile_class = MetroFirefoxProfile + + def __init__(self, binary=None): + self.binary = binary or os.environ.get('BROWSER_PATH', None) + + def wrap_command(self, command): + immersive_helper_path = os.path.join(os.path.dirname(here), + 'resources', + 'metrotestharness.exe') + command[:0] = [immersive_helper_path, '-firefoxpath'] + return command
deleted file mode 100644 --- a/testing/mozbase/mozrunner/mozrunner/base.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this file, -# You can obtain one at http://mozilla.org/MPL/2.0/. - -import os -import subprocess -import traceback - -from mozprocess.processhandler import ProcessHandler -import mozcrash -import mozlog - -from .errors import RunnerNotStartedError - - -# we can replace these methods with 'abc' -# (http://docs.python.org/library/abc.html) when we require Python 2.6+ -def abstractmethod(method): - line = method.func_code.co_firstlineno - filename = method.func_code.co_filename - - def not_implemented(*args, **kwargs): - raise NotImplementedError('Abstract method %s at File "%s", line %s ' - 'should be implemented by a concrete class' % - (repr(method), filename, line)) - return not_implemented - - -class Runner(object): - - def __init__(self, profile, clean_profile=True, process_class=None, - kp_kwargs=None, env=None, symbols_path=None): - self.clean_profile = clean_profile - self.env = env or {} - self.kp_kwargs = kp_kwargs or {} - self.process_class = process_class or ProcessHandler - self.process_handler = None - self.profile = profile - self.log = mozlog.getLogger('MozRunner') - self.symbols_path = symbols_path - - def __del__(self): - self.cleanup() - - # Once we can use 'abc' it should become an abstract property - @property - def command(self): - pass - - @property - def returncode(self): - if self.process_handler: - return self.process_handler.poll() - else: - raise RunnerNotStartedError("returncode retrieved before process started") - - def start(self, debug_args=None, interactive=False, timeout=None, outputTimeout=None): - """Run self.command in the proper environment - - returns the process id - - :param debug_args: arguments for the debugger - :param interactive: uses subprocess.Popen directly - :param timeout: see process_handler.run() - :param outputTimeout: see process_handler.run() - - """ - # ensure the runner is stopped - self.stop() - - # ensure the profile exists - if not self.profile.exists(): - self.profile.reset() - assert self.profile.exists(), "%s : failure to reset profile" % self.__class__.__name__ - - cmd = self.command - - # attach a debugger, if specified - if debug_args: - cmd = list(debug_args) + cmd - - if interactive: - self.process_handler = subprocess.Popen(cmd, env=self.env) - # TODO: other arguments - else: - # this run uses the managed processhandler - self.process_handler = self.process_class(cmd, env=self.env, **self.kp_kwargs) - self.process_handler.run(timeout, outputTimeout) - - return self.process_handler.pid - - def wait(self, timeout=None): - """Wait for the process to exit - - returns the process return code if the process exited, - returns -<signal> if the process was killed (Unix only) - returns None if the process is still running. - - :param timeout: if not None, will return after timeout seconds. - Use is_running() to determine whether or not a - timeout occured. Timeout is ignored if - interactive was set to True. - - """ - if self.is_running(): - # The interactive mode uses directly a Popen process instance. It's - # wait() method doesn't have any parameters. So handle it separately. - if isinstance(self.process_handler, subprocess.Popen): - self.process_handler.wait() - else: - self.process_handler.wait(timeout) - - elif not self.process_handler: - raise RunnerNotStartedError("Wait() called before process started") - - return self.returncode - - def is_running(self): - """Checks if the process is running - - returns True if the process is active - - """ - return self.returncode is None - - def stop(self, sig=None): - """Kill the process - - returns -<signal> when the process got killed (Unix only) - - :param sig: Signal used to kill the process, defaults to SIGKILL - (has no effect on Windows). - - """ - try: - if not self.is_running(): - return - except RunnerNotStartedError: - return - - - # The interactive mode uses directly a Popen process instance. It's - # kill() method doesn't have any parameters. So handle it separately. - if isinstance(self.process_handler, subprocess.Popen): - self.process_handler.kill() - else: - self.process_handler.kill(sig=sig) - - return self.returncode - - def reset(self): - """Reset the runner to its default state""" - if getattr(self, 'profile', False): - self.profile.reset() - - def check_for_crashes(self, dump_directory=None, dump_save_path=None, - test_name=None, quiet=False): - """Check for a possible crash and output stack trace - - :param dump_directory: Directory to search for minidump files - :param dump_save_path: Directory to save the minidump files to - :param test_name: Name to use in the crash output - :param quiet: If `True` don't print the PROCESS-CRASH message to stdout - - """ - if not dump_directory: - dump_directory = os.path.join(self.profile.profile, 'minidumps') - - crashed = False - try: - crashed = mozcrash.check_for_crashes(dump_directory, - self.symbols_path, - dump_save_path=dump_save_path, - test_name=test_name, - quiet=quiet) - except: - traceback.print_exc() - - return crashed - - def cleanup(self): - """Cleanup all runner state""" - self.stop() - - if getattr(self, 'profile', False) and self.clean_profile: - self.profile.cleanup()
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/base/__init__.py @@ -0,0 +1,3 @@ +from .runner import BaseRunner +from .device import DeviceRunner +from .browser import GeckoRuntimeRunner
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/base/browser.py @@ -0,0 +1,74 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import mozinfo +import os +import platform +import sys + +from .runner import BaseRunner + + +class GeckoRuntimeRunner(BaseRunner): + """ + The base runner class used for local gecko runtime binaries, + such as Firefox and Thunderbird. + """ + + def __init__(self, binary, cmdargs=None, **runner_args): + BaseRunner.__init__(self, **runner_args) + + self.binary = binary + self.cmdargs = cmdargs or [] + + # allows you to run an instance of Firefox separately from any other instances + self.env['MOZ_NO_REMOTE'] = '1' + # keeps Firefox attached to the terminal window after it starts + self.env['NO_EM_RESTART'] = '1' + + # set the library path if needed on linux + if sys.platform == 'linux2' and self.binary.endswith('-bin'): + dirname = os.path.dirname(self.binary) + if os.environ.get('LD_LIBRARY_PATH', None): + self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname) + else: + self.env['LD_LIBRARY_PATH'] = dirname + + @property + def command(self): + command = [self.binary, '-profile', self.profile.profile] + + _cmdargs = [i for i in self.cmdargs + if i != '-foreground'] + if len(_cmdargs) != len(self.cmdargs): + # foreground should be last; see + # https://bugzilla.mozilla.org/show_bug.cgi?id=625614 + self.cmdargs = _cmdargs + self.cmdargs.append('-foreground') + if mozinfo.isMac and '-foreground' not in self.cmdargs: + # runner should specify '-foreground' on Mac; see + # https://bugzilla.mozilla.org/show_bug.cgi?id=916512 + self.cmdargs.append('-foreground') + + # Bug 775416 - Ensure that binary options are passed in first + command[1:1] = self.cmdargs + + # If running on OS X 10.5 or older, wrap |cmd| so that it will + # be executed as an i386 binary, in case it's a 32-bit/64-bit universal + # binary. + if mozinfo.isMac and hasattr(platform, 'mac_ver') and \ + platform.mac_ver()[0][:4] < '10.6': + command = ["arch", "-arch", "i386"] + command + + if hasattr(self.app_ctx, 'wrap_command'): + command = self.app_ctx.wrap_command(command) + return command + + def start(self, *args, **kwargs): + # ensure the profile exists + if not self.profile.exists(): + self.profile.reset() + assert self.profile.exists(), "%s : failure to reset profile" % self.__class__.__name__ + + BaseRunner.start(self, *args, **kwargs)
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/base/device.py @@ -0,0 +1,111 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import print_function + +import datetime +import re +import signal +import sys +import tempfile +import time + +from .runner import BaseRunner + +class DeviceRunner(BaseRunner): + """ + The base runner class used for running gecko on + remote devices (or emulators), such as B2G. + """ + def __init__(self, device_class, device_args=None, **kwargs): + process_args = kwargs.get('process_args', {}) + process_args.update({ 'stream': sys.stdout, + 'processOutputLine': self.on_output, + 'onTimeout': self.on_timeout }) + kwargs['process_args'] = process_args + BaseRunner.__init__(self, **kwargs) + + device_args = device_args or {} + self.device = device_class(**device_args) + + process_log = tempfile.NamedTemporaryFile(suffix='pidlog') + self._env = { 'MOZ_CRASHREPORTER': '1', + 'MOZ_CRASHREPORTER_NO_REPORT': '1', + 'MOZ_CRASHREPORTER_SHUTDOWN': '1', + 'MOZ_HIDE_RESULTS_TABLE': '1', + 'MOZ_PROCESS_LOG': process_log.name, + 'NSPR_LOG_MODULES': 'signaling:5,mtransport:3', + 'R_LOG_LEVEL': '5', + 'R_LOG_DESTINATION': 'stderr', + 'R_LOG_VERBOSE': '1', + 'NO_EM_RESTART': '1', } + if kwargs.get('env'): + self._env.update(kwargs['env']) + + # In this case we need to pass in env as part of the command. + # Make this empty so runner doesn't pass anything into the + # process class. + self.env = None + + @property + def command(self): + cmd = [self.app_ctx.adb] + if self.app_ctx.dm._deviceSerial: + cmd.extend(['-s', self.app_ctx.dm._deviceSerial]) + cmd.append('shell') + for k, v in self._env.iteritems(): + cmd.append('%s=%s' % (k, v)) + cmd.append(self.app_ctx.remote_binary) + return cmd + + def start(self, *args, **kwargs): + if not self.device.proc: + self.device.start() + self.device.setup_profile(self.profile) + self.app_ctx.stop_application() + + BaseRunner.start(self, *args, **kwargs) + + timeout = 10 # seconds + starttime = datetime.datetime.now() + while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout): + if self.app_ctx.dm.processExist(self.app_ctx.remote_process): + break + time.sleep(1) + else: + print("timed out waiting for '%s' process to start" % self.app_ctx.remote_process) + + def on_output(self, line): + match = re.findall(r"TEST-START \| ([^\s]*)", line) + if match: + self.last_test = match[-1] + + def on_timeout(self, line): + self.dm.killProcess(self.app_ctx.remote_process, sig=signal.SIGABRT) + timeout = 10 # seconds + starttime = datetime.datetime.now() + while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout): + if not self.app_ctx.dm.processExist(self.app_ctx.remote_process): + break + time.sleep(1) + else: + print("timed out waiting for '%s' process to exit" % self.app_ctx.remote_process) + + msg = "%s | application timed out after %s seconds" + if self.timeout: + timeout = self.timeout + else: + timeout = self.output_timeout + msg = "%s with no output" % msg + + self.log.testFail(msg % (self.last_test, timeout)) + self.check_for_crashes() + + def check_for_crashes(self): + dump_dir = self.device.pull_minidumps() + BaseRunner.check_for_crashes(self, dump_directory=dump_dir, test_name=self.last_test) + + def cleanup(self, *args, **kwargs): + BaseRunner.cleanup(self, *args, **kwargs) + self.device.cleanup()
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/base/runner.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +from abc import ABCMeta, abstractproperty +import os +import subprocess +import traceback + +from mozprocess import ProcessHandler +import mozcrash + +from ..application import DefaultContext +from ..errors import RunnerNotStartedError + + +class BaseRunner(object): + """ + The base runner class for all mozrunner objects, both local and remote. + """ + __metaclass__ = ABCMeta + last_test = 'automation' + process_handler = None + timeout = None + output_timeout = None + + def __init__(self, app_ctx=None, profile=None, clean_profile=True, env=None, + process_class=None, process_args=None, symbols_path=None): + self.app_ctx = app_ctx or DefaultContext() + + if isinstance(profile, basestring): + self.profile = self.app_ctx.profile_class(profile=profile) + else: + self.profile = profile or self.app_ctx.profile_class(**getattr(self.app_ctx, 'profile_args', {})) + + # process environment + if env is None: + self.env = os.environ.copy() + else: + self.env = env.copy() + + self.clean_profile = clean_profile + self.process_class = process_class or ProcessHandler + self.process_args = process_args or {} + self.symbols_path = symbols_path + + def __del__(self): + self.cleanup() + + @abstractproperty + def command(self): + """Returns the command list to run.""" + pass + + @property + def returncode(self): + """ + The returncode of the process_handler. A value of None + indicates the process is still running. A negative + value indicates the process was killed with the + specified signal. + + :raises: RunnerNotStartedError + """ + if self.process_handler: + return self.process_handler.poll() + else: + raise RunnerNotStartedError("returncode accessed before runner started") + + def start(self, debug_args=None, interactive=False, timeout=None, outputTimeout=None): + """ + Run self.command in the proper environment. + + :param debug_args: arguments for a debugger + :param interactive: uses subprocess.Popen directly + :param timeout: see process_handler.run() + :param outputTimeout: see process_handler.run() + :returns: the process id + """ + self.timeout = timeout + self.output_timeout = outputTimeout + cmd = self.command + + # ensure the runner is stopped + self.stop() + + # attach a debugger, if specified + if debug_args: + cmd = list(debug_args) + cmd + + if interactive: + self.process_handler = subprocess.Popen(cmd, env=self.env) + # TODO: other arguments + else: + # this run uses the managed processhandler + self.process_handler = self.process_class(cmd, env=self.env, **self.process_args) + self.process_handler.run(self.timeout, self.output_timeout) + + return self.process_handler.pid + + def wait(self, timeout=None): + """ + Wait for the process to exit. + + :param timeout: if not None, will return after timeout seconds. + Timeout is ignored if interactive was set to True. + :returns: the process return code if process exited normally, + -<signal> if process was killed (Unix only), + None if timeout was reached and the process is still running. + :raises: RunnerNotStartedError + """ + if self.is_running(): + # The interactive mode uses directly a Popen process instance. It's + # wait() method doesn't have any parameters. So handle it separately. + if isinstance(self.process_handler, subprocess.Popen): + self.process_handler.wait() + else: + self.process_handler.wait(timeout) + + elif not self.process_handler: + raise RunnerNotStartedError("Wait() called before process started") + + return self.returncode + + def is_running(self): + """ + Checks if the process is running. + + :returns: True if the process is active + """ + return self.returncode is None + + def stop(self, sig=None): + """ + Kill the process. + + :param sig: Signal used to kill the process, defaults to SIGKILL + (has no effect on Windows). + :returns: the process return code if process was already stopped, + -<signal> if process was killed (Unix only) + :raises: RunnerNotStartedError + """ + try: + if not self.is_running(): + return self.returncode + except RunnerNotStartedError: + return + + # The interactive mode uses directly a Popen process instance. It's + # kill() method doesn't have any parameters. So handle it separately. + if isinstance(self.process_handler, subprocess.Popen): + self.process_handler.kill() + else: + self.process_handler.kill(sig=sig) + + return self.returncode + + def reset(self): + """ + Reset the runner to its default state. + """ + self.stop() + self.process_handler = None + + def check_for_crashes(self, dump_directory=None, dump_save_path=None, + test_name=None, quiet=False): + """ + Check for a possible crash and output stack trace. + + :param dump_directory: Directory to search for minidump files + :param dump_save_path: Directory to save the minidump files to + :param test_name: Name to use in the crash output + :param quiet: If `True` don't print the PROCESS-CRASH message to stdout + :returns: True if a crash was detected, otherwise False + """ + if not dump_directory: + dump_directory = os.path.join(self.profile.profile, 'minidumps') + + crashed = False + try: + crashed = mozcrash.check_for_crashes(dump_directory, + self.symbols_path, + dump_save_path=dump_save_path, + test_name=test_name, + quiet=quiet) + except: + traceback.print_exc() + + return crashed + + def cleanup(self): + """ + Cleanup all runner state + """ + self.stop()
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/cli.py @@ -0,0 +1,193 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import optparse +import os +import sys + +from mozprofile import MozProfileCLI, Profile +from .runners import ( + FirefoxRunner, + MetroRunner, + ThunderbirdRunner, +) + +from .utils import findInPath + +RUNNER_MAP = { + 'firefox': FirefoxRunner, + 'metro': MetroRunner, + 'thunderbird': ThunderbirdRunner, +} + +# Map of debugging programs to information about them +# from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59 +DEBUGGERS = {'gdb': {'interactive': True, + 'args': ['-q', '--args'],}, + 'valgrind': {'interactive': False, + 'args': ['--leak-check=full']} + } + +def debugger_arguments(debugger, arguments=None, interactive=None): + """Finds debugger arguments from debugger given and defaults + + :param debugger: name or path to debugger + :param arguments: arguments for the debugger, or None to use defaults + :param interactive: whether the debugger should run in interactive mode + + """ + # find debugger executable if not a file + executable = debugger + if not os.path.exists(executable): + executable = findInPath(debugger) + if executable is None: + raise Exception("Path to '%s' not found" % debugger) + + # if debugger not in dictionary of knowns return defaults + dirname, debugger = os.path.split(debugger) + if debugger not in DEBUGGERS: + return ([executable] + (arguments or []), bool(interactive)) + + # otherwise use the dictionary values for arguments unless specified + if arguments is None: + arguments = DEBUGGERS[debugger].get('args', []) + if interactive is None: + interactive = DEBUGGERS[debugger].get('interactive', False) + return ([executable] + arguments, interactive) + + +class CLI(MozProfileCLI): + """Command line interface""" + + module = "mozrunner" + + def __init__(self, args=sys.argv[1:]): + self.metadata = getattr(sys.modules[self.module], + 'package_metadata', + {}) + version = self.metadata.get('Version') + parser_args = {'description': self.metadata.get('Summary')} + if version: + parser_args['version'] = "%prog " + version + self.parser = optparse.OptionParser(**parser_args) + self.add_options(self.parser) + (self.options, self.args) = self.parser.parse_args(args) + + if getattr(self.options, 'info', None): + self.print_metadata() + sys.exit(0) + + # choose appropriate runner and profile classes + try: + self.runner_class = RUNNER_MAP[self.options.app] + except KeyError: + self.parser.error('Application "%s" unknown (should be one of "%s")' % + (self.options.app, ', '.join(RUNNER_MAP.keys()))) + + def add_options(self, parser): + """add options to the parser""" + + # add profile options + MozProfileCLI.add_options(self, parser) + + # add runner options + parser.add_option('-b', "--binary", + dest="binary", help="Binary path.", + metavar=None, default=None) + parser.add_option('--app', dest='app', default='firefox', + help="Application to use [DEFAULT: %default]") + parser.add_option('--app-arg', dest='appArgs', + default=[], action='append', + help="provides an argument to the test application") + parser.add_option('--debugger', dest='debugger', + help="run under a debugger, e.g. gdb or valgrind") + parser.add_option('--debugger-args', dest='debugger_args', + action='store', + help="arguments to the debugger") + parser.add_option('--interactive', dest='interactive', + action='store_true', + help="run the program interactively") + if self.metadata: + parser.add_option("--info", dest="info", default=False, + action="store_true", + help="Print module information") + + ### methods for introspecting data + + def get_metadata_from_egg(self): + import pkg_resources + ret = {} + dist = pkg_resources.get_distribution(self.module) + if dist.has_metadata("PKG-INFO"): + for line in dist.get_metadata_lines("PKG-INFO"): + key, value = line.split(':', 1) + ret[key] = value + if dist.has_metadata("requires.txt"): + ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") + return ret + + def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", + "Author", "Author-email", "License", "Platform", "Dependencies")): + for key in data: + if key in self.metadata: + print key + ": " + self.metadata[key] + + ### methods for running + + def command_args(self): + """additional arguments for the mozilla application""" + return map(os.path.expanduser, self.options.appArgs) + + def runner_args(self): + """arguments to instantiate the runner class""" + return dict(cmdargs=self.command_args(), + binary=self.options.binary) + + def create_runner(self): + profile = Profile(**self.profile_args()) + return self.runner_class(profile=profile, **self.runner_args()) + + def run(self): + runner = self.create_runner() + self.start(runner) + runner.cleanup() + + def debugger_arguments(self): + """Get the debugger arguments + + returns a 2-tuple of debugger arguments: + (debugger_arguments, interactive) + + """ + debug_args = self.options.debugger_args + if debug_args is not None: + debug_args = debug_args.split() + interactive = self.options.interactive + if self.options.debugger: + debug_args, interactive = debugger_arguments(self.options.debugger, debug_args, interactive) + return debug_args, interactive + + def start(self, runner): + """Starts the runner and waits for the application to exit + + It can also happen via a keyboard interrupt. It should be + overwritten to provide custom running of the runner instance. + + """ + # attach a debugger if specified + debug_args, interactive = self.debugger_arguments() + runner.start(debug_args=debug_args, interactive=interactive) + print 'Starting: ' + ' '.join(runner.command) + try: + runner.wait() + except KeyboardInterrupt: + runner.stop() + + +def cli(args=sys.argv[1:]): + CLI(args).run() + + +if __name__ == '__main__': + cli()
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/__init__.py @@ -0,0 +1,10 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +from emulator import Emulator +from base import Device + +import emulator_battery +import emulator_geo +import emulator_screen
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/base.py @@ -0,0 +1,227 @@ +from ConfigParser import ( + ConfigParser, + RawConfigParser +) +import datetime +import os +import posixpath +import socket +import subprocess +import tempfile +import time +import traceback + +from mozdevice import DMError + +class Device(object): + def __init__(self, app_ctx, restore=True): + self.app_ctx = app_ctx + self.dm = self.app_ctx.dm + self.restore = restore + self.added_files = set() + self.backup_files = set() + + @property + def remote_profiles(self): + """ + A list of remote profiles on the device. + """ + remote_ini = self.app_ctx.remote_profiles_ini + if not self.dm.fileExists(remote_ini): + raise Exception("Remote file '%s' not found" % remote_ini) + + local_ini = tempfile.NamedTemporaryFile() + self.dm.getFile(remote_ini, local_ini.name) + cfg = ConfigParser() + cfg.read(local_ini.name) + + profiles = [] + for section in cfg.sections(): + if cfg.has_option(section, 'Path'): + if cfg.has_option(section, 'IsRelative') and cfg.getint(section, 'IsRelative'): + profiles.append(posixpath.join(posixpath.dirname(remote_ini), \ + cfg.get(section, 'Path'))) + else: + profiles.append(cfg.get(section, 'Path')) + return profiles + + def pull_minidumps(self): + """ + Saves any minidumps found in the remote profile on the local filesystem. + + :returns: Path to directory containing the dumps. + """ + remote_dump_dir = posixpath.join(self.app_ctx.remote_profile, 'minidumps') + local_dump_dir = tempfile.mkdtemp() + self.dm.getDirectory(remote_dump_dir, local_dump_dir) + self.dm.removeDir(remote_dump_dir) + return local_dump_dir + + def setup_profile(self, profile): + """ + Copy profile to the device and update the remote profiles.ini + to point to the new profile. + + :param profile: mozprofile object to copy over. + """ + self.dm.remount() + + if self.dm.dirExists(self.app_ctx.remote_profile): + self.dm.shellCheckOutput(['rm', '-r', self.app_ctx.remote_profile]) + + self.dm.pushDir(profile.profile, self.app_ctx.remote_profile) + + extension_dir = os.path.join(profile.profile, 'extensions', 'staged') + if os.path.isdir(extension_dir): + # Copy the extensions to the B2G bundles dir. + # need to write to read-only dir + for filename in os.listdir(extension_dir): + path = posixpath.join(self.app_ctx.remote_bundles_dir, filename) + if self.dm.fileExists(path): + self.dm.shellCheckOutput(['rm', '-rf', path]) + self.dm.pushDir(extension_dir, self.app_ctx.remote_bundles_dir) + + timeout = 5 # seconds + starttime = datetime.datetime.now() + while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout): + if self.dm.fileExists(self.app_ctx.remote_profiles_ini): + break + time.sleep(1) + else: + print "timed out waiting for profiles.ini" + + local_profiles_ini = tempfile.NamedTemporaryFile() + self.dm.getFile(self.app_ctx.remote_profiles_ini, local_profiles_ini.name) + + config = ProfileConfigParser() + config.read(local_profiles_ini.name) + for section in config.sections(): + if 'Profile' in section: + config.set(section, 'IsRelative', 0) + config.set(section, 'Path', self.app_ctx.remote_profile) + + new_profiles_ini = tempfile.NamedTemporaryFile() + config.write(open(new_profiles_ini.name, 'w')) + + self.backup_file(self.app_ctx.remote_profiles_ini) + self.dm.pushFile(new_profiles_ini.name, self.app_ctx.remote_profiles_ini) + + def install_busybox(self, busybox): + """ + Installs busybox on the device. + + :param busybox: Path to busybox binary to install. + """ + self.dm.remount() + print 'pushing %s' % self.app_ctx.remote_busybox + self.dm.pushFile(busybox, self.app_ctx.remote_busybox, retryLimit=10) + # TODO for some reason using dm.shellCheckOutput doesn't work, + # while calling adb shell directly does. + args = [self.app_ctx.adb, '-s', self.dm._deviceSerial, + 'shell', 'cd /system/bin; chmod 555 busybox;' \ + 'for x in `./busybox --list`; do ln -s ./busybox $x; done'] + adb = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + adb.wait() + self.dm._verifyZip() + + def setup_port_forwarding(self, remote_port): + """ + Set up TCP port forwarding to the specified port on the device, + using any availble local port, and return the local port. + + :param remote_port: The remote port to wait on. + """ + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("",0)) + local_port = s.getsockname()[1] + s.close() + + self.dm.forward('tcp:%d' % local_port, 'tcp:%d' % remote_port) + return local_port + + def wait_for_port(self, port, timeout=300): + starttime = datetime.datetime.now() + while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(('localhost', port)) + data = sock.recv(16) + sock.close() + if ':' in data: + return True + except: + traceback.print_exc() + time.sleep(1) + return False + + def backup_file(self, remote_path): + if not self.restore: + return + + if self.dm.fileExists(remote_path): + self.dm.copyTree(remote_path, '%s.orig' % remote_path) + self.backup_files.add(remote_path) + else: + self.added_files.add(remote_path) + + def cleanup(self): + """ + Cleanup the device. + """ + if not self.restore: + return + + try: + self.dm._verifyDevice() + except DMError: + return + + self.dm.remount() + # Restore the original profile + for added_file in self.added_files: + self.dm.removeFile(added_file) + + for backup_file in self.backup_files: + if self.dm.fileExists('%s.orig' % backup_file): + self.dm.moveTree('%s.orig' % backup_file, backup_file) + + # Delete any bundled extensions + extension_dir = posixpath.join(self.app_ctx.remote_profile, 'extensions', 'staged') + if self.dm.dirExists(extension_dir): + for filename in self.dm.listFiles(extension_dir): + try: + self.dm.removeDir(posixpath.join(self.app_ctx.remote_bundles_dir, filename)) + except DMError: + pass + # Remove the test profile + self.dm.removeDir(self.app_ctx.remote_profile) + + +class ProfileConfigParser(RawConfigParser): + """ + Class to create profiles.ini config files + + Subclass of RawConfigParser that outputs .ini files in the exact + format expected for profiles.ini, which is slightly different + than the default format. + """ + + def optionxform(self, optionstr): + return optionstr + + def write(self, fp): + if self._defaults: + fp.write("[%s]\n" % ConfigParser.DEFAULTSECT) + for (key, value) in self._defaults.items(): + fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t'))) + fp.write("\n") + for section in self._sections: + fp.write("[%s]\n" % section) + for (key, value) in self._sections[section].items(): + if key == "__name__": + continue + if (value is not None) or (self._optcre == self.OPTCRE): + key = "=".join((key, str(value).replace('\n', '\n\t'))) + fp.write("%s\n" % (key)) + fp.write("\n") +
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator.py @@ -0,0 +1,258 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from telnetlib import Telnet +import datetime +import os +import shutil +import subprocess +import tempfile +import time + +from mozprocess import ProcessHandler + +from .base import Device +from .emulator_battery import EmulatorBattery +from .emulator_geo import EmulatorGeo +from .emulator_screen import EmulatorScreen +from ..utils import uses_marionette +from ..errors import TimeoutException, ScriptTimeoutException + +class ArchContext(object): + def __init__(self, arch, context, binary=None): + kernel = os.path.join(context.homedir, 'prebuilts', 'qemu-kernel', '%s', '%s') + sysdir = os.path.join(context.homedir, 'out', 'target', 'product', '%s') + if arch == 'x86': + self.binary = os.path.join(context.bindir, 'emulator-x86') + self.kernel = kernel % ('x86', 'kernel-qemu') + self.sysdir = sysdir % 'generic_x86' + self.extra_args = [] + else: + self.binary = os.path.join(context.bindir, 'emulator') + self.kernel = kernel % ('arm', 'kernel-qemu-armv7') + self.sysdir = sysdir % 'generic' + self.extra_args = ['-cpu', 'cortex-a8'] + + if binary: + self.binary = binary + + +class Emulator(Device): + logcat_proc = None + port = None + proc = None + telnet = None + + def __init__(self, app_ctx, arch, resolution=None, sdcard=None, userdata=None, + logdir=None, no_window=None, binary=None): + Device.__init__(self, app_ctx) + + self.arch = ArchContext(arch, self.app_ctx, binary=binary) + self.resolution = resolution or '320x480' + self.sdcard = None + if sdcard: + self.sdcard = self.create_sdcard(sdcard) + self.userdata = os.path.join(self.arch.sysdir, 'userdata.img') + if userdata: + self.userdata = tempfile.NamedTemporaryFile(prefix='qemu-userdata') + shutil.copyfile(userdata, self.userdata) + self.logdir = logdir + self.no_window = no_window + + self.battery = EmulatorBattery(self) + self.geo = EmulatorGeo(self) + self.screen = EmulatorScreen(self) + + @property + def args(self): + """ + Arguments to pass into the emulator binary. + """ + qemu_args = [self.arch.binary, + '-kernel', self.arch.kernel, + '-sysdir', self.arch.sysdir, + '-data', self.userdata] + if self.no_window: + qemu_args.append('-no-window') + if self.sdcard: + qemu_args.extend(['-sdcard', self.sdcard]) + qemu_args.extend(['-memory', '512', + '-partition-size', '512', + '-verbose', + '-skin', self.resolution, + '-gpu', 'on', + '-qemu'] + self.arch.extra_args) + return qemu_args + + def _get_online_devices(self): + return set([d[0] for d in self.dm.devices() if d[1] != 'offline']) + + def start(self): + """ + Starts a new emulator. + """ + original_devices = self._get_online_devices() + + qemu_log = None + qemu_proc_args = {} + if self.logdir: + # save output from qemu to logfile + qemu_log = os.path.join(self.logdir, 'qemu.log') + if os.path.isfile(qemu_log): + self._rotate_log(qemu_log) + qemu_proc_args['logfile'] = qemu_log + else: + qemu_proc_args['processOutputLine'] = lambda line: None + self.proc = ProcessHandler(self.args, **qemu_proc_args) + self.proc.run() + + devices = self._get_online_devices() + now = datetime.datetime.now() + while (devices - original_devices) == set([]): + time.sleep(1) + if datetime.datetime.now() - now > datetime.timedelta(seconds=60): + raise TimeoutException('timed out waiting for emulator to start') + devices = self._get_online_devices() + self.connect(devices - original_devices) + + def connect(self, devices=None): + """ + Connects to an already running emulator. + """ + devices = list(devices or self._get_online_devices()) + serial = [d for d in devices if d.startswith('emulator')][0] + self.dm._deviceSerial = serial + self.dm.connect() + self.port = int(serial[serial.rindex('-')+1:]) + + self.geo.set_default_location() + self.screen.initialize() + + print self.logdir + if self.logdir: + # save logcat + logcat_log = os.path.join(self.logdir, '%s.log' % serial) + if os.path.isfile(logcat_log): + self._rotate_log(logcat_log) + logcat_args = [self.app_ctx.adb, '-s', '%s' % serial, + 'logcat', '-v', 'threadtime'] + self.logcat_proc = ProcessHandler(logcat_args, logfile=logcat_log) + self.logcat_proc.run() + + # setup DNS fix for networking + self.app_ctx.dm.shellCheckOutput(['setprop', 'net.dns1', '10.0.2.3']) + + def create_sdcard(self, sdcard_size): + """ + Creates an sdcard partition in the emulator. + + :param sdcard_size: Size of partition to create, e.g '10MB'. + """ + mksdcard = self.app_ctx.which('mksdcard') + path = tempfile.mktemp(prefix='sdcard') + sdargs = [mksdcard, '-l', 'mySdCard', sdcard_size, path] + sd = subprocess.Popen(sdargs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + retcode = sd.wait() + if retcode: + raise Exception('unable to create sdcard: exit code %d: %s' + % (retcode, sd.stdout.read())) + return path + + def cleanup(self): + """ + Cleans up and kills the emulator. + """ + Device.cleanup(self) + if self.proc: + self.proc.kill() + self.proc = None + + # Remove temporary sdcard + if self.sdcard and os.path.isfile(self.sdcard): + os.remove(self.sdcard) + + def _rotate_log(self, srclog, index=1): + """ + Rotate a logfile, by recursively rotating logs further in the sequence, + deleting the last file if necessary. + """ + basename = os.path.basename(srclog) + basename = basename[:-len('.log')] + if index > 1: + basename = basename[:-len('.1')] + basename = '%s.%d.log' % (basename, index) + + destlog = os.path.join(self.logdir, basename) + if os.path.isfile(destlog): + if index == 3: + os.remove(destlog) + else: + self._rotate_log(destlog, index+1) + shutil.move(srclog, destlog) + + # TODO this function is B2G specific and shouldn't live here + @uses_marionette + def wait_for_system_message(self, marionette): + marionette.set_script_timeout(45000) + # Telephony API's won't be available immediately upon emulator + # boot; we have to wait for the syste-message-listener-ready + # message before we'll be able to use them successfully. See + # bug 792647. + print 'waiting for system-message-listener-ready...' + try: + marionette.execute_async_script(""" +waitFor( + function() { marionetteScriptFinished(true); }, + function() { return isSystemMessageListenerReady(); } +); + """) + except ScriptTimeoutException: + print 'timed out' + # We silently ignore the timeout if it occurs, since + # isSystemMessageListenerReady() isn't available on + # older emulators. 45s *should* be enough of a delay + # to allow telephony API's to work. + pass + print '...done' + + # TODO this function is B2G specific and shouldn't live here + @uses_marionette + def wait_for_homescreen(self, marionette): + print 'waiting for homescreen...' + + marionette.set_context(marionette.CONTEXT_CONTENT) + marionette.execute_async_script(""" +log('waiting for mozbrowserloadend'); +window.addEventListener('mozbrowserloadend', function loaded(aEvent) { + log('received mozbrowserloadend for ' + aEvent.target.src); + if (aEvent.target.src.indexOf('ftu') != -1 || aEvent.target.src.indexOf('homescreen') != -1) { + window.removeEventListener('mozbrowserloadend', loaded); + marionetteScriptFinished(); + } +});""", script_timeout=120000) + print '...done' + + def _get_telnet_response(self, command=None): + output = [] + assert(self.telnet) + if command is not None: + self.telnet.write('%s\n' % command) + while True: + line = self.telnet.read_until('\n') + output.append(line.rstrip()) + if line.startswith('OK'): + return output + elif line.startswith('KO:'): + raise Exception('bad telnet response: %s' % line) + + def _run_telnet(self, command): + if not self.telnet: + self.telnet = Telnet('localhost', self.port) + self._get_telnet_response() + return self._get_telnet_response(command) + + def __del__(self): + if self.telnet: + self.telnet.write('exit\n') + self.telnet.read_all()
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator_battery.py @@ -0,0 +1,53 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +class EmulatorBattery(object): + + def __init__(self, emulator): + self.emulator = emulator + + def get_state(self): + status = {} + state = {} + + response = self.emulator._run_telnet('power display') + for line in response: + if ':' in line: + field, value = line.split(':') + value = value.strip() + if value == 'true': + value = True + elif value == 'false': + value = False + elif field == 'capacity': + value = float(value) + status[field] = value + + state['level'] = status.get('capacity', 0.0) / 100 + if status.get('AC') == 'online': + state['charging'] = True + else: + state['charging'] = False + + return state + + def get_charging(self): + return self.get_state()['charging'] + + def get_level(self): + return self.get_state()['level'] + + def set_level(self, level): + self.emulator._run_telnet('power capacity %d' % (level * 100)) + + def set_charging(self, charging): + if charging: + cmd = 'power ac on' + else: + cmd = 'power ac off' + self.emulator._run_telnet(cmd) + + charging = property(get_charging, set_charging) + level = property(get_level, set_level) +
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator_geo.py @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +class EmulatorGeo(object): + + def __init__(self, emulator): + self.emulator = emulator + + def set_default_location(self): + self.lon = -122.08769 + self.lat = 37.41857 + self.set_location(self.lon, self.lat) + + def set_location(self, lon, lat): + self.emulator._run_telnet('geo fix %0.5f %0.5f' % (self.lon, self.lat)) +
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/devices/emulator_screen.py @@ -0,0 +1,89 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +class EmulatorScreen(object): + """Class for screen related emulator commands.""" + + SO_PORTRAIT_PRIMARY = 'portrait-primary' + SO_PORTRAIT_SECONDARY = 'portrait-secondary' + SO_LANDSCAPE_PRIMARY = 'landscape-primary' + SO_LANDSCAPE_SECONDARY = 'landscape-secondary' + + def __init__(self, emulator): + self.emulator = emulator + + def initialize(self): + self.orientation = self.SO_PORTRAIT_PRIMARY + + def _get_raw_orientation(self): + """Get the raw value of the current device orientation.""" + response = self.emulator._run_telnet('sensor get orientation') + + return response[0].split('=')[1].strip() + + def _set_raw_orientation(self, data): + """Set the raw value of the specified device orientation.""" + self.emulator._run_telnet('sensor set orientation %s' % data) + + def get_orientation(self): + """Get the current device orientation. + + Returns; + orientation -- Orientation of the device. One of: + SO_PORTRAIT_PRIMARY - system buttons at the bottom + SO_PORTRIAT_SECONDARY - system buttons at the top + SO_LANDSCAPE_PRIMARY - system buttons at the right + SO_LANDSCAPE_SECONDARY - system buttons at the left + + """ + data = self._get_raw_orientation() + + if data == '0:-90:0': + orientation = self.SO_PORTRAIT_PRIMARY + elif data == '0:90:0': + orientation = self.SO_PORTRAIT_SECONDARY + elif data == '0:0:90': + orientation = self.SO_LANDSCAPE_PRIMARY + elif data == '0:0:-90': + orientation = self.SO_LANDSCAPE_SECONDARY + else: + raise ValueError('Unknown orientation sensor value: %s.' % data) + + return orientation + + def set_orientation(self, orientation): + """Set the specified device orientation. + + Args + orientation -- Orientation of the device. One of: + SO_PORTRAIT_PRIMARY - system buttons at the bottom + SO_PORTRIAT_SECONDARY - system buttons at the top + SO_LANDSCAPE_PRIMARY - system buttons at the right + SO_LANDSCAPE_SECONDARY - system buttons at the left + """ + orientation = SCREEN_ORIENTATIONS[orientation] + + if orientation == self.SO_PORTRAIT_PRIMARY: + data = '0:-90:0' + elif orientation == self.SO_PORTRAIT_SECONDARY: + data = '0:90:0' + elif orientation == self.SO_LANDSCAPE_PRIMARY: + data = '0:0:90' + elif orientation == self.SO_LANDSCAPE_SECONDARY: + data = '0:0:-90' + else: + raise ValueError('Invalid orientation: %s' % orientation) + + self._set_raw_orientation(data) + + orientation = property(get_orientation, set_orientation) + + +SCREEN_ORIENTATIONS = {"portrait": EmulatorScreen.SO_PORTRAIT_PRIMARY, + "landscape": EmulatorScreen.SO_LANDSCAPE_PRIMARY, + "portrait-primary": EmulatorScreen.SO_PORTRAIT_PRIMARY, + "landscape-primary": EmulatorScreen.SO_LANDSCAPE_PRIMARY, + "portrait-secondary": EmulatorScreen.SO_PORTRAIT_SECONDARY, + "landscape-secondary": EmulatorScreen.SO_LANDSCAPE_SECONDARY} +
--- a/testing/mozbase/mozrunner/mozrunner/errors.py +++ b/testing/mozbase/mozrunner/mozrunner/errors.py @@ -1,11 +1,16 @@ #!/usr/bin/env python # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. class RunnerException(Exception): """Base exception handler for mozrunner related errors""" - class RunnerNotStartedError(RunnerException): """Exception handler in case the runner hasn't been started""" + +class TimeoutException(RunnerException): + """Raised on timeout waiting for targets to start.""" + +class ScriptTimeoutException(RunnerException): + """Raised on timeout waiting for execute_script to finish."""
deleted file mode 100644 --- a/testing/mozbase/mozrunner/mozrunner/local.py +++ /dev/null @@ -1,362 +0,0 @@ -#!/usr/bin/env python - -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this file, -# You can obtain one at http://mozilla.org/MPL/2.0/. - -import ConfigParser -import mozinfo -import optparse -import os -import platform -import subprocess -import sys - -if mozinfo.isMac: - from plistlib import readPlist - -from mozprofile import Profile, FirefoxProfile, MetroFirefoxProfile, ThunderbirdProfile, MozProfileCLI - -from .base import Runner -from .utils import findInPath, get_metadata_from_egg - - -__all__ = ['CLI', - 'cli', - 'LocalRunner', - 'local_runners', - 'package_metadata', - 'FirefoxRunner', - 'MetroFirefoxRunner', - 'ThunderbirdRunner'] - - -package_metadata = get_metadata_from_egg('mozrunner') - - -# Map of debugging programs to information about them -# from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59 -debuggers = {'gdb': {'interactive': True, - 'args': ['-q', '--args'],}, - 'valgrind': {'interactive': False, - 'args': ['--leak-check=full']} - } - - -def debugger_arguments(debugger, arguments=None, interactive=None): - """Finds debugger arguments from debugger given and defaults - - :param debugger: name or path to debugger - :param arguments: arguments for the debugger, or None to use defaults - :param interactive: whether the debugger should run in interactive mode - - """ - # find debugger executable if not a file - executable = debugger - if not os.path.exists(executable): - executable = findInPath(debugger) - if executable is None: - raise Exception("Path to '%s' not found" % debugger) - - # if debugger not in dictionary of knowns return defaults - dirname, debugger = os.path.split(debugger) - if debugger not in debuggers: - return ([executable] + (arguments or []), bool(interactive)) - - # otherwise use the dictionary values for arguments unless specified - if arguments is None: - arguments = debuggers[debugger].get('args', []) - if interactive is None: - interactive = debuggers[debugger].get('interactive', False) - return ([executable] + arguments, interactive) - - -class LocalRunner(Runner): - """Handles all running operations. Finds bins, runs and kills the process""" - - profile_class = Profile # profile class to use by default - - @classmethod - def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None, - clean_profile=True, process_class=None, **kwargs): - profile = cls.profile_class(**(profile_args or {})) - return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs, - clean_profile=clean_profile, process_class=process_class, **kwargs) - - def __init__(self, profile, binary, cmdargs=None, env=None, - kp_kwargs=None, clean_profile=None, process_class=None, **kwargs): - - Runner.__init__(self, profile, clean_profile=clean_profile, kp_kwargs=kp_kwargs, - process_class=process_class, env=env, **kwargs) - - # find the binary - self.binary = binary - if not self.binary: - raise Exception("Binary not specified") - if not os.path.exists(self.binary): - raise OSError("Binary path does not exist: %s" % self.binary) - - # To be safe the absolute path of the binary should be used - self.binary = os.path.abspath(self.binary) - - # allow Mac binaries to be specified as an app bundle - plist = '%s/Contents/Info.plist' % self.binary - if mozinfo.isMac and os.path.exists(plist): - info = readPlist(plist) - self.binary = os.path.join(self.binary, "Contents/MacOS/", - info['CFBundleExecutable']) - - self.cmdargs = cmdargs or [] - _cmdargs = [i for i in self.cmdargs - if i != '-foreground'] - if len(_cmdargs) != len(self.cmdargs): - # foreground should be last; see - # https://bugzilla.mozilla.org/show_bug.cgi?id=625614 - self.cmdargs = _cmdargs - self.cmdargs.append('-foreground') - if mozinfo.isMac and '-foreground' not in self.cmdargs: - # runner should specify '-foreground' on Mac; see - # https://bugzilla.mozilla.org/show_bug.cgi?id=916512 - self.cmdargs.append('-foreground') - - # process environment - if env is None: - self.env = os.environ.copy() - else: - self.env = env.copy() - # allows you to run an instance of Firefox separately from any other instances - self.env['MOZ_NO_REMOTE'] = '1' - # keeps Firefox attached to the terminal window after it starts - self.env['NO_EM_RESTART'] = '1' - - # set the library path if needed on linux - if sys.platform == 'linux2' and self.binary.endswith('-bin'): - dirname = os.path.dirname(self.binary) - if os.environ.get('LD_LIBRARY_PATH', None): - self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname) - else: - self.env['LD_LIBRARY_PATH'] = dirname - - @property - def command(self): - """Returns the command list to run""" - commands = [self.binary, '-profile', self.profile.profile] - - # Bug 775416 - Ensure that binary options are passed in first - commands[1:1] = self.cmdargs - - # If running on OS X 10.5 or older, wrap |cmd| so that it will - # be executed as an i386 binary, in case it's a 32-bit/64-bit universal - # binary. - if mozinfo.isMac and hasattr(platform, 'mac_ver') and \ - platform.mac_ver()[0][:4] < '10.6': - commands = ["arch", "-arch", "i386"] + commands - - return commands - - def get_repositoryInfo(self): - """Read repository information from application.ini and platform.ini""" - config = ConfigParser.RawConfigParser() - dirname = os.path.dirname(self.binary) - repository = { } - - for file, section in [('application', 'App'), ('platform', 'Build')]: - config.read(os.path.join(dirname, '%s.ini' % file)) - - for key, id in [('SourceRepository', 'repository'), - ('SourceStamp', 'changeset')]: - try: - repository['%s_%s' % (file, id)] = config.get(section, key); - except: - repository['%s_%s' % (file, id)] = None - - return repository - - -class FirefoxRunner(LocalRunner): - """Specialized LocalRunner subclass for running Firefox.""" - - profile_class = FirefoxProfile - - def __init__(self, profile, binary=None, **kwargs): - - # if no binary given take it from the BROWSER_PATH environment variable - binary = binary or os.environ.get('BROWSER_PATH') - LocalRunner.__init__(self, profile, binary, **kwargs) - - -class MetroFirefoxRunner(LocalRunner): - """Specialized LocalRunner subclass for running Firefox Metro""" - - profile_class = MetroFirefoxProfile - - # helper application to launch Firefox in Metro mode - here = os.path.dirname(os.path.abspath(__file__)) - immersiveHelperPath = os.path.sep.join([here, - 'resources', - 'metrotestharness.exe']) - - def __init__(self, profile, binary=None, **kwargs): - - # if no binary given take it from the BROWSER_PATH environment variable - binary = binary or os.environ.get('BROWSER_PATH') - LocalRunner.__init__(self, profile, binary, **kwargs) - - if not os.path.exists(self.immersiveHelperPath): - raise OSError('Can not find Metro launcher: %s' % self.immersiveHelperPath) - - if not mozinfo.isWin: - raise Exception('Firefox Metro mode is only supported on Windows 8 and onwards') - - @property - def command(self): - command = LocalRunner.command.fget(self) - command[:0] = [self.immersiveHelperPath, '-firefoxpath'] - - return command - - -class ThunderbirdRunner(LocalRunner): - """Specialized LocalRunner subclass for running Thunderbird""" - profile_class = ThunderbirdProfile - - -local_runners = {'firefox': FirefoxRunner, - 'metrofirefox' : MetroFirefoxRunner, - 'thunderbird': ThunderbirdRunner} - - -class CLI(MozProfileCLI): - """Command line interface""" - - module = "mozrunner" - - def __init__(self, args=sys.argv[1:]): - self.metadata = getattr(sys.modules[self.module], - 'package_metadata', - {}) - version = self.metadata.get('Version') - parser_args = {'description': self.metadata.get('Summary')} - if version: - parser_args['version'] = "%prog " + version - self.parser = optparse.OptionParser(**parser_args) - self.add_options(self.parser) - (self.options, self.args) = self.parser.parse_args(args) - - if getattr(self.options, 'info', None): - self.print_metadata() - sys.exit(0) - - # choose appropriate runner and profile classes - try: - self.runner_class = local_runners[self.options.app] - except KeyError: - self.parser.error('Application "%s" unknown (should be one of "%s")' % - (self.options.app, ', '.join(local_runners.keys()))) - - def add_options(self, parser): - """add options to the parser""" - - # add profile options - MozProfileCLI.add_options(self, parser) - - # add runner options - parser.add_option('-b', "--binary", - dest="binary", help="Binary path.", - metavar=None, default=None) - parser.add_option('--app', dest='app', default='firefox', - help="Application to use [DEFAULT: %default]") - parser.add_option('--app-arg', dest='appArgs', - default=[], action='append', - help="provides an argument to the test application") - parser.add_option('--debugger', dest='debugger', - help="run under a debugger, e.g. gdb or valgrind") - parser.add_option('--debugger-args', dest='debugger_args', - action='store', - help="arguments to the debugger") - parser.add_option('--interactive', dest='interactive', - action='store_true', - help="run the program interactively") - if self.metadata: - parser.add_option("--info", dest="info", default=False, - action="store_true", - help="Print module information") - - ### methods for introspecting data - - def get_metadata_from_egg(self): - import pkg_resources - ret = {} - dist = pkg_resources.get_distribution(self.module) - if dist.has_metadata("PKG-INFO"): - for line in dist.get_metadata_lines("PKG-INFO"): - key, value = line.split(':', 1) - ret[key] = value - if dist.has_metadata("requires.txt"): - ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") - return ret - - def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", - "Author", "Author-email", "License", "Platform", "Dependencies")): - for key in data: - if key in self.metadata: - print key + ": " + self.metadata[key] - - ### methods for running - - def command_args(self): - """additional arguments for the mozilla application""" - return map(os.path.expanduser, self.options.appArgs) - - def runner_args(self): - """arguments to instantiate the runner class""" - return dict(cmdargs=self.command_args(), - binary=self.options.binary, - profile_args=self.profile_args()) - - def create_runner(self): - return self.runner_class.create(**self.runner_args()) - - def run(self): - runner = self.create_runner() - self.start(runner) - runner.cleanup() - - def debugger_arguments(self): - """Get the debugger arguments - - returns a 2-tuple of debugger arguments: - (debugger_arguments, interactive) - - """ - debug_args = self.options.debugger_args - if debug_args is not None: - debug_args = debug_args.split() - interactive = self.options.interactive - if self.options.debugger: - debug_args, interactive = debugger_arguments(self.options.debugger, debug_args, interactive) - return debug_args, interactive - - def start(self, runner): - """Starts the runner and waits for the application to exit - - It can also happen via a keyboard interrupt. It should be - overwritten to provide custom running of the runner instance. - - """ - # attach a debugger if specified - debug_args, interactive = self.debugger_arguments() - runner.start(debug_args=debug_args, interactive=interactive) - print 'Starting: ' + ' '.join(runner.command) - try: - runner.wait() - except KeyboardInterrupt: - runner.stop() - - -def cli(args=sys.argv[1:]): - CLI(args).run() - - -if __name__ == '__main__': - cli()
deleted file mode 100644 --- a/testing/mozbase/mozrunner/mozrunner/remote.py +++ /dev/null @@ -1,382 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this file, -# You can obtain one at http://mozilla.org/MPL/2.0/. - -import ConfigParser -import os -import posixpath -import re -import signal -from StringIO import StringIO -import subprocess -import sys -import tempfile -import time - -from mozdevice import DMError -import mozfile -import mozlog - -from .base import Runner - - -__all__ = ['B2GRunner', - 'RemoteRunner', - 'remote_runners'] - - -class RemoteRunner(Runner): - - def __init__(self, profile, - devicemanager, - clean_profile=None, - process_class=None, - env=None, - remote_test_root=None, - restore=True, - **kwargs): - - Runner.__init__(self, profile, clean_profile=clean_profile, - process_class=process_class, env=env, **kwargs) - self.log = mozlog.getLogger('RemoteRunner') - - self.dm = devicemanager - self.last_test = None - self.remote_test_root = remote_test_root or self.dm.getDeviceRoot() - self.log.info('using %s as test_root' % self.remote_test_root) - self.remote_profile = posixpath.join(self.remote_test_root, 'profile') - self.restore = restore - self.added_files = set() - self.backup_files = set() - - def backup_file(self, remote_path): - if not self.restore: - return - - if self.dm.fileExists(remote_path): - self.dm.shellCheckOutput(['dd', 'if=%s' % remote_path, 'of=%s.orig' % remote_path]) - self.backup_files.add(remote_path) - else: - self.added_files.add(remote_path) - - def check_for_crashes(self, last_test=None): - last_test = last_test or self.last_test - remote_dump_dir = posixpath.join(self.remote_profile, 'minidumps') - crashed = False - - self.log.info("checking for crashes in '%s'" % remote_dump_dir) - if self.dm.dirExists(remote_dump_dir): - local_dump_dir = tempfile.mkdtemp() - self.dm.getDirectory(remote_dump_dir, local_dump_dir) - - crashed = Runner.check_for_crashes(self, local_dump_dir, \ - test_name=last_test) - mozfile.remove(local_dump_dir) - self.dm.removeDir(remote_dump_dir) - - return crashed - - def cleanup(self): - if not self.restore: - return - - Runner.cleanup(self) - - self.dm.remount() - # Restore the original profile - for added_file in self.added_files: - self.dm.removeFile(added_file) - - for backup_file in self.backup_files: - if self.dm.fileExists('%s.orig' % backup_file): - self.dm.shellCheckOutput(['dd', 'if=%s.orig' % backup_file, 'of=%s' % backup_file]) - self.dm.removeFile("%s.orig" % backup_file) - - # Delete any bundled extensions - extension_dir = posixpath.join(self.remote_profile, 'extensions', 'staged') - if self.dm.dirExists(extension_dir): - for filename in self.dm.listFiles(extension_dir): - try: - self.dm.removeDir(posixpath.join(self.bundles_dir, filename)) - except DMError: - pass - # Remove the test profile - self.dm.removeDir(self.remote_profile) - - -class B2GRunner(RemoteRunner): - - def __init__(self, profile, devicemanager, marionette=None, context_chrome=True, - test_script=None, test_script_args=None, - marionette_port=None, emulator=None, **kwargs): - - remote_test_root = kwargs.get('remote_test_root') - if not remote_test_root: - kwargs['remote_test_root'] = '/data/local' - RemoteRunner.__init__(self, profile, devicemanager, **kwargs) - self.log = mozlog.getLogger('B2GRunner') - - tmpfd, processLog = tempfile.mkstemp(suffix='pidlog') - os.close(tmpfd) - tmp_env = self.env or {} - self.env = { 'MOZ_CRASHREPORTER': '1', - 'MOZ_CRASHREPORTER_NO_REPORT': '1', - 'MOZ_HIDE_RESULTS_TABLE': '1', - 'MOZ_PROCESS_LOG': processLog, - 'NSPR_LOG_MODULES': 'signaling:5,mtransport:3', - 'R_LOG_LEVEL': '5', - 'R_LOG_DESTINATION': 'stderr', - 'R_LOG_VERBOSE': '1', - 'NO_EM_RESTART': '1', } - self.env.update(tmp_env) - self.last_test = "automation" - - self.marionette = marionette - if self.marionette is not None: - if marionette_port is None: - marionette_port = self.marionette.port - elif self.marionette.port != marionette_port: - raise ValueError("Got a marionette object and a port but they don't match") - - if emulator is None: - emulator = marionette.emulator - elif marionette.emulator != emulator: - raise ValueError("Got a marionette object and an emulator argument but they don't match") - - self.marionette_port = marionette_port - self.emulator = emulator - - self.context_chrome = context_chrome - self.test_script = test_script - self.test_script_args = test_script_args - self.remote_profiles_ini = '/data/b2g/mozilla/profiles.ini' - self.bundles_dir = '/system/b2g/distribution/bundles' - - @property - def command(self): - cmd = [self.dm._adbPath] - if self.dm._deviceSerial: - cmd.extend(['-s', self.dm._deviceSerial]) - cmd.append('shell') - for k, v in self.env.iteritems(): - cmd.append("%s=%s" % (k, v)) - cmd.append('/system/bin/b2g.sh') - return cmd - - def start(self, timeout=None, outputTimeout=None): - self.timeout = timeout - self.outputTimeout = outputTimeout - self._setup_remote_profile() - # reboot device so it starts up with the proper profile - if not self.emulator: - self.dm.reboot(wait=True) - #wait for wlan to come up - if not self._wait_for_net(): - raise Exception("network did not come up, please configure the network" + - " prior to running before running the automation framework") - - self.dm.shellCheckOutput(['stop', 'b2g']) - - # For some reason user.js in the profile doesn't get picked up. - # Manually copy it over to prefs.js. See bug 1009730 for more details. - self.dm.moveTree(posixpath.join(self.remote_profile, 'user.js'), - posixpath.join(self.remote_profile, 'prefs.js')) - - self.kp_kwargs.update({'stream': sys.stdout, - 'processOutputLine': self.on_output, - 'onTimeout': self.on_timeout,}) - self.process_handler = self.process_class(self.command, **self.kp_kwargs) - self.process_handler.run(timeout=timeout, outputTimeout=outputTimeout) - - # Set up port forwarding again for Marionette, since any that - # existed previously got wiped out by the reboot. - if self.emulator is None: - subprocess.Popen([self.dm._adbPath, - 'forward', - 'tcp:%s' % self.marionette_port, - 'tcp:2828']).communicate() - - if self.marionette is not None: - self.start_marionette() - - if self.test_script is not None: - self.start_tests() - - def start_marionette(self): - self.marionette.wait_for_port() - - # start a marionette session - session = self.marionette.start_session() - if 'b2g' not in session: - raise Exception("bad session value %s returned by start_session" % session) - - if self.marionette.emulator: - # Disable offline status management (bug 777145), otherwise the network - # will be 'offline' when the mochitests start. Presumably, the network - # won't be offline on a real device, so we only do this for emulators. - self.marionette.set_context(self.marionette.CONTEXT_CHROME) - self.marionette.execute_script(""" - Components.utils.import("resource://gre/modules/Services.jsm"); - Services.io.manageOfflineStatus = false; - Services.io.offline = false; - """) - - if self.context_chrome: - self.marionette.set_context(self.marionette.CONTEXT_CHROME) - else: - self.marionette.set_context(self.marionette.CONTEXT_CONTENT) - - - def start_tests(self): - #self.marionette.execute_script(""" - # var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch); - # var homeUrl = prefs.getCharPref("browser.homescreenURL"); - # dump(homeURL + "\n"); - #""") - - # run the script that starts the tests - if os.path.isfile(self.test_script): - script = open(self.test_script, 'r') - self.marionette.execute_script(script.read(), script_args=self.test_script_args) - script.close() - elif isinstance(self.test_script, basestring): - self.marionette.execute_script(self.test_script, script_args=self.test_script_args) - - def on_output(self, line): - match = re.findall(r"TEST-START \| ([^\s]*)", line) - if match: - self.last_test = match[-1] - - def on_timeout(self): - self.dm.killProcess('/system/b2g/b2g', sig=signal.SIGABRT) - - msg = "%s | application timed out after %s seconds" - - if self.timeout: - timeout = self.timeout - else: - timeout = self.outputTimeout - msg = "%s with no output" % msg - - self.log.testFail(msg % (self.last_test, timeout)) - self.check_for_crashes() - - def _get_device_status(self, serial=None): - # If we know the device serial number, we look for that, - # otherwise we use the (presumably only) device shown in 'adb devices'. - serial = serial or self.dm._deviceSerial - status = 'unknown' - - proc = subprocess.Popen([self.dm._adbPath, 'devices'], stdout=subprocess.PIPE) - line = proc.stdout.readline() - while line != '': - result = re.match('(.*?)\t(.*)', line) - if result: - thisSerial = result.group(1) - if not serial or thisSerial == serial: - serial = thisSerial - status = result.group(2) - break - line = proc.stdout.readline() - return (serial, status) - - def _wait_for_net(self): - active = False - time_out = 0 - while not active and time_out < 40: - proc = subprocess.Popen([self.dm._adbPath, 'shell', '/system/bin/netcfg'], stdout=subprocess.PIPE) - proc.stdout.readline() # ignore first line - line = proc.stdout.readline() - while line != "": - if (re.search(r'UP\s+(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line)): - active = True - break - line = proc.stdout.readline() - time_out += 1 - time.sleep(1) - return active - - def _setup_remote_profile(self): - """Copy profile and update the remote profiles.ini to point to the new profile""" - self.dm.remount() - - # copy the profile to the device. - if self.dm.dirExists(self.remote_profile): - self.dm.shellCheckOutput(['rm', '-r', self.remote_profile]) - - try: - self.dm.pushDir(self.profile.profile, self.remote_profile) - except DMError: - self.log.error("Automation Error: Unable to copy profile to device.") - raise - - extension_dir = os.path.join(self.profile.profile, 'extensions', 'staged') - if os.path.isdir(extension_dir): - # Copy the extensions to the B2G bundles dir. - # need to write to read-only dir - for filename in os.listdir(extension_dir): - fpath = os.path.join(self.bundles_dir, filename) - if self.dm.fileExists(fpath): - self.dm.shellCheckOutput(['rm', '-rf', fpath]) - try: - self.dm.pushDir(extension_dir, self.bundles_dir) - except DMError: - self.log.error("Automation Error: Unable to copy extensions to device.") - raise - - if not self.dm.fileExists(self.remote_profiles_ini): - raise DMError("The profiles.ini file '%s' does not exist on the device" % self.remote_profiles_ini) - - local_profiles_ini = tempfile.NamedTemporaryFile() - self.dm.getFile(self.remote_profiles_ini, local_profiles_ini.name) - - config = ProfileConfigParser() - config.read(local_profiles_ini.name) - for section in config.sections(): - if 'Profile' in section: - config.set(section, 'IsRelative', 0) - config.set(section, 'Path', self.remote_profile) - - new_profiles_ini = tempfile.NamedTemporaryFile() - config.write(open(new_profiles_ini.name, 'w')) - - self.backup_file(self.remote_profiles_ini) - self.dm.pushFile(new_profiles_ini.name, self.remote_profiles_ini) - - def cleanup(self): - RemoteRunner.cleanup(self) - if getattr(self.marionette, 'instance', False): - self.marionette.instance.close() - del self.marionette - - -class ProfileConfigParser(ConfigParser.RawConfigParser): - """Class to create profiles.ini config files - - Subclass of RawConfigParser that outputs .ini files in the exact - format expected for profiles.ini, which is slightly different - than the default format. - - """ - - def optionxform(self, optionstr): - return optionstr - - def write(self, fp): - if self._defaults: - fp.write("[%s]\n" % ConfigParser.DEFAULTSECT) - for (key, value) in self._defaults.items(): - fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t'))) - fp.write("\n") - for section in self._sections: - fp.write("[%s]\n" % section) - for (key, value) in self._sections[section].items(): - if key == "__name__": - continue - if (value is not None) or (self._optcre == self.OPTCRE): - key = "=".join((key, str(value).replace('\n', '\n\t'))) - fp.write("%s\n" % (key)) - fp.write("\n") - -remote_runners = {'b2g': 'B2GRunner', - 'fennec': 'FennecRunner'}
new file mode 100644 --- /dev/null +++ b/testing/mozbase/mozrunner/mozrunner/runners.py @@ -0,0 +1,155 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +""" +This module contains a set of shortcut methods that create runners for commonly +used Mozilla applications, such as Firefox or B2G emulator. +""" + +from .application import get_app_context +from .base import DeviceRunner, GeckoRuntimeRunner +from .devices import Emulator + + +def Runner(*args, **kwargs): + """ + Create a generic GeckoRuntime runner. + + :param binary: Path to binary. + :param cmdargs: Arguments to pass into binary. + :param profile: Profile object to use. + :param env: Environment variables to pass into the gecko process. + :param clean_profile: If True, restores profile back to original state. + :param process_class: Class used to launch the binary. + :param process_args: Arguments to pass into process_class. + :param symbols_path: Path to symbol files used for crash analysis. + :returns: A generic GeckoRuntimeRunner. + """ + return GeckoRuntimeRunner(*args, **kwargs) + + +def FirefoxRunner(*args, **kwargs): + """ + Create a desktop Firefox runner. + + :param binary: Path to Firefox binary. + :param cmdargs: Arguments to pass into binary. + :param profile: Profile object to use. + :param env: Environment variables to pass into the gecko process. + :param clean_profile: If True, restores profile back to original state. + :param process_class: Class used to launch the binary. + :param process_args: Arguments to pass into process_class. + :param symbols_path: Path to symbol files used for crash analysis. + :returns: A GeckoRuntimeRunner for Firefox. + """ + kwargs['app_ctx'] = get_app_context('firefox')() + return GeckoRuntimeRunner(*args, **kwargs) + + +def ThunderbirdRunner(*args, **kwargs): + """ + Create a desktop Thunderbird runner. + + :param binary: Path to Thunderbird binary. + :param cmdargs: Arguments to pass into binary. + :param profile: Profile object to use. + :param env: Environment variables to pass into the gecko process. + :param clean_profile: If True, restores profile back to original state. + :param process_class: Class used to launch the binary. + :param process_args: Arguments to pass into process_class. + :param symbols_path: Path to symbol files used for crash analysis. + :returns: A GeckoRuntimeRunner for Thunderbird. + """ + kwargs['app_ctx'] = get_app_context('thunderbird')() + return GeckoRuntimeRunner(*args, **kwargs) + + +def MetroRunner(*args, **kwargs): + """ + Create a Windows metro Firefox runner. + + :param binary: Path to metro Firefox binary. + :param cmdargs: Arguments to pass into binary. + :param profile: Profile object to use. + :param env: Environment variables to pass into the gecko process. + :param clean_profile: If True, restores profile back to original state. + :param process_class: Class used to launch the binary. + :param process_args: Arguments to pass into process_class. + :param symbols_path: Path to symbol files used for crash analysis. + :returns: A GeckoRuntimeRunner for metro Firefox. + """ + kwargs['app_ctx'] = get_app_context('metro')() + return GeckoRuntimeRunner(*args, **kwargs) + + +def B2GDesktopRunner(*args, **kwargs): + """ + Create a B2G desktop runner. + + :param binary: Path to b2g desktop binary. + :param cmdargs: Arguments to pass into binary. + :param profile: Profile object to use. + :param env: Environment variables to pass into the gecko process. + :param clean_profile: If True, restores profile back to original state. + :param process_class: Class used to launch the binary. + :param process_args: Arguments to pass into process_class. + :param symbols_path: Path to symbol files used for crash analysis. + :returns: A GeckoRuntimeRunner for b2g desktop. + """ + # There is no difference between a generic and b2g desktop runner, + # but expose a separate entry point for clarity. + return Runner(*args, **kwargs) + + +def B2GEmulatorRunner(arch='arm', + b2g_home=None, + adb_path=None, + logdir=None, + binary=None, + no_window=None, + resolution=None, + sdcard=None, + userdata=None, + **kwargs): + """ + Create a B2G emulator runner. + + :param arch: The architecture of the emulator, either 'arm' or 'x86'. Defaults to 'arm'. + :param b2g_home: Path to root B2G repository. + :param logdir: Path to save logfiles such as logcat and qemu output. + :param no_window: Run emulator without a window. + :param resolution: Screen resolution to set emulator to, e.g '800x1000'. + :param sdcard: Path to local emulated sdcard storage. + :param userdata: Path to custom userdata image. + :param profile: Profile object to use. + :param env: Environment variables to pass into the b2g.sh process. + :param clean_profile: If True, restores profile back to original state. + :param process_class: Class used to launch the b2g.sh process. + :param process_args: Arguments to pass into the b2g.sh process. + :param symbols_path: Path to symbol files used for crash analysis. + :returns: A DeviceRunner for B2G emulators. + """ + kwargs['app_ctx'] = get_app_context('b2g')(b2g_home, adb_path=adb_path) + device_args = { 'app_ctx': kwargs['app_ctx'], + 'arch': arch, + 'binary': binary, + 'resolution': resolution, + 'sdcard': sdcard, + 'userdata': userdata, + 'no_window': no_window, + 'logdir': logdir } + return DeviceRunner(device_class=Emulator, + device_args=device_args, + **kwargs) + + +runners = { + 'default': Runner, + 'b2g_desktop': B2GDesktopRunner, + 'b2g_emulator': B2GEmulatorRunner, + 'firefox': FirefoxRunner, + 'metro': MetroRunner, + 'thunderbird': ThunderbirdRunner, +} +
--- a/testing/mozbase/mozrunner/mozrunner/utils.py +++ b/testing/mozbase/mozrunner/mozrunner/utils.py @@ -1,19 +1,20 @@ #!/usr/bin/env python # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. """Utility functions for mozrunner""" -__all__ = ['findInPath', 'get_metadata_from_egg'] +__all__ = ['findInPath', 'get_metadata_from_egg', 'uses_marionette'] +from functools import wraps import mozinfo import os import sys ### python package method metadata by introspection try: import pkg_resources @@ -58,8 +59,38 @@ def findInPath(fileName, path=os.environ return os.path.join(dir, fileName) if mozinfo.isWin: if os.path.isfile(os.path.join(dir, fileName + ".exe")): return os.path.join(dir, fileName + ".exe") if __name__ == '__main__': for i in sys.argv[1:]: print findInPath(i) + + +def _find_marionette_in_args(*args, **kwargs): + try: + m = [a for a in args + tuple(kwargs.values()) if hasattr(a, 'session')][0] + except IndexError: + print("Can only apply decorator to function using a marionette object") + raise + return m + +def uses_marionette(func): + """Decorator which creates a marionette session and deletes it + afterwards if one doesn't already exist. + """ + @wraps(func) + def _(*args, **kwargs): + m = _find_marionette_in_args(*args, **kwargs) + delete_session = False + if not m.session: + delete_session = True + m.start_session() + + m.set_context(m.CONTEXT_CHROME) + ret = func(*args, **kwargs) + + if delete_session: + m.delete_session() + + return ret + return _
--- a/testing/mozbase/mozrunner/setup.py +++ b/testing/mozbase/mozrunner/setup.py @@ -1,22 +1,22 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. import sys -from setuptools import setup +from setuptools import setup, find_packages PACKAGE_NAME = 'mozrunner' -PACKAGE_VERSION = '5.37' +PACKAGE_VERSION = '6.0' desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)""" deps = ['mozcrash >= 0.11', - 'mozdevice >= 0.30', + 'mozdevice >= 0.37', 'mozfile >= 1.0', 'mozinfo >= 0.7', 'mozlog >= 1.5', 'mozprocess >= 0.17', 'mozprofile >= 0.18', ] # we only support python 2 right now @@ -34,17 +34,17 @@ setup(name=PACKAGE_NAME, 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules', ], keywords='mozilla', author='Mozilla Automation and Tools team', author_email='tools@lists.mozilla.org', url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase', license='MPL 2.0', - packages=['mozrunner'], + packages=find_packages(), package_data={'mozrunner': [ 'resources/metrotestharness.exe' ]}, zip_safe=False, install_requires = deps, entry_points=""" # -*- Entry points: -*- [console_scripts]
--- a/testing/profiles/prefs_b2g_unittest.js +++ b/testing/profiles/prefs_b2g_unittest.js @@ -1,10 +1,10 @@ // Prefs specific to b2g mochitests +user_pref("b2g.system_manifest_url","app://test-container.gaiamobile.org/manifest.webapp"); user_pref("b2g.system_startup_url","app://test-container.gaiamobile.org/index.html"); -user_pref("b2g.system_manifest_url","app://test-container.gaiamobile.org/manifest.webapp"); -user_pref("dom.mozBrowserFramesEnabled", "%(OOP)s"); +user_pref("dom.ipc.browser_frames.oop_by_default", false); user_pref("dom.ipc.tabs.disabled", false); -user_pref("dom.ipc.browser_frames.oop_by_default", false); +user_pref("dom.mozBrowserFramesEnabled", "%(OOP)s"); user_pref("dom.mozBrowserFramesWhitelist","app://test-container.gaiamobile.org,http://mochi.test:8888"); +user_pref("dom.testing.datastore_enabled_for_hosted_apps", true); user_pref("marionette.force-local", true); -user_pref("dom.testing.datastore_enabled_for_hosted_apps", true);
--- a/testing/tps/tps/firefoxrunner.py +++ b/testing/tps/tps/firefoxrunner.py @@ -74,22 +74,12 @@ class TPSFirefoxRunner(object): if profile is None: profile = Profile() self.profile = profile if self.binary is None and self.url: self.binary = self.download_build() if self.runner is None: - self.runner = FirefoxRunner(self.profile, binary=self.binary) - - self.runner.profile = self.profile - - if env is not None: - self.runner.env.update(env) + self.runner = FirefoxRunner(profile=self.profile, binary=self.binary, env=env, cmdargs=args) - if args is not None: - self.runner.cmdargs = copy.copy(args) - - self.runner.start() - returncode = self.runner.wait(timeout) - - return returncode + self.runner.start(timeout=timeout) + return self.runner.wait()
--- a/testing/xpcshell/mach_commands.py +++ b/testing/xpcshell/mach_commands.py @@ -351,17 +351,17 @@ class B2GXPCShellRunner(MozbuildObject): import runtestsb2g parser = runtestsb2g.B2GOptions() options, args = parser.parse_args([]) options.b2g_path = b2g_home options.busybox = busybox or os.environ.get('BUSYBOX') options.localLib = self.bin_dir options.localBin = self.bin_dir - options.logcat_dir = self.xpcshell_dir + options.logdir = self.xpcshell_dir options.manifest = os.path.join(self.xpcshell_dir, 'xpcshell_b2g.ini') options.mozInfo = os.path.join(self.topobjdir, 'mozinfo.json') options.objdir = self.topobjdir options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols'), options.testingModulesDir = os.path.join(self.tests_dir, 'modules') options.testsRootDir = self.xpcshell_dir options.testPath = test_path options.use_device_libs = True
--- a/testing/xpcshell/remotexpcshelltests.py +++ b/testing/xpcshell/remotexpcshelltests.py @@ -121,23 +121,23 @@ class RemoteXPCShellTestThread(xpcshell. self.xpcsCmd] def testTimeout(self, test_file, proc): self.timedout = True if not self.retry: self.log.error("TEST-UNEXPECTED-FAIL | %s | Test timed out" % test_file) self.kill(proc) - def launchProcess(self, cmd, stdout, stderr, env, cwd): + def launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=None): self.timedout = False cmd.insert(1, self.remoteHere) outputFile = "xpcshelloutput" with open(outputFile, 'w+') as f: try: - self.shellReturnCode = self.device.shell(cmd, f) + self.shellReturnCode = self.device.shell(cmd, f, timeout=timeout+10) except devicemanager.DMError as e: if self.timedout: # If the test timed out, there is a good chance the SUTagent also # timed out and failed to return a return code, generating a # DMError. Ignore the DMError to simplify the error report. self.shellReturnCode = None pass else:
--- a/testing/xpcshell/runtestsb2g.py +++ b/testing/xpcshell/runtestsb2g.py @@ -14,20 +14,20 @@ from mozdevice import devicemanagerADB, DEVICE_TEST_ROOT = '/data/local/tests' from marionette import Marionette class B2GXPCShellTestThread(RemoteXPCShellTestThread): # Overridden - def launchProcess(self, cmd, stdout, stderr, env, cwd): + def launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=None): try: # This returns 1 even when tests pass - hardcode returncode to 0 (bug 773703) - outputFile = RemoteXPCShellTestThread.launchProcess(self, cmd, stdout, stderr, env, cwd) + outputFile = RemoteXPCShellTestThread.launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=timeout) self.shellReturnCode = 0 except DMError: self.shellReturnCode = -1 outputFile = "xpcshelloutput" f = open(outputFile, "a") f.write("\n%s" % traceback.format_exc()) f.close() return outputFile @@ -121,20 +121,20 @@ class B2GOptions(RemoteXPCShellOptions): dest='use_device_libs', help="Don't push .so's") defaults['use_device_libs'] = False self.add_option("--gecko-path", action="store", type="string", dest="geckoPath", help="the path to a gecko distribution that should " "be installed on the emulator prior to test") defaults["geckoPath"] = None - self.add_option("--logcat-dir", action="store", - type="string", dest="logcat_dir", - help="directory to store logcat dump files") - defaults["logcat_dir"] = None + self.add_option("--logdir", action="store", + type="string", dest="logdir", + help="directory to store log files") + defaults["logdir"] = None self.add_option('--busybox', action='store', type='string', dest='busybox', help="Path to busybox binary to install on device") defaults['busybox'] = None defaults["remoteTestRoot"] = DEVICE_TEST_ROOT defaults['dm_trans'] = 'adb' defaults['debugger'] = None @@ -144,33 +144,33 @@ class B2GOptions(RemoteXPCShellOptions): def verifyRemoteOptions(self, options): if options.b2g_path is None: self.error("Need to specify a --b2gpath") if options.geckoPath and not options.emulator: self.error("You must specify --emulator if you specify --gecko-path") - if options.logcat_dir and not options.emulator: - self.error("You must specify --emulator if you specify --logcat-dir") + if options.logdir and not options.emulator: + self.error("You must specify --emulator if you specify --logdir") return RemoteXPCShellOptions.verifyRemoteOptions(self, options) def run_remote_xpcshell(parser, options, args): options = parser.verifyRemoteOptions(options) # Create the Marionette instance kwargs = {} if options.emulator: kwargs['emulator'] = options.emulator if options.no_window: kwargs['noWindow'] = True if options.geckoPath: kwargs['gecko_path'] = options.geckoPath - if options.logcat_dir: - kwargs['logcat_dir'] = options.logcat_dir + if options.logdir: + kwargs['logdir'] = options.logdir if options.busybox: kwargs['busybox'] = options.busybox if options.symbolsPath: kwargs['symbols_path'] = options.symbolsPath if options.b2g_path: kwargs['homedir'] = options.emu_path or options.b2g_path if options.address: host, port = options.address.split(':')
--- a/testing/xpcshell/runxpcshelltests.py +++ b/testing/xpcshell/runxpcshelltests.py @@ -227,21 +227,23 @@ class XPCShellTestThread(Thread): break self.process_line(line) if self.saw_proc_start and not self.saw_proc_end: self.has_failure_output = True return proc.communicate() - def launchProcess(self, cmd, stdout, stderr, env, cwd): + def launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=None): """ Simple wrapper to launch a process. On a remote system, this is more complex and we need to overload this function. """ + # timeout is needed by remote and b2g xpcshell to extend the + # devicemanager.shell() timeout. It is not used in this function. if HAVE_PSUTIL: popen_func = psutil.Popen else: popen_func = Popen proc = popen_func(cmd, stdout=stdout, stderr=stderr, env=env, cwd=cwd) return proc @@ -618,17 +620,17 @@ class XPCShellTestThread(Thread): try: self.log.info("TEST-INFO | %s | running test ..." % name) if self.verbose: self.logCommand(name, completeCmd, test_dir) startTime = time.time() proc = self.launchProcess(completeCmd, - stdout=self.pStdout, stderr=self.pStderr, env=self.env, cwd=test_dir) + stdout=self.pStdout, stderr=self.pStderr, env=self.env, cwd=test_dir, timeout=testTimeoutInterval) if self.interactive: self.log.info("TEST-INFO | %s | Process ID: %d" % (name, proc.pid)) stdout, stderr = self.communicate(proc) if self.interactive: # Not sure what else to do here...