summaryrefslogtreecommitdiff
path: root/build/mobile/remoteautomation.py
diff options
context:
space:
mode:
Diffstat (limited to 'build/mobile/remoteautomation.py')
-rw-r--r--build/mobile/remoteautomation.py401
1 files changed, 0 insertions, 401 deletions
diff --git a/build/mobile/remoteautomation.py b/build/mobile/remoteautomation.py
deleted file mode 100644
index f098d5bbaf..0000000000
--- a/build/mobile/remoteautomation.py
+++ /dev/null
@@ -1,401 +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 datetime
-import glob
-import time
-import re
-import os
-import posixpath
-import tempfile
-import shutil
-import subprocess
-import sys
-
-from automation import Automation
-from mozdevice import DMError, DeviceManager
-from mozlog import get_default_logger
-import mozcrash
-
-# 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.",
- "Unexpected value from nativeGetEnabledTags: 0" ]
-
-class RemoteAutomation(Automation):
- _devicemanager = None
-
- # Part of a hack for Robocop: "am COMMAND" is handled specially if COMMAND
- # is in this set. See usages below.
- _specialAmCommands = ('instrument', 'start')
-
- def __init__(self, deviceManager, appName = '', remoteLog = None,
- processArgs=None):
- self._devicemanager = deviceManager
- self._appName = appName
- self._remoteProfile = None
- self._remoteLog = remoteLog
- self._processArgs = processArgs or {};
-
- # 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, debugger=False, dmdPath=None, lsanPath=None):
- # 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 = {}
-
- if dmdPath:
- env['MOZ_REPLACE_MALLOC_LIB'] = os.path.join(dmdPath, 'libdmd.so')
-
- # Except for the mochitest results table hiding option, which isn't
- # passed to runtestsremote.py as an actual option, but through the
- # MOZ_HIDE_RESULTS_TABLE environment variable.
- if 'MOZ_HIDE_RESULTS_TABLE' in os.environ:
- env['MOZ_HIDE_RESULTS_TABLE'] = os.environ['MOZ_HIDE_RESULTS_TABLE']
-
- env['MOZ_CRASHREPORTER_DISABLE'] = '1'
-
- # Crash on non-local network connections by default.
- # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
- # enable non-local connections for the purposes of local testing.
- # Don't override the user's choice here. See bug 1049688.
- env.setdefault('MOZ_DISABLE_NONLOCAL_CONNECTIONS', '1')
-
- # Send an env var noting that we are in automation. Passing any
- # value except the empty string will declare the value to exist.
- #
- # This may be used to disabled network connections during testing, e.g.
- # Switchboard & telemetry uploads.
- env.setdefault('MOZ_IN_AUTOMATION', '1')
-
- # Set WebRTC logging in case it is not set yet.
- # On Android, environment variables cannot contain ',' so the
- # standard WebRTC setting for NSPR_LOG_MODULES is not available.
- # env.setdefault('NSPR_LOG_MODULES', 'signaling:5,mtransport:5,datachannel:5,jsep:5,MediaPipelineFactory:5')
- env.setdefault('R_LOG_LEVEL', '6')
- env.setdefault('R_LOG_DESTINATION', 'stderr')
- env.setdefault('R_LOG_VERBOSE', '1')
-
- return env
-
- def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, outputHandler=None):
- """ Wait for tests to finish.
- If maxTime seconds elapse or no output is detected for timeout
- seconds, kill the process and fail the test.
- """
- # maxTime is used to override the default timeout, we should honor that
- status = proc.wait(timeout = maxTime, noOutputTimeout = timeout)
- self.lastTestSeen = proc.getLastTestSeen
-
- topActivity = self._devicemanager.getTopActivity()
- if topActivity == proc.procName:
- proc.kill(True)
- if status == 1:
- 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)
- if status == 2:
- print "TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output" \
- % (self.lastTestSeen, int(timeout))
-
- return status
-
- def deleteANRs(self):
- # empty ANR traces.txt file; usually need root permissions
- # we make it empty and writable so we can test the ANR reporter later
- traces = "/data/anr/traces.txt"
- try:
- self._devicemanager.shellCheckOutput(['echo', '', '>', traces], root=True,
- timeout=DeviceManager.short_timeout)
- self._devicemanager.shellCheckOutput(['chmod', '666', traces], root=True,
- timeout=DeviceManager.short_timeout)
- 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)
- if t:
- stripped = t.strip()
- if len(stripped) > 0:
- print "Contents of %s:" % traces
- print t
- # Once reported, delete traces
- self.deleteANRs()
- except DMError:
- print "Error pulling %s" % traces
- except IOError:
- print "Error pulling %s" % traces
- else:
- print "%s not found" % traces
-
- def deleteTombstones(self):
- # delete any existing tombstone files from device
- remoteDir = "/data/tombstones"
- try:
- self._devicemanager.shellCheckOutput(['rm', '-r', remoteDir], root=True,
- timeout=DeviceManager.short_timeout)
- except DMError:
- # This may just indicate that the tombstone directory is missing
- pass
-
- def checkForTombstones(self):
- # pull any tombstones from device and move to MOZ_UPLOAD_DIR
- remoteDir = "/data/tombstones"
- blobberUploadDir = os.environ.get('MOZ_UPLOAD_DIR', None)
- if blobberUploadDir:
- if not os.path.exists(blobberUploadDir):
- os.mkdir(blobberUploadDir)
- if self._devicemanager.dirExists(remoteDir):
- # copy tombstone files from device to local blobber upload directory
- try:
- self._devicemanager.shellCheckOutput(['chmod', '777', remoteDir], root=True,
- timeout=DeviceManager.short_timeout)
- self._devicemanager.shellCheckOutput(['chmod', '666', os.path.join(remoteDir, '*')], root=True,
- timeout=DeviceManager.short_timeout)
- self._devicemanager.getDirectory(remoteDir, blobberUploadDir, False)
- except DMError:
- # This may just indicate that no tombstone files are present
- pass
- self.deleteTombstones()
- # add a .txt file extension to each tombstone file name, so
- # that blobber will upload it
- for f in glob.glob(os.path.join(blobberUploadDir, "tombstone_??")):
- # add a unique integer to the file name, in case there are
- # multiple tombstones generated with the same name, for
- # instance, after multiple robocop tests
- for i in xrange(1, sys.maxint):
- newname = "%s.%d.txt" % (f, i)
- if not os.path.exists(newname):
- os.rename(f, newname)
- break
- else:
- print "%s does not exist; tombstone check skipped" % remoteDir
- else:
- print "MOZ_UPLOAD_DIR not defined; tombstone check skipped"
-
- def checkForCrashes(self, directory, symbolsPath):
- self.checkForANRs()
- self.checkForTombstones()
-
- logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters)
-
- javaException = mozcrash.check_for_java_exception(logcat, test_name=self.lastTestSeen)
- if javaException:
- return True
-
- # No crash reporting means we can't say anything.
- return False
-
-
- 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] in RemoteAutomation._specialAmCommands:
- 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 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, self._appName,
- **self._processArgs)
-
- # 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, app=None,
- messageLogger=None):
- self.dm = dm
- self.stdoutlen = 0
- self.lastTestSeen = "remoteautomation.py"
- self.proc = dm.launchProcess(cmd, stdout, cwd, env, True)
- self.messageLogger = messageLogger
-
- if (self.proc is None):
- if cmd[0] == 'am':
- self.proc = stdout
- else:
- raise Exception("unable to launch process")
- self.procName = cmd[0].split('/')[-1]
- if cmd[0] == 'am' and cmd[1] in RemoteAutomation._specialAmCommands:
- self.procName = app
-
- # Setting timeout at 1 hour since on a remote device this takes much longer.
- # Temporarily increased to 90 minutes because no more chunks can be created.
- self.timeout = 5400
- # The benefit of the following sleep is unclear; it was formerly 15 seconds
- time.sleep(1)
-
- # Used to buffer log messages until we meet a line break
- self.logBuffer = ""
-
- @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
-
- def read_stdout(self):
- """
- Fetch the full remote log file using devicemanager, process them and
- return whether there were any new log entries since the last call.
- """
- if not self.dm.fileExists(self.proc):
- return False
- try:
- newLogContent = self.dm.pullFile(self.proc, self.stdoutlen)
- 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 False
- if not newLogContent:
- return False
-
- self.stdoutlen += len(newLogContent)
-
- if self.messageLogger is None:
- testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", newLogContent)
- if testStartFilenames:
- self.lastTestSeen = testStartFilenames[-1]
- print newLogContent
- return True
-
- self.logBuffer += newLogContent
- lines = self.logBuffer.split('\n')
- lines = [l for l in lines if l]
-
- if lines:
- if self.logBuffer.endswith('\n'):
- # all lines are complete; no need to buffer
- self.logBuffer = ""
- else:
- # keep the last (unfinished) line in the buffer
- self.logBuffer = lines[-1]
- del lines[-1]
-
- if not lines:
- return False
-
- for line in lines:
- # This passes the line to the logger (to be logged or buffered)
- parsed_messages = self.messageLogger.write(line)
- for message in parsed_messages:
- if isinstance(message, dict) and message.get('action') == 'test_start':
- self.lastTestSeen = message['test']
- return True
-
- @property
- def getLastTestSeen(self):
- return self.lastTestSeen
-
- # Wait for the remote process to end (or for its activity to go to background).
- # While waiting, periodically retrieve the process output and print it.
- # If the process is still running after *timeout* seconds, return 1;
- # If the process is still running but no output is received in *noOutputTimeout*
- # seconds, return 2;
- # Else, once the process exits/goes to background, return 0.
- def wait(self, timeout = None, noOutputTimeout = None):
- timer = 0
- noOutputTimer = 0
- interval = 10
- if timeout == None:
- timeout = self.timeout
- status = 0
- top = self.procName
- slowLog = False
- while (top == self.procName):
- # Get log updates on each interval, but if it is taking
- # too long, only do it every 60 seconds
- if (not slowLog) or (timer % 60 == 0):
- startRead = datetime.datetime.utcnow()
- hasOutput = self.read_stdout()
- if (datetime.datetime.utcnow() - startRead) > datetime.timedelta(seconds=5):
- slowLog = True
- if hasOutput:
- noOutputTimer = 0
- time.sleep(interval)
- timer += interval
- noOutputTimer += interval
- if (timer > timeout):
- status = 1
- break
- if (noOutputTimeout and noOutputTimer > noOutputTimeout):
- status = 2
- break
- top = self.dm.getTopActivity()
- # Flush anything added to stdout during the sleep
- self.read_stdout()
- return status
-
- def kill(self, stagedShutdown = False):
- if stagedShutdown:
- # Trigger an ANR report with "kill -3" (SIGQUIT)
- self.dm.killProcess(self.procName, 3)
- time.sleep(3)
- # Trigger a breakpad dump with "kill -6" (SIGABRT)
- self.dm.killProcess(self.procName, 6)
- # Wait for process to end
- retries = 0
- while retries < 3:
- pid = self.dm.processExist(self.procName)
- if pid and pid > 0:
- print "%s still alive after SIGABRT: waiting..." % self.procName
- time.sleep(5)
- else:
- return
- retries += 1
- self.dm.killProcess(self.procName, 9)
- pid = self.dm.processExist(self.procName)
- if pid and pid > 0:
- self.dm.killProcess(self.procName)
- else:
- self.dm.killProcess(self.procName)