Bug 997244 - Pull emulator.py out of marionette and into mozrunner, r=wlach,mdas,jgriffin

This commit is contained in:
Andrew Halberstadt 2014-06-18 13:30:12 -04:00
parent 5f8a63a8d4
commit 77ca422eca
69 changed files with 1840 additions and 1925 deletions

View File

@ -194,7 +194,7 @@ class B2GRemoteAutomation(Automation):
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
@ -262,7 +262,7 @@ class B2GRemoteAutomation(Automation):
'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)

View File

@ -131,7 +131,7 @@ class MachCommands(MachCommandBase):
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()

View File

@ -22,16 +22,19 @@ 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)
@ -71,8 +74,8 @@ class B2GDesktopReftest(RefTest):
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:
@ -151,14 +154,13 @@ class B2GDesktopReftest(RefTest):
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:

View File

@ -201,7 +201,7 @@ class ReftestRunner(MozbuildObject):
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
@ -335,9 +335,9 @@ def B2GCommand(func):
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')

View File

@ -58,9 +58,9 @@ class B2GOptions(ReftestOptions):
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",
@ -101,10 +101,10 @@ class B2GOptions(ReftestOptions):
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")
@ -166,8 +166,8 @@ class B2GOptions(ReftestOptions):
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"
@ -497,8 +497,8 @@ def run_remote_reftests(parser, options, args):
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:
@ -511,19 +511,21 @@ def run_remote_reftests(parser, options, args):
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)

View File

@ -5,7 +5,7 @@
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",
@ -15,7 +15,7 @@ config = {
"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",
@ -25,7 +25,7 @@ config = {
"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",
@ -34,7 +34,7 @@ config = {
"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",
@ -43,7 +43,7 @@ config = {
"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",
],

View File

@ -6,21 +6,49 @@ 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

View File

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

View File

@ -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

View File

@ -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

View File

@ -25,21 +25,20 @@ class GeckoInstance(object):
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'
@ -57,13 +56,13 @@ class GeckoInstance(object):
# 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()
@ -72,24 +71,19 @@ class GeckoInstance(object):
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}
apps = {'b2g': B2GDesktopInstance,
'b2gdesktop': B2GDesktopInstance}
class CloneRunner(Runner):
profile_class = Profile.clone
class NullOutput(object):
def __call__(self, line):
pass
apps = {'b2g': B2GDesktopInstance,
'b2gdesktop': B2GDesktopInstance}

View File

@ -6,27 +6,20 @@ 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):
"""
@ -451,32 +444,24 @@ class Marionette(object):
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
@ -485,7 +470,7 @@ class Marionette(object):
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:
@ -504,52 +489,56 @@ class Marionette(object):
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:
@ -570,26 +559,6 @@ class Marionette(object):
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):
@ -609,7 +578,7 @@ class Marionette(object):
@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:
@ -623,8 +592,8 @@ class Marionette(object):
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.
@ -646,7 +615,7 @@ class Marionette(object):
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)
@ -657,9 +626,12 @@ class Marionette(object):
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})
@ -671,62 +643,59 @@ class Marionette(object):
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
@ -1459,4 +1428,4 @@ class Marionette(object):
"""
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()

View File

@ -367,11 +367,11 @@ class BaseMarionetteOptions(OptionParser):
'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',
@ -385,11 +385,11 @@ class BaseMarionetteOptions(OptionParser):
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',
@ -441,10 +441,6 @@ class BaseMarionetteOptions(OptionParser):
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',
@ -525,8 +521,8 @@ class BaseMarionetteOptions(OptionParser):
'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:
@ -562,11 +558,11 @@ 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,
@ -575,8 +571,8 @@ class BaseMarionetteTestRunner(object):
**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
@ -587,13 +583,12 @@ class BaseMarionetteTestRunner(object):
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
@ -640,9 +635,9 @@ class BaseMarionetteTestRunner(object):
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
@ -717,8 +712,7 @@ class BaseMarionetteTestRunner(object):
if self.emulator:
kwargs.update({
'homedir': self.homedir,
'logcat_dir': self.logcat_dir,
'gecko_path': self.gecko_path,
'logdir': self.logdir,
})
if self.address:
@ -741,10 +735,10 @@ class BaseMarionetteTestRunner(object):
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
@ -757,7 +751,7 @@ class BaseMarionetteTestRunner(object):
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

View File

@ -9,14 +9,10 @@ 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)

View File

@ -3,7 +3,7 @@
# 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):

View File

@ -3,7 +3,7 @@
# 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):

View File

@ -3,7 +3,7 @@
# 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):
@ -19,7 +19,7 @@ class TestEmulatorContent(MarionetteTestCase):
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)
""");

View File

@ -3,7 +3,7 @@
# 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

View File

@ -3,7 +3,7 @@
# 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):

View File

@ -5,7 +5,7 @@
import urllib
from by import By
from marionette import JavascriptException, MarionetteException
from errors import JavascriptException, MarionetteException
from marionette_test import MarionetteTestCase
def inline(doc):

View File

@ -5,7 +5,7 @@
from marionette_test import MarionetteTestCase
from marionette import HTMLElement
from by import By
from marionette import NoSuchElementException
from errors import NoSuchElementException
class TestElements(MarionetteTestCase):

View File

@ -5,7 +5,7 @@
from marionette_test import MarionetteTestCase
from marionette import HTMLElement
from by import By
from marionette import NoSuchElementException
from errors import NoSuchElementException
class TestElementsChrome(MarionetteTestCase):

View File

@ -3,7 +3,7 @@
# 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):

View File

@ -3,8 +3,7 @@
# 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):

View File

@ -4,9 +4,9 @@
# 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"

View File

@ -3,7 +3,7 @@
# 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):

View File

@ -4,7 +4,7 @@
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

View File

@ -1,6 +1,6 @@
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

View File

@ -3,7 +3,7 @@
# 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):

View File

@ -3,7 +3,7 @@
# 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):

View File

@ -3,7 +3,7 @@
# 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):

View File

@ -5,7 +5,7 @@
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):

View File

@ -4,7 +4,7 @@
from marionette_test import MarionetteTestCase
from keys import Keys
from marionette import ElementNotVisibleException
from errors import ElementNotVisibleException
class TestTyping(MarionetteTestCase):

View File

@ -3,8 +3,8 @@ 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

View File

@ -178,7 +178,7 @@ class MochitestRunner(MozbuildObject):
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)
@ -555,9 +555,9 @@ def B2GCommand(func):
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 \

View File

@ -705,11 +705,11 @@ class B2GOptions(MochitestOptions):
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'],
@ -738,7 +738,6 @@ class B2GOptions(MochitestOptions):
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
@ -757,8 +756,8 @@ class B2GOptions(MochitestOptions):
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)

View File

@ -1255,9 +1255,9 @@ class Mochitest(MochitestUtilsMixin):
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)
@ -1266,12 +1266,7 @@ class Mochitest(MochitestUtilsMixin):
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,

View File

@ -9,7 +9,6 @@ import shutil
import sys
import tempfile
import threading
import time
import traceback
here = os.path.abspath(os.path.dirname(__file__))
@ -17,7 +16,6 @@ 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
@ -25,20 +23,20 @@ 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 = []
@ -121,11 +119,6 @@ class B2GMochitest(MochitestUtilsMixin):
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:
@ -139,15 +132,44 @@ class B2GMochitest(MochitestUtilsMixin):
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
@ -158,7 +180,8 @@ class B2GMochitest(MochitestUtilsMixin):
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()
@ -171,23 +194,14 @@ class B2GMochitest(MochitestUtilsMixin):
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)
@ -202,6 +216,10 @@ class B2GDeviceMochitest(B2GMochitest, Mochitest):
# 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
@ -228,14 +246,14 @@ class B2GDeviceMochitest(B2GMochitest, Mochitest):
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
@ -255,6 +273,7 @@ class B2GDesktopMochitest(B2GMochitest, Mochitest):
# 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,
@ -282,49 +301,27 @@ class B2GDesktopMochitest(B2GMochitest, Mochitest):
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)
@ -349,13 +346,12 @@ def run_remote_mochitests(parser, options):
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':

View File

@ -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:

View File

@ -12,4 +12,5 @@ correctly handling the case where the system crashes.
mozfile
mozprofile
mozprocess
mozrunner
mozcrash

View File

@ -4,7 +4,6 @@
import hashlib
import mozlog
import socket
import os
import posixpath
import re
@ -378,13 +377,15 @@ class DeviceManager(object):
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)

View File

@ -29,11 +29,12 @@ class DeviceManagerADB(DeviceManager):
_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
@ -58,28 +59,33 @@ class DeviceManagerADB(DeviceManager):
# 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:

View File

@ -5,7 +5,7 @@
from setuptools import setup
PACKAGE_NAME = 'mozdevice'
PACKAGE_VERSION = '0.36'
PACKAGE_VERSION = '0.37'
deps = ['mozfile >= 1.0',
'mozlog',

View File

@ -693,7 +693,7 @@ falling back to not using job objects for managing child processes"""
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

View File

@ -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 .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 cli
import devices
import utils

View File

@ -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

View File

@ -0,0 +1,3 @@
from .runner import BaseRunner
from .device import DeviceRunner
from .browser import GeckoRuntimeRunner

View File

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

View File

@ -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)
def cleanup(self, *args, **kwargs):
BaseRunner.cleanup(self, *args, **kwargs)
self.device.cleanup()

View File

@ -3,79 +3,88 @@
# 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.processhandler import ProcessHandler
from mozprocess import ProcessHandler
import mozcrash
import mozlog
from .errors import RunnerNotStartedError
from ..application import DefaultContext
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
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 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
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', {}))
class Runner(object):
# process environment
if env is None:
self.env = os.environ.copy()
else:
self.env = env.copy()
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.process_args = process_args or {}
self.symbols_path = symbols_path
def __del__(self):
self.cleanup()
# Once we can use 'abc' it should become an abstract property
@property
@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 retrieved before process started")
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
"""
Run self.command in the proper environment.
returns the process id
:param debug_args: arguments for the debugger
: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()
# 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
@ -85,23 +94,21 @@ class Runner(object):
# 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)
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
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.
"""
Wait for the process to exit.
: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.
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
@ -117,29 +124,29 @@ class Runner(object):
return self.returncode
def is_running(self):
"""Checks if the process is running
returns True if the process is active
"""
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)
"""
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
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):
@ -150,19 +157,22 @@ class Runner(object):
return self.returncode
def reset(self):
"""Reset the runner to its default state"""
if getattr(self, 'profile', False):
self.profile.reset()
"""
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
"""
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')
@ -180,8 +190,7 @@ class Runner(object):
return crashed
def cleanup(self):
"""Cleanup all runner state"""
"""
Cleanup all runner state
"""
self.stop()
if getattr(self, 'profile', False) and self.clean_profile:
self.profile.cleanup()

View File

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

View File

@ -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

View File

@ -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")

View File

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

View File

@ -62,6 +62,8 @@ class EmulatorScreen(object):
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:
@ -76,3 +78,12 @@ class EmulatorScreen(object):
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}

View File

@ -6,6 +6,11 @@
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."""

View File

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

View File

@ -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'}

View File

@ -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,
}

View File

@ -6,9 +6,10 @@
"""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
@ -63,3 +64,33 @@ def findInPath(fileName, path=os.environ['PATH']):
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 _

View File

@ -3,15 +3,15 @@
# 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',
@ -39,7 +39,7 @@ setup(name=PACKAGE_NAME,
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'
]},

View File

@ -1,10 +1,10 @@
// Prefs specific to b2g mochitests
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.tabs.disabled", false);
user_pref("b2g.system_startup_url","app://test-container.gaiamobile.org/index.html");
user_pref("dom.ipc.browser_frames.oop_by_default", false);
user_pref("dom.ipc.tabs.disabled", false);
user_pref("dom.mozBrowserFramesEnabled", "%(OOP)s");
user_pref("dom.mozBrowserFramesWhitelist","app://test-container.gaiamobile.org,http://mochi.test:8888");
user_pref("marionette.force-local", true);
user_pref("dom.testing.datastore_enabled_for_hosted_apps", true);
user_pref("marionette.force-local", true);

View File

@ -79,17 +79,7 @@ class TPSFirefoxRunner(object):
self.binary = self.download_build()
if self.runner is None:
self.runner = FirefoxRunner(self.profile, binary=self.binary)
self.runner = FirefoxRunner(profile=self.profile, binary=self.binary, env=env, cmdargs=args)
self.runner.profile = self.profile
if env is not None:
self.runner.env.update(env)
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()

View File

@ -356,7 +356,7 @@ class B2GXPCShellRunner(MozbuildObject):
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

View File

@ -126,13 +126,13 @@ class RemoteXPCShellTestThread(xpcshell.XPCShellTestThread):
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

View File

@ -19,10 +19,10 @@ 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
@ -126,10 +126,10 @@ class B2GOptions(RemoteXPCShellOptions):
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")
@ -149,8 +149,8 @@ class B2GOptions(RemoteXPCShellOptions):
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):
@ -164,8 +164,8 @@ def run_remote_xpcshell(parser, options, args):
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:

View File

@ -232,11 +232,13 @@ class XPCShellTestThread(Thread):
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:
@ -623,7 +625,7 @@ class XPCShellTestThread(Thread):
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))