mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 997244 - Pull emulator.py out of marionette and into mozrunner, r=wlach,mdas,jgriffin
This commit is contained in:
parent
21b90d48e2
commit
0b14f45d59
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
],
|
||||
|
@ -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
|
||||
|
@ -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)
|
@ -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
|
@ -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
|
@ -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}
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
""");
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
from marionette_test import MarionetteTestCase
|
||||
from keys import Keys
|
||||
from marionette import ElementNotVisibleException
|
||||
from errors import ElementNotVisibleException
|
||||
|
||||
|
||||
class TestTyping(MarionetteTestCase):
|
||||
|
@ -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
|
||||
|
@ -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 \
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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':
|
||||
|
177
testing/mozbase/docs/mozrunner.rst
Normal file
177
testing/mozbase/docs/mozrunner.rst
Normal 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:
|
@ -12,4 +12,5 @@ correctly handling the case where the system crashes.
|
||||
mozfile
|
||||
mozprofile
|
||||
mozprocess
|
||||
mozrunner
|
||||
mozcrash
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -5,7 +5,7 @@
|
||||
from setuptools import setup
|
||||
|
||||
PACKAGE_NAME = 'mozdevice'
|
||||
PACKAGE_VERSION = '0.36'
|
||||
PACKAGE_VERSION = '0.37'
|
||||
|
||||
deps = ['mozfile >= 1.0',
|
||||
'mozlog',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
129
testing/mozbase/mozrunner/mozrunner/application.py
Normal file
129
testing/mozbase/mozrunner/mozrunner/application.py
Normal 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
|
3
testing/mozbase/mozrunner/mozrunner/base/__init__.py
Normal file
3
testing/mozbase/mozrunner/mozrunner/base/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .runner import BaseRunner
|
||||
from .device import DeviceRunner
|
||||
from .browser import GeckoRuntimeRunner
|
74
testing/mozbase/mozrunner/mozrunner/base/browser.py
Normal file
74
testing/mozbase/mozrunner/mozrunner/base/browser.py
Normal 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)
|
111
testing/mozbase/mozrunner/mozrunner/base/device.py
Normal file
111
testing/mozbase/mozrunner/mozrunner/base/device.py
Normal 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()
|
139
testing/mozbase/mozrunner/mozrunner/base.py → testing/mozbase/mozrunner/mozrunner/base/runner.py
Executable file → Normal file
139
testing/mozbase/mozrunner/mozrunner/base.py → testing/mozbase/mozrunner/mozrunner/base/runner.py
Executable file → Normal 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()
|
193
testing/mozbase/mozrunner/mozrunner/cli.py
Normal file
193
testing/mozbase/mozrunner/mozrunner/cli.py
Normal 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()
|
10
testing/mozbase/mozrunner/mozrunner/devices/__init__.py
Normal file
10
testing/mozbase/mozrunner/mozrunner/devices/__init__.py
Normal 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
|
227
testing/mozbase/mozrunner/mozrunner/devices/base.py
Normal file
227
testing/mozbase/mozrunner/mozrunner/devices/base.py
Normal 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")
|
||||
|
258
testing/mozbase/mozrunner/mozrunner/devices/emulator.py
Normal file
258
testing/mozbase/mozrunner/mozrunner/devices/emulator.py
Normal 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()
|
@ -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}
|
||||
|
@ -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."""
|
||||
|
@ -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()
|
@ -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'}
|
155
testing/mozbase/mozrunner/mozrunner/runners.py
Normal file
155
testing/mozbase/mozrunner/mozrunner/runners.py
Normal 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,
|
||||
}
|
||||
|
@ -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 _
|
||||
|
@ -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'
|
||||
]},
|
||||
|
@ -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);
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user