# HG changeset patch # User Geoff Brown # Date 1521849987 21600 # Node ID aad77f9bd0931feb72b3cc04a2030e4cc97573f1 # Parent e0b5db6d0f07d22abff0171d5538fab56a07934a Bug 1440714 - Convert Android browser test harnesses to adb.py; r=bc This affects Android robocop, mochitest (all flavors) and reftests (all flavors). diff --git a/build/mobile/remoteautomation.py b/build/mobile/remoteautomation.py --- a/build/mobile/remoteautomation.py +++ b/build/mobile/remoteautomation.py @@ -8,65 +8,45 @@ import time import re import os import posixpath import tempfile import shutil import sys from automation import Automation -from mozdevice import DMError, DeviceManager from mozlog import get_default_logger from mozscreenshot import dump_screen 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 - def __init__(self, deviceManager, appName = '', remoteLog = None, + def __init__(self, device, appName = '', remoteProfile = None, remoteLog = None, processArgs=None): - self._dm = deviceManager + self._device = device self._appName = appName - self._remoteProfile = None + self._remoteProfile = remoteProfile self._remoteLog = remoteLog self._processArgs = processArgs or {}; self.lastTestSeen = "remoteautomation.py" Automation.__init__(self) - def setDeviceManager(self, deviceManager): - self._dm = deviceManager - - def setAppName(self, appName): - self._appName = appName - - def setRemoteProfile(self, remoteProfile): - self._remoteProfile = remoteProfile - - 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, lsanPath=None, ubsanPath=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 = {} - # 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'] - if crashreporter and not debugger: env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' env['MOZ_CRASHREPORTER'] = '1' else: env['MOZ_CRASHREPORTER_DISABLE'] = '1' # Crash on non-local network connections by default. # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily @@ -96,17 +76,17 @@ class RemoteAutomation(Automation): If maxTime seconds elapse or no output is detected for timeout seconds, kill the process and fail the test. """ proc.utilityPath = utilityPath # 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._dm.getTopActivity() + topActivity = self._device.get_top_activity(timeout=60) 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 " \ @@ -117,71 +97,53 @@ class RemoteAutomation(Automation): 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._dm.shellCheckOutput(['echo', '', '>', traces], root=True, - timeout=DeviceManager.short_timeout) - self._dm.shellCheckOutput(['chmod', '666', traces], root=True, - timeout=DeviceManager.short_timeout) - except DMError: - print "Error deleting %s" % traces - pass + self._device.shell_output('echo > %s' % traces, root=True) + self._device.shell_output('chmod 666 %s' % traces, root=True) + except Exception as e: + print "Error deleting %s: %s" % (traces, str(e)) def checkForANRs(self): traces = "/data/anr/traces.txt" - if self._dm.fileExists(traces): + if self._device.is_file(traces): try: - t = self._dm.pullFile(traces) + t = self._device.get_file(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 + except Exception as e: + print "Error pulling %s: %s" % (traces, str(e)) else: print "%s not found" % traces def deleteTombstones(self): # delete any tombstone files from device - remoteDir = "/data/tombstones" - try: - self._dm.shellCheckOutput(['rm', '-r', tombstones], root=True, - timeout=DeviceManager.short_timeout) - except DMError: - # This may just indicate that the tombstone directory is missing - pass + self._device.rm("/data/tombstones", force=True, recursive=True, root=True) def checkForTombstones(self): # pull any tombstones from device and move to MOZ_UPLOAD_DIR remoteDir = "/data/tombstones" uploadDir = os.environ.get('MOZ_UPLOAD_DIR', None) if uploadDir: if not os.path.exists(uploadDir): os.mkdir(uploadDir) - if self._dm.dirExists(remoteDir): + if self._device.is_dir(remoteDir): # copy tombstone files from device to local upload directory - try: - self._dm.shellCheckOutput(['chmod', '777', remoteDir], root=True, - timeout=DeviceManager.short_timeout) - self._dm.shellCheckOutput(['chmod', '666', os.path.join(remoteDir, '*')], - root=True, timeout=DeviceManager.short_timeout) - self._dm.getDirectory(remoteDir, uploadDir, False) - except DMError: - # This may just indicate that no tombstone files are present - pass + self._device.chmod(remoteDir, recursive=True, root=True) + self._device.pull(remoteDir, uploadDir) self.deleteTombstones() for f in glob.glob(os.path.join(uploadDir, "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): @@ -191,121 +153,135 @@ class RemoteAutomation(Automation): 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._dm.getLogcat(filterOutRegexps=fennecLogcatFilters) + logcat = self._device.get_logcat(filter_out_regexps=fennecLogcatFilters) javaException = mozcrash.check_for_java_exception(logcat, test_name=self.lastTestSeen) 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 = posixpath.join(self._remoteProfile, 'minidumps') - if not self._dm.dirExists(remoteCrashDir): + if not self._device.is_dir(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 return True - self._dm.getDirectory(remoteCrashDir, dumpDir) + self._device.pull(remoteCrashDir, dumpDir) logger = get_default_logger() crashed = mozcrash.log_crashes(logger, dumpDir, symbolsPath, test=self.lastTestSeen) finally: try: shutil.rmtree(dumpDir) - except: - print "WARNING: unable to remove directory: %s" % dumpDir + except Exception as e: + print "WARNING: unable to remove directory %s: %s" % (dumpDir, str(e)) 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 + # Hack for robocop, if app is "am" and extraArgs contains the rest of the stuff, lets # assume extraArgs is all we need if app == "am" and extraArgs[0] in ('instrument', 'start'): return app, extraArgs cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs) try: args.remove('-foreground') except: pass return app, args def Process(self, cmd, stdout = None, stderr = None, env = None, cwd = None): - return self.RProcess(self._dm, cmd, self._remoteLog, env, cwd, self._appName, + return self.RProcess(self._device, cmd, self._remoteLog, env, cwd, self._appName, **self._processArgs) class RProcess(object): - dm = None - def __init__(self, dm, cmd, stdout=None, env=None, cwd=None, app=None, + def __init__(self, device, cmd, stdout=None, env=None, cwd=None, app=None, messageLogger=None, counts=None): - self.dm = dm - self.stdoutlen = 0 + self.device = device self.lastTestSeen = "remoteautomation.py" - self.proc = dm.launchProcess(cmd, stdout, cwd, env, True) self.messageLogger = messageLogger + self.proc = stdout + self.procName = cmd[0].split(posixpath.sep)[-1] + self.stdoutlen = 0 self.utilityPath = None self.counts = counts if self.counts is not None: self.counts['pass'] = 0 self.counts['fail'] = 0 self.counts['todo'] = 0 - if self.proc is None: - self.proc = stdout - self.procName = cmd[0].split(posixpath.sep)[-1] - if cmd[0] == 'am' and cmd[1] in ('instrument', 'start'): + if cmd[0] == 'am': + cmd = ' '.join(cmd) self.procName = app + if not self.device.shell_bool(cmd): + print "remote_automation.py failed to launch %s" % cmd + else: + args = cmd + if args[0] == app: + args = args[1:] + url = args[-1:][0] + if url.startswith('/'): + # this is probably a reftest profile directory, not a url + url = None + else: + args = args[:-1] + if 'geckoview' in app: + self.device.launch_geckoview_example(app, moz_env=env, extra_args=args, url=url) + else: + self.device.launch_fennec(app, moz_env=env, extra_args=args, url=url) # 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 # 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: + procs = self.device.get_process_list() + # limit the comparison to the first 75 characters due to a + # limitation in processname length in android. + pids = [proc[0] for proc in procs if proc[1] == self.procName[:75]] + + if pids is None or len(pids) < 1: return 0 - return pid + return pids[0] 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. + Fetch the full remote log file, log any new content and return True if new + content processed. """ - if not self.dm.fileExists(self.proc): + if not self.device.is_file(self.proc): return False try: - newLogContent = self.dm.pullFile(self.proc, self.stdoutlen) - except DMError: + newLogContent = self.device.get_file(self.proc, offset=self.stdoutlen) + except Exception: return False if not newLogContent: return False self.stdoutlen += len(newLogContent) if self.messageLogger is None: testStartFilenames = re.findall(r"TEST-START \| ([^\s]*)", newLogContent) @@ -368,17 +344,17 @@ class RemoteAutomation(Automation): timer = 0 noOutputTimer = 0 interval = 10 if timeout == None: timeout = self.timeout status = 0 top = self.procName slowLog = False - while (top == self.procName): + while top == self.procName: # Get log updates on each interval, but if it is taking # too long, only do it every 60 seconds hasOutput = False if (not slowLog) or (timer % 60 == 0): startRead = datetime.datetime.now() hasOutput = self.read_stdout() if (datetime.datetime.now() - startRead) > datetime.timedelta(seconds=5): slowLog = True @@ -391,43 +367,50 @@ class RemoteAutomation(Automation): noOutputTimer += interval if (timer > timeout): status = 1 break if (noOutputTimeout and noOutputTimer > noOutputTimeout): status = 2 break if not hasOutput: - top = self.dm.getTopActivity() + top = self.device.get_top_activity(timeout=60) # Flush anything added to stdout during the sleep self.read_stdout() return status def kill(self, stagedShutdown = False): if self.utilityPath: # Take a screenshot to capture the screen state just before # the application is killed. There are on-device screenshot # options but they rarely work well with Firefox on the # Android emulator. dump_screen provides an effective # screenshot of the emulator and its host desktop. dump_screen(self.utilityPath, get_default_logger()) if stagedShutdown: # Trigger an ANR report with "kill -3" (SIGQUIT) - self.dm.killProcess(self.procName, 3) + try: + self.device.pkill(self.procName, sig=3, attempts=1) + except: + pass time.sleep(3) # Trigger a breakpad dump with "kill -6" (SIGABRT) - self.dm.killProcess(self.procName, 6) + try: + self.device.pkill(self.procName, sig=6, attempts=1) + except: + pass # Wait for process to end retries = 0 while retries < 3: - pid = self.dm.processExist(self.procName) - if pid and pid > 0: + if self.device.process_exist(self.procName): 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) + try: + self.device.pkill(self.procName, sig=9, attempts=1) + except: + print "%s still alive after SIGKILL!" % self.procName + if self.device.process_exist(self.procName): + self.device.pkill(self.procName) else: - self.dm.killProcess(self.procName) + self.device.pkill(self.procName) diff --git a/layout/tools/reftest/reftestcommandline.py b/layout/tools/reftest/reftestcommandline.py --- a/layout/tools/reftest/reftestcommandline.py +++ b/layout/tools/reftest/reftestcommandline.py @@ -401,50 +401,29 @@ class RemoteArgumentsParser(ReftestArgum # app, xrePath and utilityPath variables are set in main function self.set_defaults(logFile="reftest.log", app="", xrePath="", utilityPath="", localLogName=None) - self.add_argument("--remote-app-path", - action="store", - type=str, - dest="remoteAppPath", - help="Path to remote executable relative to device root using only " - "forward slashes. Either this or app must be specified, " - "but not both.") - self.add_argument("--adbpath", action="store", type=str, dest="adb_path", default="adb", help="path to adb") - self.add_argument("--deviceIP", - action="store", - type=str, - dest="deviceIP", - help="ip address of remote device to test") - self.add_argument("--deviceSerial", action="store", type=str, dest="deviceSerial", help="adb serial number of remote device to test") - self.add_argument("--devicePort", - action="store", - type=str, - default="20701", - dest="devicePort", - help="port of remote device to test") - self.add_argument("--remote-webserver", action="store", type=str, dest="remoteWebServer", help="IP Address of the webserver hosting the reftest content") self.add_argument("--http-port", action="store", @@ -453,31 +432,16 @@ class RemoteArgumentsParser(ReftestArgum help="port of the web server for http traffic") self.add_argument("--ssl-port", action="store", type=str, dest="sslPort", help="Port for https traffic to the web server") - self.add_argument("--remote-logfile", - action="store", - type=str, - dest="remoteLogFile", - default="reftest.log", - help="Name of log file on the device relative to device root. " - "PLEASE USE ONLY A FILENAME.") - - self.add_argument("--pidfile", - action="store", - type=str, - dest="pidFile", - default="", - help="name of the pidfile to generate") - self.add_argument("--remoteTestRoot", action="store", type=str, dest="remoteTestRoot", help="remote directory to use as test root " "(eg. /mnt/sdcard/tests or /data/local/tests)") self.add_argument("--httpd-path", @@ -488,88 +452,40 @@ class RemoteArgumentsParser(ReftestArgum self.add_argument("--no-device-info", action="store_false", dest="printDeviceInfo", default=True, help="do not display verbose diagnostics about the remote device") def validate_remote(self, options, automation): - # Ensure our defaults are set properly for everything we can infer - if not options.remoteTestRoot: - options.remoteTestRoot = automation._dm.deviceRoot + \ - '/reftest' - options.remoteProfile = options.remoteTestRoot + "/profile" - if options.remoteWebServer is None: options.remoteWebServer = self.get_ip() - # Verify that our remotewebserver is set properly if options.remoteWebServer == '127.0.0.1': self.error("ERROR: Either you specified the loopback for the remote webserver or ", "your local IP cannot be detected. " "Please provide the local ip in --remote-webserver") if not options.httpPort: options.httpPort = automation.DEFAULT_HTTP_PORT if not options.sslPort: options.sslPort = automation.DEFAULT_SSL_PORT - # One of remoteAppPath (relative path to application) or the app (executable) must be - # set, but not both. If both are set, we destroy the user's selection for app - # so instead of silently destroying a user specificied setting, we - # error. - if options.remoteAppPath and options.app: - self.error( - "ERROR: You cannot specify both the remoteAppPath and the app") - elif options.remoteAppPath: - options.app = options.remoteTestRoot + "/" + options.remoteAppPath - elif options.app is None: - # Neither remoteAppPath nor app are set -- error - self.error("ERROR: You must specify either appPath or app") - if options.xrePath is None: self.error( "ERROR: You must specify the path to the controller xre directory") else: # Ensure xrepath is a full path options.xrePath = os.path.abspath(options.xrePath) - options.localLogName = options.remoteLogFile - options.remoteLogFile = options.remoteTestRoot + \ - '/' + options.remoteLogFile - - # Ensure that the options.logfile (which the base class uses) is set to - # the remote setting when running remote. Also, if the user set the - # log file name there, use that instead of reusing the remotelogfile as - # above. - if options.logFile: - # If the user specified a local logfile name use that - options.localLogName = options.logFile - - options.logFile = options.remoteLogFile - - if options.pidFile != "": - with open(options.pidFile, 'w') as f: - f.write(str(os.getpid())) - # httpd-path is specified by standard makefile targets and may be specified # on the command line to select a particular version of httpd.js. If not # specified, try to select the one from hostutils.zip, as required in # bug 882932. if not options.httpdPath: options.httpdPath = os.path.join(options.utilityPath, "components") - if not options.ignoreWindowSize: - parts = automation._dm.getInfo( - 'screen')['screen'][0].split() - width = int(parts[0].split(':')[1]) - height = int(parts[1].split(':')[1]) - if (width < 1366 or height < 1050): - self.error("ERROR: Invalid screen resolution %sx%s, " - "please adjust to 1366x1050 or higher" % ( - width, height)) - # Disable e10s by default on Android because we don't run Android # e10s jobs anywhere yet. options.e10s = False return options diff --git a/layout/tools/reftest/remotereftest.py b/layout/tools/reftest/remotereftest.py --- a/layout/tools/reftest/remotereftest.py +++ b/layout/tools/reftest/remotereftest.py @@ -1,24 +1,24 @@ # 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 logging import os +import posixpath import psutil import signal import sys import tempfile import time import traceback import urllib2 from contextlib import closing -import mozdevice +from mozdevice import ADBAndroid import mozinfo from automation import Automation from remoteautomation import RemoteAutomation, fennecLogcatFilters from output import OutputHandler from runreftest import RefTest, ReftestResolver import reftestcommandline @@ -49,75 +49,69 @@ class ReftestServer: """ Web server used to serve Reftests, for closer fidelity to the real web. It is virtually identical to the server used in mochitest and will only be used for running reftests remotely. Bug 581257 has been filed to refactor this wrapper around httpd.js into it's own class and use it in both remote and non-remote testing. """ def __init__(self, automation, options, scriptDir): self.automation = automation - self._utilityPath = options.utilityPath - self._xrePath = options.xrePath - self._profileDir = options.serverProfilePath + self.utilityPath = options.utilityPath + self.xrePath = options.xrePath + self.profileDir = options.serverProfilePath self.webServer = options.remoteWebServer self.httpPort = options.httpPort self.scriptDir = scriptDir - self.pidFile = options.pidFile - self._httpdPath = os.path.abspath(options.httpdPath) + self.httpdPath = os.path.abspath(options.httpdPath) if options.remoteWebServer == "10.0.2.2": # probably running an Android emulator and 10.0.2.2 will # not be visible from host shutdownServer = "127.0.0.1" else: shutdownServer = self.webServer self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { "server": shutdownServer, "port": self.httpPort} def start(self): "Run the Refest server, returning the process ID of the server." - env = self.automation.environment(xrePath=self._xrePath) + env = self.automation.environment(xrePath=self.xrePath) env["XPCOM_DEBUG_BREAK"] = "warn" if self.automation.IS_WIN32: - env["PATH"] = env["PATH"] + ";" + self._xrePath + env["PATH"] = env["PATH"] + ";" + self.xrePath - args = ["-g", self._xrePath, - "-f", os.path.join(self._httpdPath, "httpd.js"), + args = ["-g", self.xrePath, + "-f", os.path.join(self.httpdPath, "httpd.js"), "-e", "const _PROFILE_PATH = '%(profile)s';const _SERVER_PORT = " "'%(port)s'; const _SERVER_ADDR ='%(server)s';" % { - "profile": self._profileDir.replace('\\', '\\\\'), "port": self.httpPort, + "profile": self.profileDir.replace('\\', '\\\\'), "port": self.httpPort, "server": self.webServer}, "-f", os.path.join(self.scriptDir, "server.js")] - xpcshell = os.path.join(self._utilityPath, + xpcshell = os.path.join(self.utilityPath, "xpcshell" + self.automation.BIN_SUFFIX) if not os.access(xpcshell, os.F_OK): raise Exception('xpcshell not found at %s' % xpcshell) if self.automation.elf_arm(xpcshell): raise Exception('xpcshell at %s is an ARM binary; please use ' 'the --utility-path argument to specify the path ' 'to a desktop version.' % xpcshell) self._process = self.automation.Process([xpcshell] + args, env=env) pid = self._process.pid if pid < 0: print "TEST-UNEXPECTED-FAIL | remotereftests.py | Error starting server." return 2 self.automation.log.info("INFO | remotereftests.py | Server pid: %d", pid) - if (self.pidFile != ""): - f = open(self.pidFile + ".xpcshell.pid", 'w') - f.write("%s" % pid) - f.close() - def ensureReady(self, timeout): assert timeout >= 0 - aliveFile = os.path.join(self._profileDir, "server_alive.txt") + aliveFile = os.path.join(self.profileDir, "server_alive.txt") i = 0 while i < timeout: if os.path.exists(aliveFile): break time.sleep(1) i += 1 else: print ("TEST-UNEXPECTED-FAIL | remotereftests.py | " @@ -138,47 +132,84 @@ class ReftestServer: self.automation.log.info("Failed to shutdown server at %s" % self.shutdownURL) traceback.print_exc() self._process.kill() class RemoteReftest(RefTest): use_marionette = False - remoteApp = '' resolver_cls = RemoteReftestResolver - def __init__(self, automation, devicemanager, options, scriptDir): + def __init__(self, options, scriptDir): RefTest.__init__(self, options.suite) self.run_by_manifest = False - self.automation = automation - self._devicemanager = devicemanager self.scriptDir = scriptDir - self.remoteApp = options.app + self.localLogName = options.localLogName + + verbose = False + if options.log_tbpl_level == 'debug' or options.log_mach_level == 'debug': + verbose = True + print "set verbose!" + self.device = ADBAndroid(adb=options.adb_path, + device=options.deviceSerial, + test_root=options.remoteTestRoot, + verbose=verbose) + + if options.remoteTestRoot is None: + options.remoteTestRoot = posixpath.join(self.device.test_root, "reftest") + options.remoteProfile = posixpath.join(options.remoteTestRoot, "profile") + options.remoteLogFile = posixpath.join(options.remoteTestRoot, "reftest.log") + options.logFile = options.remoteLogFile self.remoteProfile = options.remoteProfile self.remoteTestRoot = options.remoteTestRoot - self.remoteLogFile = options.remoteLogFile - self.remoteCache = os.path.join(options.remoteTestRoot, "cache/") - self.localLogName = options.localLogName - self.pidFile = options.pidFile - if self.automation.IS_DEBUG_BUILD: - self.SERVER_STARTUP_TIMEOUT = 180 - else: - self.SERVER_STARTUP_TIMEOUT = 90 - self.automation.deleteANRs() - self.automation.deleteTombstones() - self._devicemanager.removeDir(self.remoteCache) + + if not options.ignoreWindowSize: + parts = self.device.get_info( + 'screen')['screen'][0].split() + width = int(parts[0].split(':')[1]) + height = int(parts[1].split(':')[1]) + if (width < 1366 or height < 1050): + self.error("ERROR: Invalid screen resolution %sx%s, " + "please adjust to 1366x1050 or higher" % ( + width, height)) self._populate_logger(options) self.outputHandler = OutputHandler(self.log, options.utilityPath, options.symbolsPath) # RemoteAutomation.py's 'messageLogger' is also used by mochitest. Mimic a mochitest # MessageLogger object to re-use this code path. self.outputHandler.write = self.outputHandler.__call__ + self.automation = RemoteAutomation(self.device, options.app, self.remoteProfile, + options.remoteLogFile, processArgs=None) self.automation._processArgs['messageLogger'] = self.outputHandler + self.environment = self.automation.environment + if self.automation.IS_DEBUG_BUILD: + self.SERVER_STARTUP_TIMEOUT = 180 + else: + self.SERVER_STARTUP_TIMEOUT = 90 + + self.remoteCache = os.path.join(options.remoteTestRoot, "cache/") + + # Check that Firefox is installed + expected = options.app.split('/')[-1] + if not self.device.is_app_installed(expected): + raise Exception("%s is not installed on this device" % expected) + + self.automation.deleteANRs() + self.automation.deleteTombstones() + self.device.clear_logcat() + + self.device.rm(self.remoteCache, force=True, recursive=True) + + procName = options.app.split('/')[-1] + self.device.pkill(procName) + if self.device.process_exist(procName): + self.log.error("unable to kill %s before starting tests!" % procName) + def findPath(self, paths, filename=None): for path in paths: p = path if filename: p = os.path.join(p, filename) if os.path.exists(self.getFullPath(p)): return path return None @@ -282,51 +313,52 @@ class RemoteReftest(RefTest): # Because Fennec is a little wacky (see bug 1156817) we need to load the # reftest pages at 1.0 zoom, rather than zooming to fit the CSS viewport. prefs["apz.allow_zooming"] = False # Set the extra prefs. profile.set_preferences(prefs) try: - self._devicemanager.pushDir(profileDir, options.remoteProfile) - self._devicemanager.chmodDir(options.remoteProfile) - except mozdevice.DMError: + self.device.push(profileDir, options.remoteProfile) + self.device.chmod(options.remoteProfile, recursive=True) + except Exception: print "Automation Error: Failed to copy profiledir to device" raise return profile def copyExtraFilesToProfile(self, options, profile): profileDir = profile.profile RefTest.copyExtraFilesToProfile(self, options, profile) - try: - self._devicemanager.pushDir(profileDir, options.remoteProfile) - self._devicemanager.chmodDir(options.remoteProfile) - except mozdevice.DMError: - print "Automation Error: Failed to copy extra files to device" - raise + if len(os.listdir(profileDir)) > 0: + try: + self.device.push(profileDir, options.remoteProfile) + self.device.chmod(options.remoteProfile, recursive=True) + except Exception: + print "Automation Error: Failed to copy extra files to device" + raise def printDeviceInfo(self, printLogcat=False): try: if printLogcat: - logcat = self._devicemanager.getLogcat(filterOutRegexps=fennecLogcatFilters) + logcat = self.device.get_logcat(filter_out_regexps=fennecLogcatFilters) print ''.join(logcat) print "Device info:" - devinfo = self._devicemanager.getInfo() + devinfo = self.device.get_info() for category in devinfo: if type(devinfo[category]) is list: print " %s:" % category for item in devinfo[category]: print " %s" % item else: print " %s: %s" % (category, devinfo[category]) - print "Test root: %s" % self._devicemanager.deviceRoot - except mozdevice.DMError: - print "WARNING: Error getting device information" + print "Test root: %s" % self.device.test_root + except Exception as e: + print "WARNING: Error getting device information: %s" % str(e) def environment(self, **kwargs): return self.automation.environment(**kwargs) def buildBrowserEnv(self, options, profileDir): browserEnv = RefTest.buildBrowserEnv(self, options, profileDir) # remove desktop environment not used on device if "XPCOM_MEM_BLOAT_LOG" in browserEnv: @@ -357,74 +389,25 @@ class RemoteReftest(RefTest): debuggerInfo=debuggerInfo, symbolsPath=symbolsPath, timeout=timeout) self.cleanup(profile.profile) return status def cleanup(self, profileDir): - # Pull results back from device - if self.remoteLogFile and \ - self._devicemanager.fileExists(self.remoteLogFile): - self._devicemanager.getFile(self.remoteLogFile, self.localLogName) - else: - print "WARNING: Unable to retrieve log file (%s) from remote " \ - "device" % self.remoteLogFile - self._devicemanager.removeDir(self.remoteProfile) - self._devicemanager.removeDir(self.remoteCache) - self._devicemanager.removeDir(self.remoteTestRoot) + self.device.rm(self.remoteTestRoot, force=True, recursive=True) + self.device.rm(self.remoteProfile, force=True, recursive=True) + self.device.rm(self.remoteCache, force=True, recursive=True) RefTest.cleanup(self, profileDir) - if (self.pidFile != ""): - try: - os.remove(self.pidFile) - os.remove(self.pidFile + ".xpcshell.pid") - except Exception: - print ("Warning: cleaning up pidfile '%s' was unsuccessful " - "from the test harness" % self.pidFile) def run_test_harness(parser, options): - dm_args = { - 'deviceRoot': options.remoteTestRoot, - 'host': options.deviceIP, - 'port': options.devicePort, - } - - dm_args['adbPath'] = options.adb_path - if not dm_args['host']: - dm_args['deviceSerial'] = options.deviceSerial - if options.log_tbpl_level == 'debug' or options.log_mach_level == 'debug': - dm_args['logLevel'] = logging.DEBUG - - try: - dm = mozdevice.DroidADB(**dm_args) - except mozdevice.DMError: - traceback.print_exc() - print ("Automation Error: exception while initializing devicemanager. " - "Most likely the device is not in a testable state.") - return 1 - - automation = RemoteAutomation(None) - automation.setDeviceManager(dm) - - # Set up the defaults and ensure options are set - parser.validate_remote(options, automation) - - # Check that Firefox is installed - expected = options.app.split('/')[-1] - installed = dm.shellCheckOutput(['pm', 'list', 'packages', expected]) - if expected not in installed: - print "%s is not installed on this device" % expected - return 1 - - automation.setAppName(options.app) - automation.setRemoteProfile(options.remoteProfile) - automation.setRemoteLog(options.remoteLogFile) - reftest = RemoteReftest(automation, dm, options, SCRIPT_DIRECTORY) + reftest = RemoteReftest(options, SCRIPT_DIRECTORY) + parser.validate_remote(options, reftest.automation) parser.validate(options, reftest) if mozinfo.info['debug']: print "changing timeout for remote debug reftests from %s to 600 seconds" % options.timeout options.timeout = 600 # Hack in a symbolic link for jsreftest os.system("ln -s ../jsreftest " + str(os.path.join(SCRIPT_DIRECTORY, "jsreftest"))) @@ -437,30 +420,21 @@ def run_test_harness(parser, options): reftest.killNamedProc('ssltunnel') reftest.killNamedProc('xpcshell') # Start the webserver retVal = reftest.startWebServer(options) if retVal: return retVal - procName = options.app.split('/')[-1] - dm.killProcess(procName) - if dm.processExist(procName): - print "unable to kill %s before starting tests!" % procName - if options.printDeviceInfo: reftest.printDeviceInfo() -# an example manifest name to use on the cli -# manifest = "http://" + options.remoteWebServer + -# "/reftests/layout/reftests/reftest-sanity/reftest.list" retVal = 0 try: - dm.recordLogcat() if options.verify: retVal = reftest.verifyTests(options.tests, options) else: retVal = reftest.runTests(options.tests, options) except Exception: print "Automation Error: Exception caught while running tests" traceback.print_exc() retVal = 1 diff --git a/testing/mochitest/mochitest_options.py b/testing/mochitest/mochitest_options.py --- a/testing/mochitest/mochitest_options.py +++ b/testing/mochitest/mochitest_options.py @@ -3,22 +3,20 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from abc import ABCMeta, abstractmethod, abstractproperty from argparse import ArgumentParser, SUPPRESS from distutils.util import strtobool from distutils import spawn from itertools import chain from urlparse import urlparse -import logging import json import os import tempfile -from mozdevice import DroidADB from mozprofile import DEFAULT_PORTS import mozinfo import mozlog import moznetwork here = os.path.abspath(os.path.dirname(__file__)) @@ -857,51 +855,27 @@ class MochitestArguments(ArgumentContain return options class AndroidArguments(ArgumentContainer): """Android specific arguments.""" args = [ - [["--remote-app-path"], - {"dest": "remoteAppPath", - "help": "Path to remote executable relative to device root using \ - only forward slashes. Either this or app must be specified \ - but not both.", - "default": None, - }], - [["--deviceIP"], - {"dest": "deviceIP", - "help": "ip address of remote device to test", - "default": None, - }], [["--deviceSerial"], {"dest": "deviceSerial", "help": "ip address of remote device to test", "default": None, }], [["--adbpath"], {"dest": "adbPath", - "default": None, + "default": "adb", "help": "Path to adb binary.", "suppress": True, }], - [["--devicePort"], - {"dest": "devicePort", - "type": int, - "default": 20701, - "help": "port of remote device to test", - }], - [["--remote-logfile"], - {"dest": "remoteLogFile", - "default": None, - "help": "Name of log file on the device relative to the device \ - root. PLEASE ONLY USE A FILENAME.", - }], [["--remote-webserver"], {"dest": "remoteWebServer", "default": None, "help": "ip address where the remote web server is hosted at", }], [["--http-port"], {"dest": "httpPort", "default": DEFAULT_PORTS['http'], @@ -909,93 +883,58 @@ class AndroidArguments(ArgumentContainer "suppress": True, }], [["--ssl-port"], {"dest": "sslPort", "default": DEFAULT_PORTS['https'], "help": "ssl port of the remote web server", "suppress": True, }], - [["--robocop-ini"], - {"dest": "robocopIni", - "default": "", - "help": "name of the .ini file containing the list of tests to run", - }], [["--robocop-apk"], {"dest": "robocopApk", "default": "", "help": "name of the Robocop APK to use for ADB test running", }], [["--remoteTestRoot"], {"dest": "remoteTestRoot", "default": None, "help": "remote directory to use as test root \ (eg. /mnt/sdcard/tests or /data/local/tests)", "suppress": True, }], ] defaults = { - 'dm': None, # we don't want to exclude specialpowers on android just yet 'extensionsToExclude': [], # mochijar doesn't get installed via marionette on android 'extensionsToInstall': [os.path.join(here, 'mochijar')], 'logFile': 'mochitest.log', 'utilityPath': None, } def validate(self, parser, options, context): """Validate android options.""" if build_obj: options.log_mach = '-' - device_args = {'deviceRoot': options.remoteTestRoot} - device_args['adbPath'] = options.adbPath - if options.deviceIP: - device_args['host'] = options.deviceIP - device_args['port'] = options.devicePort - elif options.deviceSerial: - device_args['deviceSerial'] = options.deviceSerial - - if options.log_tbpl_level == 'debug' or options.log_mach_level == 'debug': - device_args['logLevel'] = logging.DEBUG - options.dm = DroidADB(**device_args) - - if not options.remoteTestRoot: - options.remoteTestRoot = options.dm.deviceRoot - if options.remoteWebServer is None: if os.name != "nt": options.remoteWebServer = moznetwork.get_ip() else: parser.error( "you must specify a --remote-webserver=") options.webServer = options.remoteWebServer - if options.remoteLogFile is None: - options.remoteLogFile = options.remoteTestRoot + \ - '/logs/mochitest.log' - - if options.remoteLogFile.count('/') < 1: - options.remoteLogFile = options.remoteTestRoot + \ - '/' + options.remoteLogFile - - if options.remoteAppPath and options.app: - parser.error( - "You cannot specify both the remoteAppPath and the app setting") - elif options.remoteAppPath: - options.app = options.remoteTestRoot + "/" + options.remoteAppPath - elif options.app is None: + if options.app is None: if build_obj: options.app = build_obj.substs['ANDROID_PACKAGE_NAME'] else: - # Neither remoteAppPath nor app are set -- error parser.error("You must specify either appPath or app") if build_obj and 'MOZ_HOST_BIN' in os.environ: options.xrePath = os.environ['MOZ_HOST_BIN'] # Only reset the xrePath if it wasn't provided if options.xrePath is None: options.xrePath = options.utilityPath @@ -1003,28 +942,18 @@ class AndroidArguments(ArgumentContainer if build_obj: options.topsrcdir = build_obj.topsrcdir if options.pidFile != "": f = open(options.pidFile, 'w') f.write("%s" % os.getpid()) f.close() - # Robocop specific options - if options.robocopIni != "": - if not os.path.exists(options.robocopIni): - parser.error( - "Unable to find specified robocop .ini manifest '%s'" % - options.robocopIni) - options.robocopIni = os.path.abspath(options.robocopIni) - - if not options.robocopApk and build_obj: - apk = build_obj.substs.get('GRADLE_ANDROID_APP_ANDROIDTEST_APK') - if apk and os.path.exists(apk): - options.robocopApk = apk + if not options.robocopApk and build_obj: + options.robocopApk = build_obj.substs.get('GRADLE_ANDROID_APP_ANDROIDTEST_APK') if options.robocopApk != "": if not os.path.exists(options.robocopApk): parser.error( "Unable to find robocop APK '%s'" % options.robocopApk) options.robocopApk = os.path.abspath(options.robocopApk) diff --git a/testing/mochitest/runrobocop.py b/testing/mochitest/runrobocop.py --- a/testing/mochitest/runrobocop.py +++ b/testing/mochitest/runrobocop.py @@ -1,14 +1,15 @@ # 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 json import os +import posixpath import sys import tempfile import traceback from collections import defaultdict sys.path.insert( 0, os.path.abspath( os.path.realpath( @@ -16,62 +17,73 @@ sys.path.insert( from automation import Automation from remoteautomation import RemoteAutomation, fennecLogcatFilters from runtests import KeyValueParseError, MochitestDesktop, MessageLogger, parseKeyValue from mochitest_options import MochitestArgumentParser from manifestparser import TestManifest from manifestparser.filters import chunk_by_slice -import mozdevice +from mozdevice import ADBAndroid +import mozfile import mozinfo SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) class RobocopTestRunner(MochitestDesktop): """ A test harness for Robocop. Robocop tests are UI tests for Firefox for Android, based on the Robotium test framework. This harness leverages some functionality from mochitest, for convenience. """ - auto = None - dm = None # Some robocop tests run for >60 seconds without generating any output. NO_OUTPUT_TIMEOUT = 180 - def __init__(self, automation, devmgr, options): + def __init__(self, options, message_logger): """ Simple one-time initialization. """ MochitestDesktop.__init__(self, options.flavor, vars(options)) - self.auto = automation - self.dm = devmgr - self.dm.default_timeout = 320 + verbose = False + if options.log_tbpl_level == 'debug' or options.log_mach_level == 'debug': + verbose = True + self.device = ADBAndroid(adb=options.adbPath, + device=options.deviceSerial, + test_root=options.remoteTestRoot, + verbose=verbose) + + # Check that Firefox is installed + expected = options.app.split('/')[-1] + if not self.device.is_app_installed(expected): + raise Exception("%s is not installed on this device" % expected) + + options.logFile = "robocop.log" + if options.remoteTestRoot is None: + options.remoteTestRoot = self.device.test_root + self.remoteProfile = posixpath.join(options.remoteTestRoot, "profile") + self.remoteProfileCopy = posixpath.join(options.remoteTestRoot, "profile-copy") + + self.remoteConfigFile = posixpath.join(options.remoteTestRoot, "robotium.config") + self.remoteLogFile = posixpath.join(options.remoteTestRoot, "logs", "robocop.log") + self.options = options - self.options.logFile = "robocop.log" + + process_args = {'messageLogger': message_logger} + self.auto = RemoteAutomation(self.device, options.remoteappname, self.remoteProfile, + self.remoteLogFile, processArgs=process_args) self.environment = self.auto.environment - self.deviceRoot = self.dm.getDeviceRoot() - self.remoteProfile = options.remoteTestRoot + "/profile" - self.remoteProfileCopy = options.remoteTestRoot + "/profile-copy" - self.auto.setRemoteProfile(self.remoteProfile) - self.remoteConfigFile = os.path.join( - self.deviceRoot, "robotium.config") - self.remoteLog = options.remoteLogFile - self.auto.setRemoteLog(self.remoteLog) + self.remoteScreenshots = "/mnt/sdcard/Robotium-Screenshots" - self.remoteMozLog = os.path.join(options.remoteTestRoot, "mozlog") - self.auto.setServerInfo( - self.options.webServer, self.options.httpPort, self.options.sslPort) + self.remoteMozLog = posixpath.join(options.remoteTestRoot, "mozlog") + self.localLog = options.logFile self.localProfile = None - self.auto.setAppName(self.options.remoteappname) self.certdbNew = True - self.remoteCopyAvailable = True self.passed = 0 self.failed = 0 self.todo = 0 def startup(self): """ Second-stage initialization: One-time initialization which may require cleanup. """ @@ -80,33 +92,33 @@ class RobocopTestRunner(MochitestDesktop # with future tests, typically because the old server is keeping the port in use. # Try to avoid those failures by checking for and killing servers before # trying to start new ones. self.killNamedProc('ssltunnel') self.killNamedProc('xpcshell') self.auto.deleteANRs() self.auto.deleteTombstones() procName = self.options.app.split('/')[-1] - self.dm.killProcess(procName) - if self.dm.processExist(procName): + self.device.pkill(procName) + if self.device.process_exist(procName): self.log.warning("unable to kill %s before running tests!" % procName) - self.dm.removeDir(self.remoteScreenshots) - self.dm.removeDir(self.remoteMozLog) - self.dm.mkDir(self.remoteMozLog) - self.dm.mkDir(os.path.dirname(self.options.remoteLogFile)) + self.device.rm(self.remoteScreenshots, force=True, recursive=True) + self.device.rm(self.remoteMozLog, force=True, recursive=True) + self.device.mkdir(self.remoteMozLog) + logParent = posixpath.dirname(self.remoteLogFile) + self.device.rm(logParent, force=True, recursive=True) + self.device.mkdir(logParent) # Add Android version (SDK level) to mozinfo so that manifest entries # can be conditional on android_version. - androidVersion = self.dm.shellCheckOutput( - ['getprop', 'ro.build.version.sdk']) self.log.info( "Android sdk version '%s'; will use this to filter manifests" % - str(androidVersion)) - mozinfo.info['android_version'] = androidVersion + str(self.device.version)) + mozinfo.info['android_version'] = str(self.device.version) if self.options.robocopApk: - self.dm._checkCmd(["install", "-r", self.options.robocopApk]) + self.device.install_app(self.options.robocopApk, replace=True) self.log.debug("Robocop APK %s installed" % self.options.robocopApk) # Display remote diagnostics; if running in mach, keep output terse. if self.options.log_mach is None: self.printDeviceInfo() self.setupLocalPaths() self.buildProfile() # ignoreSSLTunnelExts is a workaround for bug 1109310 @@ -117,33 +129,32 @@ class RobocopTestRunner(MochitestDesktop self.log.debug("Servers started") def cleanup(self): """ Cleanup at end of job run. """ self.log.debug("Cleaning up...") self.stopServers() - self.dm.killProcess(self.options.app.split('/')[-1]) - blobberUploadDir = os.environ.get('MOZ_UPLOAD_DIR', None) - if blobberUploadDir: + self.device.pkill(self.options.app.split('/')[-1]) + uploadDir = os.environ.get('MOZ_UPLOAD_DIR', None) + if uploadDir: self.log.debug("Pulling any remote moz logs and screenshots to %s." % - blobberUploadDir) - self.dm.getDirectory(self.remoteMozLog, blobberUploadDir) - self.dm.getDirectory(self.remoteScreenshots, blobberUploadDir) + uploadDir) + self.device.pull(self.remoteMozLog, uploadDir) + self.device.pull(self.remoteScreenshots, uploadDir) MochitestDesktop.cleanup(self, self.options) if self.localProfile: - os.system("rm -Rf %s" % self.localProfile) - self.dm.removeDir(self.remoteProfile) - self.dm.removeDir(self.remoteProfileCopy) - self.dm.removeDir(self.remoteScreenshots) - self.dm.removeDir(self.remoteMozLog) - self.dm.removeFile(self.remoteConfigFile) - if self.dm.fileExists(self.remoteLog): - self.dm.removeFile(self.remoteLog) + mozfile.remove(self.localProfile) + self.device.rm(self.remoteProfile, force=True, recursive=True) + self.device.rm(self.remoteProfileCopy, force=True, recursive=True) + self.device.rm(self.remoteScreenshots, force=True, recursive=True) + self.device.rm(self.remoteMozLog, force=True, recursive=True) + self.device.rm(self.remoteConfigFile, force=True) + self.device.rm(self.remoteLogFile, force=True) self.log.debug("Cleanup complete.") def findPath(self, paths, filename=None): for path in paths: p = path if filename: p = os.path.join(p, filename) if os.path.exists(self.getFullPath(p)): @@ -232,43 +243,31 @@ class RobocopTestRunner(MochitestDesktop ]) manifest = MochitestDesktop.buildProfile(self, self.options) self.localProfile = self.options.profilePath self.log.debug("Profile created at %s" % self.localProfile) # some files are not needed for robocop; save time by not pushing os.remove(os.path.join(self.localProfile, 'userChrome.css')) try: - self.dm.pushDir(self.localProfile, self.remoteProfileCopy) - except mozdevice.DMError: + self.device.push(self.localProfile, self.remoteProfileCopy) + except Exception: self.log.error( "Automation Error: Unable to copy profile to device.") raise return manifest def setupRemoteProfile(self): """ Remove any remote profile and re-create it. """ self.log.debug("Updating remote profile at %s" % self.remoteProfile) - self.dm.removeDir(self.remoteProfile) - if self.remoteCopyAvailable: - try: - self.dm.shellCheckOutput( - ['cp', '-r', self.remoteProfileCopy, self.remoteProfile], - root=True, timeout=60) - except mozdevice.DMError: - # For instance, cp is not available on some older versions of - # Android. - self.log.info( - "Unable to copy remote profile; falling back to push.") - self.remoteCopyAvailable = False - if not self.remoteCopyAvailable: - self.dm.pushDir(self.localProfile, self.remoteProfile) + self.device.rm(self.remoteProfile, force=True, recursive=True) + self.device.cp(self.remoteProfileCopy, self.remoteProfile, recursive=True) def parseLocalLog(self): """ Read and parse the local log file, noting any failures. """ with open(self.localLog) as currentLog: data = currentLog.readlines() os.unlink(self.localLog) @@ -324,46 +323,46 @@ class RobocopTestRunner(MochitestDesktop def printDeviceInfo(self, printLogcat=False): """ Log remote device information and logcat (if requested). This is similar to printDeviceInfo in runtestsremote.py """ try: if printLogcat: - logcat = self.dm.getLogcat( + logcat = self.device.get_logcat( filterOutRegexps=fennecLogcatFilters) self.log.info( '\n' + ''.join(logcat).decode( 'utf-8', 'replace')) self.log.info("Device info:") - devinfo = self.dm.getInfo() + devinfo = self.device.get_info() for category in devinfo: if type(devinfo[category]) is list: self.log.info(" %s:" % category) for item in devinfo[category]: self.log.info(" %s" % item) else: self.log.info(" %s: %s" % (category, devinfo[category])) - self.log.info("Test root: %s" % self.dm.deviceRoot) - except mozdevice.DMError: - self.log.warning("Error getting device information") + self.log.info("Test root: %s" % self.device.test_root) + except Exception as e: + self.log.warning("Error getting device information: %s" % str(e)) def setupRobotiumConfig(self, browserEnv): """ Create robotium.config and push it to the device. """ fHandle = tempfile.NamedTemporaryFile(suffix='.config', prefix='robotium-', dir=os.getcwd(), delete=False) - fHandle.write("profile=%s\n" % (self.remoteProfile)) - fHandle.write("logfile=%s\n" % (self.options.remoteLogFile)) + fHandle.write("profile=%s\n" % self.remoteProfile) + fHandle.write("logfile=%s\n" % self.remoteLogFile) fHandle.write("host=http://mochi.test:8888/tests\n") fHandle.write( "rawhost=http://%s:%s/tests\n" % (self.options.remoteWebServer, self.options.httpPort)) if browserEnv: envstr = "" delim = "" for key, value in browserEnv.items(): @@ -373,18 +372,18 @@ class RobocopTestRunner(MochitestDesktop "in our value, unable to process value. key=%s,value=%s" % (key, value)) self.log.error("browserEnv=%s" % browserEnv) except ValueError: envstr += "%s%s=%s" % (delim, key, value) delim = "," fHandle.write("envvars=%s\n" % envstr) fHandle.close() - self.dm.removeFile(self.remoteConfigFile) - self.dm.pushFile(fHandle.name, self.remoteConfigFile) + self.device.rm(self.remoteConfigFile, force=True) + self.device.push(fHandle.name, self.remoteConfigFile) os.unlink(fHandle.name) def buildBrowserEnv(self): """ Return an environment dictionary suitable for remote use. This is similar to buildBrowserEnv in runtestsremote.py. """ @@ -415,62 +414,58 @@ class RobocopTestRunner(MochitestDesktop Run the specified test. """ self.log.debug("Running test %s" % test['name']) self.mozLogName = "moz-%s.log" % test['name'] browserEnv = self.buildBrowserEnv() self.setupRobotiumConfig(browserEnv) self.setupRemoteProfile() self.options.app = "am" + timeout = None if self.options.autorun: # This launches a test (using "am instrument") and instructs # Fennec to /quit/ the browser (using Robocop:Quit) and to # /finish/ all opened activities. browserArgs = [ "instrument", - "-w", "-e", "quit_and_finish", "1", - "-e", "deviceroot", self.deviceRoot, + "-e", "deviceroot", self.device.test_root, "-e", "class", "org.mozilla.gecko.tests.%s" % test['name'].split('/')[-1].split('.java')[0], "org.mozilla.roboexample.test/org.mozilla.gecko.FennecInstrumentationTestRunner"] else: # This does not launch a test at all. It launches an activity # that starts Fennec and then waits indefinitely, since cat # never returns. browserArgs = ["start", "-n", "org.mozilla.roboexample.test/org.mozilla." "gecko.LaunchFennecWithConfigurationActivity", "&&", "cat"] - self.dm.default_timeout = sys.maxint # Forever. + timeout = sys.maxint # Forever. + self.log.info("") self.log.info("Serving mochi.test Robocop root at http://%s:%s/tests/robocop/" % (self.options.remoteWebServer, self.options.httpPort)) self.log.info("") result = -1 log_result = -1 try: - self.dm.recordLogcat() - timeout = self.options.timeout + self.device.clear_logcat() + if not timeout: + timeout = self.options.timeout if not timeout: timeout = self.NO_OUTPUT_TIMEOUT result, _ = self.auto.runApp( None, browserEnv, "am", self.localProfile, browserArgs, timeout=timeout, symbolsPath=self.options.symbolsPath) self.log.debug("runApp completes with status %d" % result) if result != 0: self.log.error("runApp() exited with code %s" % result) - if self.dm.fileExists(self.remoteLog): - self.dm.getFile(self.remoteLog, self.localLog) - self.dm.removeFile(self.remoteLog) - self.log.debug("Remote log %s retrieved to %s" % - (self.remoteLog, self.localLog)) - else: - self.log.warning( - "Unable to retrieve log file (%s) from remote device" % - self.remoteLog) + if self.device.is_file(self.remoteLogFile): + self.device.pull(self.remoteLogFile, self.localLog) + self.device.rm(self.remoteLogFile) log_result = self.parseLocalLog() if result != 0 or log_result != 0: # Display remote diagnostics; if running in mach, keep output # terse. if self.options.log_mach is None: self.printDeviceInfo(printLogcat=True) except Exception: self.log.error( @@ -482,17 +477,17 @@ class RobocopTestRunner(MochitestDesktop return result def runTests(self): self.startup() if isinstance(self.options.manifestFile, TestManifest): mp = self.options.manifestFile else: mp = TestManifest(strict=False) - mp.read(self.options.robocopIni) + mp.read("robocop.ini") filters = [] if self.options.totalChunks: filters.append( chunk_by_slice(self.options.thisChunk, self.options.totalChunks)) robocop_tests = mp.active_tests( exists=False, filters=filters, **mozinfo.info) if not self.options.autorun: # Force a single loop iteration. The iteration will start Fennec and @@ -533,28 +528,18 @@ class RobocopTestRunner(MochitestDesktop def run_test_harness(parser, options): parser.validate(options) if options is None: raise ValueError( "Invalid options specified, use --help for a list of valid options") message_logger = MessageLogger(logger=None) - process_args = {'messageLogger': message_logger} - auto = RemoteAutomation(None, "fennec", processArgs=process_args) - auto.setDeviceManager(options.dm) runResult = -1 - robocop = RobocopTestRunner(auto, options.dm, options) - - # Check that Firefox is installed - expected = options.app.split('/')[-1] - installed = options.dm.shellCheckOutput(['pm', 'list', 'packages', expected]) - if expected not in installed: - robocop.log.error("%s is not installed on this device" % expected) - return 1 + robocop = RobocopTestRunner(options, message_logger) try: message_logger.logger = robocop.log message_logger.buffering = False robocop.message_logger = message_logger robocop.log.debug("options=%s" % vars(options)) runResult = robocop.runTests() except KeyboardInterrupt: @@ -563,19 +548,19 @@ def run_test_harness(parser, options): except Exception: traceback.print_exc() robocop.log.error( "runrobocop.py | Received unexpected exception while running tests") runResult = 1 finally: try: robocop.cleanup() - except mozdevice.DMError: + except Exception: # ignore device error while cleaning up - pass + traceback.print_exc() message_logger.finish() return runResult def main(args=sys.argv[1:]): parser = MochitestArgumentParser(app='android') options = parser.parse_args(args) return run_test_harness(parser, options) diff --git a/testing/mochitest/runtestsremote.py b/testing/mochitest/runtestsremote.py --- a/testing/mochitest/runtestsremote.py +++ b/testing/mochitest/runtestsremote.py @@ -1,87 +1,123 @@ # 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 posixpath import sys import traceback sys.path.insert( 0, os.path.abspath( os.path.realpath( os.path.dirname(__file__)))) from automation import Automation from remoteautomation import RemoteAutomation, fennecLogcatFilters from runtests import MochitestDesktop, MessageLogger from mochitest_options import MochitestArgumentParser -import mozdevice +from mozdevice import ADBAndroid import mozinfo SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) class MochiRemote(MochitestDesktop): - _automation = None - _dm = None localProfile = None logMessages = [] - def __init__(self, automation, devmgr, options): + def __init__(self, options): MochitestDesktop.__init__(self, options.flavor, vars(options)) + verbose = False + if options.log_tbpl_level == 'debug' or options.log_mach_level == 'debug': + verbose = True if hasattr(options, 'log'): delattr(options, 'log') - self._automation = automation - self._dm = devmgr + self.certdbNew = True self.chromePushed = False - self.environment = self._automation.environment - self.remoteProfile = os.path.join(options.remoteTestRoot, "profile/") - self.remoteModulesDir = os.path.join(options.remoteTestRoot, "modules/") - self.remoteCache = os.path.join(options.remoteTestRoot, "cache/") - self._automation.setRemoteProfile(self.remoteProfile) - self.remoteLog = options.remoteLogFile - self.localLog = options.logFile - self._automation.deleteANRs() - self._automation.deleteTombstones() - self.certdbNew = True - self.remoteMozLog = os.path.join(options.remoteTestRoot, "mozlog") - self._dm.removeDir(self.remoteMozLog) - self._dm.mkDir(self.remoteMozLog) - self.remoteChromeTestDir = os.path.join( - options.remoteTestRoot, - "chrome") - self._dm.removeDir(self.remoteChromeTestDir) - self._dm.mkDir(self.remoteChromeTestDir) - self._dm.removeDir(self.remoteProfile) - self._dm.removeDir(self.remoteCache) + self.mozLogName = "moz.log" + + self.device = ADBAndroid(adb=options.adbPath, + device=options.deviceSerial, + test_root=options.remoteTestRoot, + verbose=verbose) + + if options.remoteTestRoot is None: + options.remoteTestRoot = self.device.test_root + options.dumpOutputDirectory = options.remoteTestRoot + self.remoteLogFile = posixpath.join(options.remoteTestRoot, "logs", "mochitest.log") + logParent = posixpath.dirname(self.remoteLogFile) + self.device.rm(logParent, force=True, recursive=True) + self.device.mkdir(logParent) + + self.remoteProfile = posixpath.join(options.remoteTestRoot, "profile/") + self.device.rm(self.remoteProfile, force=True, recursive=True) + + self.counts = dict() + self.message_logger = MessageLogger(logger=None) + self.message_logger.logger = self.log + process_args = {'messageLogger': self.message_logger, 'counts': self.counts} + self.automation = RemoteAutomation(self.device, options.remoteappname, self.remoteProfile, + self.remoteLogFile, processArgs=process_args) + self.environment = self.automation.environment + + # Check that Firefox is installed + expected = options.app.split('/')[-1] + if not self.device.is_app_installed(expected): + raise Exception("%s is not installed on this device" % expected) + + self.automation.deleteANRs() + self.automation.deleteTombstones() + self.device.clear_logcat() + + self.remoteModulesDir = posixpath.join(options.remoteTestRoot, "modules/") + + self.remoteCache = posixpath.join(options.remoteTestRoot, "cache/") + self.device.rm(self.remoteCache, force=True, recursive=True) + # move necko cache to a location that can be cleaned up options.extraPrefs += ["browser.cache.disk.parent_directory=%s" % self.remoteCache] + self.remoteMozLog = posixpath.join(options.remoteTestRoot, "mozlog") + self.device.rm(self.remoteMozLog, force=True, recursive=True) + self.device.mkdir(self.remoteMozLog) + + self.remoteChromeTestDir = posixpath.join( + options.remoteTestRoot, + "chrome") + self.device.rm(self.remoteChromeTestDir, force=True, recursive=True) + self.device.mkdir(self.remoteChromeTestDir) + + procName = options.app.split('/')[-1] + self.device.pkill(procName) + if self.device.process_exist(procName): + self.log.warning("unable to kill %s before running tests!" % procName) + + # Add Android version (SDK level) to mozinfo so that manifest entries + # can be conditional on android_version. + self.log.info( + "Android sdk version '%s'; will use this to filter manifests" % + str(self.device.version)) + mozinfo.info['android_version'] = str(self.device.version) + def cleanup(self, options, final=False): if final: - self._dm.removeDir(self.remoteChromeTestDir) + self.device.rm(self.remoteChromeTestDir, force=True, recursive=True) self.chromePushed = False - blobberUploadDir = os.environ.get('MOZ_UPLOAD_DIR', None) - if blobberUploadDir: - self._dm.getDirectory(self.remoteMozLog, blobberUploadDir) - else: - if self._dm.fileExists(self.remoteLog): - self._dm.getFile(self.remoteLog, self.localLog) - self._dm.removeFile(self.remoteLog) - else: - self.log.warning( - "Unable to retrieve log file (%s) from remote device" % - self.remoteLog) - self._dm.removeDir(self.remoteProfile) - self._dm.removeDir(self.remoteCache) + uploadDir = os.environ.get('MOZ_UPLOAD_DIR', None) + if uploadDir: + self.device.pull(self.remoteMozLog, uploadDir) + self.device.rm(self.remoteLogFile, force=True) + self.device.rm(self.remoteProfile, force=True, recursive=True) + self.device.rm(self.remoteCache, force=True, recursive=True) MochitestDesktop.cleanup(self, options, final) self.localProfile = None def findPath(self, paths, filename=None): for path in paths: p = path if filename: p = os.path.join(p, filename) @@ -174,19 +210,19 @@ class MochiRemote(MochitestDesktop): debuggerInfo, ignoreSSLTunnelExts=True) restoreRemotePaths() def buildProfile(self, options): restoreRemotePaths = self.switchToLocalPaths(options) if options.testingModulesDir: try: - self._dm.pushDir(options.testingModulesDir, self.remoteModulesDir) - self._dm.chmodDir(self.remoteModulesDir) - except mozdevice.DMError: + self.device.push(options.testingModulesDir, self.remoteModulesDir) + self.device.chmod(self.remoteModulesDir, recursive=True) + except Exception: self.log.error( "Automation Error: Unable to copy test modules to device.") raise savedTestingModulesDir = options.testingModulesDir options.testingModulesDir = self.remoteModulesDir else: savedTestingModulesDir = None manifest = MochitestDesktop.buildProfile(self, options) @@ -208,71 +244,69 @@ class MochiRemote(MochitestDesktop): "chrome://mochikit/content/browser-test-overlay.xul") path = os.path.join(options.profilePath, 'extensions', 'staged', 'mochikit@mozilla.org', 'chrome.manifest') with open(path, "a") as f: f.write(chrome) return manifest def buildURLOptions(self, options, env): - self.localLog = options.logFile - options.logFile = self.remoteLog - options.fileLevel = 'INFO' + saveLogFile = options.logFile + options.logFile = self.remoteLogFile options.profilePath = self.localProfile env["MOZ_HIDE_RESULTS_TABLE"] = "1" retVal = MochitestDesktop.buildURLOptions(self, options, env) # we really need testConfig.js (for browser chrome) try: - self._dm.pushDir(options.profilePath, self.remoteProfile) - self._dm.chmodDir(self.remoteProfile) - except mozdevice.DMError: - self.log.error( - "Automation Error: Unable to copy profile to device.") + self.device.push(options.profilePath, self.remoteProfile) + self.device.chmod(self.remoteProfile, recursive=True) + except Exception: + self.log.error("Automation Error: Unable to copy profile to device.") raise options.profilePath = self.remoteProfile - options.logFile = self.localLog + options.logFile = saveLogFile return retVal def getChromeTestDir(self, options): local = super(MochiRemote, self).getChromeTestDir(options) remote = self.remoteChromeTestDir if options.flavor == 'chrome' and not self.chromePushed: self.log.info("pushing %s to %s on device..." % (local, remote)) local = os.path.join(local, "chrome") - self._dm.pushDir(local, remote) + self.device.push(local, remote) self.chromePushed = True return remote def getLogFilePath(self, logFile): return logFile def printDeviceInfo(self, printLogcat=False): try: if printLogcat: - logcat = self._dm.getLogcat( + logcat = self.device.get_logcat( filterOutRegexps=fennecLogcatFilters) self.log.info( '\n' + ''.join(logcat).decode( 'utf-8', 'replace')) self.log.info("Device info:") - devinfo = self._dm.getInfo() + devinfo = self.device.get_info() for category in devinfo: if type(devinfo[category]) is list: self.log.info(" %s:" % category) for item in devinfo[category]: self.log.info(" %s" % item) else: self.log.info(" %s: %s" % (category, devinfo[category])) - self.log.info("Test root: %s" % self._dm.deviceRoot) - except mozdevice.DMError: - self.log.warning("Error getting device information") + self.log.info("Test root: %s" % self.device.test_root) + except Exception as e: + self.log.warning("Error getting device information: %s" % str(e)) def getGMPPluginPath(self, options): # TODO: bug 1149374 return None def buildBrowserEnv(self, options, debugger=False): browserEnv = MochitestDesktop.buildBrowserEnv( self, @@ -296,107 +330,60 @@ class MochiRemote(MochitestDesktop): # automation.py/remoteautomation `runApp` takes the profile path, # whereas runtest.py's `runApp` takes a mozprofile object. if 'profileDir' not in kwargs and 'profile' in kwargs: kwargs['profileDir'] = kwargs.pop('profile').profile # remove args not supported by automation.py kwargs.pop('marionette_args', None) - ret, _ = self._automation.runApp(*args, **kwargs) + ret, _ = self.automation.runApp(*args, **kwargs) self.countpass += self.counts['pass'] self.countfail += self.counts['fail'] self.counttodo += self.counts['todo'] return ret, None def run_test_harness(parser, options): parser.validate(options) - message_logger = MessageLogger(logger=None) - counts = dict() - process_args = {'messageLogger': message_logger, 'counts': counts} - auto = RemoteAutomation(None, "fennec", processArgs=process_args) - if options is None: raise ValueError("Invalid options specified, use --help for a list of valid options") options.runByManifest = False # roboextender is used by mochitest-chrome tests like test_java_addons.html, # but not by any plain mochitests if options.flavor != 'chrome': options.extensionsToExclude.append('roboextender@mozilla.org') - dm = options.dm - auto.setDeviceManager(dm) - mochitest = MochiRemote(auto, dm, options) - options.dm = None - - log = mochitest.log - message_logger.logger = log - mochitest.message_logger = message_logger - mochitest.counts = counts - - # Check that Firefox is installed - expected = options.app.split('/')[-1] - installed = dm.shellCheckOutput(['pm', 'list', 'packages', expected]) - if expected not in installed: - log.error("%s is not installed on this device" % expected) - return 1 - - auto.setAppName(options.remoteappname) - - logParent = os.path.dirname(options.remoteLogFile) - dm.removeDir(logParent) - dm.mkDir(logParent) - auto.setRemoteLog(options.remoteLogFile) - auto.setServerInfo(options.webServer, options.httpPort, options.sslPort) + mochitest = MochiRemote(options) if options.log_mach is None: mochitest.printDeviceInfo() - # Add Android version (SDK level) to mozinfo so that manifest entries - # can be conditional on android_version. - androidVersion = dm.shellCheckOutput(['getprop', 'ro.build.version.sdk']) - log.info( - "Android sdk version '%s'; will use this to filter manifests" % - str(androidVersion)) - mozinfo.info['android_version'] = androidVersion - - deviceRoot = dm.deviceRoot - options.dumpOutputDirectory = deviceRoot - - procName = options.app.split('/')[-1] - dm.killProcess(procName) - if dm.processExist(procName): - log.warning("unable to kill %s before running tests!" % procName) - - mochitest.mozLogName = "moz.log" try: - dm.recordLogcat() if options.verify: retVal = mochitest.verifyTests(options) else: retVal = mochitest.runTests(options) except Exception: - log.error("Automation Error: Exception caught while running tests") + mochitest.log.error("Automation Error: Exception caught while running tests") traceback.print_exc() - mochitest.stopServers() try: mochitest.cleanup(options) - except mozdevice.DMError: + except Exception: # device error cleaning up... oh well! - pass + traceback.print_exc() retVal = 1 if options.log_mach is None: mochitest.printDeviceInfo(printLogcat=True) - message_logger.finish() + mochitest.message_logger.finish() return retVal def main(args=sys.argv[1:]): parser = MochitestArgumentParser(app='android') options = parser.parse_args(args) diff --git a/testing/xpcshell/remotexpcshelltests.py b/testing/xpcshell/remotexpcshelltests.py --- a/testing/xpcshell/remotexpcshelltests.py +++ b/testing/xpcshell/remotexpcshelltests.py @@ -240,18 +240,17 @@ class RemoteXPCShellTestThread(xpcshell. # via devicemanager. class XPCShellRemote(xpcshell.XPCShellTests, object): def __init__(self, devmgr, options, log): xpcshell.XPCShellTests.__init__(self, log) # Add Android version (SDK level) to mozinfo so that manifest entries # can be conditional on android_version. - androidVersion = devmgr.shellCheckOutput(['getprop', 'ro.build.version.sdk']) - mozinfo.info['android_version'] = androidVersion + mozinfo.info['android_version'] = self.device.version self.localLib = options['localLib'] self.localBin = options['localBin'] self.options = options self.device = devmgr self.pathMapping = [] self.remoteTestRoot = "%s/xpc" % self.device.deviceRoot # remoteBinDir contains xpcshell and its wrapper script, both of which must