michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: import time michael@0: import re michael@0: import os michael@0: import tempfile michael@0: import shutil michael@0: import subprocess michael@0: michael@0: from automation import Automation michael@0: from devicemanager import DMError michael@0: import mozcrash michael@0: michael@0: # signatures for logcat messages that we don't care about much michael@0: fennecLogcatFilters = [ "The character encoding of the HTML document was not declared", michael@0: "Use of Mutation Events is deprecated. Use MutationObserver instead.", michael@0: "Unexpected value from nativeGetEnabledTags: 0" ] michael@0: michael@0: class RemoteAutomation(Automation): michael@0: _devicemanager = None michael@0: michael@0: def __init__(self, deviceManager, appName = '', remoteLog = None): michael@0: self._devicemanager = deviceManager michael@0: self._appName = appName michael@0: self._remoteProfile = None michael@0: self._remoteLog = remoteLog michael@0: michael@0: # Default our product to fennec michael@0: self._product = "fennec" michael@0: self.lastTestSeen = "remoteautomation.py" michael@0: Automation.__init__(self) michael@0: michael@0: def setDeviceManager(self, deviceManager): michael@0: self._devicemanager = deviceManager michael@0: michael@0: def setAppName(self, appName): michael@0: self._appName = appName michael@0: michael@0: def setRemoteProfile(self, remoteProfile): michael@0: self._remoteProfile = remoteProfile michael@0: michael@0: def setProduct(self, product): michael@0: self._product = product michael@0: michael@0: def setRemoteLog(self, logfile): michael@0: self._remoteLog = logfile michael@0: michael@0: # Set up what we need for the remote environment michael@0: def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None): michael@0: # Because we are running remote, we don't want to mimic the local env michael@0: # so no copying of os.environ michael@0: if env is None: michael@0: env = {} michael@0: michael@0: if dmdPath: michael@0: env['DMD'] = '1' michael@0: env['MOZ_REPLACE_MALLOC_LIB'] = os.path.join(dmdPath, 'libdmd.so') michael@0: michael@0: # Except for the mochitest results table hiding option, which isn't michael@0: # passed to runtestsremote.py as an actual option, but through the michael@0: # MOZ_HIDE_RESULTS_TABLE environment variable. michael@0: if 'MOZ_HIDE_RESULTS_TABLE' in os.environ: michael@0: env['MOZ_HIDE_RESULTS_TABLE'] = os.environ['MOZ_HIDE_RESULTS_TABLE'] michael@0: michael@0: if crashreporter and not debugger: michael@0: env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' michael@0: env['MOZ_CRASHREPORTER'] = '1' michael@0: else: michael@0: env['MOZ_CRASHREPORTER_DISABLE'] = '1' michael@0: michael@0: # Crash on non-local network connections. michael@0: env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1' michael@0: michael@0: return env michael@0: michael@0: def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath): michael@0: """ Wait for tests to finish. michael@0: If maxTime seconds elapse or no output is detected for timeout michael@0: seconds, kill the process and fail the test. michael@0: """ michael@0: # maxTime is used to override the default timeout, we should honor that michael@0: status = proc.wait(timeout = maxTime, noOutputTimeout = timeout) michael@0: self.lastTestSeen = proc.getLastTestSeen michael@0: michael@0: topActivity = self._devicemanager.getTopActivity() michael@0: if topActivity == proc.procName: michael@0: proc.kill() michael@0: if status == 1: michael@0: if maxTime: michael@0: print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \ michael@0: "allowed maximum time of %s seconds" % (self.lastTestSeen, maxTime) michael@0: else: michael@0: print "TEST-UNEXPECTED-FAIL | %s | application ran for longer than " \ michael@0: "allowed maximum time" % (self.lastTestSeen) michael@0: if status == 2: michael@0: print "TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output" \ michael@0: % (self.lastTestSeen, int(timeout)) michael@0: michael@0: return status michael@0: michael@0: def deleteANRs(self): michael@0: # empty ANR traces.txt file; usually need root permissions michael@0: # we make it empty and writable so we can test the ANR reporter later michael@0: traces = "/data/anr/traces.txt" michael@0: try: michael@0: self._devicemanager.shellCheckOutput(['echo', '', '>', traces], root=True) michael@0: self._devicemanager.shellCheckOutput(['chmod', '666', traces], root=True) michael@0: except DMError: michael@0: print "Error deleting %s" % traces michael@0: pass michael@0: michael@0: def checkForANRs(self): michael@0: traces = "/data/anr/traces.txt" michael@0: if self._devicemanager.fileExists(traces): michael@0: try: michael@0: t = self._devicemanager.pullFile(traces) michael@0: print "Contents of %s:" % traces michael@0: print t michael@0: # Once reported, delete traces michael@0: self.deleteANRs() michael@0: except DMError: michael@0: print "Error pulling %s" % traces michael@0: pass michael@0: else: michael@0: print "%s not found" % traces michael@0: michael@0: def checkForCrashes(self, directory, symbolsPath): michael@0: self.checkForANRs() michael@0: michael@0: logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters) michael@0: javaException = mozcrash.check_for_java_exception(logcat) michael@0: if javaException: michael@0: return True michael@0: michael@0: # If crash reporting is disabled (MOZ_CRASHREPORTER!=1), we can't say michael@0: # anything. michael@0: if not self.CRASHREPORTER: michael@0: return False michael@0: michael@0: try: michael@0: dumpDir = tempfile.mkdtemp() michael@0: remoteCrashDir = self._remoteProfile + '/minidumps/' michael@0: if not self._devicemanager.dirExists(remoteCrashDir): michael@0: # If crash reporting is enabled (MOZ_CRASHREPORTER=1), the michael@0: # minidumps directory is automatically created when Fennec michael@0: # (first) starts, so its lack of presence is a hint that michael@0: # something went wrong. michael@0: print "Automation Error: No crash directory (%s) found on remote device" % remoteCrashDir michael@0: # Whilst no crash was found, the run should still display as a failure michael@0: return True michael@0: self._devicemanager.getDirectory(remoteCrashDir, dumpDir) michael@0: crashed = Automation.checkForCrashes(self, dumpDir, symbolsPath) michael@0: michael@0: finally: michael@0: try: michael@0: shutil.rmtree(dumpDir) michael@0: except: michael@0: print "WARNING: unable to remove directory: %s" % dumpDir michael@0: return crashed michael@0: michael@0: def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): michael@0: # If remote profile is specified, use that instead michael@0: if (self._remoteProfile): michael@0: profileDir = self._remoteProfile michael@0: michael@0: # Hack for robocop, if app & testURL == None and extraArgs contains the rest of the stuff, lets michael@0: # assume extraArgs is all we need michael@0: if app == "am" and extraArgs[0] == "instrument": michael@0: return app, extraArgs michael@0: michael@0: cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs) michael@0: # Remove -foreground if it exists, if it doesn't this just returns michael@0: try: michael@0: args.remove('-foreground') michael@0: except: michael@0: pass michael@0: #TODO: figure out which platform require NO_EM_RESTART michael@0: # return app, ['--environ:NO_EM_RESTART=1'] + args michael@0: return app, args michael@0: michael@0: def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = None): michael@0: if stdout == None or stdout == -1 or stdout == subprocess.PIPE: michael@0: stdout = self._remoteLog michael@0: michael@0: return self.RProcess(self._devicemanager, cmd, stdout, stderr, env, cwd, self._appName) michael@0: michael@0: # be careful here as this inner class doesn't have access to outer class members michael@0: class RProcess(object): michael@0: # device manager process michael@0: dm = None michael@0: def __init__(self, dm, cmd, stdout = None, stderr = None, env = None, cwd = None, app = None): michael@0: self.dm = dm michael@0: self.stdoutlen = 0 michael@0: self.lastTestSeen = "remoteautomation.py" michael@0: self.proc = dm.launchProcess(cmd, stdout, cwd, env, True) michael@0: if (self.proc is None): michael@0: if cmd[0] == 'am': michael@0: self.proc = stdout michael@0: else: michael@0: raise Exception("unable to launch process") michael@0: self.procName = cmd[0].split('/')[-1] michael@0: if cmd[0] == 'am' and cmd[1] == "instrument": michael@0: self.procName = app michael@0: print "Robocop process name: "+self.procName michael@0: michael@0: # Setting timeout at 1 hour since on a remote device this takes much longer michael@0: self.timeout = 3600 michael@0: # The benefit of the following sleep is unclear; it was formerly 15 seconds michael@0: time.sleep(1) michael@0: michael@0: @property michael@0: def pid(self): michael@0: pid = self.dm.processExist(self.procName) michael@0: # HACK: we should probably be more sophisticated about monitoring michael@0: # running processes for the remote case, but for now we'll assume michael@0: # that this method can be called when nothing exists and it is not michael@0: # an error michael@0: if pid is None: michael@0: return 0 michael@0: return pid michael@0: michael@0: @property michael@0: def stdout(self): michael@0: """ Fetch the full remote log file using devicemanager and return just michael@0: the new log entries since the last call (as a multi-line string). michael@0: """ michael@0: if self.dm.fileExists(self.proc): michael@0: try: michael@0: newLogContent = self.dm.pullFile(self.proc, self.stdoutlen) michael@0: except DMError: michael@0: # we currently don't retry properly in the pullFile michael@0: # function in dmSUT, so an error here is not necessarily michael@0: # the end of the world michael@0: return '' michael@0: self.stdoutlen += len(newLogContent) michael@0: # Match the test filepath from the last TEST-START line found in the new michael@0: # log content. These lines are in the form: michael@0: # 1234 INFO TEST-START | /filepath/we/wish/to/capture.html\n michael@0: testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", newLogContent) michael@0: if testStartFilenames: michael@0: self.lastTestSeen = testStartFilenames[-1] michael@0: return newLogContent.strip('\n').strip() michael@0: else: michael@0: return '' michael@0: michael@0: @property michael@0: def getLastTestSeen(self): michael@0: return self.lastTestSeen michael@0: michael@0: # Wait for the remote process to end (or for its activity to go to background). michael@0: # While waiting, periodically retrieve the process output and print it. michael@0: # If the process is still running after *timeout* seconds, return 1; michael@0: # If the process is still running but no output is received in *noOutputTimeout* michael@0: # seconds, return 2; michael@0: # Else, once the process exits/goes to background, return 0. michael@0: def wait(self, timeout = None, noOutputTimeout = None): michael@0: timer = 0 michael@0: noOutputTimer = 0 michael@0: interval = 20 michael@0: michael@0: if timeout == None: michael@0: timeout = self.timeout michael@0: michael@0: status = 0 michael@0: while (self.dm.getTopActivity() == self.procName): michael@0: # retrieve log updates every 60 seconds michael@0: if timer % 60 == 0: michael@0: t = self.stdout michael@0: if t != '': michael@0: print t michael@0: noOutputTimer = 0 michael@0: michael@0: time.sleep(interval) michael@0: timer += interval michael@0: noOutputTimer += interval michael@0: if (timer > timeout): michael@0: status = 1 michael@0: break michael@0: if (noOutputTimeout and noOutputTimer > noOutputTimeout): michael@0: status = 2 michael@0: break michael@0: michael@0: # Flush anything added to stdout during the sleep michael@0: print self.stdout michael@0: michael@0: return status michael@0: michael@0: def kill(self): michael@0: self.dm.killProcess(self.procName)