mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-30 05:35:31 +00:00
315 lines
13 KiB
Python
315 lines
13 KiB
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 time
|
|
import re
|
|
import os
|
|
import tempfile
|
|
import shutil
|
|
import subprocess
|
|
|
|
from automation import Automation
|
|
from devicemanager import NetworkTools, DMError
|
|
|
|
# signatures for logcat messages that we don't care about much
|
|
fennecLogcatFilters = [ "The character encoding of the HTML document was not declared",
|
|
"Use of Mutation Events is deprecated. Use MutationObserver instead." ]
|
|
|
|
class RemoteAutomation(Automation):
|
|
_devicemanager = None
|
|
|
|
def __init__(self, deviceManager, appName = '', remoteLog = None):
|
|
self._devicemanager = deviceManager
|
|
self._appName = appName
|
|
self._remoteProfile = None
|
|
self._remoteLog = remoteLog
|
|
|
|
# Default our product to fennec
|
|
self._product = "fennec"
|
|
self.lastTestSeen = "remoteautomation.py"
|
|
Automation.__init__(self)
|
|
|
|
def setDeviceManager(self, deviceManager):
|
|
self._devicemanager = deviceManager
|
|
|
|
def setAppName(self, appName):
|
|
self._appName = appName
|
|
|
|
def setRemoteProfile(self, remoteProfile):
|
|
self._remoteProfile = remoteProfile
|
|
|
|
def setProduct(self, product):
|
|
self._product = product
|
|
|
|
def setRemoteLog(self, logfile):
|
|
self._remoteLog = logfile
|
|
|
|
# Set up what we need for the remote environment
|
|
def environment(self, env = None, xrePath = None, crashreporter = True):
|
|
# Because we are running remote, we don't want to mimic the local env
|
|
# so no copying of os.environ
|
|
if env is None:
|
|
env = {}
|
|
|
|
# Except for the mochitest results table hiding option, which isn't
|
|
# passed to runtestsremote.py as an actual option, but through the
|
|
# MOZ_CRASHREPORTER_DISABLE environment variable.
|
|
if 'MOZ_HIDE_RESULTS_TABLE' in os.environ:
|
|
env['MOZ_HIDE_RESULTS_TABLE'] = os.environ['MOZ_HIDE_RESULTS_TABLE']
|
|
|
|
if crashreporter:
|
|
env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
|
|
env['MOZ_CRASHREPORTER'] = '1'
|
|
else:
|
|
env['MOZ_CRASHREPORTER_DISABLE'] = '1'
|
|
|
|
return env
|
|
|
|
def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
|
|
""" Wait for tests to finish (as evidenced by the process exiting),
|
|
or for maxTime elapse, in which case kill the process regardless.
|
|
"""
|
|
# maxTime is used to override the default timeout, we should honor that
|
|
status = proc.wait(timeout = maxTime)
|
|
self.lastTestSeen = proc.getLastTestSeen
|
|
|
|
if (status == 1 and self._devicemanager.getTopActivity() == proc.procName):
|
|
# Then we timed out, make sure Fennec is dead
|
|
if maxTime:
|
|
print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
|
|
"allowed maximum time of %s seconds" % (self.lastTestSeen, maxTime)
|
|
else:
|
|
print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \
|
|
"allowed maximum time" % (self.lastTestSeen)
|
|
proc.kill()
|
|
|
|
return status
|
|
|
|
def checkForJavaException(self, logcat):
|
|
found_exception = False
|
|
for i, line in enumerate(logcat):
|
|
if "REPORTING UNCAUGHT EXCEPTION" in line or "FATAL EXCEPTION" in line:
|
|
# Strip away the date, time, logcat tag and pid from the next two lines and
|
|
# concatenate the remainder to form a concise summary of the exception.
|
|
#
|
|
# For example:
|
|
#
|
|
# 01-30 20:15:41.937 E/GeckoAppShell( 1703): >>> REPORTING UNCAUGHT EXCEPTION FROM THREAD 9 ("GeckoBackgroundThread")
|
|
# 01-30 20:15:41.937 E/GeckoAppShell( 1703): java.lang.NullPointerException
|
|
# 01-30 20:15:41.937 E/GeckoAppShell( 1703): at org.mozilla.gecko.GeckoApp$21.run(GeckoApp.java:1833)
|
|
# 01-30 20:15:41.937 E/GeckoAppShell( 1703): at android.os.Handler.handleCallback(Handler.java:587)
|
|
# 01-30 20:15:41.937 E/GeckoAppShell( 1703): at android.os.Handler.dispatchMessage(Handler.java:92)
|
|
# 01-30 20:15:41.937 E/GeckoAppShell( 1703): at android.os.Looper.loop(Looper.java:123)
|
|
# 01-30 20:15:41.937 E/GeckoAppShell( 1703): at org.mozilla.gecko.util.GeckoBackgroundThread.run(GeckoBackgroundThread.java:31)
|
|
#
|
|
# -> java.lang.NullPointerException at org.mozilla.gecko.GeckoApp$21.run(GeckoApp.java:1833)
|
|
found_exception = True
|
|
logre = re.compile(r".*\): \t?(.*)")
|
|
m = logre.search(logcat[i+1])
|
|
if m and m.group(1):
|
|
top_frame = m.group(1)
|
|
m = logre.search(logcat[i+2])
|
|
if m and m.group(1):
|
|
top_frame = top_frame + m.group(1)
|
|
print "PROCESS-CRASH | java-exception | %s" % top_frame
|
|
break
|
|
return found_exception
|
|
|
|
def deleteANRs(self):
|
|
# delete ANR traces.txt file; usually need root permissions
|
|
traces = "/data/anr/traces.txt"
|
|
try:
|
|
self._devicemanager.shellCheckOutput(['rm', traces], root=True)
|
|
except DMError:
|
|
print "Error deleting %s" % traces
|
|
pass
|
|
|
|
def checkForANRs(self):
|
|
traces = "/data/anr/traces.txt"
|
|
if self._devicemanager.fileExists(traces):
|
|
try:
|
|
t = self._devicemanager.pullFile(traces)
|
|
print "Contents of %s:" % traces
|
|
print t
|
|
# Once reported, delete traces
|
|
self.deleteANRs()
|
|
except DMError:
|
|
print "Error pulling %s" % traces
|
|
pass
|
|
else:
|
|
print "%s not found" % traces
|
|
|
|
def checkForCrashes(self, directory, symbolsPath):
|
|
self.checkForANRs()
|
|
|
|
logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
|
|
javaException = self.checkForJavaException(logcat)
|
|
if javaException:
|
|
return True
|
|
|
|
# If crash reporting is disabled (MOZ_CRASHREPORTER!=1), we can't say
|
|
# anything.
|
|
if not self.CRASHREPORTER:
|
|
return False
|
|
|
|
try:
|
|
dumpDir = tempfile.mkdtemp()
|
|
remoteCrashDir = self._remoteProfile + '/minidumps/'
|
|
if not self._devicemanager.dirExists(remoteCrashDir):
|
|
# If crash reporting is enabled (MOZ_CRASHREPORTER=1), the
|
|
# minidumps directory is automatically created when Fennec
|
|
# (first) starts, so its lack of presence is a hint that
|
|
# something went wrong.
|
|
print "Automation Error: No crash directory (%s) found on remote device" % remoteCrashDir
|
|
# Whilst no crash was found, the run should still display as a failure
|
|
return True
|
|
self._devicemanager.getDirectory(remoteCrashDir, dumpDir)
|
|
crashed = Automation.checkForCrashes(self, dumpDir, symbolsPath)
|
|
|
|
finally:
|
|
try:
|
|
shutil.rmtree(dumpDir)
|
|
except:
|
|
print "WARNING: unable to remove directory: %s" % dumpDir
|
|
return crashed
|
|
|
|
def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
|
|
# If remote profile is specified, use that instead
|
|
if (self._remoteProfile):
|
|
profileDir = self._remoteProfile
|
|
|
|
# Hack for robocop, if app & testURL == None and extraArgs contains the rest of the stuff, lets
|
|
# assume extraArgs is all we need
|
|
if app == "am" and extraArgs[0] == "instrument":
|
|
return app, extraArgs
|
|
|
|
cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)
|
|
# Remove -foreground if it exists, if it doesn't this just returns
|
|
try:
|
|
args.remove('-foreground')
|
|
except:
|
|
pass
|
|
#TODO: figure out which platform require NO_EM_RESTART
|
|
# return app, ['--environ:NO_EM_RESTART=1'] + args
|
|
return app, args
|
|
|
|
def getLanIp(self):
|
|
nettools = NetworkTools()
|
|
return nettools.getLanIp()
|
|
|
|
def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = None):
|
|
if stdout == None or stdout == -1 or stdout == subprocess.PIPE:
|
|
stdout = self._remoteLog
|
|
|
|
return self.RProcess(self._devicemanager, cmd, stdout, stderr, env, cwd)
|
|
|
|
# be careful here as this inner class doesn't have access to outer class members
|
|
class RProcess(object):
|
|
# device manager process
|
|
dm = None
|
|
def __init__(self, dm, cmd, stdout = None, stderr = None, env = None, cwd = None):
|
|
self.dm = dm
|
|
self.stdoutlen = 0
|
|
self.lastTestSeen = "remoteautomation.py"
|
|
self.proc = dm.launchProcess(cmd, stdout, cwd, env, True)
|
|
if (self.proc is None):
|
|
if cmd[0] == 'am':
|
|
self.proc = stdout
|
|
else:
|
|
raise Exception("unable to launch process")
|
|
exepath = cmd[0]
|
|
name = exepath.split('/')[-1]
|
|
self.procName = name
|
|
# Hack for Robocop: Derive the actual process name from the command line.
|
|
# We expect something like:
|
|
# ['am', 'instrument', '-w', '-e', 'class', 'org.mozilla.fennec.tests.testBookmark', 'org.mozilla.roboexample.test/android.test.InstrumentationTestRunner']
|
|
# and want to derive 'org.mozilla.fennec'.
|
|
if cmd[0] == 'am' and cmd[1] == "instrument":
|
|
try:
|
|
i = cmd.index("class")
|
|
except ValueError:
|
|
# no "class" argument -- maybe this isn't robocop?
|
|
i = -1
|
|
if (i > 0):
|
|
classname = cmd[i+1]
|
|
parts = classname.split('.')
|
|
try:
|
|
i = parts.index("tests")
|
|
except ValueError:
|
|
# no "tests" component -- maybe this isn't robocop?
|
|
i = -1
|
|
if (i > 0):
|
|
self.procName = '.'.join(parts[0:i])
|
|
print "Robocop derived process name: "+self.procName
|
|
|
|
# Setting timeout at 1 hour since on a remote device this takes much longer
|
|
self.timeout = 3600
|
|
# The benefit of the following sleep is unclear; it was formerly 15 seconds
|
|
time.sleep(1)
|
|
|
|
@property
|
|
def pid(self):
|
|
pid = self.dm.processExist(self.procName)
|
|
# HACK: we should probably be more sophisticated about monitoring
|
|
# running processes for the remote case, but for now we'll assume
|
|
# that this method can be called when nothing exists and it is not
|
|
# an error
|
|
if pid is None:
|
|
return 0
|
|
return pid
|
|
|
|
@property
|
|
def stdout(self):
|
|
""" Fetch the full remote log file using devicemanager and return just
|
|
the new log entries since the last call (as a multi-line string).
|
|
"""
|
|
if self.dm.fileExists(self.proc):
|
|
try:
|
|
t = self.dm.pullFile(self.proc)
|
|
except DMError:
|
|
# we currently don't retry properly in the pullFile
|
|
# function in dmSUT, so an error here is not necessarily
|
|
# the end of the world
|
|
return ''
|
|
newLogContent = t[self.stdoutlen:]
|
|
self.stdoutlen = len(t)
|
|
# Match the test filepath from the last TEST-START line found in the new
|
|
# log content. These lines are in the form:
|
|
# 1234 INFO TEST-START | /filepath/we/wish/to/capture.html\n
|
|
testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", newLogContent)
|
|
if testStartFilenames:
|
|
self.lastTestSeen = testStartFilenames[-1]
|
|
return newLogContent.strip('\n').strip()
|
|
else:
|
|
return ''
|
|
|
|
@property
|
|
def getLastTestSeen(self):
|
|
return self.lastTestSeen
|
|
|
|
def wait(self, timeout = None):
|
|
timer = 0
|
|
interval = 5
|
|
|
|
if timeout == None:
|
|
timeout = self.timeout
|
|
|
|
while (self.dm.getTopActivity() == self.procName):
|
|
t = self.stdout
|
|
if t != '': print t
|
|
time.sleep(interval)
|
|
timer += interval
|
|
if (timer > timeout):
|
|
break
|
|
|
|
# Flush anything added to stdout during the sleep
|
|
print self.stdout
|
|
|
|
if (timer >= timeout):
|
|
return 1
|
|
return 0
|
|
|
|
def kill(self):
|
|
self.dm.killProcess(self.procName)
|