|
@@ -0,0 +1,9078 @@
|
|
|
+# HG changeset patch
|
|
|
+# User Ian Neal <iann_cvs@blueyonder.co.uk>
|
|
|
+# Date 1720364642 -3600
|
|
|
+# Parent 95cbce82a999b85cc352403f8de9db0d6df0a541
|
|
|
+Bug 1906540 - Remove mozrunner devices and mozdevice from SeaMonkey - mozdevice part. r=frg a=frg
|
|
|
+
|
|
|
+diff --git a/config/rules.mk b/config/rules.mk
|
|
|
+--- a/config/rules.mk
|
|
|
++++ b/config/rules.mk
|
|
|
+@@ -63,23 +63,16 @@ CPP_UNIT_TESTS_FILES = $(CPP_UNIT_TESTS)
|
|
|
+ CPP_UNIT_TESTS_DEST = $(DIST)/cppunittests
|
|
|
+ CPP_UNIT_TESTS_TARGET = target
|
|
|
+ INSTALL_TARGETS += CPP_UNIT_TESTS
|
|
|
+ endif
|
|
|
+
|
|
|
+ run-cppunittests::
|
|
|
+ @$(PYTHON3) $(MOZILLA_DIR)/testing/runcppunittests.py --xre-path=$(DIST)/bin --symbols-path=$(DIST)/crashreporter-symbols $(CPP_UNIT_TESTS)
|
|
|
+
|
|
|
+-cppunittests-remote:
|
|
|
+- $(PYTHON3) -u $(MOZILLA_DIR)/testing/remotecppunittests.py \
|
|
|
+- --xre-path=$(DEPTH)/dist/bin \
|
|
|
+- --localLib=$(DEPTH)/dist/$(MOZ_APP_NAME) \
|
|
|
+- --deviceIP=${TEST_DEVICE} \
|
|
|
+- $(CPP_UNIT_TESTS) $(EXTRA_TEST_ARGS); \
|
|
|
+-
|
|
|
+ endif # COMPILE_ENVIRONMENT
|
|
|
+ endif # CPP_UNIT_TESTS
|
|
|
+ endif # ENABLE_TESTS
|
|
|
+
|
|
|
+
|
|
|
+ #
|
|
|
+ # Library rules
|
|
|
+ #
|
|
|
+diff --git a/js/src/jit-test/jit_test.py b/js/src/jit-test/jit_test.py
|
|
|
+--- a/js/src/jit-test/jit_test.py
|
|
|
++++ b/js/src/jit-test/jit_test.py
|
|
|
+@@ -189,26 +189,16 @@ def main(argv):
|
|
|
+
|
|
|
+ if not (os.path.isfile(js_shell) and os.access(js_shell, os.X_OK)):
|
|
|
+ if (platform.system() != 'Windows' or
|
|
|
+ os.path.isfile(js_shell) or not
|
|
|
+ os.path.isfile(js_shell + ".exe") or not
|
|
|
+ os.access(js_shell + ".exe", os.X_OK)):
|
|
|
+ op.error('shell is not executable: ' + js_shell)
|
|
|
+
|
|
|
+- if jittests.stdio_might_be_broken():
|
|
|
+- # Prefer erring on the side of caution and not using stdio if
|
|
|
+- # it might be broken on this platform. The file-redirect
|
|
|
+- # fallback should work on any platform, so at worst by
|
|
|
+- # guessing wrong we might have slowed down the tests a bit.
|
|
|
+- #
|
|
|
+- # XXX technically we could check for broken stdio, but it
|
|
|
+- # really seems like overkill.
|
|
|
+- options.avoid_stdio = True
|
|
|
+-
|
|
|
+ if options.retest:
|
|
|
+ options.read_tests = options.retest
|
|
|
+ options.write_failures = options.retest
|
|
|
+
|
|
|
+ test_list = []
|
|
|
+ read_all = True
|
|
|
+
|
|
|
+ # No point in adding in noasmjs and wasm-baseline variants if the
|
|
|
+diff --git a/js/src/tests/lib/jittests.py b/js/src/tests/lib/jittests.py
|
|
|
+--- a/js/src/tests/lib/jittests.py
|
|
|
++++ b/js/src/tests/lib/jittests.py
|
|
|
+@@ -659,20 +659,17 @@ def process_test_results(results, num_te
|
|
|
+ return print_test_summary(num_tests, failures, complete, doing, options)
|
|
|
+
|
|
|
+ def run_tests(tests, num_tests, prefix, options, remote=False):
|
|
|
+ slog = None
|
|
|
+ if options.format == 'automation':
|
|
|
+ slog = TestLogger("jittests")
|
|
|
+ slog.suite_start()
|
|
|
+
|
|
|
+- if remote:
|
|
|
+- ok = run_tests_remote(tests, num_tests, prefix, options, slog)
|
|
|
+- else:
|
|
|
+- ok = run_tests_local(tests, num_tests, prefix, options, slog)
|
|
|
++ ok = run_tests_local(tests, num_tests, prefix, options, slog)
|
|
|
+
|
|
|
+ if slog:
|
|
|
+ slog.suite_end()
|
|
|
+
|
|
|
+ return ok
|
|
|
+
|
|
|
+ def run_tests_local(tests, num_tests, prefix, options, slog):
|
|
|
+ # The jstests tasks runner requires the following options. The names are
|
|
|
+@@ -688,27 +685,16 @@ def run_tests_local(tests, num_tests, pr
|
|
|
+ # The test runner wants the prefix as a static on the Test class.
|
|
|
+ JitTest.js_cmd_prefix = prefix
|
|
|
+
|
|
|
+ pb = create_progressbar(num_tests, options)
|
|
|
+ gen = run_all_tests(tests, prefix, pb, shim_options)
|
|
|
+ ok = process_test_results(gen, num_tests, pb, options, slog)
|
|
|
+ return ok
|
|
|
+
|
|
|
+-def get_remote_results(tests, device, prefix, options):
|
|
|
+- try:
|
|
|
+- for i in xrange(0, options.repeat):
|
|
|
+- for test in tests:
|
|
|
+- yield run_test_remote(test, device, prefix, options)
|
|
|
+- except Exception as e:
|
|
|
+- # After a device error, the device is typically in a
|
|
|
+- # state where all further tests will fail so there is no point in
|
|
|
+- # continuing here.
|
|
|
+- sys.stderr.write("Error running remote tests: {}".format(e.message))
|
|
|
+-
|
|
|
+ def push_libs(options, device):
|
|
|
+ # This saves considerable time in pushing unnecessary libraries
|
|
|
+ # to the device but needs to be updated if the dependencies change.
|
|
|
+ required_libs = ['libnss3.so', 'libmozglue.so', 'libnspr4.so',
|
|
|
+ 'libplc4.so', 'libplds4.so']
|
|
|
+
|
|
|
+ for file in os.listdir(options.local_lib):
|
|
|
+ if file in required_libs:
|
|
|
+@@ -716,57 +702,10 @@ def push_libs(options, device):
|
|
|
+ device.push(os.path.join(options.local_lib, file), remote_file)
|
|
|
+
|
|
|
+ def push_progs(options, device, progs):
|
|
|
+ for local_file in progs:
|
|
|
+ remote_file = posixpath.join(options.remote_test_root,
|
|
|
+ os.path.basename(local_file))
|
|
|
+ device.push(local_file, remote_file)
|
|
|
+
|
|
|
+-def run_tests_remote(tests, num_tests, prefix, options, slog):
|
|
|
+- # Setup device with everything needed to run our tests.
|
|
|
+- from mozdevice import ADBAndroid
|
|
|
+- device = ADBAndroid(device=options.device_serial,
|
|
|
+- test_root=options.remote_test_root)
|
|
|
+-
|
|
|
+- # Update the test root to point to our test directory.
|
|
|
+- jit_tests_dir = posixpath.join(options.remote_test_root, 'jit-tests')
|
|
|
+- options.remote_test_root = posixpath.join(jit_tests_dir, 'jit-tests')
|
|
|
+-
|
|
|
+- # Push js shell and libraries.
|
|
|
+- device.rm(jit_tests_dir, force=True, recursive=True)
|
|
|
+- device.mkdir(options.remote_test_root, parents=True)
|
|
|
+- push_libs(options, device)
|
|
|
+- push_progs(options, device, [prefix[0]])
|
|
|
+- device.chmod(options.remote_test_root, recursive=True)
|
|
|
+-
|
|
|
+- JitTest.CacheDir = posixpath.join(options.remote_test_root, '.js-cache')
|
|
|
+- device.mkdir(JitTest.CacheDir)
|
|
|
+-
|
|
|
+- device.push(JS_TESTS_DIR, posixpath.join(jit_tests_dir, 'tests'),
|
|
|
+- timeout=600)
|
|
|
+-
|
|
|
+- device.push(os.path.dirname(TEST_DIR), options.remote_test_root,
|
|
|
+- timeout=600)
|
|
|
+- prefix[0] = os.path.join(options.remote_test_root, 'js')
|
|
|
+-
|
|
|
+- # Run all tests.
|
|
|
+- pb = create_progressbar(num_tests, options)
|
|
|
+- gen = get_remote_results(tests, device, prefix, options)
|
|
|
+- ok = process_test_results(gen, num_tests, pb, options, slog)
|
|
|
+- return ok
|
|
|
+-
|
|
|
+-def platform_might_be_android():
|
|
|
+- try:
|
|
|
+- # The python package for SL4A provides an |android| module.
|
|
|
+- # If that module is present, we're likely in SL4A-python on
|
|
|
+- # device. False positives and negatives are possible,
|
|
|
+- # however.
|
|
|
+- import android # NOQA: F401
|
|
|
+- return True
|
|
|
+- except ImportError:
|
|
|
+- return False
|
|
|
+-
|
|
|
+-def stdio_might_be_broken():
|
|
|
+- return platform_might_be_android()
|
|
|
+-
|
|
|
+ if __name__ == '__main__':
|
|
|
+ print('Use ../jit-test/jit_test.py to run these tests.')
|
|
|
+diff --git a/layout/tools/reftest/moz.build b/layout/tools/reftest/moz.build
|
|
|
+--- a/layout/tools/reftest/moz.build
|
|
|
++++ b/layout/tools/reftest/moz.build
|
|
|
+@@ -16,17 +16,16 @@ FINAL_TARGET_FILES += ['bootstrap.js']
|
|
|
+ TEST_HARNESS_FILES.reftest += [
|
|
|
+ '/build/mobile/remoteautomation.py',
|
|
|
+ '/build/pgo/server-locations.txt',
|
|
|
+ '/testing/mochitest/server.js',
|
|
|
+ 'mach_test_package_commands.py',
|
|
|
+ 'output.py',
|
|
|
+ 'reftest-preferences.js',
|
|
|
+ 'reftestcommandline.py',
|
|
|
+- 'remotereftest.py',
|
|
|
+ 'runreftest.py',
|
|
|
+ ]
|
|
|
+
|
|
|
+ TEST_HARNESS_FILES.reftest.chrome += [
|
|
|
+ 'chrome/binding.xml',
|
|
|
+ 'chrome/userContent.css',
|
|
|
+ ]
|
|
|
+
|
|
|
+diff --git a/layout/tools/reftest/remotereftest.py b/layout/tools/reftest/remotereftest.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/layout/tools/reftest/remotereftest.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,452 +0,0 @@
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
+-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import, print_function
|
|
|
+-
|
|
|
+-import os
|
|
|
+-import posixpath
|
|
|
+-import psutil
|
|
|
+-import signal
|
|
|
+-import sys
|
|
|
+-import tempfile
|
|
|
+-import time
|
|
|
+-import traceback
|
|
|
+-import urllib2
|
|
|
+-from contextlib import closing
|
|
|
+-
|
|
|
+-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
|
|
|
+-
|
|
|
+-# We need to know our current directory so that we can serve our test files from it.
|
|
|
+-SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
|
|
|
+-
|
|
|
+-
|
|
|
+-class RemoteReftestResolver(ReftestResolver):
|
|
|
+- def absManifestPath(self, path):
|
|
|
+- script_abs_path = os.path.join(SCRIPT_DIRECTORY, path)
|
|
|
+- if os.path.exists(script_abs_path):
|
|
|
+- rv = script_abs_path
|
|
|
+- elif os.path.exists(os.path.abspath(path)):
|
|
|
+- rv = os.path.abspath(path)
|
|
|
+- else:
|
|
|
+- print("Could not find manifest %s" % script_abs_path, file=sys.stderr)
|
|
|
+- sys.exit(1)
|
|
|
+- return os.path.normpath(rv)
|
|
|
+-
|
|
|
+- def manifestURL(self, options, path):
|
|
|
+- # Dynamically build the reftest URL if possible, beware that args[0] should exist 'inside'
|
|
|
+- # webroot. It's possible for this url to have a leading "..", but reftest.js will fix that
|
|
|
+- relPath = os.path.relpath(path, SCRIPT_DIRECTORY)
|
|
|
+- return "http://%s:%s/%s" % (options.remoteWebServer, options.httpPort, relPath)
|
|
|
+-
|
|
|
+-
|
|
|
+-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.webServer = options.remoteWebServer
|
|
|
+- self.httpPort = options.httpPort
|
|
|
+- self.scriptDir = scriptDir
|
|
|
+- 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["XPCOM_DEBUG_BREAK"] = "warn"
|
|
|
+- if self.automation.IS_WIN32:
|
|
|
+- env["PATH"] = env["PATH"] + ";" + self.xrePath
|
|
|
+-
|
|
|
+- 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,
|
|
|
+- "server": self.webServer},
|
|
|
+- "-f", os.path.join(self.scriptDir, "server.js")]
|
|
|
+-
|
|
|
+- 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)
|
|
|
+-
|
|
|
+- def ensureReady(self, timeout):
|
|
|
+- assert timeout >= 0
|
|
|
+-
|
|
|
+- 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 | "
|
|
|
+- "Timed out while waiting for server startup.")
|
|
|
+- self.stop()
|
|
|
+- return 1
|
|
|
+-
|
|
|
+- def stop(self):
|
|
|
+- if hasattr(self, '_process'):
|
|
|
+- try:
|
|
|
+- with closing(urllib2.urlopen(self.shutdownURL)) as c:
|
|
|
+- c.read()
|
|
|
+-
|
|
|
+- rtncode = self._process.poll()
|
|
|
+- if (rtncode is None):
|
|
|
+- self._process.terminate()
|
|
|
+- except Exception:
|
|
|
+- self.automation.log.info("Failed to shutdown server at %s" %
|
|
|
+- self.shutdownURL)
|
|
|
+- traceback.print_exc()
|
|
|
+- self._process.kill()
|
|
|
+-
|
|
|
+-
|
|
|
+-class RemoteReftest(RefTest):
|
|
|
+- use_marionette = False
|
|
|
+- resolver_cls = RemoteReftestResolver
|
|
|
+-
|
|
|
+- def __init__(self, options, scriptDir):
|
|
|
+- RefTest.__init__(self, options.suite)
|
|
|
+- self.run_by_manifest = False
|
|
|
+- self.scriptDir = scriptDir
|
|
|
+- 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
|
|
|
+-
|
|
|
+- 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
|
|
|
+-
|
|
|
+- def startWebServer(self, options):
|
|
|
+- """ Create the webserver on the host and start it up """
|
|
|
+- remoteXrePath = options.xrePath
|
|
|
+- remoteUtilityPath = options.utilityPath
|
|
|
+- localAutomation = Automation()
|
|
|
+- localAutomation.IS_WIN32 = False
|
|
|
+- localAutomation.IS_LINUX = False
|
|
|
+- localAutomation.IS_MAC = False
|
|
|
+- localAutomation.UNIXISH = False
|
|
|
+- hostos = sys.platform
|
|
|
+- if (hostos == 'mac' or hostos == 'darwin'):
|
|
|
+- localAutomation.IS_MAC = True
|
|
|
+- elif (hostos == 'linux' or hostos == 'linux2'):
|
|
|
+- localAutomation.IS_LINUX = True
|
|
|
+- localAutomation.UNIXISH = True
|
|
|
+- elif (hostos == 'win32' or hostos == 'win64'):
|
|
|
+- localAutomation.BIN_SUFFIX = ".exe"
|
|
|
+- localAutomation.IS_WIN32 = True
|
|
|
+-
|
|
|
+- paths = [options.xrePath, localAutomation.DIST_BIN]
|
|
|
+- options.xrePath = self.findPath(paths)
|
|
|
+- if options.xrePath is None:
|
|
|
+- print("ERROR: unable to find xulrunner path for %s, "
|
|
|
+- "please specify with --xre-path" % (os.name))
|
|
|
+- return 1
|
|
|
+- paths.append("bin")
|
|
|
+- paths.append(os.path.join("..", "bin"))
|
|
|
+-
|
|
|
+- xpcshell = "xpcshell"
|
|
|
+- if (os.name == "nt"):
|
|
|
+- xpcshell += ".exe"
|
|
|
+-
|
|
|
+- if (options.utilityPath):
|
|
|
+- paths.insert(0, options.utilityPath)
|
|
|
+- options.utilityPath = self.findPath(paths, xpcshell)
|
|
|
+- if options.utilityPath is None:
|
|
|
+- print("ERROR: unable to find utility path for %s, "
|
|
|
+- "please specify with --utility-path" % (os.name))
|
|
|
+- return 1
|
|
|
+-
|
|
|
+- options.serverProfilePath = tempfile.mkdtemp()
|
|
|
+- self.server = ReftestServer(localAutomation, options, self.scriptDir)
|
|
|
+- retVal = self.server.start()
|
|
|
+- if retVal:
|
|
|
+- return retVal
|
|
|
+- retVal = self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
|
|
|
+- if retVal:
|
|
|
+- return retVal
|
|
|
+-
|
|
|
+- options.xrePath = remoteXrePath
|
|
|
+- options.utilityPath = remoteUtilityPath
|
|
|
+- return 0
|
|
|
+-
|
|
|
+- def stopWebServer(self, options):
|
|
|
+- self.server.stop()
|
|
|
+-
|
|
|
+- def killNamedProc(self, pname, orphans=True):
|
|
|
+- """ Kill processes matching the given command name """
|
|
|
+- self.log.info("Checking for %s processes..." % pname)
|
|
|
+-
|
|
|
+- for proc in psutil.process_iter():
|
|
|
+- try:
|
|
|
+- if proc.name() == pname:
|
|
|
+- procd = proc.as_dict(attrs=['pid', 'ppid', 'name', 'username'])
|
|
|
+- if proc.ppid() == 1 or not orphans:
|
|
|
+- self.log.info("killing %s" % procd)
|
|
|
+- try:
|
|
|
+- os.kill(proc.pid, getattr(signal, "SIGKILL", signal.SIGTERM))
|
|
|
+- except Exception as e:
|
|
|
+- self.log.info("Failed to kill process %d: %s" % (proc.pid, str(e)))
|
|
|
+- else:
|
|
|
+- self.log.info("NOT killing %s (not an orphan?)" % procd)
|
|
|
+- except Exception:
|
|
|
+- # may not be able to access process info for all processes
|
|
|
+- continue
|
|
|
+-
|
|
|
+- def createReftestProfile(self, options, **kwargs):
|
|
|
+- profile = RefTest.createReftestProfile(self,
|
|
|
+- options,
|
|
|
+- server=options.remoteWebServer,
|
|
|
+- port=options.httpPort,
|
|
|
+- **kwargs)
|
|
|
+- profileDir = profile.profile
|
|
|
+- prefs = {}
|
|
|
+- prefs["app.update.url.android"] = ""
|
|
|
+- prefs["browser.firstrun.show.localepicker"] = False
|
|
|
+- prefs["reftest.remote"] = True
|
|
|
+- prefs["datareporting.policy.dataSubmissionPolicyBypassAcceptance"] = True
|
|
|
+- # move necko cache to a location that can be cleaned up
|
|
|
+- prefs["browser.cache.disk.parent_directory"] = self.remoteCache
|
|
|
+-
|
|
|
+- prefs["layout.css.devPixelsPerPx"] = "1.0"
|
|
|
+- # 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.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)
|
|
|
+- 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.device.get_logcat(filter_out_regexps=fennecLogcatFilters)
|
|
|
+- print(''.join(logcat))
|
|
|
+- print("Device info:")
|
|
|
+- 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.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:
|
|
|
+- del browserEnv["XPCOM_MEM_BLOAT_LOG"]
|
|
|
+- return browserEnv
|
|
|
+-
|
|
|
+- def runApp(self, options, cmdargs=None, timeout=None, debuggerInfo=None, symbolsPath=None,
|
|
|
+- valgrindPath=None, valgrindArgs=None, valgrindSuppFiles=None, **profileArgs):
|
|
|
+- if cmdargs is None:
|
|
|
+- cmdargs = []
|
|
|
+-
|
|
|
+- if self.use_marionette:
|
|
|
+- cmdargs.append('-marionette')
|
|
|
+-
|
|
|
+- binary = options.app
|
|
|
+- profile = self.createReftestProfile(options, **profileArgs)
|
|
|
+-
|
|
|
+- # browser environment
|
|
|
+- env = self.buildBrowserEnv(options, profile.profile)
|
|
|
+-
|
|
|
+- self.log.info("Running with e10s: {}".format(options.e10s))
|
|
|
+- status, self.lastTestSeen = self.automation.runApp(None, env,
|
|
|
+- binary,
|
|
|
+- profile.profile,
|
|
|
+- cmdargs,
|
|
|
+- utilityPath=options.utilityPath,
|
|
|
+- xrePath=options.xrePath,
|
|
|
+- debuggerInfo=debuggerInfo,
|
|
|
+- symbolsPath=symbolsPath,
|
|
|
+- timeout=timeout)
|
|
|
+-
|
|
|
+- self.cleanup(profile.profile)
|
|
|
+- return status
|
|
|
+-
|
|
|
+- def cleanup(self, profileDir):
|
|
|
+- 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)
|
|
|
+-
|
|
|
+-
|
|
|
+-def run_test_harness(parser, options):
|
|
|
+- 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")))
|
|
|
+-
|
|
|
+- # Despite our efforts to clean up servers started by this script, in practice
|
|
|
+- # we still see infrequent cases where a process is orphaned and interferes
|
|
|
+- # 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.
|
|
|
+- reftest.killNamedProc('ssltunnel')
|
|
|
+- reftest.killNamedProc('xpcshell')
|
|
|
+-
|
|
|
+- # Start the webserver
|
|
|
+- retVal = reftest.startWebServer(options)
|
|
|
+- if retVal:
|
|
|
+- return retVal
|
|
|
+-
|
|
|
+- if options.printDeviceInfo:
|
|
|
+- reftest.printDeviceInfo()
|
|
|
+-
|
|
|
+- retVal = 0
|
|
|
+- try:
|
|
|
+- 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
|
|
|
+-
|
|
|
+- reftest.stopWebServer(options)
|
|
|
+-
|
|
|
+- if options.printDeviceInfo:
|
|
|
+- reftest.printDeviceInfo(printLogcat=True)
|
|
|
+-
|
|
|
+- return retVal
|
|
|
+-
|
|
|
+-
|
|
|
+-if __name__ == "__main__":
|
|
|
+- parser = reftestcommandline.RemoteArgumentsParser()
|
|
|
+- options = parser.parse_args()
|
|
|
+- sys.exit(run_test_harness(parser, options))
|
|
|
+diff --git a/python/mozperftest/setup.py b/python/mozperftest/setup.py
|
|
|
+--- a/python/mozperftest/setup.py
|
|
|
++++ b/python/mozperftest/setup.py
|
|
|
+@@ -4,17 +4,17 @@
|
|
|
+
|
|
|
+ from __future__ import absolute_import
|
|
|
+
|
|
|
+ from setuptools import setup
|
|
|
+
|
|
|
+ PACKAGE_NAME = "mozperftest"
|
|
|
+ PACKAGE_VERSION = "0.1"
|
|
|
+
|
|
|
+-deps = ["mozlog >= 6.0", "mozdevice >= 3.0.2", "mozproxy", "mozinfo"]
|
|
|
++deps = ["mozlog >= 6.0", "mozproxy", "mozinfo"]
|
|
|
+
|
|
|
+ setup(
|
|
|
+ name=PACKAGE_NAME,
|
|
|
+ version=PACKAGE_VERSION,
|
|
|
+ description="Mozilla's mach perftest command",
|
|
|
+ classifiers=[
|
|
|
+ "Programming Language :: Python :: 3.6",
|
|
|
+ ],
|
|
|
+diff --git a/testing/config/mozbase_requirements.txt b/testing/config/mozbase_requirements.txt
|
|
|
+--- a/testing/config/mozbase_requirements.txt
|
|
|
++++ b/testing/config/mozbase_requirements.txt
|
|
|
+@@ -1,12 +1,11 @@
|
|
|
+ ../mozbase/manifestparser
|
|
|
+ ../mozbase/mozcrash
|
|
|
+ ../mozbase/mozdebug
|
|
|
+-../mozbase/mozdevice
|
|
|
+ ../mozbase/mozfile
|
|
|
+ ../mozbase/mozhttpd
|
|
|
+ ../mozbase/mozinfo
|
|
|
+ ../mozbase/mozinstall
|
|
|
+ ../mozbase/mozleak
|
|
|
+ ../mozbase/mozlog
|
|
|
+ ../mozbase/moznetwork
|
|
|
+ ../mozbase/mozprocess
|
|
|
+diff --git a/testing/marionette/harness/requirements.txt b/testing/marionette/harness/requirements.txt
|
|
|
+--- a/testing/marionette/harness/requirements.txt
|
|
|
++++ b/testing/marionette/harness/requirements.txt
|
|
|
+@@ -1,13 +1,12 @@
|
|
|
+ browsermob-proxy >= 0.8.0
|
|
|
+ manifestparser >= 1.1
|
|
|
+ marionette-driver >= 2.8.0
|
|
|
+ mozcrash >= 1.1.0
|
|
|
+-mozdevice >= 3.0.0
|
|
|
+ mozinfo >= 1.0.0
|
|
|
+ mozlog >= 4.0
|
|
|
+ moznetwork >= 0.27
|
|
|
+ mozprocess >= 1.0.0
|
|
|
+ mozprofile >= 2.2.0
|
|
|
+ mozrunner >= 7.4.0
|
|
|
+ moztest >= 0.8
|
|
|
+ mozversion >= 2.1.0
|
|
|
+diff --git a/testing/mochitest/mach_commands.py b/testing/mochitest/mach_commands.py
|
|
|
+--- a/testing/mochitest/mach_commands.py
|
|
|
++++ b/testing/mochitest/mach_commands.py
|
|
|
+@@ -55,26 +55,17 @@ TESTS_NOT_FOUND = '''
|
|
|
+ The mochitest command could not find any mochitests under the following
|
|
|
+ test path(s):
|
|
|
+
|
|
|
+ {}
|
|
|
+
|
|
|
+ Please check spelling and make sure there are mochitests living there.
|
|
|
+ '''.lstrip()
|
|
|
+
|
|
|
+-ROBOCOP_TESTS_NOT_FOUND = '''
|
|
|
+-The robocop command could not find any tests under the following
|
|
|
+-test path(s):
|
|
|
+-
|
|
|
+-{}
|
|
|
+-
|
|
|
+-Please check spelling and make sure the named tests exist.
|
|
|
+-'''.lstrip()
|
|
|
+-
|
|
|
+-SUPPORTED_APPS = ['firefox', 'android']
|
|
|
++SUPPORTED_APPS = ['firefox']
|
|
|
+
|
|
|
+ parser = None
|
|
|
+
|
|
|
+
|
|
|
+ class MochitestRunner(MozbuildObject):
|
|
|
+
|
|
|
+ """Easily run mochitests.
|
|
|
+
|
|
|
+@@ -149,60 +140,16 @@ class MochitestRunner(MozbuildObject):
|
|
|
+ options.keep_open = True
|
|
|
+
|
|
|
+ # We need this to enable colorization of output.
|
|
|
+ self.log_manager.enable_unstructured()
|
|
|
+ result = mochitest.run_test_harness(parser, options)
|
|
|
+ self.log_manager.disable_unstructured()
|
|
|
+ return result
|
|
|
+
|
|
|
+- def run_android_test(self, context, tests, suite=None, **kwargs):
|
|
|
+- host_ret = verify_host_bin()
|
|
|
+- if host_ret != 0:
|
|
|
+- return host_ret
|
|
|
+-
|
|
|
+- import imp
|
|
|
+- path = os.path.join(self.mochitest_dir, 'runtestsremote.py')
|
|
|
+- with open(path, 'r') as fh:
|
|
|
+- imp.load_module('runtestsremote', fh, path,
|
|
|
+- ('.py', 'r', imp.PY_SOURCE))
|
|
|
+- import runtestsremote
|
|
|
+-
|
|
|
+- options = Namespace(**kwargs)
|
|
|
+-
|
|
|
+- from manifestparser import TestManifest
|
|
|
+- if tests and not options.manifestFile:
|
|
|
+- manifest = TestManifest()
|
|
|
+- manifest.tests.extend(tests)
|
|
|
+- options.manifestFile = manifest
|
|
|
+-
|
|
|
+- return runtestsremote.run_test_harness(parser, options)
|
|
|
+-
|
|
|
+- def run_robocop_test(self, context, tests, suite=None, **kwargs):
|
|
|
+- host_ret = verify_host_bin()
|
|
|
+- if host_ret != 0:
|
|
|
+- return host_ret
|
|
|
+-
|
|
|
+- import imp
|
|
|
+- path = os.path.join(self.mochitest_dir, 'runrobocop.py')
|
|
|
+- with open(path, 'r') as fh:
|
|
|
+- imp.load_module('runrobocop', fh, path,
|
|
|
+- ('.py', 'r', imp.PY_SOURCE))
|
|
|
+- import runrobocop
|
|
|
+-
|
|
|
+- options = Namespace(**kwargs)
|
|
|
+-
|
|
|
+- from manifestparser import TestManifest
|
|
|
+- if tests and not options.manifestFile:
|
|
|
+- manifest = TestManifest()
|
|
|
+- manifest.tests.extend(tests)
|
|
|
+- options.manifestFile = manifest
|
|
|
+-
|
|
|
+- return runrobocop.run_test_harness(parser, options)
|
|
|
+-
|
|
|
+ # parser
|
|
|
+
|
|
|
+
|
|
|
+ def setup_argument_parser():
|
|
|
+ build_obj = MozbuildObject.from_environment(cwd=here)
|
|
|
+
|
|
|
+ build_path = os.path.join(build_obj.topobjdir, 'build')
|
|
|
+ if build_path not in sys.path:
|
|
|
+@@ -219,24 +166,16 @@ def setup_argument_parser():
|
|
|
+ path = os.path.join(here, "runtests.py")
|
|
|
+
|
|
|
+ with open(path, 'r') as fh:
|
|
|
+ imp.load_module('mochitest', fh, path,
|
|
|
+ ('.py', 'r', imp.PY_SOURCE))
|
|
|
+
|
|
|
+ from mochitest_options import MochitestArgumentParser
|
|
|
+
|
|
|
+- if conditions.is_android(build_obj):
|
|
|
+- # On Android, check for a connected device (and offer to start an
|
|
|
+- # emulator if appropriate) before running tests. This check must
|
|
|
+- # be done in this admittedly awkward place because
|
|
|
+- # MochitestArgumentParser initialization fails if no device is found.
|
|
|
+- from mozrunner.devices.android_device import verify_android_device
|
|
|
+- verify_android_device(build_obj, install=True, xre=True)
|
|
|
+-
|
|
|
+ global parser
|
|
|
+ parser = MochitestArgumentParser()
|
|
|
+ return parser
|
|
|
+
|
|
|
+
|
|
|
+ def verify_host_bin():
|
|
|
+ # validate MOZ_HOST_BIN environment variables for Android tests
|
|
|
+ MOZ_HOST_BIN = os.environ.get('MOZ_HOST_BIN')
|
|
|
+@@ -373,22 +312,17 @@ class MachCommands(MachCommandBase):
|
|
|
+ reason = 'requires {}'.format(' or '.join(apps))
|
|
|
+ else:
|
|
|
+ reason = 'excluded by the command line'
|
|
|
+ msg.append(' mochitest -f {} ({})'.format(name, reason))
|
|
|
+ print(SUPPORTED_TESTS_NOT_FOUND.format(
|
|
|
+ buildapp, '\n'.join(sorted(msg))))
|
|
|
+ return 1
|
|
|
+
|
|
|
+- if buildapp == 'android':
|
|
|
+- from mozrunner.devices.android_device import grant_runtime_permissions
|
|
|
+- grant_runtime_permissions(self)
|
|
|
+- run_mochitest = mochitest.run_android_test
|
|
|
+- else:
|
|
|
+- run_mochitest = mochitest.run_desktop_test
|
|
|
++ run_mochitest = mochitest.run_desktop_test
|
|
|
+
|
|
|
+ overall = None
|
|
|
+ for (flavor, subsuite), tests in sorted(suites.items()):
|
|
|
+ fobj = ALL_FLAVORS[flavor]
|
|
|
+
|
|
|
+ harness_args = kwargs.copy()
|
|
|
+ harness_args['subsuite'] = subsuite
|
|
|
+ harness_args.update(fobj.get('extra_args', {}))
|
|
|
+@@ -408,59 +342,16 @@ class MachCommands(MachCommandBase):
|
|
|
+
|
|
|
+ # Only shutdown the logger if we created it
|
|
|
+ if kwargs['log'].name == 'mach-mochitest':
|
|
|
+ kwargs['log'].shutdown()
|
|
|
+
|
|
|
+ return overall
|
|
|
+
|
|
|
+
|
|
|
+-@CommandProvider
|
|
|
+-class RobocopCommands(MachCommandBase):
|
|
|
+-
|
|
|
+- @Command('robocop', category='testing',
|
|
|
+- conditions=[conditions.is_android],
|
|
|
+- description='Run a Robocop test.',
|
|
|
+- parser=setup_argument_parser)
|
|
|
+- @CommandArgument('--serve', default=False, action='store_true',
|
|
|
+- help='Run no tests but start the mochi.test web server '
|
|
|
+- 'and launch Fennec with a test profile.')
|
|
|
+- def run_robocop(self, serve=False, **kwargs):
|
|
|
+- if serve:
|
|
|
+- kwargs['autorun'] = False
|
|
|
+-
|
|
|
+- if not kwargs.get('robocopIni'):
|
|
|
+- kwargs['robocopIni'] = os.path.join(self.topobjdir, '_tests', 'testing',
|
|
|
+- 'mochitest', 'robocop.ini')
|
|
|
+-
|
|
|
+- from mozbuild.controller.building import BuildDriver
|
|
|
+- self._ensure_state_subdir_exists('.')
|
|
|
+-
|
|
|
+- test_paths = kwargs['test_paths']
|
|
|
+- kwargs['test_paths'] = []
|
|
|
+-
|
|
|
+- from moztest.resolve import TestResolver
|
|
|
+- resolver = self._spawn(TestResolver)
|
|
|
+- tests = list(resolver.resolve_tests(paths=test_paths, cwd=self._mach_context.cwd,
|
|
|
+- flavor='instrumentation', subsuite='robocop'))
|
|
|
+- driver = self._spawn(BuildDriver)
|
|
|
+- driver.install_tests(tests)
|
|
|
+-
|
|
|
+- if len(tests) < 1:
|
|
|
+- print(ROBOCOP_TESTS_NOT_FOUND.format('\n'.join(
|
|
|
+- sorted(list(test_paths)))))
|
|
|
+- return 1
|
|
|
+-
|
|
|
+- from mozrunner.devices.android_device import grant_runtime_permissions
|
|
|
+- grant_runtime_permissions(self)
|
|
|
+-
|
|
|
+- mochitest = self._spawn(MochitestRunner)
|
|
|
+- return mochitest.run_robocop_test(self._mach_context, tests, 'robocop', **kwargs)
|
|
|
+-
|
|
|
+-
|
|
|
+ # NOTE python/mach/mach/commands/commandinfo.py references this function
|
|
|
+ # by name. If this function is renamed or removed, that file should
|
|
|
+ # be updated accordingly as well.
|
|
|
+ def REMOVED(cls):
|
|
|
+ """Command no longer exists! Use |mach mochitest| instead.
|
|
|
+
|
|
|
+ The |mach mochitest| command will automatically detect which flavors and
|
|
|
+ subsuites exist in a given directory. If desired, flavors and subsuites
|
|
|
+diff --git a/testing/mochitest/moz.build b/testing/mochitest/moz.build
|
|
|
+--- a/testing/mochitest/moz.build
|
|
|
++++ b/testing/mochitest/moz.build
|
|
|
+@@ -52,19 +52,17 @@ TEST_HARNESS_FILES.testing.mochitest +=
|
|
|
+ 'leaks.py',
|
|
|
+ 'mach_test_package_commands.py',
|
|
|
+ 'manifest.webapp',
|
|
|
+ 'manifestLibrary.js',
|
|
|
+ 'mochitest_options.py',
|
|
|
+ 'nested_setup.js',
|
|
|
+ 'pywebsocket_wrapper.py',
|
|
|
+ 'redirect.html',
|
|
|
+- 'runrobocop.py',
|
|
|
+ 'runtests.py',
|
|
|
+- 'runtestsremote.py',
|
|
|
+ 'server.js',
|
|
|
+ 'start_desktop.js',
|
|
|
+ ]
|
|
|
+
|
|
|
+ TEST_HARNESS_FILES.testing.mochitest.embed += [
|
|
|
+ 'embed/Xm5i5kbIXzc',
|
|
|
+ 'embed/Xm5i5kbIXzc^headers^',
|
|
|
+ ]
|
|
|
+diff --git a/testing/mochitest/runrobocop.py b/testing/mochitest/runrobocop.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mochitest/runrobocop.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,572 +0,0 @@
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
+-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import print_function
|
|
|
+-
|
|
|
+-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(
|
|
|
+- os.path.dirname(__file__))))
|
|
|
+-
|
|
|
+-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
|
|
|
+-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.
|
|
|
+- """
|
|
|
+- # Some robocop tests run for >60 seconds without generating any output.
|
|
|
+- NO_OUTPUT_TIMEOUT = 180
|
|
|
+-
|
|
|
+- def __init__(self, options, message_logger):
|
|
|
+- """
|
|
|
+- Simple one-time initialization.
|
|
|
+- """
|
|
|
+- MochitestDesktop.__init__(self, options.flavor, vars(options))
|
|
|
+-
|
|
|
+- 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
|
|
|
+-
|
|
|
+- 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.remoteScreenshots = "/mnt/sdcard/Robotium-Screenshots"
|
|
|
+- self.remoteMozLog = posixpath.join(options.remoteTestRoot, "mozlog")
|
|
|
+-
|
|
|
+- self.localLog = options.logFile
|
|
|
+- self.localProfile = None
|
|
|
+- self.certdbNew = True
|
|
|
+- self.passed = 0
|
|
|
+- self.failed = 0
|
|
|
+- self.todo = 0
|
|
|
+-
|
|
|
+- def startup(self):
|
|
|
+- """
|
|
|
+- Second-stage initialization: One-time initialization which may require cleanup.
|
|
|
+- """
|
|
|
+- # Despite our efforts to clean up servers started by this script, in practice
|
|
|
+- # we still see infrequent cases where a process is orphaned and interferes
|
|
|
+- # 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.device.pkill(procName)
|
|
|
+- if self.device.process_exist(procName):
|
|
|
+- self.log.warning("unable to kill %s before running tests!" % procName)
|
|
|
+- 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.
|
|
|
+- 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)
|
|
|
+- if 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
|
|
|
+- self.startServers(
|
|
|
+- self.options,
|
|
|
+- debuggerInfo=None,
|
|
|
+- ignoreSSLTunnelExts=True)
|
|
|
+- self.log.debug("Servers started")
|
|
|
+-
|
|
|
+- def cleanup(self):
|
|
|
+- """
|
|
|
+- Cleanup at end of job run.
|
|
|
+- """
|
|
|
+- self.log.debug("Cleaning up...")
|
|
|
+- self.stopServers()
|
|
|
+- 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." %
|
|
|
+- uploadDir)
|
|
|
+- self.device.pull(self.remoteMozLog, uploadDir)
|
|
|
+- self.device.pull(self.remoteScreenshots, uploadDir)
|
|
|
+- MochitestDesktop.cleanup(self, self.options)
|
|
|
+- if self.localProfile:
|
|
|
+- 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)):
|
|
|
+- return path
|
|
|
+- return None
|
|
|
+-
|
|
|
+- def makeLocalAutomation(self):
|
|
|
+- localAutomation = Automation()
|
|
|
+- localAutomation.IS_WIN32 = False
|
|
|
+- localAutomation.IS_LINUX = False
|
|
|
+- localAutomation.IS_MAC = False
|
|
|
+- localAutomation.UNIXISH = False
|
|
|
+- hostos = sys.platform
|
|
|
+- if (hostos == 'mac' or hostos == 'darwin'):
|
|
|
+- localAutomation.IS_MAC = True
|
|
|
+- elif (hostos == 'linux' or hostos == 'linux2'):
|
|
|
+- localAutomation.IS_LINUX = True
|
|
|
+- localAutomation.UNIXISH = True
|
|
|
+- elif (hostos == 'win32' or hostos == 'win64'):
|
|
|
+- localAutomation.BIN_SUFFIX = ".exe"
|
|
|
+- localAutomation.IS_WIN32 = True
|
|
|
+- return localAutomation
|
|
|
+-
|
|
|
+- def setupLocalPaths(self):
|
|
|
+- """
|
|
|
+- Setup xrePath and utilityPath and verify xpcshell.
|
|
|
+-
|
|
|
+- This is similar to switchToLocalPaths in runtestsremote.py.
|
|
|
+- """
|
|
|
+- localAutomation = self.makeLocalAutomation()
|
|
|
+- paths = [
|
|
|
+- self.options.xrePath,
|
|
|
+- localAutomation.DIST_BIN
|
|
|
+- ]
|
|
|
+- self.options.xrePath = self.findPath(paths)
|
|
|
+- if self.options.xrePath is None:
|
|
|
+- self.log.error(
|
|
|
+- "unable to find xulrunner path for %s, please specify with --xre-path" %
|
|
|
+- os.name)
|
|
|
+- sys.exit(1)
|
|
|
+- self.log.debug("using xre path %s" % self.options.xrePath)
|
|
|
+- xpcshell = "xpcshell"
|
|
|
+- if (os.name == "nt"):
|
|
|
+- xpcshell += ".exe"
|
|
|
+- if self.options.utilityPath:
|
|
|
+- paths = [self.options.utilityPath, self.options.xrePath]
|
|
|
+- else:
|
|
|
+- paths = [self.options.xrePath]
|
|
|
+- self.options.utilityPath = self.findPath(paths, xpcshell)
|
|
|
+- if self.options.utilityPath is None:
|
|
|
+- self.log.error(
|
|
|
+- "unable to find utility path for %s, please specify with --utility-path" %
|
|
|
+- os.name)
|
|
|
+- sys.exit(1)
|
|
|
+- self.log.debug("using utility path %s" % self.options.utilityPath)
|
|
|
+- xpcshell_path = os.path.join(self.options.utilityPath, xpcshell)
|
|
|
+- if localAutomation.elf_arm(xpcshell_path):
|
|
|
+- self.log.error('xpcshell at %s is an ARM binary; please use '
|
|
|
+- 'the --utility-path argument to specify the path '
|
|
|
+- 'to a desktop version.' % xpcshell_path)
|
|
|
+- sys.exit(1)
|
|
|
+- self.log.debug("xpcshell found at %s" % xpcshell_path)
|
|
|
+-
|
|
|
+- def buildProfile(self):
|
|
|
+- """
|
|
|
+- Build a profile locally, keep it locally for use by servers and
|
|
|
+- push a copy to the remote profile-copy directory.
|
|
|
+-
|
|
|
+- This is similar to buildProfile in runtestsremote.py.
|
|
|
+- """
|
|
|
+- self.options.extraPrefs.append('browser.search.suggest.enabled=true')
|
|
|
+- self.options.extraPrefs.append('browser.search.suggest.prompted=true')
|
|
|
+- self.options.extraPrefs.append('layout.css.devPixelsPerPx=1.0')
|
|
|
+- self.options.extraPrefs.append('browser.chrome.dynamictoolbar=false')
|
|
|
+- self.options.extraPrefs.append('browser.snippets.enabled=false')
|
|
|
+- self.options.extraPrefs.append('extensions.autoupdate.enabled=false')
|
|
|
+-
|
|
|
+- # Override the telemetry init delay for integration testing.
|
|
|
+- self.options.extraPrefs.append('toolkit.telemetry.initDelay=1')
|
|
|
+-
|
|
|
+- self.options.extensionsToExclude.extend([
|
|
|
+- 'mochikit@mozilla.org',
|
|
|
+- 'worker-test@mozilla.org.xpi',
|
|
|
+- 'workerbootstrap-test@mozilla.org.xpi',
|
|
|
+- 'indexedDB-test@mozilla.org.xpi',
|
|
|
+- ])
|
|
|
+-
|
|
|
+- 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.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.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)
|
|
|
+- start_found = False
|
|
|
+- end_found = False
|
|
|
+- fail_found = False
|
|
|
+- for line in data:
|
|
|
+- try:
|
|
|
+- message = json.loads(line)
|
|
|
+- if not isinstance(message, dict) or 'action' not in message:
|
|
|
+- continue
|
|
|
+- except ValueError:
|
|
|
+- continue
|
|
|
+- if message['action'] == 'test_end':
|
|
|
+- end_found = True
|
|
|
+- start_found = False
|
|
|
+- break
|
|
|
+- if start_found and not end_found:
|
|
|
+- if 'status' in message:
|
|
|
+- if 'expected' in message:
|
|
|
+- self.failed += 1
|
|
|
+- elif message['status'] == 'PASS':
|
|
|
+- self.passed += 1
|
|
|
+- elif message['status'] == 'FAIL':
|
|
|
+- self.todo += 1
|
|
|
+- if message['action'] == 'test_start':
|
|
|
+- start_found = True
|
|
|
+- if 'expected' in message:
|
|
|
+- fail_found = True
|
|
|
+- result = 0
|
|
|
+- if fail_found:
|
|
|
+- result = 1
|
|
|
+- if not end_found:
|
|
|
+- self.log.info(
|
|
|
+- "PROCESS-CRASH | Automation Error: Missing end of test marker (process crashed?)")
|
|
|
+- result = 1
|
|
|
+- return result
|
|
|
+-
|
|
|
+- def logTestSummary(self):
|
|
|
+- """
|
|
|
+- Print a summary of all tests run to stdout, for treeherder parsing
|
|
|
+- (logging via self.log does not work here).
|
|
|
+- """
|
|
|
+- print("0 INFO TEST-START | Shutdown")
|
|
|
+- print("1 INFO Passed: %s" % (self.passed))
|
|
|
+- print("2 INFO Failed: %s" % (self.failed))
|
|
|
+- print("3 INFO Todo: %s" % (self.todo))
|
|
|
+- print("4 INFO SimpleTest FINISHED")
|
|
|
+- if self.failed > 0:
|
|
|
+- return 1
|
|
|
+- return 0
|
|
|
+-
|
|
|
+- 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.device.get_logcat(
|
|
|
+- filterOutRegexps=fennecLogcatFilters)
|
|
|
+- self.log.info(
|
|
|
+- '\n' +
|
|
|
+- ''.join(logcat).decode(
|
|
|
+- 'utf-8',
|
|
|
+- 'replace'))
|
|
|
+- self.log.info("Device info:")
|
|
|
+- 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.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.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():
|
|
|
+- try:
|
|
|
+- value.index(',')
|
|
|
+- self.log.error("setupRobotiumConfig: browserEnv - Found a ',' "
|
|
|
+- "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.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.
|
|
|
+- """
|
|
|
+- browserEnv = self.environment(
|
|
|
+- xrePath=None,
|
|
|
+- debugger=None)
|
|
|
+- # remove desktop environment not used on device
|
|
|
+- if "XPCOM_MEM_BLOAT_LOG" in browserEnv:
|
|
|
+- del browserEnv["XPCOM_MEM_BLOAT_LOG"]
|
|
|
+- browserEnv["MOZ_LOG_FILE"] = os.path.join(
|
|
|
+- self.remoteMozLog,
|
|
|
+- self.mozLogName)
|
|
|
+-
|
|
|
+- try:
|
|
|
+- browserEnv.update(
|
|
|
+- dict(
|
|
|
+- parseKeyValue(
|
|
|
+- self.options.environment,
|
|
|
+- context='--setenv')))
|
|
|
+- except KeyValueParseError as e:
|
|
|
+- self.log.error(str(e))
|
|
|
+- return None
|
|
|
+-
|
|
|
+- return browserEnv
|
|
|
+-
|
|
|
+- def runSingleTest(self, test):
|
|
|
+- """
|
|
|
+- 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",
|
|
|
+- "-e", "quit_and_finish", "1",
|
|
|
+- "-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"]
|
|
|
+- 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.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.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(
|
|
|
+- "Automation Error: Exception caught while running tests")
|
|
|
+- traceback.print_exc()
|
|
|
+- result = 1
|
|
|
+- self.log.debug("Test %s completes with status %d (log status %d)" %
|
|
|
+- (test['name'], int(result), int(log_result)))
|
|
|
+- return result
|
|
|
+-
|
|
|
+- def runTests(self):
|
|
|
+- self.startup()
|
|
|
+- if isinstance(self.options.manifestFile, TestManifest):
|
|
|
+- mp = self.options.manifestFile
|
|
|
+- else:
|
|
|
+- mp = TestManifest(strict=False)
|
|
|
+- 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
|
|
|
+- # the httpd server, but not actually run a test.
|
|
|
+- self.options.test_paths = [robocop_tests[0]['name']]
|
|
|
+- active_tests = []
|
|
|
+- for test in robocop_tests:
|
|
|
+- if self.options.test_paths and test['name'] not in self.options.test_paths:
|
|
|
+- continue
|
|
|
+- if 'disabled' in test:
|
|
|
+- self.log.info('TEST-INFO | skipping %s | %s' %
|
|
|
+- (test['name'], test['disabled']))
|
|
|
+- continue
|
|
|
+- active_tests.append(test)
|
|
|
+-
|
|
|
+- tests_by_manifest = defaultdict(list)
|
|
|
+- for test in active_tests:
|
|
|
+- tests_by_manifest[test['manifest']].append(test['name'])
|
|
|
+- self.log.suite_start(tests_by_manifest)
|
|
|
+-
|
|
|
+- worstTestResult = None
|
|
|
+- for test in active_tests:
|
|
|
+- result = self.runSingleTest(test)
|
|
|
+- if worstTestResult is None or worstTestResult == 0:
|
|
|
+- worstTestResult = result
|
|
|
+- if worstTestResult is None:
|
|
|
+- self.log.warning(
|
|
|
+- "No tests run. Did you pass an invalid TEST_PATH?")
|
|
|
+- worstTestResult = 1
|
|
|
+- else:
|
|
|
+- print("INFO | runtests.py | Test summary: start.")
|
|
|
+- logResult = self.logTestSummary()
|
|
|
+- print("INFO | runtests.py | Test summary: end.")
|
|
|
+- if worstTestResult == 0:
|
|
|
+- worstTestResult = logResult
|
|
|
+- return worstTestResult
|
|
|
+-
|
|
|
+-
|
|
|
+-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)
|
|
|
+- runResult = -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:
|
|
|
+- robocop.log.info("runrobocop.py | Received keyboard interrupt")
|
|
|
+- runResult = -1
|
|
|
+- except Exception:
|
|
|
+- traceback.print_exc()
|
|
|
+- robocop.log.error(
|
|
|
+- "runrobocop.py | Received unexpected exception while running tests")
|
|
|
+- runResult = 1
|
|
|
+- finally:
|
|
|
+- try:
|
|
|
+- robocop.cleanup()
|
|
|
+- except Exception:
|
|
|
+- # ignore device error while cleaning up
|
|
|
+- 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)
|
|
|
+-
|
|
|
+-
|
|
|
+-if __name__ == "__main__":
|
|
|
+- sys.exit(main())
|
|
|
+diff --git a/testing/mochitest/runtestsremote.py b/testing/mochitest/runtestsremote.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mochitest/runtestsremote.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,394 +0,0 @@
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
+-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-import os
|
|
|
+-import 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
|
|
|
+-
|
|
|
+-from mozdevice import ADBAndroid
|
|
|
+-import mozinfo
|
|
|
+-
|
|
|
+-SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
|
|
|
+-
|
|
|
+-
|
|
|
+-class MochiRemote(MochitestDesktop):
|
|
|
+- localProfile = None
|
|
|
+- logMessages = []
|
|
|
+-
|
|
|
+- 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.certdbNew = True
|
|
|
+- self.chromePushed = False
|
|
|
+- 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.device.rm(self.remoteChromeTestDir, force=True, recursive=True)
|
|
|
+- self.chromePushed = False
|
|
|
+- 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)
|
|
|
+- if os.path.exists(self.getFullPath(p)):
|
|
|
+- return path
|
|
|
+- return None
|
|
|
+-
|
|
|
+- def makeLocalAutomation(self):
|
|
|
+- localAutomation = Automation()
|
|
|
+- localAutomation.IS_WIN32 = False
|
|
|
+- localAutomation.IS_LINUX = False
|
|
|
+- localAutomation.IS_MAC = False
|
|
|
+- localAutomation.UNIXISH = False
|
|
|
+- hostos = sys.platform
|
|
|
+- if (hostos == 'mac' or hostos == 'darwin'):
|
|
|
+- localAutomation.IS_MAC = True
|
|
|
+- elif (hostos == 'linux' or hostos == 'linux2'):
|
|
|
+- localAutomation.IS_LINUX = True
|
|
|
+- localAutomation.UNIXISH = True
|
|
|
+- elif (hostos == 'win32' or hostos == 'win64'):
|
|
|
+- localAutomation.BIN_SUFFIX = ".exe"
|
|
|
+- localAutomation.IS_WIN32 = True
|
|
|
+- return localAutomation
|
|
|
+-
|
|
|
+- # This seems kludgy, but this class uses paths from the remote host in the
|
|
|
+- # options, except when calling up to the base class, which doesn't
|
|
|
+- # understand the distinction. This switches out the remote values for local
|
|
|
+- # ones that the base class understands. This is necessary for the web
|
|
|
+- # server, SSL tunnel and profile building functions.
|
|
|
+- def switchToLocalPaths(self, options):
|
|
|
+- """ Set local paths in the options, return a function that will restore remote values """
|
|
|
+- remoteXrePath = options.xrePath
|
|
|
+- remoteProfilePath = options.profilePath
|
|
|
+- remoteUtilityPath = options.utilityPath
|
|
|
+-
|
|
|
+- localAutomation = self.makeLocalAutomation()
|
|
|
+- paths = [
|
|
|
+- options.xrePath,
|
|
|
+- localAutomation.DIST_BIN,
|
|
|
+- ]
|
|
|
+- options.xrePath = self.findPath(paths)
|
|
|
+- if options.xrePath is None:
|
|
|
+- self.log.error(
|
|
|
+- "unable to find xulrunner path for %s, please specify with --xre-path" %
|
|
|
+- os.name)
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- xpcshell = "xpcshell"
|
|
|
+- if (os.name == "nt"):
|
|
|
+- xpcshell += ".exe"
|
|
|
+-
|
|
|
+- if options.utilityPath:
|
|
|
+- paths = [options.utilityPath, options.xrePath]
|
|
|
+- else:
|
|
|
+- paths = [options.xrePath]
|
|
|
+- options.utilityPath = self.findPath(paths, xpcshell)
|
|
|
+-
|
|
|
+- if options.utilityPath is None:
|
|
|
+- self.log.error(
|
|
|
+- "unable to find utility path for %s, please specify with --utility-path" %
|
|
|
+- os.name)
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- xpcshell_path = os.path.join(options.utilityPath, xpcshell)
|
|
|
+- if localAutomation.elf_arm(xpcshell_path):
|
|
|
+- self.log.error('xpcshell at %s is an ARM binary; please use '
|
|
|
+- 'the --utility-path argument to specify the path '
|
|
|
+- 'to a desktop version.' % xpcshell_path)
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- if self.localProfile:
|
|
|
+- options.profilePath = self.localProfile
|
|
|
+- else:
|
|
|
+- options.profilePath = None
|
|
|
+-
|
|
|
+- def fixup():
|
|
|
+- options.xrePath = remoteXrePath
|
|
|
+- options.utilityPath = remoteUtilityPath
|
|
|
+- options.profilePath = remoteProfilePath
|
|
|
+-
|
|
|
+- return fixup
|
|
|
+-
|
|
|
+- def startServers(self, options, debuggerInfo):
|
|
|
+- """ Create the servers on the host and start them up """
|
|
|
+- restoreRemotePaths = self.switchToLocalPaths(options)
|
|
|
+- # ignoreSSLTunnelExts is a workaround for bug 1109310
|
|
|
+- MochitestDesktop.startServers(
|
|
|
+- self,
|
|
|
+- options,
|
|
|
+- debuggerInfo,
|
|
|
+- ignoreSSLTunnelExts=True)
|
|
|
+- restoreRemotePaths()
|
|
|
+-
|
|
|
+- def buildProfile(self, options):
|
|
|
+- restoreRemotePaths = self.switchToLocalPaths(options)
|
|
|
+- if options.testingModulesDir:
|
|
|
+- try:
|
|
|
+- 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)
|
|
|
+- if savedTestingModulesDir:
|
|
|
+- options.testingModulesDir = savedTestingModulesDir
|
|
|
+- self.localProfile = options.profilePath
|
|
|
+-
|
|
|
+- restoreRemotePaths()
|
|
|
+- options.profilePath = self.remoteProfile
|
|
|
+- return manifest
|
|
|
+-
|
|
|
+- def addChromeToProfile(self, options):
|
|
|
+- manifest = MochitestDesktop.addChromeToProfile(self, options)
|
|
|
+-
|
|
|
+- # Support Firefox (browser), SeaMonkey (navigator), and Webapp Runtime (webapp).
|
|
|
+- if options.flavor == 'chrome':
|
|
|
+- # append overlay to chrome.manifest
|
|
|
+- chrome = ("overlay chrome://browser/content/browser.xul "
|
|
|
+- "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):
|
|
|
+- 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.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 = 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.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.device.get_logcat(
|
|
|
+- filterOutRegexps=fennecLogcatFilters)
|
|
|
+- self.log.info(
|
|
|
+- '\n' +
|
|
|
+- ''.join(logcat).decode(
|
|
|
+- 'utf-8',
|
|
|
+- 'replace'))
|
|
|
+- self.log.info("Device info:")
|
|
|
+- 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.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,
|
|
|
+- options,
|
|
|
+- debugger=debugger)
|
|
|
+- # remove desktop environment not used on device
|
|
|
+- if "XPCOM_MEM_BLOAT_LOG" in browserEnv:
|
|
|
+- del browserEnv["XPCOM_MEM_BLOAT_LOG"]
|
|
|
+- # override mozLogs to avoid processing in MochitestDesktop base class
|
|
|
+- self.mozLogs = None
|
|
|
+- browserEnv["MOZ_LOG_FILE"] = os.path.join(
|
|
|
+- self.remoteMozLog,
|
|
|
+- self.mozLogName)
|
|
|
+- if options.dmd:
|
|
|
+- browserEnv['DMD'] = '1'
|
|
|
+- return browserEnv
|
|
|
+-
|
|
|
+- def runApp(self, *args, **kwargs):
|
|
|
+- """front-end automation.py's `runApp` functionality until FennecRunner is written"""
|
|
|
+-
|
|
|
+- # 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)
|
|
|
+- 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)
|
|
|
+-
|
|
|
+- 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')
|
|
|
+-
|
|
|
+- mochitest = MochiRemote(options)
|
|
|
+-
|
|
|
+- if options.log_mach is None:
|
|
|
+- mochitest.printDeviceInfo()
|
|
|
+-
|
|
|
+- try:
|
|
|
+- if options.verify:
|
|
|
+- retVal = mochitest.verifyTests(options)
|
|
|
+- else:
|
|
|
+- retVal = mochitest.runTests(options)
|
|
|
+- except Exception:
|
|
|
+- mochitest.log.error("Automation Error: Exception caught while running tests")
|
|
|
+- traceback.print_exc()
|
|
|
+- try:
|
|
|
+- mochitest.cleanup(options)
|
|
|
+- except Exception:
|
|
|
+- # device error cleaning up... oh well!
|
|
|
+- traceback.print_exc()
|
|
|
+- retVal = 1
|
|
|
+-
|
|
|
+- if options.log_mach is None:
|
|
|
+- mochitest.printDeviceInfo(printLogcat=True)
|
|
|
+-
|
|
|
+- mochitest.message_logger.finish()
|
|
|
+-
|
|
|
+- return retVal
|
|
|
+-
|
|
|
+-
|
|
|
+-def main(args=sys.argv[1:]):
|
|
|
+- parser = MochitestArgumentParser(app='android')
|
|
|
+- options = parser.parse_args(args)
|
|
|
+-
|
|
|
+- return run_test_harness(parser, options)
|
|
|
+-
|
|
|
+-
|
|
|
+-if __name__ == "__main__":
|
|
|
+- sys.exit(main())
|
|
|
+diff --git a/testing/mozbase/moz.build b/testing/mozbase/moz.build
|
|
|
+--- a/testing/mozbase/moz.build
|
|
|
++++ b/testing/mozbase/moz.build
|
|
|
+@@ -20,17 +20,16 @@ PYTHON_UNITTEST_MANIFESTS += [
|
|
|
+ 'moztest/tests/manifest.ini',
|
|
|
+ 'mozversion/tests/manifest.ini',
|
|
|
+ ]
|
|
|
+
|
|
|
+ python_modules = [
|
|
|
+ 'manifestparser',
|
|
|
+ 'mozcrash',
|
|
|
+ 'mozdebug',
|
|
|
+- 'mozdevice',
|
|
|
+ 'mozfile',
|
|
|
+ 'mozhttpd',
|
|
|
+ 'mozinfo',
|
|
|
+ 'mozinstall',
|
|
|
+ 'mozleak',
|
|
|
+ 'mozlog',
|
|
|
+ 'moznetwork',
|
|
|
+ 'mozprocess',
|
|
|
+diff --git a/testing/mozbase/mozdevice/adb_tests/test_device_running_adb_as_root.py b/testing/mozbase/mozdevice/adb_tests/test_device_running_adb_as_root.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/adb_tests/test_device_running_adb_as_root.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,52 +0,0 @@
|
|
|
+-"""
|
|
|
+- This test is to test devices that adbd does not get started as root.
|
|
|
+- Specifically devices that have ro.secure == 1 and ro.debuggable == 1
|
|
|
+-
|
|
|
+- Running this test case requires various reboots which makes it a
|
|
|
+- very slow test case to run.
|
|
|
+-"""
|
|
|
+-
|
|
|
+-from __future__ import absolute_import, print_function
|
|
|
+-
|
|
|
+-import unittest
|
|
|
+-import sys
|
|
|
+-
|
|
|
+-from mozdevice import DeviceManagerADB
|
|
|
+-
|
|
|
+-
|
|
|
+-class TestFileOperations(unittest.TestCase):
|
|
|
+-
|
|
|
+- def setUp(self):
|
|
|
+- dm = DeviceManagerADB()
|
|
|
+- dm.reboot(wait=True)
|
|
|
+-
|
|
|
+- def test_run_adb_as_root_parameter(self):
|
|
|
+- dm = DeviceManagerADB()
|
|
|
+- self.assertTrue(dm.processInfo("adbd")[2] != "root")
|
|
|
+- dm = DeviceManagerADB(runAdbAsRoot=True)
|
|
|
+- self.assertTrue(dm.processInfo("adbd")[2] == "root")
|
|
|
+-
|
|
|
+- def test_after_reboot_adb_runs_as_root(self):
|
|
|
+- dm = DeviceManagerADB(runAdbAsRoot=True)
|
|
|
+- self.assertTrue(dm.processInfo("adbd")[2] == "root")
|
|
|
+- dm.reboot(wait=True)
|
|
|
+- self.assertTrue(dm.processInfo("adbd")[2] == "root")
|
|
|
+-
|
|
|
+- def tearDown(self):
|
|
|
+- dm = DeviceManagerADB()
|
|
|
+- dm.reboot()
|
|
|
+-
|
|
|
+-
|
|
|
+-if __name__ == "__main__":
|
|
|
+- dm = DeviceManagerADB()
|
|
|
+- if not dm.devices():
|
|
|
+- print("There are no connected adb devices")
|
|
|
+- sys.exit(1)
|
|
|
+- else:
|
|
|
+- if not (int(dm._runCmd(["shell", "getprop", "ro.secure"]).output[0]) and
|
|
|
+- int(dm._runCmd(["shell", "getprop", "ro.debuggable"]).output[0])):
|
|
|
+- print("This test case is meant for devices with devices that start "
|
|
|
+- "adbd as non-root and allows for adbd to be restarted as root.")
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- unittest.main()
|
|
|
+diff --git a/testing/mozbase/mozdevice/adb_tests/test_devicemanagerADB.py b/testing/mozbase/mozdevice/adb_tests/test_devicemanagerADB.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/adb_tests/test_devicemanagerADB.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,220 +0,0 @@
|
|
|
+-"""
|
|
|
+- Info:
|
|
|
+- This tests DeviceManagerADB with a real device
|
|
|
+-
|
|
|
+- Requirements:
|
|
|
+- - You must have a device connected
|
|
|
+- - It should be listed under 'adb devices'
|
|
|
+-
|
|
|
+- Notes:
|
|
|
+- - Not all functions have been covered.
|
|
|
+- In particular, functions from the parent class
|
|
|
+- - No testing of properties is done
|
|
|
+- - The test case are very simple and it could be
|
|
|
+- done with deeper inspection of the return values
|
|
|
+-
|
|
|
+- Author(s):
|
|
|
+- - Armen Zambrano <armenzg@mozilla.com>
|
|
|
+-
|
|
|
+- Functions that are not being tested:
|
|
|
+- - launchProcess - DEPRECATED
|
|
|
+- - getIP
|
|
|
+- - recordLogcat
|
|
|
+- - saveScreenshot
|
|
|
+- - validateDir
|
|
|
+- - mkDirs
|
|
|
+- - getDeviceRoot
|
|
|
+- - shellCheckOutput
|
|
|
+- - processExist
|
|
|
+-
|
|
|
+- I assume these functions are only useful for Android
|
|
|
+- - getAppRoot()
|
|
|
+- - updateApp()
|
|
|
+- - uninstallApp()
|
|
|
+- - uninstallAppAndReboot()
|
|
|
+-"""
|
|
|
+-from __future__ import absolute_import, print_function
|
|
|
+-
|
|
|
+-import os
|
|
|
+-import re
|
|
|
+-import socket
|
|
|
+-import sys
|
|
|
+-import tempfile
|
|
|
+-import unittest
|
|
|
+-from StringIO import StringIO
|
|
|
+-
|
|
|
+-from mozdevice import DeviceManagerADB, DMError
|
|
|
+-
|
|
|
+-
|
|
|
+-def find_mount_permissions(dm, mount_path):
|
|
|
+- for mount_point in dm._runCmd(["shell", "mount"]).output:
|
|
|
+- if mount_point.find(mount_path) > 0:
|
|
|
+- return re.search('(ro|rw)(?=,)', mount_point).group(0)
|
|
|
+-
|
|
|
+-
|
|
|
+-class DeviceManagerADBTestCase(unittest.TestCase):
|
|
|
+- tempLocalDir = "tempDir"
|
|
|
+- tempLocalFile = os.path.join(tempLocalDir, "tempfile.txt")
|
|
|
+- tempRemoteDir = None
|
|
|
+- tempRemoteFile = None
|
|
|
+- tempRemoteSystemFile = None
|
|
|
+-
|
|
|
+- def setUp(self):
|
|
|
+- self.assertTrue(find_mount_permissions(self.dm, "/system"), "ro")
|
|
|
+-
|
|
|
+- self.assertTrue(os.path.exists(self.tempLocalDir))
|
|
|
+- self.assertTrue(os.path.exists(self.tempLocalFile))
|
|
|
+-
|
|
|
+- if self.dm.fileExists(self.tempRemoteFile):
|
|
|
+- self.dm.removeFile(self.tempRemoteFile)
|
|
|
+- self.assertFalse(self.dm.fileExists(self.tempRemoteFile))
|
|
|
+-
|
|
|
+- if self.dm.fileExists(self.tempRemoteSystemFile):
|
|
|
+- self.dm.removeFile(self.tempRemoteSystemFile)
|
|
|
+-
|
|
|
+- self.assertTrue(self.dm.dirExists(self.tempRemoteDir))
|
|
|
+-
|
|
|
+- @classmethod
|
|
|
+- def setUpClass(self):
|
|
|
+- self.dm = DeviceManagerADB()
|
|
|
+- if not os.path.exists(self.tempLocalDir):
|
|
|
+- os.mkdir(self.tempLocalDir)
|
|
|
+- if not os.path.exists(self.tempLocalFile):
|
|
|
+- # Create empty file
|
|
|
+- open(self.tempLocalFile, 'w').close()
|
|
|
+- self.tempRemoteDir = self.dm.getTempDir()
|
|
|
+- self.tempRemoteFile = os.path.join(self.tempRemoteDir,
|
|
|
+- os.path.basename(self.tempLocalFile))
|
|
|
+- self.tempRemoteSystemFile = \
|
|
|
+- os.path.join("/system", os.path.basename(self.tempLocalFile))
|
|
|
+-
|
|
|
+- @classmethod
|
|
|
+- def tearDownClass(self):
|
|
|
+- os.remove(self.tempLocalFile)
|
|
|
+- os.rmdir(self.tempLocalDir)
|
|
|
+- if self.dm.dirExists(self.tempRemoteDir):
|
|
|
+- # self.tempRemoteFile will get deleted with it
|
|
|
+- self.dm.removeDir(self.tempRemoteDir)
|
|
|
+- if self.dm.fileExists(self.tempRemoteSystemFile):
|
|
|
+- self.dm.removeFile(self.tempRemoteSystemFile)
|
|
|
+-
|
|
|
+-
|
|
|
+-class TestFileOperations(DeviceManagerADBTestCase):
|
|
|
+-
|
|
|
+- def test_make_and_remove_directory(self):
|
|
|
+- dir1 = os.path.join(self.tempRemoteDir, "dir1")
|
|
|
+- self.assertFalse(self.dm.dirExists(dir1))
|
|
|
+- self.dm.mkDir(dir1)
|
|
|
+- self.assertTrue(self.dm.dirExists(dir1))
|
|
|
+- self.dm.removeDir(dir1)
|
|
|
+- self.assertFalse(self.dm.dirExists(dir1))
|
|
|
+-
|
|
|
+- def test_push_and_remove_file(self):
|
|
|
+- self.dm.pushFile(self.tempLocalFile, self.tempRemoteFile)
|
|
|
+- self.assertTrue(self.dm.fileExists(self.tempRemoteFile))
|
|
|
+- self.dm.removeFile(self.tempRemoteFile)
|
|
|
+- self.assertFalse(self.dm.fileExists(self.tempRemoteFile))
|
|
|
+-
|
|
|
+- def test_push_and_pull_file(self):
|
|
|
+- self.dm.pushFile(self.tempLocalFile, self.tempRemoteFile)
|
|
|
+- self.assertTrue(self.dm.fileExists(self.tempRemoteFile))
|
|
|
+- self.assertFalse(os.path.exists("pulled.txt"))
|
|
|
+- self.dm.getFile(self.tempRemoteFile, "pulled.txt")
|
|
|
+- self.assertTrue(os.path.exists("pulled.txt"))
|
|
|
+- os.remove("pulled.txt")
|
|
|
+-
|
|
|
+- def test_push_and_pull_directory_and_list_files(self):
|
|
|
+- self.dm.removeDir(self.tempRemoteDir)
|
|
|
+- self.assertFalse(self.dm.dirExists(self.tempRemoteDir))
|
|
|
+- self.dm.pushDir(self.tempLocalDir, self.tempRemoteDir)
|
|
|
+- self.assertTrue(self.dm.dirExists(self.tempRemoteDir))
|
|
|
+- response = self.dm.listFiles(self.tempRemoteDir)
|
|
|
+- # The local dir that was pushed contains the tempLocalFile
|
|
|
+- self.assertIn(os.path.basename(self.tempLocalFile), response)
|
|
|
+- # Create a temp dir to pull to
|
|
|
+- temp_dir = tempfile.mkdtemp()
|
|
|
+- self.assertTrue(os.path.exists(temp_dir))
|
|
|
+- self.dm.getDirectory(self.tempRemoteDir, temp_dir)
|
|
|
+- self.assertTrue(os.path.exists(self.tempLocalFile))
|
|
|
+-
|
|
|
+- def test_move_and_remove_directories(self):
|
|
|
+- dir1 = os.path.join(self.tempRemoteDir, "dir1")
|
|
|
+- dir2 = os.path.join(self.tempRemoteDir, "dir2")
|
|
|
+-
|
|
|
+- self.assertFalse(self.dm.dirExists(dir1))
|
|
|
+- self.dm.mkDir(dir1)
|
|
|
+- self.assertTrue(self.dm.dirExists(dir1))
|
|
|
+-
|
|
|
+- self.assertFalse(self.dm.dirExists(dir2))
|
|
|
+- self.dm.moveTree(dir1, dir2)
|
|
|
+- self.assertTrue(self.dm.dirExists(dir2))
|
|
|
+-
|
|
|
+- self.dm.removeDir(dir1)
|
|
|
+- self.dm.removeDir(dir2)
|
|
|
+- self.assertFalse(self.dm.dirExists(dir1))
|
|
|
+- self.assertFalse(self.dm.dirExists(dir2))
|
|
|
+-
|
|
|
+- def test_push_and_remove_system_file(self):
|
|
|
+- out = StringIO()
|
|
|
+- self.assertTrue(find_mount_permissions(self.dm, "/system") == "ro")
|
|
|
+- self.assertFalse(self.dm.fileExists(self.tempRemoteSystemFile))
|
|
|
+- self.assertRaises(DMError, self.dm.pushFile, self.tempLocalFile, self.tempRemoteSystemFile)
|
|
|
+- self.dm.shell(['mount', '-w', '-o', 'remount', '/system'], out)
|
|
|
+- self.assertTrue(find_mount_permissions(self.dm, "/system") == "rw")
|
|
|
+- self.assertFalse(self.dm.fileExists(self.tempRemoteSystemFile))
|
|
|
+- self.dm.pushFile(self.tempLocalFile, self.tempRemoteSystemFile)
|
|
|
+- self.assertTrue(self.dm.fileExists(self.tempRemoteSystemFile))
|
|
|
+- self.dm.removeFile(self.tempRemoteSystemFile)
|
|
|
+- self.assertFalse(self.dm.fileExists(self.tempRemoteSystemFile))
|
|
|
+- self.dm.shell(['mount', '-r', '-o', 'remount', '/system'], out)
|
|
|
+- out.close()
|
|
|
+- self.assertTrue(find_mount_permissions(self.dm, "/system") == "ro")
|
|
|
+-
|
|
|
+-
|
|
|
+-class TestOther(DeviceManagerADBTestCase):
|
|
|
+-
|
|
|
+- def test_get_list_of_processes(self):
|
|
|
+- self.assertEquals(type(self.dm.getProcessList()), list)
|
|
|
+-
|
|
|
+- def test_get_current_time(self):
|
|
|
+- self.assertEquals(type(self.dm.getCurrentTime()), int)
|
|
|
+-
|
|
|
+- def test_get_info(self):
|
|
|
+- self.assertEquals(type(self.dm.getInfo()), dict)
|
|
|
+-
|
|
|
+- def test_list_devices(self):
|
|
|
+- self.assertEquals(len(list(self.dm.devices())), 1)
|
|
|
+-
|
|
|
+- def test_shell(self):
|
|
|
+- out = StringIO()
|
|
|
+- self.dm.shell(["echo", "$COMPANY", ";", "pwd"], out,
|
|
|
+- env={"COMPANY": "Mozilla"}, cwd="/", timeout=4, root=True)
|
|
|
+- output = str(out.getvalue()).rstrip().splitlines()
|
|
|
+- out.close()
|
|
|
+- self.assertEquals(output, ['Mozilla', '/'])
|
|
|
+-
|
|
|
+- def test_port_forwarding(self):
|
|
|
+- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
+- s.bind(("", 0))
|
|
|
+- port = s.getsockname()[1]
|
|
|
+- s.close()
|
|
|
+- # If successful then no exception is raised
|
|
|
+- self.dm.forward("tcp:%s" % port, "tcp:2828")
|
|
|
+-
|
|
|
+- def test_port_forwarding_error(self):
|
|
|
+- self.assertRaises(DMError, self.dm.forward, "", "")
|
|
|
+-
|
|
|
+-
|
|
|
+-if __name__ == '__main__':
|
|
|
+- dm = DeviceManagerADB()
|
|
|
+- if not dm.devices():
|
|
|
+- print("There are no connected adb devices")
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- if find_mount_permissions(dm, "/system") == "rw":
|
|
|
+- print("We've found out that /system is mounted as 'rw'. This is because the command "
|
|
|
+- "'adb remount' has been run before running this test case. Please reboot the "
|
|
|
+- "device and try again.")
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- unittest.main()
|
|
|
+diff --git a/testing/mozbase/mozdevice/mozdevice/__init__.py b/testing/mozbase/mozdevice/mozdevice/__init__.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/mozdevice/__init__.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,17 +0,0 @@
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
+-# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import
|
|
|
+-
|
|
|
+-from .adb import ADBError, ADBRootError, ADBTimeoutError
|
|
|
+-from .adb import ADBProcess, ADBCommand, ADBHost, ADBDevice
|
|
|
+-from .adb_android import ADBAndroid
|
|
|
+-from .adb_b2g import ADBB2G
|
|
|
+-from .devicemanager import DeviceManager, DMError
|
|
|
+-from .devicemanagerADB import DeviceManagerADB
|
|
|
+-from .droid import DroidADB
|
|
|
+-
|
|
|
+-__all__ = ['ADBError', 'ADBRootError', 'ADBTimeoutError', 'ADBProcess', 'ADBCommand', 'ADBHost',
|
|
|
+- 'ADBDevice', 'ADBAndroid', 'ADBB2G', 'DeviceManager', 'DMError',
|
|
|
+- 'DeviceManagerADB', 'DroidADB']
|
|
|
+diff --git a/testing/mozbase/mozdevice/mozdevice/adb.py b/testing/mozbase/mozdevice/mozdevice/adb.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/mozdevice/adb.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,2264 +0,0 @@
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
+-# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import
|
|
|
+-
|
|
|
+-import os
|
|
|
+-import posixpath
|
|
|
+-import re
|
|
|
+-import shutil
|
|
|
+-import subprocess
|
|
|
+-import tempfile
|
|
|
+-import time
|
|
|
+-import traceback
|
|
|
+-
|
|
|
+-from abc import ABCMeta, abstractmethod
|
|
|
+-from distutils import dir_util
|
|
|
+-
|
|
|
+-
|
|
|
+-class ADBProcess(object):
|
|
|
+- """ADBProcess encapsulates the data related to executing the adb process."""
|
|
|
+-
|
|
|
+- def __init__(self, args):
|
|
|
+- #: command argument argument list.
|
|
|
+- self.args = args
|
|
|
+- #: Temporary file handle to be used for stdout.
|
|
|
+- self.stdout_file = tempfile.TemporaryFile()
|
|
|
+- #: boolean indicating if the command timed out.
|
|
|
+- self.timedout = None
|
|
|
+- #: exitcode of the process.
|
|
|
+- self.exitcode = None
|
|
|
+- #: subprocess Process object used to execute the command.
|
|
|
+- self.proc = subprocess.Popen(args,
|
|
|
+- stdout=self.stdout_file,
|
|
|
+- stderr=subprocess.STDOUT)
|
|
|
+-
|
|
|
+- @property
|
|
|
+- def stdout(self):
|
|
|
+- """Return the contents of stdout."""
|
|
|
+- if not self.stdout_file or self.stdout_file.closed:
|
|
|
+- content = ""
|
|
|
+- else:
|
|
|
+- self.stdout_file.seek(0, os.SEEK_SET)
|
|
|
+- content = self.stdout_file.read().rstrip()
|
|
|
+- return content
|
|
|
+-
|
|
|
+- def __str__(self):
|
|
|
+- return ('args: %s, exitcode: %s, stdout: %s' % (
|
|
|
+- ' '.join(self.args), self.exitcode, self.stdout))
|
|
|
+-
|
|
|
+-# ADBError, ADBRootError, and ADBTimeoutError are treated
|
|
|
+-# differently in order that unhandled ADBRootErrors and
|
|
|
+-# ADBTimeoutErrors can be handled distinctly from ADBErrors.
|
|
|
+-
|
|
|
+-
|
|
|
+-class ADBError(Exception):
|
|
|
+- """ADBError is raised in situations where a command executed on a
|
|
|
+- device either exited with a non-zero exitcode or when an
|
|
|
+- unexpected error condition has occurred. Generally, ADBErrors can
|
|
|
+- be handled and the device can continue to be used.
|
|
|
+- """
|
|
|
+- pass
|
|
|
+-
|
|
|
+-
|
|
|
+-class ADBListDevicesError(ADBError):
|
|
|
+- """ADBListDevicesError is raised when errors are found listing the
|
|
|
+- devices, typically not any permissions.
|
|
|
+-
|
|
|
+- The devices information is stocked with the *devices* member.
|
|
|
+- """
|
|
|
+-
|
|
|
+- def __init__(self, msg, devices):
|
|
|
+- ADBError.__init__(self, msg)
|
|
|
+- self.devices = devices
|
|
|
+-
|
|
|
+-
|
|
|
+-class ADBRootError(Exception):
|
|
|
+- """ADBRootError is raised when a shell command is to be executed as
|
|
|
+- root but the device does not support it. This error is fatal since
|
|
|
+- there is no recovery possible by the script. You must either root
|
|
|
+- your device or change your scripts to not require running as root.
|
|
|
+- """
|
|
|
+- pass
|
|
|
+-
|
|
|
+-
|
|
|
+-class ADBTimeoutError(Exception):
|
|
|
+- """ADBTimeoutError is raised when either a host command or shell
|
|
|
+- command takes longer than the specified timeout to execute. The
|
|
|
+- timeout value is set in the ADBCommand constructor and is 300 seconds by
|
|
|
+- default. This error is typically fatal since the host is having
|
|
|
+- problems communicating with the device. You may be able to recover
|
|
|
+- by rebooting, but this is not guaranteed.
|
|
|
+-
|
|
|
+- Recovery options are:
|
|
|
+-
|
|
|
+- * Killing and restarting the adb server via
|
|
|
+- ::
|
|
|
+-
|
|
|
+- adb kill-server; adb start-server
|
|
|
+-
|
|
|
+- * Rebooting the device manually.
|
|
|
+- * Rebooting the host.
|
|
|
+- """
|
|
|
+- pass
|
|
|
+-
|
|
|
+-
|
|
|
+-class ADBCommand(object):
|
|
|
+- """ADBCommand provides a basic interface to adb commands
|
|
|
+- which is used to provide the 'command' methods for the
|
|
|
+- classes ADBHost and ADBDevice.
|
|
|
+-
|
|
|
+- ADBCommand should only be used as the base class for other
|
|
|
+- classes and should not be instantiated directly. To enforce this
|
|
|
+- restriction calling ADBCommand's constructor will raise a
|
|
|
+- NonImplementedError exception.
|
|
|
+-
|
|
|
+- ::
|
|
|
+-
|
|
|
+- from mozdevice import ADBCommand
|
|
|
+-
|
|
|
+- try:
|
|
|
+- adbcommand = ADBCommand()
|
|
|
+- except NotImplementedError:
|
|
|
+- print "ADBCommand can not be instantiated."
|
|
|
+- """
|
|
|
+-
|
|
|
+- def __init__(self,
|
|
|
+- adb='adb',
|
|
|
+- adb_host=None,
|
|
|
+- adb_port=None,
|
|
|
+- logger_name='adb',
|
|
|
+- timeout=300,
|
|
|
+- verbose=False):
|
|
|
+- """Initializes the ADBCommand object.
|
|
|
+-
|
|
|
+- :param str adb: path to adb executable. Defaults to 'adb'.
|
|
|
+- :param adb_host: host of the adb server.
|
|
|
+- :type adb_host: str or None
|
|
|
+- :param adb_port: port of the adb server.
|
|
|
+- :type adb_port: integer or None
|
|
|
+- :param str logger_name: logging logger name. Defaults to 'adb'.
|
|
|
+-
|
|
|
+- :raises: * ADBError
|
|
|
+- * ADBTimeoutError
|
|
|
+- """
|
|
|
+- if self.__class__ == ADBCommand:
|
|
|
+- raise NotImplementedError
|
|
|
+-
|
|
|
+- self._logger = self._get_logger(logger_name)
|
|
|
+- self._verbose = verbose
|
|
|
+- self._adb_path = adb
|
|
|
+- self._adb_host = adb_host
|
|
|
+- self._adb_port = adb_port
|
|
|
+- self._timeout = timeout
|
|
|
+- self._polling_interval = 0.1
|
|
|
+- self._adb_version = ''
|
|
|
+-
|
|
|
+- self._logger.debug("%s: %s" % (self.__class__.__name__,
|
|
|
+- self.__dict__))
|
|
|
+-
|
|
|
+- # catch early a missing or non executable adb command
|
|
|
+- # and get the adb version while we are at it.
|
|
|
+- try:
|
|
|
+- output = subprocess.Popen([adb, 'version'],
|
|
|
+- stdout=subprocess.PIPE,
|
|
|
+- stderr=subprocess.PIPE).communicate()
|
|
|
+- re_version = re.compile(r'Android Debug Bridge version (.*)')
|
|
|
+- self._adb_version = re_version.match(output[0]).group(1)
|
|
|
+- except Exception as exc:
|
|
|
+- raise ADBError('%s: %s is not executable.' % (exc, adb))
|
|
|
+-
|
|
|
+- def _get_logger(self, logger_name):
|
|
|
+- logger = None
|
|
|
+- try:
|
|
|
+- import mozlog
|
|
|
+- logger = mozlog.get_default_logger(logger_name)
|
|
|
+- except ImportError:
|
|
|
+- pass
|
|
|
+-
|
|
|
+- if logger is None:
|
|
|
+- import logging
|
|
|
+- logger = logging.getLogger(logger_name)
|
|
|
+- return logger
|
|
|
+-
|
|
|
+- # Host Command methods
|
|
|
+-
|
|
|
+- def command(self, cmds, device_serial=None, timeout=None):
|
|
|
+- """Executes an adb command on the host.
|
|
|
+-
|
|
|
+- :param list cmds: The command and its arguments to be
|
|
|
+- executed.
|
|
|
+- :param device_serial: The device's
|
|
|
+- serial number if the adb command is to be executed against
|
|
|
+- a specific device.
|
|
|
+- :type device_serial: str or None
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError. This timeout is per adb call. The
|
|
|
+- total time spent may exceed this value. If it is not
|
|
|
+- specified, the value set in the ADBCommand constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: :class:`mozdevice.ADBProcess`
|
|
|
+-
|
|
|
+- command() provides a low level interface for executing
|
|
|
+- commands on the host via adb.
|
|
|
+-
|
|
|
+- command() executes on the host in such a fashion that stdout
|
|
|
+- of the adb process is a file handle on the host and
|
|
|
+- the exit code is available as the exit code of the adb
|
|
|
+- process.
|
|
|
+-
|
|
|
+- The caller provides a list containing commands, as well as a
|
|
|
+- timeout period in seconds.
|
|
|
+-
|
|
|
+- A subprocess is spawned to execute adb with stdout and stderr
|
|
|
+- directed to a temporary file. If the process takes longer than
|
|
|
+- the specified timeout, the process is terminated.
|
|
|
+-
|
|
|
+- It is the caller's responsibilty to clean up by closing
|
|
|
+- the stdout temporary file.
|
|
|
+- """
|
|
|
+- args = [self._adb_path]
|
|
|
+- if self._adb_host:
|
|
|
+- args.extend(['-H', self._adb_host])
|
|
|
+- if self._adb_port:
|
|
|
+- args.extend(['-P', str(self._adb_port)])
|
|
|
+- if device_serial:
|
|
|
+- args.extend(['-s', device_serial, 'wait-for-device'])
|
|
|
+- args.extend(cmds)
|
|
|
+-
|
|
|
+- adb_process = ADBProcess(args)
|
|
|
+-
|
|
|
+- if timeout is None:
|
|
|
+- timeout = self._timeout
|
|
|
+-
|
|
|
+- start_time = time.time()
|
|
|
+- adb_process.exitcode = adb_process.proc.poll()
|
|
|
+- while ((time.time() - start_time) <= timeout and
|
|
|
+- adb_process.exitcode is None):
|
|
|
+- time.sleep(self._polling_interval)
|
|
|
+- adb_process.exitcode = adb_process.proc.poll()
|
|
|
+- if adb_process.exitcode is None:
|
|
|
+- adb_process.proc.kill()
|
|
|
+- adb_process.timedout = True
|
|
|
+- adb_process.exitcode = adb_process.proc.poll()
|
|
|
+-
|
|
|
+- adb_process.stdout_file.seek(0, os.SEEK_SET)
|
|
|
+-
|
|
|
+- return adb_process
|
|
|
+-
|
|
|
+- def command_output(self, cmds, device_serial=None, timeout=None):
|
|
|
+- """Executes an adb command on the host returning stdout.
|
|
|
+-
|
|
|
+- :param list cmds: The command and its arguments to be
|
|
|
+- executed.
|
|
|
+- :param device_serial: The device's
|
|
|
+- serial number if the adb command is to be executed against
|
|
|
+- a specific device.
|
|
|
+- :type device_serial: str or None
|
|
|
+- :param timeout: The maximum time in seconds
|
|
|
+- for any spawned adb process to complete before throwing
|
|
|
+- an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBCommand constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: string - content of stdout.
|
|
|
+-
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- adb_process = None
|
|
|
+- try:
|
|
|
+- # Need to force the use of the ADBCommand class's command
|
|
|
+- # since ADBDevice will redefine command and call its
|
|
|
+- # own version otherwise.
|
|
|
+- adb_process = ADBCommand.command(self, cmds,
|
|
|
+- device_serial=device_serial,
|
|
|
+- timeout=timeout)
|
|
|
+- if adb_process.timedout:
|
|
|
+- raise ADBTimeoutError("%s" % adb_process)
|
|
|
+- elif adb_process.exitcode:
|
|
|
+- raise ADBError("%s" % adb_process)
|
|
|
+- output = adb_process.stdout_file.read().rstrip()
|
|
|
+- if self._verbose:
|
|
|
+- self._logger.debug('command_output: %s, '
|
|
|
+- 'timeout: %s, '
|
|
|
+- 'timedout: %s, '
|
|
|
+- 'exitcode: %s, output: %s' %
|
|
|
+- (' '.join(adb_process.args),
|
|
|
+- timeout,
|
|
|
+- adb_process.timedout,
|
|
|
+- adb_process.exitcode,
|
|
|
+- output))
|
|
|
+-
|
|
|
+- return output
|
|
|
+- finally:
|
|
|
+- if adb_process and isinstance(adb_process.stdout_file, file):
|
|
|
+- adb_process.stdout_file.close()
|
|
|
+-
|
|
|
+-
|
|
|
+-class ADBHost(ADBCommand):
|
|
|
+- """ADBHost provides a basic interface to adb host commands
|
|
|
+- which do not target a specific device.
|
|
|
+-
|
|
|
+- ::
|
|
|
+-
|
|
|
+- from mozdevice import ADBHost
|
|
|
+-
|
|
|
+- adbhost = ADBHost()
|
|
|
+- adbhost.start_server()
|
|
|
+- """
|
|
|
+-
|
|
|
+- def __init__(self,
|
|
|
+- adb='adb',
|
|
|
+- adb_host=None,
|
|
|
+- adb_port=None,
|
|
|
+- logger_name='adb',
|
|
|
+- timeout=300,
|
|
|
+- verbose=False):
|
|
|
+- """Initializes the ADBHost object.
|
|
|
+-
|
|
|
+- :param str adb: path to adb executable. Defaults to 'adb'.
|
|
|
+- :param adb_host: host of the adb server.
|
|
|
+- :type adb_host: str or None
|
|
|
+- :param adb_port: port of the adb server.
|
|
|
+- :type adb_port: integer or None
|
|
|
+- :param str logger_name: logging logger name. Defaults to 'adb'.
|
|
|
+-
|
|
|
+- :raises: * ADBError
|
|
|
+- * ADBTimeoutError
|
|
|
+- """
|
|
|
+- ADBCommand.__init__(self, adb=adb, adb_host=adb_host,
|
|
|
+- adb_port=adb_port, logger_name=logger_name,
|
|
|
+- timeout=timeout, verbose=verbose)
|
|
|
+-
|
|
|
+- def command(self, cmds, timeout=None):
|
|
|
+- """Executes an adb command on the host.
|
|
|
+-
|
|
|
+- :param list cmds: The command and its arguments to be
|
|
|
+- executed.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError. This timeout is per adb call. The
|
|
|
+- total time spent may exceed this value. If it is not
|
|
|
+- specified, the value set in the ADBHost constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: :class:`mozdevice.ADBProcess`
|
|
|
+-
|
|
|
+- command() provides a low level interface for executing
|
|
|
+- commands on the host via adb.
|
|
|
+-
|
|
|
+- command() executes on the host in such a fashion that stdout
|
|
|
+- of the adb process is a file handle on the host and
|
|
|
+- the exit code is available as the exit code of the adb
|
|
|
+- process.
|
|
|
+-
|
|
|
+- The caller provides a list containing commands, as well as a
|
|
|
+- timeout period in seconds.
|
|
|
+-
|
|
|
+- A subprocess is spawned to execute adb with stdout and stderr
|
|
|
+- directed to a temporary file. If the process takes longer than
|
|
|
+- the specified timeout, the process is terminated.
|
|
|
+-
|
|
|
+- It is the caller's responsibilty to clean up by closing
|
|
|
+- the stdout temporary file.
|
|
|
+- """
|
|
|
+- return ADBCommand.command(self, cmds, timeout=timeout)
|
|
|
+-
|
|
|
+- def command_output(self, cmds, timeout=None):
|
|
|
+- """Executes an adb command on the host returning stdout.
|
|
|
+-
|
|
|
+- :param list cmds: The command and its arguments to be
|
|
|
+- executed.
|
|
|
+- :param timeout: The maximum time in seconds
|
|
|
+- for any spawned adb process to complete before throwing
|
|
|
+- an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBHost constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: string - content of stdout.
|
|
|
+-
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- return ADBCommand.command_output(self, cmds, timeout=timeout)
|
|
|
+-
|
|
|
+- def start_server(self, timeout=None):
|
|
|
+- """Starts the adb server.
|
|
|
+-
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError. This timeout is per adb call. The
|
|
|
+- total time spent may exceed this value. If it is not
|
|
|
+- specified, the value set in the ADBHost constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+-
|
|
|
+- Attempting to use start_server with any adb_host value other than None
|
|
|
+- will fail with an ADBError exception.
|
|
|
+-
|
|
|
+- You will need to start the server on the remote host via the command:
|
|
|
+-
|
|
|
+- .. code-block:: shell
|
|
|
+-
|
|
|
+- adb -a fork-server server
|
|
|
+-
|
|
|
+- If you wish the remote adb server to restart automatically, you can
|
|
|
+- enclose the command in a loop as in:
|
|
|
+-
|
|
|
+- .. code-block:: shell
|
|
|
+-
|
|
|
+- while true; do
|
|
|
+- adb -a fork-server server
|
|
|
+- done
|
|
|
+- """
|
|
|
+- self.command_output(["start-server"], timeout=timeout)
|
|
|
+-
|
|
|
+- def kill_server(self, timeout=None):
|
|
|
+- """Kills the adb server.
|
|
|
+-
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError. This timeout is per adb call. The
|
|
|
+- total time spent may exceed this value. If it is not
|
|
|
+- specified, the value set in the ADBHost constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- self.command_output(["kill-server"], timeout=timeout)
|
|
|
+-
|
|
|
+- def devices(self, timeout=None):
|
|
|
+- """Executes adb devices -l and returns a list of objects describing attached devices.
|
|
|
+-
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError. This timeout is per adb call. The
|
|
|
+- total time spent may exceed this value. If it is not
|
|
|
+- specified, the value set in the ADBHost constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: an object contain
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBListDevicesError
|
|
|
+- * ADBError
|
|
|
+-
|
|
|
+- The output of adb devices -l ::
|
|
|
+-
|
|
|
+- $ adb devices -l
|
|
|
+- List of devices attached
|
|
|
+- b313b945 device usb:1-7 product:d2vzw model:SCH_I535 device:d2vzw
|
|
|
+-
|
|
|
+- is parsed and placed into an object as in
|
|
|
+-
|
|
|
+- [{'device_serial': 'b313b945', 'state': 'device', 'product': 'd2vzw',
|
|
|
+- 'usb': '1-7', 'device': 'd2vzw', 'model': 'SCH_I535' }]
|
|
|
+- """
|
|
|
+- # b313b945 device usb:1-7 product:d2vzw model:SCH_I535 device:d2vzw
|
|
|
+- # from Android system/core/adb/transport.c statename()
|
|
|
+- re_device_info = re.compile(
|
|
|
+- r"([^\s]+)\s+(offline|bootloader|device|host|recovery|sideload|"
|
|
|
+- "no permissions|unauthorized|unknown)")
|
|
|
+- devices = []
|
|
|
+- lines = self.command_output(["devices", "-l"], timeout=timeout).splitlines()
|
|
|
+- for line in lines:
|
|
|
+- if line == 'List of devices attached ':
|
|
|
+- continue
|
|
|
+- match = re_device_info.match(line)
|
|
|
+- if match:
|
|
|
+- device = {
|
|
|
+- 'device_serial': match.group(1),
|
|
|
+- 'state': match.group(2)
|
|
|
+- }
|
|
|
+- remainder = line[match.end(2):].strip()
|
|
|
+- if remainder:
|
|
|
+- try:
|
|
|
+- device.update(dict([j.split(':')
|
|
|
+- for j in remainder.split(' ')]))
|
|
|
+- except ValueError:
|
|
|
+- self._logger.warning('devices: Unable to parse '
|
|
|
+- 'remainder for device %s' % line)
|
|
|
+- devices.append(device)
|
|
|
+- for device in devices:
|
|
|
+- if device['state'] == 'no permissions':
|
|
|
+- raise ADBListDevicesError(
|
|
|
+- "No permissions to detect devices. You should restart the"
|
|
|
+- " adb server as root:\n"
|
|
|
+- "\n# adb kill-server\n# adb start-server\n"
|
|
|
+- "\nor maybe configure your udev rules.",
|
|
|
+- devices)
|
|
|
+- return devices
|
|
|
+-
|
|
|
+-
|
|
|
+-class ADBDevice(ADBCommand):
|
|
|
+- """ADBDevice is an abstract base class which provides methods which
|
|
|
+- can be used to interact with the associated Android or B2G based
|
|
|
+- device. It must be used via one of the concrete implementations in
|
|
|
+- :class:`ADBAndroid` or :class:`ADBB2G`.
|
|
|
+- """
|
|
|
+- __metaclass__ = ABCMeta
|
|
|
+-
|
|
|
+- def __init__(self,
|
|
|
+- device=None,
|
|
|
+- adb='adb',
|
|
|
+- adb_host=None,
|
|
|
+- adb_port=None,
|
|
|
+- test_root='',
|
|
|
+- logger_name='adb',
|
|
|
+- timeout=300,
|
|
|
+- verbose=False,
|
|
|
+- device_ready_retry_wait=20,
|
|
|
+- device_ready_retry_attempts=3):
|
|
|
+- """Initializes the ADBDevice object.
|
|
|
+-
|
|
|
+- :param device: When a string is passed, it is interpreted as the
|
|
|
+- device serial number. This form is not compatible with
|
|
|
+- devices containing a ":" in the serial; in this case
|
|
|
+- ValueError will be raised.
|
|
|
+- When a dictionary is passed it must have one or both of
|
|
|
+- the keys "device_serial" and "usb". This is compatible
|
|
|
+- with the dictionaries in the list returned by
|
|
|
+- ADBHost.devices(). If the value of device_serial is a
|
|
|
+- valid serial not containing a ":" it will be used to
|
|
|
+- identify the device, otherwise the value of the usb key,
|
|
|
+- prefixed with "usb:" is used.
|
|
|
+- If None is passed and there is exactly one device attached
|
|
|
+- to the host, that device is used. If there is more than one
|
|
|
+- device attached, ValueError is raised. If no device is
|
|
|
+- attached the constructor will block until a device is
|
|
|
+- attached or the timeout is reached.
|
|
|
+- :type device: dict, str or None
|
|
|
+- :param adb_host: host of the adb server to connect to.
|
|
|
+- :type adb_host: str or None
|
|
|
+- :param adb_port: port of the adb server to connect to.
|
|
|
+- :type adb_port: integer or None
|
|
|
+- :param str logger_name: logging logger name. Defaults to 'adb'.
|
|
|
+- :param integer device_ready_retry_wait: number of seconds to wait
|
|
|
+- between attempts to check if the device is ready after a
|
|
|
+- reboot.
|
|
|
+- :param integer device_ready_retry_attempts: number of attempts when
|
|
|
+- checking if a device is ready.
|
|
|
+-
|
|
|
+- :raises: * ADBError
|
|
|
+- * ADBTimeoutError
|
|
|
+- * ValueError
|
|
|
+- """
|
|
|
+- ADBCommand.__init__(self, adb=adb, adb_host=adb_host,
|
|
|
+- adb_port=adb_port, logger_name=logger_name,
|
|
|
+- timeout=timeout, verbose=verbose)
|
|
|
+- self._device_serial = self._get_device_serial(device)
|
|
|
+- self._initial_test_root = test_root
|
|
|
+- self._test_root = None
|
|
|
+- self._device_ready_retry_wait = device_ready_retry_wait
|
|
|
+- self._device_ready_retry_attempts = device_ready_retry_attempts
|
|
|
+- self._have_root_shell = False
|
|
|
+- self._have_su = False
|
|
|
+- self._have_android_su = False
|
|
|
+-
|
|
|
+- # Catch exceptions due to the potential for segfaults
|
|
|
+- # calling su when using an improperly rooted device.
|
|
|
+-
|
|
|
+- # Note this check to see if adbd is running is performed on
|
|
|
+- # the device in the state it exists in when the ADBDevice is
|
|
|
+- # initialized. It may be the case that it has been manipulated
|
|
|
+- # since its last boot and that its current state does not
|
|
|
+- # match the state the device will have immediately after a
|
|
|
+- # reboot. For example, if adb root was called manually prior
|
|
|
+- # to ADBDevice being initialized, then self._have_root_shell
|
|
|
+- # will not reflect the state of the device after it has been
|
|
|
+- # rebooted again. Therefore this check will need to be
|
|
|
+- # performed again after a reboot.
|
|
|
+-
|
|
|
+- self._check_adb_root(timeout=timeout)
|
|
|
+-
|
|
|
+- uid = 'uid=0'
|
|
|
+- # Do we have a 'Superuser' sh like su?
|
|
|
+- try:
|
|
|
+- if self.shell_output("su -c id", timeout=timeout).find(uid) != -1:
|
|
|
+- self._have_su = True
|
|
|
+- self._logger.info("su -c supported")
|
|
|
+- except ADBError:
|
|
|
+- self._logger.debug("Check for su -c failed")
|
|
|
+-
|
|
|
+- # Do we have Android's su?
|
|
|
+- try:
|
|
|
+- if self.shell_output("su 0 id", timeout=timeout).find(uid) != -1:
|
|
|
+- self._have_android_su = True
|
|
|
+- self._logger.info("su 0 supported")
|
|
|
+- except ADBError:
|
|
|
+- self._logger.debug("Check for su 0 failed")
|
|
|
+-
|
|
|
+- self._mkdir_p = None
|
|
|
+- # Force the use of /system/bin/ls or /system/xbin/ls in case
|
|
|
+- # there is /sbin/ls which embeds ansi escape codes to colorize
|
|
|
+- # the output. Detect if we are using busybox ls. We want each
|
|
|
+- # entry on a single line and we don't want . or ..
|
|
|
+- if self.shell_bool("/system/bin/ls /data/local/tmp", timeout=timeout):
|
|
|
+- self._ls = "/system/bin/ls"
|
|
|
+- elif self.shell_bool("/system/xbin/ls /data/local/tmp", timeout=timeout):
|
|
|
+- self._ls = "/system/xbin/ls"
|
|
|
+- else:
|
|
|
+- raise ADBError("ADBDevice.__init__: ls not found")
|
|
|
+- try:
|
|
|
+- self.shell_output("%s -1A /data/local/tmp" % self._ls, timeout=timeout)
|
|
|
+- self._ls += " -1A"
|
|
|
+- except ADBError:
|
|
|
+- self._ls += " -a"
|
|
|
+-
|
|
|
+- self._logger.info("%s supported" % self._ls)
|
|
|
+-
|
|
|
+- # Do we have cp?
|
|
|
+- self._have_cp = self.shell_bool("type cp", timeout=timeout)
|
|
|
+- self._logger.info("Native cp support: %s" % self._have_cp)
|
|
|
+-
|
|
|
+- # Do we have chmod -R?
|
|
|
+- try:
|
|
|
+- self._chmod_R = False
|
|
|
+- re_recurse = re.compile(r'[-]R')
|
|
|
+- chmod_output = self.shell_output("chmod --help", timeout=timeout)
|
|
|
+- match = re_recurse.search(chmod_output)
|
|
|
+- if match:
|
|
|
+- self._chmod_R = True
|
|
|
+- except (ADBError, ADBTimeoutError) as e:
|
|
|
+- self._logger.debug('Check chmod -R: %s' % e)
|
|
|
+- match = re_recurse.search(e.message)
|
|
|
+- if match:
|
|
|
+- self._chmod_R = True
|
|
|
+- self._logger.info("Native chmod -R support: %s" % self._chmod_R)
|
|
|
+-
|
|
|
+- self._logger.debug("ADBDevice: %s" % self.__dict__)
|
|
|
+-
|
|
|
+- def _get_device_serial(self, device):
|
|
|
+- if device is None:
|
|
|
+- devices = ADBHost(adb=self._adb_path, adb_host=self._adb_host,
|
|
|
+- adb_port=self._adb_port).devices()
|
|
|
+- if len(devices) > 1:
|
|
|
+- raise ValueError("ADBDevice called with multiple devices "
|
|
|
+- "attached and no device specified")
|
|
|
+- elif len(devices) == 0:
|
|
|
+- # We could error here, but this way we'll wait-for-device before we next
|
|
|
+- # run a command, which seems more friendly
|
|
|
+- return
|
|
|
+- device = devices[0]
|
|
|
+-
|
|
|
+- def is_valid_serial(serial):
|
|
|
+- return ":" not in serial or serial.startswith("usb:")
|
|
|
+-
|
|
|
+- if isinstance(device, (str, unicode)):
|
|
|
+- # Treat this as a device serial
|
|
|
+- if not is_valid_serial(device):
|
|
|
+- raise ValueError("Device serials containing ':' characters are "
|
|
|
+- "invalid. Pass the output from "
|
|
|
+- "ADBHost.devices() for the device instead")
|
|
|
+- return device
|
|
|
+-
|
|
|
+- serial = device.get("device_serial")
|
|
|
+- if serial is not None and is_valid_serial(serial):
|
|
|
+- return serial
|
|
|
+- usb = device.get("usb")
|
|
|
+- if usb is not None:
|
|
|
+- return "usb:%s" % usb
|
|
|
+-
|
|
|
+- raise ValueError("Unable to get device serial")
|
|
|
+-
|
|
|
+- def _check_adb_root(self, timeout=None):
|
|
|
+- self._have_root_shell = False
|
|
|
+- uid = 'uid=0'
|
|
|
+- # Is shell already running as root?
|
|
|
+- try:
|
|
|
+- if self.shell_output("id", timeout=timeout).find(uid) != -1:
|
|
|
+- self._have_root_shell = True
|
|
|
+- self._logger.info("adbd running as root")
|
|
|
+- except ADBError:
|
|
|
+- self._logger.debug("Check for root shell failed")
|
|
|
+-
|
|
|
+- # Do we need to run adb root to get a root shell?
|
|
|
+- try:
|
|
|
+- if (not self._have_root_shell and self.command_output(
|
|
|
+- ["root"],
|
|
|
+- timeout=timeout).find("cannot run as root") == -1):
|
|
|
+- self._have_root_shell = True
|
|
|
+- self._logger.info("adbd restarted as root")
|
|
|
+- except ADBError:
|
|
|
+- self._logger.debug("Check for root adbd failed")
|
|
|
+-
|
|
|
+- @staticmethod
|
|
|
+- def _escape_command_line(cmd):
|
|
|
+- """Utility function to return escaped and quoted version of command
|
|
|
+- line.
|
|
|
+- """
|
|
|
+- quoted_cmd = []
|
|
|
+-
|
|
|
+- for arg in cmd:
|
|
|
+- arg.replace('&', r'\&')
|
|
|
+-
|
|
|
+- needs_quoting = False
|
|
|
+- for char in [' ', '(', ')', '"', '&']:
|
|
|
+- if arg.find(char) >= 0:
|
|
|
+- needs_quoting = True
|
|
|
+- break
|
|
|
+- if needs_quoting:
|
|
|
+- arg = "'%s'" % arg
|
|
|
+-
|
|
|
+- quoted_cmd.append(arg)
|
|
|
+-
|
|
|
+- return " ".join(quoted_cmd)
|
|
|
+-
|
|
|
+- @staticmethod
|
|
|
+- def _get_exitcode(file_obj):
|
|
|
+- """Get the exitcode from the last line of the file_obj for shell
|
|
|
+- commands.
|
|
|
+- """
|
|
|
+- file_obj.seek(0, os.SEEK_END)
|
|
|
+-
|
|
|
+- line = ''
|
|
|
+- length = file_obj.tell()
|
|
|
+- offset = 1
|
|
|
+- while length - offset >= 0:
|
|
|
+- file_obj.seek(-offset, os.SEEK_END)
|
|
|
+- char = file_obj.read(1)
|
|
|
+- if not char:
|
|
|
+- break
|
|
|
+- if char != '\r' and char != '\n':
|
|
|
+- line = char + line
|
|
|
+- elif line:
|
|
|
+- # we have collected everything up to the beginning of the line
|
|
|
+- break
|
|
|
+- offset += 1
|
|
|
+-
|
|
|
+- match = re.match(r'rc=([0-9]+)', line)
|
|
|
+- if match:
|
|
|
+- exitcode = int(match.group(1))
|
|
|
+- file_obj.seek(-1, os.SEEK_CUR)
|
|
|
+- file_obj.truncate()
|
|
|
+- else:
|
|
|
+- exitcode = None
|
|
|
+-
|
|
|
+- return exitcode
|
|
|
+-
|
|
|
+- @property
|
|
|
+- def test_root(self):
|
|
|
+- """
|
|
|
+- The test_root property returns the directory on the device where
|
|
|
+- temporary test files are stored.
|
|
|
+-
|
|
|
+- The first time test_root it is called it determines and caches a value
|
|
|
+- for the test root on the device. It determines the appropriate test
|
|
|
+- root by attempting to create a 'dummy' directory on each of a list of
|
|
|
+- directories and returning the first successful directory as the
|
|
|
+- test_root value.
|
|
|
+-
|
|
|
+- The default list of directories checked by test_root are:
|
|
|
+-
|
|
|
+- - /storage/sdcard0/tests
|
|
|
+- - /storage/sdcard1/tests
|
|
|
+- - /sdcard/tests
|
|
|
+- - /mnt/sdcard/tests
|
|
|
+- - /data/local/tests
|
|
|
+-
|
|
|
+- You may override the default list by providing a test_root argument to
|
|
|
+- the :class:`ADBDevice` constructor which will then be used when
|
|
|
+- attempting to create the 'dummy' directory.
|
|
|
+-
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- if self._test_root is not None:
|
|
|
+- return self._test_root
|
|
|
+-
|
|
|
+- if self._initial_test_root:
|
|
|
+- paths = [self._initial_test_root]
|
|
|
+- else:
|
|
|
+- paths = ['/storage/sdcard0/tests',
|
|
|
+- '/storage/sdcard1/tests',
|
|
|
+- '/sdcard/tests',
|
|
|
+- '/mnt/sdcard/tests',
|
|
|
+- '/data/local/tests']
|
|
|
+-
|
|
|
+- max_attempts = 3
|
|
|
+- for attempt in range(1, max_attempts + 1):
|
|
|
+- for test_root in paths:
|
|
|
+- self._logger.debug("Setting test root to %s attempt %d of %d" %
|
|
|
+- (test_root, attempt, max_attempts))
|
|
|
+-
|
|
|
+- if self._try_test_root(test_root):
|
|
|
+- self._test_root = test_root
|
|
|
+- return self._test_root
|
|
|
+-
|
|
|
+- self._logger.debug('_setup_test_root: '
|
|
|
+- 'Attempt %d of %d failed to set test_root to %s' %
|
|
|
+- (attempt, max_attempts, test_root))
|
|
|
+-
|
|
|
+- if attempt != max_attempts:
|
|
|
+- time.sleep(20)
|
|
|
+-
|
|
|
+- raise ADBError("Unable to set up test root using paths: [%s]"
|
|
|
+- % ", ".join(paths))
|
|
|
+-
|
|
|
+- def _try_test_root(self, test_root):
|
|
|
+- base_path, sub_path = posixpath.split(test_root)
|
|
|
+- if not self.is_dir(base_path):
|
|
|
+- return False
|
|
|
+-
|
|
|
+- try:
|
|
|
+- dummy_dir = posixpath.join(test_root, 'dummy')
|
|
|
+- if self.is_dir(dummy_dir):
|
|
|
+- self.rm(dummy_dir, recursive=True)
|
|
|
+- self.mkdir(dummy_dir, parents=True)
|
|
|
+- except ADBError:
|
|
|
+- self._logger.debug("%s is not writable" % test_root)
|
|
|
+- return False
|
|
|
+-
|
|
|
+- return True
|
|
|
+-
|
|
|
+- # Host Command methods
|
|
|
+-
|
|
|
+- def command(self, cmds, timeout=None):
|
|
|
+- """Executes an adb command on the host against the device.
|
|
|
+-
|
|
|
+- :param list cmds: The command and its arguments to be
|
|
|
+- executed.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError. This timeout is per adb call. The
|
|
|
+- total time spent may exceed this value. If it is not
|
|
|
+- specified, the value set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: :class:`mozdevice.ADBProcess`
|
|
|
+-
|
|
|
+- command() provides a low level interface for executing
|
|
|
+- commands for a specific device on the host via adb.
|
|
|
+-
|
|
|
+- command() executes on the host in such a fashion that stdout
|
|
|
+- of the adb process are file handles on the host and
|
|
|
+- the exit code is available as the exit code of the adb
|
|
|
+- process.
|
|
|
+-
|
|
|
+- For executing shell commands on the device, use
|
|
|
+- ADBDevice.shell(). The caller provides a list containing
|
|
|
+- commands, as well as a timeout period in seconds.
|
|
|
+-
|
|
|
+- A subprocess is spawned to execute adb for the device with
|
|
|
+- stdout and stderr directed to a temporary file. If the process
|
|
|
+- takes longer than the specified timeout, the process is
|
|
|
+- terminated.
|
|
|
+-
|
|
|
+- It is the caller's responsibilty to clean up by closing
|
|
|
+- the stdout temporary file.
|
|
|
+- """
|
|
|
+-
|
|
|
+- return ADBCommand.command(self, cmds,
|
|
|
+- device_serial=self._device_serial,
|
|
|
+- timeout=timeout)
|
|
|
+-
|
|
|
+- def command_output(self, cmds, timeout=None):
|
|
|
+- """Executes an adb command on the host against the device returning
|
|
|
+- stdout.
|
|
|
+-
|
|
|
+- :param list cmds: The command and its arguments to be executed.
|
|
|
+- :param timeout: The maximum time in seconds
|
|
|
+- for any spawned adb process to complete before throwing
|
|
|
+- an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: string - content of stdout.
|
|
|
+-
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- return ADBCommand.command_output(self, cmds,
|
|
|
+- device_serial=self._device_serial,
|
|
|
+- timeout=timeout)
|
|
|
+-
|
|
|
+- # Port forwarding methods
|
|
|
+-
|
|
|
+- def _validate_port(self, port, is_local=True):
|
|
|
+- """Validate a port forwarding specifier. Raises ValueError on failure.
|
|
|
+-
|
|
|
+- :param str port: The port specifier to validate
|
|
|
+- :param bool is_local: Flag indicating whether the port represents a local port.
|
|
|
+- """
|
|
|
+- prefixes = ["tcp", "localabstract", "localreserved", "localfilesystem", "dev"]
|
|
|
+-
|
|
|
+- if not is_local:
|
|
|
+- prefixes += ["jdwp"]
|
|
|
+-
|
|
|
+- parts = port.split(":", 1)
|
|
|
+- if len(parts) != 2 or parts[0] not in prefixes:
|
|
|
+- raise ValueError("Invalid forward specifier %s" % port)
|
|
|
+-
|
|
|
+- def forward(self, local, remote, allow_rebind=True, timeout=None):
|
|
|
+- """Forward a local port to a specific port on the device.
|
|
|
+-
|
|
|
+- Ports are specified in the form:
|
|
|
+- tcp:<port>
|
|
|
+- localabstract:<unix domain socket name>
|
|
|
+- localreserved:<unix domain socket name>
|
|
|
+- localfilesystem:<unix domain socket name>
|
|
|
+- dev:<character device name>
|
|
|
+- jdwp:<process pid> (remote only)
|
|
|
+-
|
|
|
+- :param str local: Local port to forward
|
|
|
+- :param str remote: Remote port to which to forward
|
|
|
+- :param bool allow_rebind: Don't error if the local port is already forwarded
|
|
|
+- :param timeout: The maximum time in seconds
|
|
|
+- for any spawned adb process to complete before throwing
|
|
|
+- an ADBTimeoutError. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ValueError
|
|
|
+- * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+-
|
|
|
+- for port, is_local in [(local, True), (remote, False)]:
|
|
|
+- self._validate_port(port, is_local=is_local)
|
|
|
+-
|
|
|
+- cmd = ["forward", local, remote]
|
|
|
+- if not allow_rebind:
|
|
|
+- cmd.insert(1, "--no-rebind")
|
|
|
+- self.command_output(cmd, timeout=timeout)
|
|
|
+-
|
|
|
+- def list_forwards(self, timeout=None):
|
|
|
+- """Return a list of tuples specifying active forwards
|
|
|
+-
|
|
|
+- Return values are of the form (device, local, remote).
|
|
|
+-
|
|
|
+- :param timeout: The maximum time in seconds
|
|
|
+- for any spawned adb process to complete before throwing
|
|
|
+- an ADBTimeoutError. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- forwards = self.command_output(["forward", "--list"], timeout=timeout)
|
|
|
+- return [tuple(line.split(" ")) for line in forwards.splitlines() if line.strip()]
|
|
|
+-
|
|
|
+- def remove_forwards(self, local=None, timeout=None):
|
|
|
+- """Remove existing port forwards.
|
|
|
+-
|
|
|
+- :param local: local port specifier as for ADBDevice.forward. If local
|
|
|
+- is not specified removes all forwards.
|
|
|
+- :type local: str or None
|
|
|
+- :param timeout: The maximum time in seconds
|
|
|
+- for any spawned adb process to complete before throwing
|
|
|
+- an ADBTimeoutError. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ValueError
|
|
|
+- * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- cmd = ["forward"]
|
|
|
+- if local is None:
|
|
|
+- cmd.extend(["--remove-all"])
|
|
|
+- else:
|
|
|
+- self._validate_port(local, is_local=True)
|
|
|
+- cmd.extend(["--remove", local])
|
|
|
+-
|
|
|
+- self.command_output(cmd, timeout=timeout)
|
|
|
+-
|
|
|
+- # Device Shell methods
|
|
|
+-
|
|
|
+- def shell(self, cmd, env=None, cwd=None, timeout=None, root=False):
|
|
|
+- """Executes a shell command on the device.
|
|
|
+-
|
|
|
+- :param str cmd: The command to be executed.
|
|
|
+- :param env: Contains the environment variables and
|
|
|
+- their values.
|
|
|
+- :type env: dict or None
|
|
|
+- :param cwd: The directory from which to execute.
|
|
|
+- :type cwd: str or None
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError. This timeout is per adb call. The
|
|
|
+- total time spent may exceed this value. If it is not
|
|
|
+- specified, the value set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :returns: :class:`mozdevice.ADBProcess`
|
|
|
+- :raises: ADBRootError
|
|
|
+-
|
|
|
+- shell() provides a low level interface for executing commands
|
|
|
+- on the device via adb shell.
|
|
|
+-
|
|
|
+- shell() executes on the host in such as fashion that stdout
|
|
|
+- contains the stdout and stderr of the host abd process
|
|
|
+- combined with the stdout and stderr of the shell command
|
|
|
+- on the device. The exit code of shell() is the exit code of
|
|
|
+- the adb command if it was non-zero or the extracted exit code
|
|
|
+- from the output of the shell command executed on the
|
|
|
+- device.
|
|
|
+-
|
|
|
+- The caller provides a flag indicating if the command is to be
|
|
|
+- executed as root, a string for any requested working
|
|
|
+- directory, a hash defining the environment, a string
|
|
|
+- containing shell commands, as well as a timeout period in
|
|
|
+- seconds.
|
|
|
+-
|
|
|
+- The command line to be executed is created to set the current
|
|
|
+- directory, set the required environment variables, optionally
|
|
|
+- execute the command using su and to output the return code of
|
|
|
+- the command to stdout. The command list is created as a
|
|
|
+- command sequence separated by && which will terminate the
|
|
|
+- command sequence on the first command which returns a non-zero
|
|
|
+- exit code.
|
|
|
+-
|
|
|
+- A subprocess is spawned to execute adb shell for the device
|
|
|
+- with stdout and stderr directed to a temporary file. If the
|
|
|
+- process takes longer than the specified timeout, the process
|
|
|
+- is terminated. The return code is extracted from the stdout
|
|
|
+- and is then removed from the file.
|
|
|
+-
|
|
|
+- It is the caller's responsibilty to clean up by closing
|
|
|
+- the stdout temporary files.
|
|
|
+-
|
|
|
+- """
|
|
|
+- if root and not self._have_root_shell:
|
|
|
+- # If root was requested and we do not already have a root
|
|
|
+- # shell, then use the appropriate version of su to invoke
|
|
|
+- # the shell cmd. Prefer Android's su version since it may
|
|
|
+- # falsely report support for su -c.
|
|
|
+- if self._have_android_su:
|
|
|
+- cmd = "su 0 %s" % cmd
|
|
|
+- elif self._have_su:
|
|
|
+- cmd = "su -c \"%s\"" % cmd
|
|
|
+- else:
|
|
|
+- raise ADBRootError('Can not run command %s as root!' % cmd)
|
|
|
+-
|
|
|
+- # prepend cwd and env to command if necessary
|
|
|
+- if cwd:
|
|
|
+- cmd = "cd %s && %s" % (cwd, cmd)
|
|
|
+- if env:
|
|
|
+- envstr = '&& '.join(map(lambda x: 'export %s=%s' %
|
|
|
+- (x[0], x[1]), env.iteritems()))
|
|
|
+- cmd = envstr + "&& " + cmd
|
|
|
+- cmd += "; echo rc=$?"
|
|
|
+-
|
|
|
+- args = [self._adb_path]
|
|
|
+- if self._adb_host:
|
|
|
+- args.extend(['-H', self._adb_host])
|
|
|
+- if self._adb_port:
|
|
|
+- args.extend(['-P', str(self._adb_port)])
|
|
|
+- if self._device_serial:
|
|
|
+- args.extend(['-s', self._device_serial])
|
|
|
+- args.extend(["wait-for-device", "shell", cmd])
|
|
|
+- adb_process = ADBProcess(args)
|
|
|
+-
|
|
|
+- if timeout is None:
|
|
|
+- timeout = self._timeout
|
|
|
+-
|
|
|
+- start_time = time.time()
|
|
|
+- exitcode = adb_process.proc.poll()
|
|
|
+- while ((time.time() - start_time) <= timeout) and exitcode is None:
|
|
|
+- time.sleep(self._polling_interval)
|
|
|
+- exitcode = adb_process.proc.poll()
|
|
|
+- if exitcode is None:
|
|
|
+- adb_process.proc.kill()
|
|
|
+- adb_process.timedout = True
|
|
|
+- adb_process.exitcode = adb_process.proc.poll()
|
|
|
+- elif exitcode == 0:
|
|
|
+- adb_process.exitcode = self._get_exitcode(adb_process.stdout_file)
|
|
|
+- else:
|
|
|
+- adb_process.exitcode = exitcode
|
|
|
+-
|
|
|
+- adb_process.stdout_file.seek(0, os.SEEK_SET)
|
|
|
+-
|
|
|
+- return adb_process
|
|
|
+-
|
|
|
+- def shell_bool(self, cmd, env=None, cwd=None, timeout=None, root=False):
|
|
|
+- """Executes a shell command on the device returning True on success
|
|
|
+- and False on failure.
|
|
|
+-
|
|
|
+- :param str cmd: The command to be executed.
|
|
|
+- :param env: Contains the environment variables and
|
|
|
+- their values.
|
|
|
+- :type env: dict or None
|
|
|
+- :param cwd: The directory from which to execute.
|
|
|
+- :type cwd: str or None
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :returns: boolean
|
|
|
+-
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- """
|
|
|
+- adb_process = None
|
|
|
+- try:
|
|
|
+- adb_process = self.shell(cmd, env=env, cwd=cwd,
|
|
|
+- timeout=timeout, root=root)
|
|
|
+- if adb_process.timedout:
|
|
|
+- raise ADBTimeoutError("%s" % adb_process)
|
|
|
+- return adb_process.exitcode == 0
|
|
|
+- finally:
|
|
|
+- if adb_process:
|
|
|
+- adb_process.stdout_file.close()
|
|
|
+-
|
|
|
+- def shell_output(self, cmd, env=None, cwd=None, timeout=None, root=False):
|
|
|
+- """Executes an adb shell on the device returning stdout.
|
|
|
+-
|
|
|
+- :param str cmd: The command to be executed.
|
|
|
+- :param env: Contains the environment variables and their values.
|
|
|
+- :type env: dict or None
|
|
|
+- :param cwd: The directory from which to execute.
|
|
|
+- :type cwd: str or None
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError. This timeout is per
|
|
|
+- adb call. The total time spent may exceed this
|
|
|
+- value. If it is not specified, the value set
|
|
|
+- in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command
|
|
|
+- should be executed as root.
|
|
|
+- :returns: string - content of stdout.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- adb_process = None
|
|
|
+- try:
|
|
|
+- adb_process = self.shell(cmd, env=env, cwd=cwd,
|
|
|
+- timeout=timeout, root=root)
|
|
|
+- if adb_process.timedout:
|
|
|
+- raise ADBTimeoutError("%s" % adb_process)
|
|
|
+- elif adb_process.exitcode:
|
|
|
+- raise ADBError("%s" % adb_process)
|
|
|
+- output = adb_process.stdout_file.read().rstrip()
|
|
|
+- if self._verbose:
|
|
|
+- self._logger.debug('shell_output: %s, '
|
|
|
+- 'timeout: %s, '
|
|
|
+- 'root: %s, '
|
|
|
+- 'timedout: %s, '
|
|
|
+- 'exitcode: %s, '
|
|
|
+- 'output: %s' %
|
|
|
+- (' '.join(adb_process.args),
|
|
|
+- timeout,
|
|
|
+- root,
|
|
|
+- adb_process.timedout,
|
|
|
+- adb_process.exitcode,
|
|
|
+- output))
|
|
|
+-
|
|
|
+- return output
|
|
|
+- finally:
|
|
|
+- if adb_process and isinstance(adb_process.stdout_file, file):
|
|
|
+- adb_process.stdout_file.close()
|
|
|
+-
|
|
|
+- # Informational methods
|
|
|
+-
|
|
|
+- def _get_logcat_buffer_args(self, buffers):
|
|
|
+- valid_buffers = set(['radio', 'main', 'events'])
|
|
|
+- invalid_buffers = set(buffers).difference(valid_buffers)
|
|
|
+- if invalid_buffers:
|
|
|
+- raise ADBError('Invalid logcat buffers %s not in %s ' % (
|
|
|
+- list(invalid_buffers), list(valid_buffers)))
|
|
|
+- args = []
|
|
|
+- for b in buffers:
|
|
|
+- args.extend(['-b', b])
|
|
|
+- return args
|
|
|
+-
|
|
|
+- def clear_logcat(self, timeout=None, buffers=[]):
|
|
|
+- """Clears logcat via adb logcat -c.
|
|
|
+-
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError. This timeout is per
|
|
|
+- adb call. The total time spent may exceed this
|
|
|
+- value. If it is not specified, the value set
|
|
|
+- in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param list buffers: Log buffers to clear. Valid buffers are
|
|
|
+- "radio", "events", and "main". Defaults to "main".
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- buffers = self._get_logcat_buffer_args(buffers)
|
|
|
+- cmds = ["logcat", "-c"] + buffers
|
|
|
+- self.command_output(cmds, timeout=timeout)
|
|
|
+- self.shell_output("log logcat cleared", timeout=timeout)
|
|
|
+-
|
|
|
+- def get_logcat(self,
|
|
|
+- filter_specs=[
|
|
|
+- "dalvikvm:I",
|
|
|
+- "ConnectivityService:S",
|
|
|
+- "WifiMonitor:S",
|
|
|
+- "WifiStateTracker:S",
|
|
|
+- "wpa_supplicant:S",
|
|
|
+- "NetworkStateTracker:S"],
|
|
|
+- format="time",
|
|
|
+- filter_out_regexps=[],
|
|
|
+- timeout=None,
|
|
|
+- buffers=[]):
|
|
|
+- """Returns the contents of the logcat file as a list of strings.
|
|
|
+-
|
|
|
+- :param list filter_specs: Optional logcat messages to
|
|
|
+- be included.
|
|
|
+- :param str format: Optional logcat format.
|
|
|
+- :param list filterOutRexps: Optional logcat messages to be
|
|
|
+- excluded.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param list buffers: Log buffers to retrieve. Valid buffers are
|
|
|
+- "radio", "events", and "main". Defaults to "main".
|
|
|
+- :returns: list of lines logcat output.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- buffers = self._get_logcat_buffer_args(buffers)
|
|
|
+- cmds = ["logcat", "-v", format, "-d"] + buffers + filter_specs
|
|
|
+- lines = self.command_output(cmds, timeout=timeout).splitlines()
|
|
|
+-
|
|
|
+- for regex in filter_out_regexps:
|
|
|
+- lines = [line for line in lines if not re.search(regex, line)]
|
|
|
+-
|
|
|
+- return lines
|
|
|
+-
|
|
|
+- def get_prop(self, prop, timeout=None):
|
|
|
+- """Gets value of a property from the device via adb shell getprop.
|
|
|
+-
|
|
|
+- :param str prop: The propery name.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: string value of property.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- output = self.shell_output('getprop %s' % prop, timeout=timeout)
|
|
|
+- return output
|
|
|
+-
|
|
|
+- def get_state(self, timeout=None):
|
|
|
+- """Returns the device's state via adb get-state.
|
|
|
+-
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before throwing
|
|
|
+- an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: string value of adb get-state.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- output = self.command_output(["get-state"], timeout=timeout).strip()
|
|
|
+- return output
|
|
|
+-
|
|
|
+- def get_ip_address(self, interfaces=None, timeout=None):
|
|
|
+- """Returns the device's ip address, or None if it doesn't have one
|
|
|
+-
|
|
|
+- :param interfaces: Interfaces to allow, or None to allow any
|
|
|
+- non-loopback interface.
|
|
|
+- :type interfaces: list or None
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before throwing
|
|
|
+- an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: string ip address of the device or None if it could not
|
|
|
+- be found.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- if not interfaces:
|
|
|
+- interfaces = ["wlan0", "eth0"]
|
|
|
+- wifi_interface = self.shell_output('getprop wifi.interface', timeout=timeout)
|
|
|
+- self._logger.debug('get_ip_address: wifi_interface: %s' % wifi_interface)
|
|
|
+- if wifi_interface and wifi_interface not in interfaces:
|
|
|
+- interfaces = interfaces.append(wifi_interface)
|
|
|
+-
|
|
|
+- # ifconfig interface
|
|
|
+- # can return two different formats:
|
|
|
+- # eth0: ip 192.168.1.139 mask 255.255.255.0 flags [up broadcast running multicast]
|
|
|
+- # or
|
|
|
+- # wlan0 Link encap:Ethernet HWaddr 00:9A:CD:B8:39:65
|
|
|
+- # inet addr:192.168.1.38 Bcast:192.168.1.255 Mask:255.255.255.0
|
|
|
+- # inet6 addr: fe80::29a:cdff:feb8:3965/64 Scope: Link
|
|
|
+- # UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
|
|
|
+- # RX packets:180 errors:0 dropped:0 overruns:0 frame:0
|
|
|
+- # TX packets:218 errors:0 dropped:0 overruns:0 carrier:0
|
|
|
+- # collisions:0 txqueuelen:1000
|
|
|
+- # RX bytes:84577 TX bytes:31202
|
|
|
+-
|
|
|
+- re1_ip = re.compile(r'(\w+): ip ([0-9.]+) mask.*')
|
|
|
+- # re1_ip will match output of the first format
|
|
|
+- # with group 1 returning the interface and group 2 returing the ip address.
|
|
|
+-
|
|
|
+- # re2_interface will match the interface line in the second format
|
|
|
+- # while re2_ip will match the inet addr line of the second format.
|
|
|
+- re2_interface = re.compile(r'(\w+)\s+Link')
|
|
|
+- re2_ip = re.compile(r'\s+inet addr:([0-9.]+)')
|
|
|
+-
|
|
|
+- matched_interface = None
|
|
|
+- matched_ip = None
|
|
|
+- re_bad_addr = re.compile(r'127.0.0.1|0.0.0.0')
|
|
|
+-
|
|
|
+- self._logger.debug('get_ip_address: ifconfig')
|
|
|
+- for interface in interfaces:
|
|
|
+- try:
|
|
|
+- output = self.shell_output('ifconfig %s' % interface,
|
|
|
+- timeout=timeout)
|
|
|
+- except ADBError:
|
|
|
+- output = ''
|
|
|
+-
|
|
|
+- for line in output.splitlines():
|
|
|
+- if not matched_interface:
|
|
|
+- match = re1_ip.match(line)
|
|
|
+- if match:
|
|
|
+- matched_interface, matched_ip = match.groups()
|
|
|
+- else:
|
|
|
+- match = re2_interface.match(line)
|
|
|
+- if match:
|
|
|
+- matched_interface = match.group(1)
|
|
|
+- else:
|
|
|
+- match = re2_ip.match(line)
|
|
|
+- if match:
|
|
|
+- matched_ip = match.group(1)
|
|
|
+-
|
|
|
+- if matched_ip:
|
|
|
+- if not re_bad_addr.match(matched_ip):
|
|
|
+- self._logger.debug('get_ip_address: found: %s %s' %
|
|
|
+- (matched_interface, matched_ip))
|
|
|
+- return matched_ip
|
|
|
+- matched_interface = None
|
|
|
+- matched_ip = None
|
|
|
+-
|
|
|
+- self._logger.debug('get_ip_address: netcfg')
|
|
|
+- # Fall back on netcfg if ifconfig does not work.
|
|
|
+- # $ adb shell netcfg
|
|
|
+- # lo UP 127.0.0.1/8 0x00000049 00:00:00:00:00:00
|
|
|
+- # dummy0 DOWN 0.0.0.0/0 0x00000082 8e:cd:67:48:b7:c2
|
|
|
+- # rmnet0 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
|
|
|
+- # rmnet1 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
|
|
|
+- # rmnet2 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
|
|
|
+- # rmnet3 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
|
|
|
+- # rmnet4 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
|
|
|
+- # rmnet5 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
|
|
|
+- # rmnet6 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
|
|
|
+- # rmnet7 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
|
|
|
+- # sit0 DOWN 0.0.0.0/0 0x00000080 00:00:00:00:00:00
|
|
|
+- # vip0 DOWN 0.0.0.0/0 0x00001012 00:01:00:00:00:01
|
|
|
+- # wlan0 UP 192.168.1.157/24 0x00001043 38:aa:3c:1c:f6:94
|
|
|
+-
|
|
|
+- re3_netcfg = re.compile(r'(\w+)\s+UP\s+([1-9]\d{0,2}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
|
|
|
+- try:
|
|
|
+- output = self.shell_output('netcfg', timeout=timeout)
|
|
|
+- except ADBError:
|
|
|
+- output = ''
|
|
|
+- for line in output.splitlines():
|
|
|
+- match = re3_netcfg.search(line)
|
|
|
+- if match:
|
|
|
+- matched_interface, matched_ip = match.groups()
|
|
|
+- if matched_interface == "lo" or re_bad_addr.match(matched_ip):
|
|
|
+- matched_interface = None
|
|
|
+- matched_ip = None
|
|
|
+- elif matched_ip and matched_interface in interfaces:
|
|
|
+- self._logger.debug('get_ip_address: found: %s %s' %
|
|
|
+- (matched_interface, matched_ip))
|
|
|
+- return matched_ip
|
|
|
+- self._logger.debug('get_ip_address: not found')
|
|
|
+- return matched_ip
|
|
|
+-
|
|
|
+- # File management methods
|
|
|
+-
|
|
|
+- def remount(self, timeout=None):
|
|
|
+- """Remount /system/ in read/write mode
|
|
|
+-
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before throwing
|
|
|
+- an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError"""
|
|
|
+-
|
|
|
+- rv = self.command_output(["remount"], timeout=timeout)
|
|
|
+- if not rv.startswith("remount succeeded"):
|
|
|
+- raise ADBError("Unable to remount device")
|
|
|
+-
|
|
|
+- def chmod(self, path, recursive=False, mask="777", timeout=None, root=False):
|
|
|
+- """Recursively changes the permissions of a directory on the
|
|
|
+- device.
|
|
|
+-
|
|
|
+- :param str path: The directory name on the device.
|
|
|
+- :param bool recursive: Flag specifying if the command should be
|
|
|
+- executed recursively.
|
|
|
+- :param str mask: The octal permissions.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before throwing
|
|
|
+- an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- # Note that on some tests such as webappstartup, an error
|
|
|
+- # occurs during recursive calls to chmod where a "No such file
|
|
|
+- # or directory" error will occur for the
|
|
|
+- # /data/data/org.mozilla.fennec/files/mozilla/*.webapp0/lock
|
|
|
+- # which is a symbolic link to a socket: lock ->
|
|
|
+- # 127.0.0.1:+<port>. On Linux, chmod -R ignores symbolic
|
|
|
+- # links but it appear Android's version does not. We ignore
|
|
|
+- # this type of error, but pass on any other errors that are
|
|
|
+- # detected.
|
|
|
+- path = posixpath.normpath(path.strip())
|
|
|
+- self._logger.debug('chmod: path=%s, recursive=%s, mask=%s, root=%s' %
|
|
|
+- (path, recursive, mask, root))
|
|
|
+- if not recursive:
|
|
|
+- self.shell_output("chmod %s %s" % (mask, path),
|
|
|
+- timeout=timeout, root=root)
|
|
|
+- return
|
|
|
+-
|
|
|
+- if self._chmod_R:
|
|
|
+- try:
|
|
|
+- self.shell_output("chmod -R %s %s" % (mask, path),
|
|
|
+- timeout=timeout, root=root)
|
|
|
+- except ADBError as e:
|
|
|
+- if e.message.find('No such file or directory') == -1:
|
|
|
+- raise
|
|
|
+- self._logger.warning('chmod -R %s %s: Ignoring Error: %s' %
|
|
|
+- (mask, path, e.message))
|
|
|
+- return
|
|
|
+- # Obtain a list of the directories and files which match path
|
|
|
+- # and construct a shell script which explictly calls chmod on
|
|
|
+- # each of them.
|
|
|
+- entries = self.ls(path, recursive=recursive, timeout=timeout,
|
|
|
+- root=root)
|
|
|
+- tmpf = None
|
|
|
+- chmodsh = None
|
|
|
+- try:
|
|
|
+- tmpf = tempfile.NamedTemporaryFile(delete=False)
|
|
|
+- for entry in entries:
|
|
|
+- tmpf.write('chmod %s %s\n' % (mask, entry))
|
|
|
+- tmpf.close()
|
|
|
+- chmodsh = '/data/local/tmp/%s' % os.path.basename(tmpf.name)
|
|
|
+- self.push(tmpf.name, chmodsh)
|
|
|
+- self.shell_output('chmod 777 %s' % chmodsh, timeout=timeout,
|
|
|
+- root=root)
|
|
|
+- self.shell_output('sh -c %s' % chmodsh, timeout=timeout,
|
|
|
+- root=root)
|
|
|
+- finally:
|
|
|
+- if tmpf:
|
|
|
+- os.unlink(tmpf.name)
|
|
|
+- if chmodsh:
|
|
|
+- self.rm(chmodsh, timeout=timeout, root=root)
|
|
|
+-
|
|
|
+- def exists(self, path, timeout=None, root=False):
|
|
|
+- """Returns True if the path exists on the device.
|
|
|
+-
|
|
|
+- :param str path: The directory name on the device.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should be
|
|
|
+- executed as root.
|
|
|
+- :returns: boolean - True if path exists.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- """
|
|
|
+- path = posixpath.normpath(path)
|
|
|
+- return self.shell_bool('ls -a %s' % path, timeout=timeout, root=root)
|
|
|
+-
|
|
|
+- def is_dir(self, path, timeout=None, root=False):
|
|
|
+- """Returns True if path is an existing directory on the device.
|
|
|
+-
|
|
|
+- :param str path: The path on the device.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :returns: boolean - True if path exists on the device and is a
|
|
|
+- directory.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- """
|
|
|
+- path = posixpath.normpath(path)
|
|
|
+- return self.shell_bool('ls -a %s/' % path, timeout=timeout, root=root)
|
|
|
+-
|
|
|
+- def is_file(self, path, timeout=None, root=False):
|
|
|
+- """Returns True if path is an existing file on the device.
|
|
|
+-
|
|
|
+- :param str path: The file name on the device.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :returns: boolean - True if path exists on the device and is a
|
|
|
+- file.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- """
|
|
|
+- path = posixpath.normpath(path)
|
|
|
+- return (
|
|
|
+- self.exists(path, timeout=timeout, root=root) and
|
|
|
+- not self.is_dir(path, timeout=timeout, root=root))
|
|
|
+-
|
|
|
+- def list_files(self, path, timeout=None, root=False):
|
|
|
+- """Return a list of files/directories contained in a directory
|
|
|
+- on the device.
|
|
|
+-
|
|
|
+- :param str path: The directory name on the device.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :returns: list of files/directories contained in the directory.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- """
|
|
|
+- path = posixpath.normpath(path.strip())
|
|
|
+- data = []
|
|
|
+- if self.is_dir(path, timeout=timeout, root=root):
|
|
|
+- try:
|
|
|
+- data = self.shell_output("%s %s" % (self._ls, path),
|
|
|
+- timeout=timeout,
|
|
|
+- root=root).splitlines()
|
|
|
+- self._logger.debug('list_files: data: %s' % data)
|
|
|
+- except ADBError:
|
|
|
+- self._logger.error('Ignoring exception in ADBDevice.list_files\n%s' %
|
|
|
+- traceback.format_exc())
|
|
|
+- data[:] = [item for item in data if item]
|
|
|
+- self._logger.debug('list_files: %s' % data)
|
|
|
+- return data
|
|
|
+-
|
|
|
+- def ls(self, path, recursive=False, timeout=None, root=False):
|
|
|
+- """Return a list of matching files/directories on the device.
|
|
|
+-
|
|
|
+- The ls method emulates the behavior of the ls shell command.
|
|
|
+- It differs from the list_files method by supporting wild cards
|
|
|
+- and returning matches even if the path is not a directory and
|
|
|
+- by allowing a recursive listing.
|
|
|
+-
|
|
|
+- ls /sdcard always returns /sdcard and not the contents of the
|
|
|
+- sdcard path. The ls method makes the behavior consistent with
|
|
|
+- others paths by adjusting /sdcard to /sdcard/. Note this is
|
|
|
+- also the case of other sdcard related paths such as
|
|
|
+- /storage/emulated/legacy but no adjustment is made in those
|
|
|
+- cases.
|
|
|
+-
|
|
|
+- The ls method works around a Nexus 4 bug which prevents
|
|
|
+- recursive listing of directories on the sdcard unless the path
|
|
|
+- ends with "/*" by adjusting sdcard paths ending in "/" to end
|
|
|
+- with "/*". This adjustment is only made on official Nexus 4
|
|
|
+- builds with property ro.product.model "Nexus 4". Note that
|
|
|
+- this will fail to return any "hidden" files or directories
|
|
|
+- which begin with ".".
|
|
|
+-
|
|
|
+- :param str path: The directory name on the device.
|
|
|
+- :param bool recursive: Flag specifying if a recursive listing
|
|
|
+- is to be returned. If recursive is False, the returned
|
|
|
+- matches will be relative to the path. If recursive is True,
|
|
|
+- the returned matches will be absolute paths.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :returns: list of files/directories contained in the directory.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- """
|
|
|
+- path = posixpath.normpath(path.strip())
|
|
|
+- parent = ''
|
|
|
+- entries = {}
|
|
|
+-
|
|
|
+- if path == '/sdcard':
|
|
|
+- path += '/'
|
|
|
+-
|
|
|
+- # Android 2.3 and later all appear to support ls -R however
|
|
|
+- # Nexus 4 does not perform a recursive search on the sdcard
|
|
|
+- # unless the path is a directory with * wild card.
|
|
|
+- if not recursive:
|
|
|
+- recursive_flag = ''
|
|
|
+- else:
|
|
|
+- recursive_flag = '-R'
|
|
|
+- if path.startswith('/sdcard') and path.endswith('/'):
|
|
|
+- model = self.shell_output('getprop ro.product.model',
|
|
|
+- timeout=timeout,
|
|
|
+- root=root)
|
|
|
+- if model == 'Nexus 4':
|
|
|
+- path += '*'
|
|
|
+- lines = self.shell_output('%s %s %s' % (self._ls, recursive_flag, path),
|
|
|
+- timeout=timeout,
|
|
|
+- root=root).splitlines()
|
|
|
+- for line in lines:
|
|
|
+- line = line.strip()
|
|
|
+- if not line:
|
|
|
+- parent = ''
|
|
|
+- continue
|
|
|
+- if line.endswith(':'): # This is a directory
|
|
|
+- parent = line.replace(':', '/')
|
|
|
+- entry = parent
|
|
|
+- # Remove earlier entry which is marked as a file.
|
|
|
+- if parent[:-1] in entries:
|
|
|
+- del entries[parent[:-1]]
|
|
|
+- elif parent:
|
|
|
+- entry = "%s%s" % (parent, line)
|
|
|
+- else:
|
|
|
+- entry = line
|
|
|
+- entries[entry] = 1
|
|
|
+- entry_list = entries.keys()
|
|
|
+- entry_list.sort()
|
|
|
+- return entry_list
|
|
|
+-
|
|
|
+- def mkdir(self, path, parents=False, timeout=None, root=False):
|
|
|
+- """Create a directory on the device.
|
|
|
+-
|
|
|
+- :param str path: The directory name on the device
|
|
|
+- to be created.
|
|
|
+- :param bool parents: Flag indicating if the parent directories are
|
|
|
+- also to be created. Think mkdir -p path.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- path = posixpath.normpath(path)
|
|
|
+- if parents:
|
|
|
+- if self._mkdir_p is None or self._mkdir_p:
|
|
|
+- # Use shell_bool to catch the possible
|
|
|
+- # non-zero exitcode if -p is not supported.
|
|
|
+- if self.shell_bool('mkdir -p %s' % path, timeout=timeout,
|
|
|
+- root=root):
|
|
|
+- self._mkdir_p = True
|
|
|
+- return
|
|
|
+- # mkdir -p is not supported. create the parent
|
|
|
+- # directories individually.
|
|
|
+- if not self.is_dir(posixpath.dirname(path), root=root):
|
|
|
+- parts = path.split('/')
|
|
|
+- name = "/"
|
|
|
+- for part in parts[:-1]:
|
|
|
+- if part != "":
|
|
|
+- name = posixpath.join(name, part)
|
|
|
+- if not self.is_dir(name, root=root):
|
|
|
+- # Use shell_output to allow any non-zero
|
|
|
+- # exitcode to raise an ADBError.
|
|
|
+- self.shell_output('mkdir %s' % name,
|
|
|
+- timeout=timeout, root=root)
|
|
|
+-
|
|
|
+- # If parents is True and the directory does exist, we don't
|
|
|
+- # need to do anything. Otherwise we call mkdir. If the
|
|
|
+- # directory already exists or if it is a file instead of a
|
|
|
+- # directory, mkdir will fail and we will raise an ADBError.
|
|
|
+- if not parents or not self.is_dir(path, root=root):
|
|
|
+- self.shell_output('mkdir %s' % path, timeout=timeout, root=root)
|
|
|
+- if not self.is_dir(path, timeout=timeout, root=root):
|
|
|
+- raise ADBError('mkdir %s Failed' % path)
|
|
|
+-
|
|
|
+- def push(self, local, remote, timeout=None):
|
|
|
+- """Pushes a file or directory to the device.
|
|
|
+-
|
|
|
+- :param str local: The name of the local file or
|
|
|
+- directory name.
|
|
|
+- :param str remote: The name of the remote file or
|
|
|
+- directory name.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- # remove trailing /
|
|
|
+- local = os.path.normpath(local)
|
|
|
+- remote = os.path.normpath(remote)
|
|
|
+- copy_required = False
|
|
|
+- if os.path.isdir(local):
|
|
|
+- copy_required = True
|
|
|
+- temp_parent = tempfile.mkdtemp()
|
|
|
+- remote_name = os.path.basename(remote)
|
|
|
+- new_local = os.path.join(temp_parent, remote_name)
|
|
|
+- dir_util.copy_tree(local, new_local)
|
|
|
+- local = new_local
|
|
|
+- # See do_sync_push in
|
|
|
+- # https://android.googlesource.com/platform/system/core/+/master/adb/file_sync_client.cpp
|
|
|
+- # Work around change in behavior in adb 1.0.36 where if
|
|
|
+- # the remote destination directory exists, adb push will
|
|
|
+- # copy the source directory *into* the destination
|
|
|
+- # directory otherwise it will copy the source directory
|
|
|
+- # *onto* the destination directory.
|
|
|
+- if self._adb_version >= '1.0.36':
|
|
|
+- remote = '/'.join(remote.rstrip('/').split('/')[:-1])
|
|
|
+- try:
|
|
|
+- self.command_output(["push", local, remote], timeout=timeout)
|
|
|
+- except BaseException:
|
|
|
+- raise
|
|
|
+- finally:
|
|
|
+- if copy_required:
|
|
|
+- shutil.rmtree(temp_parent)
|
|
|
+-
|
|
|
+- def pull(self, remote, local, timeout=None):
|
|
|
+- """Pulls a file or directory from the device.
|
|
|
+-
|
|
|
+- :param str remote: The path of the remote file or
|
|
|
+- directory.
|
|
|
+- :param str local: The path of the local file or
|
|
|
+- directory name.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- # remove trailing /
|
|
|
+- local = os.path.normpath(local)
|
|
|
+- remote = os.path.normpath(remote)
|
|
|
+- copy_required = False
|
|
|
+- original_local = local
|
|
|
+- if self._adb_version >= '1.0.36' and \
|
|
|
+- os.path.isdir(local) and self.is_dir(remote):
|
|
|
+- # See do_sync_pull in
|
|
|
+- # https://android.googlesource.com/platform/system/core/+/master/adb/file_sync_client.cpp
|
|
|
+- # Work around change in behavior in adb 1.0.36 where if
|
|
|
+- # the local destination directory exists, adb pull will
|
|
|
+- # copy the source directory *into* the destination
|
|
|
+- # directory otherwise it will copy the source directory
|
|
|
+- # *onto* the destination directory.
|
|
|
+- #
|
|
|
+- # If the destination directory does exist, pull to its
|
|
|
+- # parent directory. If the source and destination leaf
|
|
|
+- # directory names are different, pull the source directory
|
|
|
+- # into a temporary directory and then copy the temporary
|
|
|
+- # directory onto the destination.
|
|
|
+- local_name = os.path.basename(local)
|
|
|
+- remote_name = os.path.basename(remote)
|
|
|
+- if local_name != remote_name:
|
|
|
+- copy_required = True
|
|
|
+- temp_parent = tempfile.mkdtemp()
|
|
|
+- local = os.path.join(temp_parent, remote_name)
|
|
|
+- else:
|
|
|
+- local = '/'.join(local.rstrip('/').split('/')[:-1])
|
|
|
+- try:
|
|
|
+- self.command_output(["pull", remote, local], timeout=timeout)
|
|
|
+- except BaseException:
|
|
|
+- raise
|
|
|
+- finally:
|
|
|
+- if copy_required:
|
|
|
+- dir_util.copy_tree(local, original_local)
|
|
|
+- shutil.rmtree(temp_parent)
|
|
|
+-
|
|
|
+- def rm(self, path, recursive=False, force=False, timeout=None, root=False):
|
|
|
+- """Delete files or directories on the device.
|
|
|
+-
|
|
|
+- :param str path: The path of the remote file or directory.
|
|
|
+- :param bool recursive: Flag specifying if the command is
|
|
|
+- to be applied recursively to the target. Default is False.
|
|
|
+- :param bool force: Flag which if True will not raise an
|
|
|
+- error when attempting to delete a non-existent file. Default
|
|
|
+- is False.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- cmd = "rm"
|
|
|
+- if recursive:
|
|
|
+- cmd += " -r"
|
|
|
+- try:
|
|
|
+- self.shell_output("%s %s" % (cmd, path), timeout=timeout, root=root)
|
|
|
+- if self.is_file(path, timeout=timeout, root=root):
|
|
|
+- raise ADBError('rm("%s") failed to remove file.' % path)
|
|
|
+- except ADBError as e:
|
|
|
+- if not force and 'No such file or directory' in e.message:
|
|
|
+- raise
|
|
|
+-
|
|
|
+- def rmdir(self, path, timeout=None, root=False):
|
|
|
+- """Delete empty directory on the device.
|
|
|
+-
|
|
|
+- :param str path: The directory name on the device.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- self.shell_output("rmdir %s" % path, timeout=timeout, root=root)
|
|
|
+- if self.is_dir(path, timeout=timeout, root=root):
|
|
|
+- raise ADBError('rmdir("%s") failed to remove directory.' % path)
|
|
|
+-
|
|
|
+- # Process management methods
|
|
|
+-
|
|
|
+- def get_process_list(self, timeout=None):
|
|
|
+- """Returns list of tuples (pid, name, user) for running
|
|
|
+- processes on device.
|
|
|
+-
|
|
|
+- :param timeout: The maximum time
|
|
|
+- in seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified,
|
|
|
+- the value set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: list of (pid, name, user) tuples for running processes
|
|
|
+- on the device.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- adb_process = None
|
|
|
+- try:
|
|
|
+- adb_process = self.shell("ps", timeout=timeout)
|
|
|
+- if adb_process.timedout:
|
|
|
+- raise ADBTimeoutError("%s" % adb_process)
|
|
|
+- elif adb_process.exitcode:
|
|
|
+- raise ADBError("%s" % adb_process)
|
|
|
+- # first line is the headers
|
|
|
+- header = adb_process.stdout_file.readline()
|
|
|
+- pid_i = -1
|
|
|
+- user_i = -1
|
|
|
+- els = header.split()
|
|
|
+- for i in range(len(els)):
|
|
|
+- item = els[i].lower()
|
|
|
+- if item == 'user':
|
|
|
+- user_i = i
|
|
|
+- elif item == 'pid':
|
|
|
+- pid_i = i
|
|
|
+- if user_i == -1 or pid_i == -1:
|
|
|
+- self._logger.error('get_process_list: %s' % header)
|
|
|
+- raise ADBError('get_process_list: Unknown format: %s: %s' % (
|
|
|
+- header, adb_process))
|
|
|
+- ret = []
|
|
|
+- line = adb_process.stdout_file.readline()
|
|
|
+- while line:
|
|
|
+- els = line.split()
|
|
|
+- try:
|
|
|
+- ret.append([int(els[pid_i]), els[-1], els[user_i]])
|
|
|
+- except ValueError:
|
|
|
+- self._logger.error('get_process_list: %s %s\n%s' % (
|
|
|
+- header, line, traceback.format_exc()))
|
|
|
+- raise ADBError('get_process_list: %s: %s: %s' % (
|
|
|
+- header, line, adb_process))
|
|
|
+- line = adb_process.stdout_file.readline()
|
|
|
+- self._logger.debug('get_process_list: %s' % ret)
|
|
|
+- return ret
|
|
|
+- finally:
|
|
|
+- if adb_process and isinstance(adb_process.stdout_file, file):
|
|
|
+- adb_process.stdout_file.close()
|
|
|
+-
|
|
|
+- def kill(self, pids, sig=None, attempts=3, wait=5,
|
|
|
+- timeout=None, root=False):
|
|
|
+- """Kills processes on the device given a list of process ids.
|
|
|
+-
|
|
|
+- :param list pids: process ids to be killed.
|
|
|
+- :param sig: signal to be sent to the process.
|
|
|
+- :type sig: integer or None
|
|
|
+- :param integer attempts: number of attempts to try to
|
|
|
+- kill the processes.
|
|
|
+- :param integer wait: number of seconds to wait after each attempt.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- pid_list = [str(pid) for pid in pids]
|
|
|
+- for attempt in range(attempts):
|
|
|
+- args = ["kill"]
|
|
|
+- if sig:
|
|
|
+- args.append("-%d" % sig)
|
|
|
+- args.extend(pid_list)
|
|
|
+- try:
|
|
|
+- self.shell_output(' '.join(args), timeout=timeout, root=root)
|
|
|
+- except ADBError as e:
|
|
|
+- if 'No such process' not in e.message:
|
|
|
+- raise
|
|
|
+- pid_set = set(pid_list)
|
|
|
+- current_pid_set = set([str(proc[0]) for proc in
|
|
|
+- self.get_process_list(timeout=timeout)])
|
|
|
+- pid_list = list(pid_set.intersection(current_pid_set))
|
|
|
+- if not pid_list:
|
|
|
+- break
|
|
|
+- self._logger.debug("Attempt %d of %d to kill processes %s failed" %
|
|
|
+- (attempt + 1, attempts, pid_list))
|
|
|
+- time.sleep(wait)
|
|
|
+-
|
|
|
+- if pid_list:
|
|
|
+- raise ADBError('kill: processes %s not killed' % pid_list)
|
|
|
+-
|
|
|
+- def pkill(self, appname, sig=None, attempts=3, wait=5,
|
|
|
+- timeout=None, root=False):
|
|
|
+- """Kills a processes on the device matching a name.
|
|
|
+-
|
|
|
+- :param str appname: The app name of the process to
|
|
|
+- be killed. Note that only the first 75 characters of the
|
|
|
+- process name are significant.
|
|
|
+- :param sig: optional signal to be sent to the process.
|
|
|
+- :type sig: integer or None
|
|
|
+- :param integer attempts: number of attempts to try to
|
|
|
+- kill the processes.
|
|
|
+- :param integer wait: number of seconds to wait after each attempt.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should
|
|
|
+- be executed as root.
|
|
|
+-
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- procs = self.get_process_list(timeout=timeout)
|
|
|
+- # limit the comparion to the first 75 characters due to a
|
|
|
+- # limitation in processname length in android.
|
|
|
+- pids = [proc[0] for proc in procs if proc[1] == appname[:75]]
|
|
|
+- if not pids:
|
|
|
+- return
|
|
|
+-
|
|
|
+- try:
|
|
|
+- self.kill(pids, sig, attempts=attempts, wait=wait,
|
|
|
+- timeout=timeout, root=root)
|
|
|
+- except ADBError as e:
|
|
|
+- if self.process_exist(appname, timeout=timeout):
|
|
|
+- raise e
|
|
|
+-
|
|
|
+- def process_exist(self, process_name, timeout=None):
|
|
|
+- """Returns True if process with name process_name is running on
|
|
|
+- device.
|
|
|
+-
|
|
|
+- :param str process_name: The name of the process
|
|
|
+- to check. Note that only the first 75 characters of the
|
|
|
+- process name are significant.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: boolean - True if process exists.
|
|
|
+-
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- if not isinstance(process_name, basestring):
|
|
|
+- raise ADBError("Process name %s is not a string" % process_name)
|
|
|
+-
|
|
|
+- # Filter out extra spaces.
|
|
|
+- parts = [x for x in process_name.split(' ') if x != '']
|
|
|
+- process_name = ' '.join(parts)
|
|
|
+-
|
|
|
+- # Filter out the quoted env string if it exists
|
|
|
+- # ex: '"name=value;name2=value2;etc=..." process args' -> 'process args'
|
|
|
+- parts = process_name.split('"')
|
|
|
+- if len(parts) > 2:
|
|
|
+- process_name = ' '.join(parts[2:]).strip()
|
|
|
+-
|
|
|
+- pieces = process_name.split(' ')
|
|
|
+- parts = pieces[0].split('/')
|
|
|
+- app = parts[-1]
|
|
|
+-
|
|
|
+- proc_list = self.get_process_list(timeout=timeout)
|
|
|
+- if not proc_list:
|
|
|
+- return False
|
|
|
+-
|
|
|
+- for proc in proc_list:
|
|
|
+- proc_name = proc[1].split('/')[-1]
|
|
|
+- # limit the comparion to the first 75 characters due to a
|
|
|
+- # limitation in processname length in android.
|
|
|
+- if proc_name == app[:75]:
|
|
|
+- return True
|
|
|
+- return False
|
|
|
+-
|
|
|
+- def cp(self, source, destination, recursive=False, timeout=None,
|
|
|
+- root=False):
|
|
|
+- """Copies a file or directory on the device.
|
|
|
+-
|
|
|
+- :param source: string containing the path of the source file or
|
|
|
+- directory.
|
|
|
+- :param destination: string containing the path of the destination file
|
|
|
+- or directory.
|
|
|
+- :param recursive: optional boolean indicating if a recursive copy is to
|
|
|
+- be performed. Required if the source is a directory. Defaults to
|
|
|
+- False. Think cp -R source destination.
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- source = posixpath.normpath(source)
|
|
|
+- destination = posixpath.normpath(destination)
|
|
|
+- if self._have_cp:
|
|
|
+- r = '-R' if recursive else ''
|
|
|
+- self.shell_output('cp %s %s %s' % (r, source, destination),
|
|
|
+- timeout=timeout, root=root)
|
|
|
+- return
|
|
|
+-
|
|
|
+- # Emulate cp behavior depending on if source and destination
|
|
|
+- # already exists and whether they are a directory or file.
|
|
|
+- if not self.exists(source, timeout=timeout, root=root):
|
|
|
+- raise ADBError("cp: can't stat '%s': No such file or directory" %
|
|
|
+- source)
|
|
|
+-
|
|
|
+- if self.is_file(source, timeout=timeout, root=root):
|
|
|
+- if self.is_dir(destination, timeout=timeout, root=root):
|
|
|
+- # Copy the source file into the destination directory
|
|
|
+- destination = posixpath.join(destination,
|
|
|
+- posixpath.basename(source))
|
|
|
+- self.shell_output('dd if=%s of=%s' % (source, destination),
|
|
|
+- timeout=timeout, root=root)
|
|
|
+- return
|
|
|
+-
|
|
|
+- if self.is_file(destination, timeout=timeout, root=root):
|
|
|
+- raise ADBError('cp: %s: Not a directory' % destination)
|
|
|
+-
|
|
|
+- if not recursive:
|
|
|
+- raise ADBError("cp: omitting directory '%s'" % source)
|
|
|
+-
|
|
|
+- if self.is_dir(destination, timeout=timeout, root=root):
|
|
|
+- # Copy the source directory into the destination directory.
|
|
|
+- destination_dir = posixpath.join(destination,
|
|
|
+- posixpath.basename(source))
|
|
|
+- else:
|
|
|
+- # Copy the contents of the source directory into the
|
|
|
+- # destination directory.
|
|
|
+- destination_dir = destination
|
|
|
+-
|
|
|
+- try:
|
|
|
+- # Do not create parent directories since cp does not.
|
|
|
+- self.mkdir(destination_dir, timeout=timeout, root=root)
|
|
|
+- except ADBError as e:
|
|
|
+- if 'File exists' not in e.message:
|
|
|
+- raise
|
|
|
+-
|
|
|
+- for i in self.list_files(source, timeout=timeout, root=root):
|
|
|
+- self.cp(posixpath.join(source, i),
|
|
|
+- posixpath.join(destination_dir, i),
|
|
|
+- recursive=recursive,
|
|
|
+- timeout=timeout, root=root)
|
|
|
+-
|
|
|
+- def mv(self, source, destination, timeout=None, root=False):
|
|
|
+- """Moves a file or directory on the device.
|
|
|
+-
|
|
|
+- :param source: string containing the path of the source file or
|
|
|
+- directory.
|
|
|
+- :param destination: string containing the path of the destination file
|
|
|
+- or directory.
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBRootError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- source = posixpath.normpath(source)
|
|
|
+- destination = posixpath.normpath(destination)
|
|
|
+- self.shell_output('mv %s %s' % (source, destination), timeout=timeout,
|
|
|
+- root=root)
|
|
|
+-
|
|
|
+- def reboot(self, timeout=None):
|
|
|
+- """Reboots the device.
|
|
|
+-
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+-
|
|
|
+- reboot() reboots the device, issues an adb wait-for-device in order to
|
|
|
+- wait for the device to complete rebooting, then calls is_device_ready()
|
|
|
+- to determine if the device has completed booting.
|
|
|
+- """
|
|
|
+- self.command_output(["reboot"], timeout=timeout)
|
|
|
+- # command_output automatically inserts a 'wait-for-device'
|
|
|
+- # argument to adb. Issuing an empty command is the same as adb
|
|
|
+- # -s <device> wait-for-device. We don't send an explicit
|
|
|
+- # 'wait-for-device' since that would add duplicate
|
|
|
+- # 'wait-for-device' arguments which is an error in newer
|
|
|
+- # versions of adb.
|
|
|
+- self.command_output([], timeout=timeout)
|
|
|
+- self._check_adb_root(timeout=timeout)
|
|
|
+- return self.is_device_ready(timeout=timeout)
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def is_device_ready(self, timeout=None):
|
|
|
+- """Abstract class that returns True if the device is ready.
|
|
|
+-
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- return
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def get_battery_percentage(self, timeout=None):
|
|
|
+- """Abstract class that returns the battery charge as a percentage.
|
|
|
+-
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :returns: battery charge as a percentage.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- return
|
|
|
+-
|
|
|
+- def get_info(self, directive=None, timeout=None):
|
|
|
+- """
|
|
|
+- Returns a dictionary of information strings about the device.
|
|
|
+-
|
|
|
+- :param directive: information you want to get. Options are:
|
|
|
+- - `battery` - battery charge as a percentage
|
|
|
+- - `disk` - total, free, available bytes on disk
|
|
|
+- - `id` - unique id of the device
|
|
|
+- - `os` - name of the os
|
|
|
+- - `process` - list of running processes (same as ps)
|
|
|
+- - `systime` - system time of the device
|
|
|
+- - `uptime` - uptime of the device
|
|
|
+-
|
|
|
+- If `directive` is `None`, will return all available information
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- directives = ['battery', 'disk', 'id', 'os', 'process', 'systime',
|
|
|
+- 'uptime']
|
|
|
+-
|
|
|
+- if directive in directives:
|
|
|
+- directives = [directive]
|
|
|
+-
|
|
|
+- info = {}
|
|
|
+- if 'battery' in directives:
|
|
|
+- info['battery'] = self.get_battery_percentage(timeout=timeout)
|
|
|
+- if 'disk' in directives:
|
|
|
+- info['disk'] = self.shell_output('df /data /system /sdcard',
|
|
|
+- timeout=timeout).splitlines()
|
|
|
+- if 'id' in directives:
|
|
|
+- info['id'] = self.command_output(['get-serialno'], timeout=timeout)
|
|
|
+- if 'os' in directives:
|
|
|
+- info['os'] = self.shell_output('getprop ro.build.display.id',
|
|
|
+- timeout=timeout)
|
|
|
+- if 'process' in directives:
|
|
|
+- ps = self.shell_output('ps', timeout=timeout)
|
|
|
+- info['process'] = ps.splitlines()
|
|
|
+- if 'systime' in directives:
|
|
|
+- info['systime'] = self.shell_output('date', timeout=timeout)
|
|
|
+- if 'uptime' in directives:
|
|
|
+- uptime = self.shell_output('uptime', timeout=timeout)
|
|
|
+- if uptime:
|
|
|
+- m = re.match(r'up time: ((\d+) days, )*(\d{2}):(\d{2}):(\d{2})',
|
|
|
+- uptime)
|
|
|
+- if m:
|
|
|
+- uptime = '%d days %d hours %d minutes %d seconds' % tuple(
|
|
|
+- [int(g or 0) for g in m.groups()[1:]])
|
|
|
+- info['uptime'] = uptime
|
|
|
+- return info
|
|
|
+diff --git a/testing/mozbase/mozdevice/mozdevice/adb_android.py b/testing/mozbase/mozdevice/mozdevice/adb_android.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/mozdevice/adb_android.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,496 +0,0 @@
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
+-# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import, print_function
|
|
|
+-
|
|
|
+-import os
|
|
|
+-import re
|
|
|
+-import time
|
|
|
+-
|
|
|
+-from abc import ABCMeta
|
|
|
+-
|
|
|
+-from . import version_codes
|
|
|
+-
|
|
|
+-from .adb import ADBDevice, ADBError, ADBRootError
|
|
|
+-
|
|
|
+-
|
|
|
+-class ADBAndroid(ADBDevice):
|
|
|
+- """ADBAndroid implements :class:`ADBDevice` providing Android-specific
|
|
|
+- functionality.
|
|
|
+-
|
|
|
+- ::
|
|
|
+-
|
|
|
+- from mozdevice import ADBAndroid
|
|
|
+-
|
|
|
+- adbdevice = ADBAndroid()
|
|
|
+- print(adbdevice.list_files("/mnt/sdcard"))
|
|
|
+- if adbdevice.process_exist("org.mozilla.fennec"):
|
|
|
+- print("Fennec is running")
|
|
|
+- """
|
|
|
+- __metaclass__ = ABCMeta
|
|
|
+-
|
|
|
+- def __init__(self,
|
|
|
+- device=None,
|
|
|
+- adb='adb',
|
|
|
+- adb_host=None,
|
|
|
+- adb_port=None,
|
|
|
+- test_root='',
|
|
|
+- logger_name='adb',
|
|
|
+- timeout=300,
|
|
|
+- verbose=False,
|
|
|
+- device_ready_retry_wait=20,
|
|
|
+- device_ready_retry_attempts=3):
|
|
|
+- """Initializes the ADBAndroid object.
|
|
|
+-
|
|
|
+- :param device: When a string is passed, it is interpreted as the
|
|
|
+- device serial number. This form is not compatible with
|
|
|
+- devices containing a ":" in the serial; in this case
|
|
|
+- ValueError will be raised.
|
|
|
+- When a dictionary is passed it must have one or both of
|
|
|
+- the keys "device_serial" and "usb". This is compatible
|
|
|
+- with the dictionaries in the list returned by
|
|
|
+- ADBHost.devices(). If the value of device_serial is a
|
|
|
+- valid serial not containing a ":" it will be used to
|
|
|
+- identify the device, otherwise the value of the usb key,
|
|
|
+- prefixed with "usb:" is used.
|
|
|
+- If None is passed and there is exactly one device attached
|
|
|
+- to the host, that device is used. If there is more than one
|
|
|
+- device attached, ValueError is raised. If no device is
|
|
|
+- attached the constructor will block until a device is
|
|
|
+- attached or the timeout is reached.
|
|
|
+- :type device: dict, str or None
|
|
|
+- :param adb_host: host of the adb server to connect to.
|
|
|
+- :type adb_host: str or None
|
|
|
+- :param adb_port: port of the adb server to connect to.
|
|
|
+- :type adb_port: integer or None
|
|
|
+- :param str logger_name: logging logger name. Defaults to 'adb'.
|
|
|
+- :param integer device_ready_retry_wait: number of seconds to wait
|
|
|
+- between attempts to check if the device is ready after a
|
|
|
+- reboot.
|
|
|
+- :param integer device_ready_retry_attempts: number of attempts when
|
|
|
+- checking if a device is ready.
|
|
|
+-
|
|
|
+- :raises: * ADBError
|
|
|
+- * ADBTimeoutError
|
|
|
+- * ValueError
|
|
|
+- """
|
|
|
+- ADBDevice.__init__(self, device=device, adb=adb,
|
|
|
+- adb_host=adb_host, adb_port=adb_port,
|
|
|
+- test_root=test_root,
|
|
|
+- logger_name=logger_name, timeout=timeout,
|
|
|
+- verbose=verbose,
|
|
|
+- device_ready_retry_wait=device_ready_retry_wait,
|
|
|
+- device_ready_retry_attempts=device_ready_retry_attempts)
|
|
|
+- # https://source.android.com/devices/tech/security/selinux/index.html
|
|
|
+- # setenforce
|
|
|
+- # usage: setenforce [ Enforcing | Permissive | 1 | 0 ]
|
|
|
+- # getenforce returns either Enforcing or Permissive
|
|
|
+-
|
|
|
+- try:
|
|
|
+- self.selinux = True
|
|
|
+- if self.shell_output('getenforce', timeout=timeout) != 'Permissive':
|
|
|
+- self._logger.info('Setting SELinux Permissive Mode')
|
|
|
+- self.shell_output("setenforce Permissive", timeout=timeout, root=True)
|
|
|
+- except (ADBError, ADBRootError) as e:
|
|
|
+- self._logger.warning('Unable to set SELinux Permissive due to %s.' % e)
|
|
|
+- self.selinux = False
|
|
|
+-
|
|
|
+- self.version = int(self.shell_output("getprop ro.build.version.sdk",
|
|
|
+- timeout=timeout))
|
|
|
+-
|
|
|
+- def reboot(self, timeout=None):
|
|
|
+- """Reboots the device.
|
|
|
+-
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+-
|
|
|
+- reboot() reboots the device, issues an adb wait-for-device in order to
|
|
|
+- wait for the device to complete rebooting, then calls is_device_ready()
|
|
|
+- to determine if the device has completed booting.
|
|
|
+-
|
|
|
+- If the device supports running adbd as root, adbd will be
|
|
|
+- restarted running as root. Then, if the device supports
|
|
|
+- SELinux, setenforce Permissive will be called to change
|
|
|
+- SELinux to permissive. This must be done after adbd is
|
|
|
+- restarted in order for the SELinux Permissive setting to
|
|
|
+- persist.
|
|
|
+-
|
|
|
+- """
|
|
|
+- ready = ADBDevice.reboot(self, timeout=timeout)
|
|
|
+- self._check_adb_root(timeout=timeout)
|
|
|
+- return ready
|
|
|
+-
|
|
|
+- # Informational methods
|
|
|
+-
|
|
|
+- def get_battery_percentage(self, timeout=None):
|
|
|
+- """Returns the battery charge as a percentage.
|
|
|
+-
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :returns: battery charge as a percentage.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- level = None
|
|
|
+- scale = None
|
|
|
+- percentage = 0
|
|
|
+- cmd = "dumpsys battery"
|
|
|
+- re_parameter = re.compile(r'\s+(\w+):\s+(\d+)')
|
|
|
+- lines = self.shell_output(cmd, timeout=timeout).splitlines()
|
|
|
+- for line in lines:
|
|
|
+- match = re_parameter.match(line)
|
|
|
+- if match:
|
|
|
+- parameter = match.group(1)
|
|
|
+- value = match.group(2)
|
|
|
+- if parameter == 'level':
|
|
|
+- level = float(value)
|
|
|
+- elif parameter == 'scale':
|
|
|
+- scale = float(value)
|
|
|
+- if parameter is not None and scale is not None:
|
|
|
+- percentage = 100.0 * level / scale
|
|
|
+- break
|
|
|
+- return percentage
|
|
|
+-
|
|
|
+- # System control methods
|
|
|
+-
|
|
|
+- def is_device_ready(self, timeout=None):
|
|
|
+- """Checks if a device is ready for testing.
|
|
|
+-
|
|
|
+- This method uses the android only package manager to check for
|
|
|
+- readiness.
|
|
|
+-
|
|
|
+- :param timeout: The maximum time
|
|
|
+- in seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- # command_output automatically inserts a 'wait-for-device'
|
|
|
+- # argument to adb. Issuing an empty command is the same as adb
|
|
|
+- # -s <device> wait-for-device. We don't send an explicit
|
|
|
+- # 'wait-for-device' since that would add duplicate
|
|
|
+- # 'wait-for-device' arguments which is an error in newer
|
|
|
+- # versions of adb.
|
|
|
+- self.command_output([], timeout=timeout)
|
|
|
+- pm_error_string = "Error: Could not access the Package Manager"
|
|
|
+- pm_list_commands = ["packages", "permission-groups", "permissions",
|
|
|
+- "instrumentation", "features", "libraries"]
|
|
|
+- ready_path = os.path.join(self.test_root, "ready")
|
|
|
+- for attempt in range(self._device_ready_retry_attempts):
|
|
|
+- failure = 'Unknown failure'
|
|
|
+- success = True
|
|
|
+- try:
|
|
|
+- state = self.get_state(timeout=timeout)
|
|
|
+- if state != 'device':
|
|
|
+- failure = "Device state: %s" % state
|
|
|
+- success = False
|
|
|
+- else:
|
|
|
+- if (self.selinux and self.shell_output('getenforce',
|
|
|
+- timeout=timeout) != 'Permissive'):
|
|
|
+- self._logger.info('Setting SELinux Permissive Mode')
|
|
|
+- self.shell_output("setenforce Permissive", timeout=timeout, root=True)
|
|
|
+- if self.is_dir(ready_path, timeout=timeout):
|
|
|
+- self.rmdir(ready_path, timeout=timeout)
|
|
|
+- self.mkdir(ready_path, timeout=timeout)
|
|
|
+- self.rmdir(ready_path, timeout=timeout)
|
|
|
+- # Invoke the pm list commands to see if it is up and
|
|
|
+- # running.
|
|
|
+- for pm_list_cmd in pm_list_commands:
|
|
|
+- data = self.shell_output("pm list %s" % pm_list_cmd,
|
|
|
+- timeout=timeout)
|
|
|
+- if pm_error_string in data:
|
|
|
+- failure = data
|
|
|
+- success = False
|
|
|
+- break
|
|
|
+- except ADBError as e:
|
|
|
+- success = False
|
|
|
+- failure = e.message
|
|
|
+-
|
|
|
+- if not success:
|
|
|
+- self._logger.debug('Attempt %s of %s device not ready: %s' % (
|
|
|
+- attempt + 1, self._device_ready_retry_attempts,
|
|
|
+- failure))
|
|
|
+- time.sleep(self._device_ready_retry_wait)
|
|
|
+-
|
|
|
+- return success
|
|
|
+-
|
|
|
+- def power_on(self, timeout=None):
|
|
|
+- """Sets the device's power stayon value.
|
|
|
+-
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- try:
|
|
|
+- self.shell_output('svc power stayon true',
|
|
|
+- timeout=timeout,
|
|
|
+- root=True)
|
|
|
+- except ADBError as e:
|
|
|
+- # Executing this via adb shell errors, but not interactively.
|
|
|
+- # Any other exitcode is a real error.
|
|
|
+- if 'exitcode: 137' not in e.message:
|
|
|
+- raise
|
|
|
+- self._logger.warning('Unable to set power stayon true: %s' % e)
|
|
|
+-
|
|
|
+- # Application management methods
|
|
|
+-
|
|
|
+- def install_app(self, apk_path, timeout=None):
|
|
|
+- """Installs an app on the device.
|
|
|
+-
|
|
|
+- :param str apk_path: The apk file name to be installed.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- cmd = ["install"]
|
|
|
+- if self.version >= version_codes.M:
|
|
|
+- cmd.append("-g")
|
|
|
+- cmd.append(apk_path)
|
|
|
+- data = self.command_output(cmd, timeout=timeout)
|
|
|
+- if data.find('Success') == -1:
|
|
|
+- raise ADBError("install failed for %s. Got: %s" %
|
|
|
+- (apk_path, data))
|
|
|
+-
|
|
|
+- def is_app_installed(self, app_name, timeout=None):
|
|
|
+- """Returns True if an app is installed on the device.
|
|
|
+-
|
|
|
+- :param str app_name: The name of the app to be checked.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- pm_error_string = 'Error: Could not access the Package Manager'
|
|
|
+- data = self.shell_output("pm list package %s" % app_name, timeout=timeout)
|
|
|
+- if pm_error_string in data:
|
|
|
+- raise ADBError(pm_error_string)
|
|
|
+- if app_name not in data:
|
|
|
+- return False
|
|
|
+- return True
|
|
|
+-
|
|
|
+- def launch_application(self, app_name, activity_name, intent, url=None,
|
|
|
+- extras=None, wait=True, fail_if_running=True,
|
|
|
+- timeout=None):
|
|
|
+- """Launches an Android application
|
|
|
+-
|
|
|
+- :param str app_name: Name of application (e.g. `com.android.chrome`)
|
|
|
+- :param str activity_name: Name of activity to launch (e.g. `.Main`)
|
|
|
+- :param str intent: Intent to launch application with
|
|
|
+- :param url: URL to open
|
|
|
+- :type url: str or None
|
|
|
+- :param extras: Extra arguments for application.
|
|
|
+- :type extras: dict or None
|
|
|
+- :param bool wait: If True, wait for application to start before
|
|
|
+- returning.
|
|
|
+- :param bool fail_if_running: Raise an exception if instance of
|
|
|
+- application is already running.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- # If fail_if_running is True, we throw an exception here. Only one
|
|
|
+- # instance of an application can be running at once on Android,
|
|
|
+- # starting a new instance may not be what we want depending on what
|
|
|
+- # we want to do
|
|
|
+- if fail_if_running and self.process_exist(app_name, timeout=timeout):
|
|
|
+- raise ADBError("Only one instance of an application may be running "
|
|
|
+- "at once")
|
|
|
+-
|
|
|
+- acmd = ["am", "start"] + \
|
|
|
+- ["-W" if wait else '', "-n", "%s/%s" % (app_name, activity_name)]
|
|
|
+-
|
|
|
+- if intent:
|
|
|
+- acmd.extend(["-a", intent])
|
|
|
+-
|
|
|
+- if extras:
|
|
|
+- for (key, val) in extras.iteritems():
|
|
|
+- if isinstance(val, int):
|
|
|
+- extra_type_param = "--ei"
|
|
|
+- elif isinstance(val, bool):
|
|
|
+- extra_type_param = "--ez"
|
|
|
+- else:
|
|
|
+- extra_type_param = "--es"
|
|
|
+- acmd.extend([extra_type_param, str(key), str(val)])
|
|
|
+-
|
|
|
+- if url:
|
|
|
+- acmd.extend(["-d", url])
|
|
|
+-
|
|
|
+- cmd = self._escape_command_line(acmd)
|
|
|
+- self.shell_output(cmd, timeout=timeout)
|
|
|
+-
|
|
|
+- def launch_fennec(self, app_name, intent="android.intent.action.VIEW",
|
|
|
+- moz_env=None, extra_args=None, url=None, wait=True,
|
|
|
+- fail_if_running=True, timeout=None):
|
|
|
+- """Convenience method to launch Fennec on Android with various
|
|
|
+- debugging arguments
|
|
|
+-
|
|
|
+- :param str app_name: Name of fennec application (e.g.
|
|
|
+- `org.mozilla.fennec`)
|
|
|
+- :param str intent: Intent to launch application.
|
|
|
+- :param moz_env: Mozilla specific environment to pass into
|
|
|
+- application.
|
|
|
+- :type moz_env: str or None
|
|
|
+- :param extra_args: Extra arguments to be parsed by fennec.
|
|
|
+- :type extra_args: str or None
|
|
|
+- :param url: URL to open
|
|
|
+- :type url: str or None
|
|
|
+- :param bool wait: If True, wait for application to start before
|
|
|
+- returning.
|
|
|
+- :param bool fail_if_running: Raise an exception if instance of
|
|
|
+- application is already running.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- extras = {}
|
|
|
+-
|
|
|
+- if moz_env:
|
|
|
+- # moz_env is expected to be a dictionary of environment variables:
|
|
|
+- # Fennec itself will set them when launched
|
|
|
+- for (env_count, (env_key, env_val)) in enumerate(moz_env.iteritems()):
|
|
|
+- extras["env" + str(env_count)] = env_key + "=" + env_val
|
|
|
+-
|
|
|
+- # Additional command line arguments that fennec will read and use (e.g.
|
|
|
+- # with a custom profile)
|
|
|
+- if extra_args:
|
|
|
+- extras['args'] = " ".join(extra_args)
|
|
|
+-
|
|
|
+- self.launch_application(app_name, "org.mozilla.gecko.BrowserApp",
|
|
|
+- intent, url=url, extras=extras,
|
|
|
+- wait=wait, fail_if_running=fail_if_running,
|
|
|
+- timeout=timeout)
|
|
|
+-
|
|
|
+- def stop_application(self, app_name, timeout=None, root=False):
|
|
|
+- """Stops the specified application
|
|
|
+-
|
|
|
+- For Android 3.0+, we use the "am force-stop" to do this, which
|
|
|
+- is reliable and does not require root. For earlier versions of
|
|
|
+- Android, we simply try to manually kill the processes started
|
|
|
+- by the app repeatedly until none is around any more. This is
|
|
|
+- less reliable and does require root.
|
|
|
+-
|
|
|
+- :param str app_name: Name of application (e.g. `com.android.chrome`)
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :param bool root: Flag specifying if the command should be
|
|
|
+- executed as root.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- if self.version >= version_codes.HONEYCOMB:
|
|
|
+- self.shell_output("am force-stop %s" % app_name,
|
|
|
+- timeout=timeout, root=root)
|
|
|
+- else:
|
|
|
+- num_tries = 0
|
|
|
+- max_tries = 5
|
|
|
+- while self.process_exist(app_name, timeout=timeout):
|
|
|
+- if num_tries > max_tries:
|
|
|
+- raise ADBError("Couldn't successfully kill %s after %s "
|
|
|
+- "tries" % (app_name, max_tries))
|
|
|
+- self.pkill(app_name, timeout=timeout, root=root)
|
|
|
+- num_tries += 1
|
|
|
+-
|
|
|
+- # sleep for a short duration to make sure there are no
|
|
|
+- # additional processes in the process of being launched
|
|
|
+- # (this is not 100% guaranteed to work since it is inherently
|
|
|
+- # racey, but it's the best we can do)
|
|
|
+- time.sleep(1)
|
|
|
+-
|
|
|
+- def uninstall_app(self, app_name, reboot=False, timeout=None):
|
|
|
+- """Uninstalls an app on the device.
|
|
|
+-
|
|
|
+- :param str app_name: The name of the app to be
|
|
|
+- uninstalled.
|
|
|
+- :param bool reboot: Flag indicating that the device should
|
|
|
+- be rebooted after the app is uninstalled. No reboot occurs
|
|
|
+- if the app is not installed.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- if self.is_app_installed(app_name, timeout=timeout):
|
|
|
+- data = self.command_output(["uninstall", app_name], timeout=timeout)
|
|
|
+- if data.find('Success') == -1:
|
|
|
+- self._logger.debug('uninstall_app failed: %s' % data)
|
|
|
+- raise ADBError("uninstall failed for %s. Got: %s" % (app_name, data))
|
|
|
+- if reboot:
|
|
|
+- self.reboot(timeout=timeout)
|
|
|
+-
|
|
|
+- def update_app(self, apk_path, timeout=None):
|
|
|
+- """Updates an app on the device and reboots.
|
|
|
+-
|
|
|
+- :param str apk_path: The apk file name to be
|
|
|
+- updated.
|
|
|
+- :param timeout: The maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :type timeout: integer or None
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- cmd = ["install", "-r"]
|
|
|
+- if self.version >= version_codes.M:
|
|
|
+- cmd.append("-g")
|
|
|
+- cmd.append(apk_path)
|
|
|
+- output = self.command_output(cmd, timeout=timeout)
|
|
|
+- self.reboot(timeout=timeout)
|
|
|
+- return output
|
|
|
+diff --git a/testing/mozbase/mozdevice/mozdevice/adb_b2g.py b/testing/mozbase/mozdevice/mozdevice/adb_b2g.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/mozdevice/adb_b2g.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,124 +0,0 @@
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
+-# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import
|
|
|
+-
|
|
|
+-import traceback
|
|
|
+-
|
|
|
+-import mozfile
|
|
|
+-
|
|
|
+-from .adb import ADBDevice, ADBError
|
|
|
+-
|
|
|
+-
|
|
|
+-class ADBB2G(ADBDevice):
|
|
|
+- """ADBB2G implements :class:`ADBDevice` providing B2G-specific
|
|
|
+- functionality.
|
|
|
+-
|
|
|
+- ::
|
|
|
+-
|
|
|
+- from mozdevice import ADBB2G
|
|
|
+-
|
|
|
+- adbdevice = ADBB2G()
|
|
|
+- print adbdevice.list_files("/mnt/sdcard")
|
|
|
+- if adbdevice.process_exist("b2g"):
|
|
|
+- print "B2G is running"
|
|
|
+- """
|
|
|
+-
|
|
|
+- def get_battery_percentage(self, timeout=None):
|
|
|
+- """Returns the battery charge as a percentage.
|
|
|
+-
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :returns: battery charge as a percentage.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- with mozfile.NamedTemporaryFile() as tf:
|
|
|
+- self.pull('/sys/class/power_supply/battery/capacity', tf.name,
|
|
|
+- timeout=timeout)
|
|
|
+- try:
|
|
|
+- with open(tf.name) as tf2:
|
|
|
+- return tf2.read().splitlines()[0]
|
|
|
+- except Exception as e:
|
|
|
+- raise ADBError(traceback.format_exception_only(
|
|
|
+- type(e), e)[0].strip())
|
|
|
+-
|
|
|
+- def get_memory_total(self, timeout=None):
|
|
|
+- """Returns the total memory available with units.
|
|
|
+-
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADBDevice constructor is used.
|
|
|
+- :returns: memory total with units.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- meminfo = {}
|
|
|
+- with mozfile.NamedTemporaryFile() as tf:
|
|
|
+- self.pull('/proc/meminfo', tf.name, timeout=timeout)
|
|
|
+- try:
|
|
|
+- with open(tf.name) as tf2:
|
|
|
+- for line in tf2.read().splitlines():
|
|
|
+- key, value = line.split(':')
|
|
|
+- meminfo[key] = value.strip()
|
|
|
+- except Exception as e:
|
|
|
+- raise ADBError(traceback.format_exception_only(
|
|
|
+- type(e), e)[0].strip())
|
|
|
+- return meminfo['MemTotal']
|
|
|
+-
|
|
|
+- def get_info(self, directive=None, timeout=None):
|
|
|
+- """
|
|
|
+- Returns a dictionary of information strings about the device.
|
|
|
+-
|
|
|
+- :param directive: information you want to get. Options are:
|
|
|
+- - `battery` - battery charge as a percentage
|
|
|
+- - `disk` - total, free, available bytes on disk
|
|
|
+- - `id` - unique id of the device
|
|
|
+- - `memtotal` - total memory available on the device
|
|
|
+- - `os` - name of the os
|
|
|
+- - `process` - list of running processes (same as ps)
|
|
|
+- - `systime` - system time of the device
|
|
|
+- - `uptime` - uptime of the device
|
|
|
+-
|
|
|
+- If `directive` is `None`, will return all available information
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- info = super(ADBB2G, self).get_info(directive=directive,
|
|
|
+- timeout=timeout)
|
|
|
+-
|
|
|
+- directives = ['memtotal']
|
|
|
+- if directive in directives:
|
|
|
+- directives = [directive]
|
|
|
+-
|
|
|
+- if 'memtotal' in directives:
|
|
|
+- info['memtotal'] = self.get_memory_total(timeout=timeout)
|
|
|
+- return info
|
|
|
+-
|
|
|
+- def is_device_ready(self, timeout=None):
|
|
|
+- """Returns True if the device is ready.
|
|
|
+-
|
|
|
+- :param timeout: optional integer specifying the maximum time in
|
|
|
+- seconds for any spawned adb process to complete before
|
|
|
+- throwing an ADBTimeoutError.
|
|
|
+- This timeout is per adb call. The total time spent
|
|
|
+- may exceed this value. If it is not specified, the value
|
|
|
+- set in the ADB constructor is used.
|
|
|
+- :raises: * ADBTimeoutError
|
|
|
+- * ADBError
|
|
|
+- """
|
|
|
+- return self.shell_bool('ls /sbin', timeout=timeout)
|
|
|
+diff --git a/testing/mozbase/mozdevice/mozdevice/devicemanager.py b/testing/mozbase/mozdevice/mozdevice/devicemanager.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/mozdevice/devicemanager.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,638 +0,0 @@
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
+-# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import
|
|
|
+-
|
|
|
+-import hashlib
|
|
|
+-import mozlog
|
|
|
+-import logging
|
|
|
+-import os
|
|
|
+-import posixpath
|
|
|
+-import re
|
|
|
+-import struct
|
|
|
+-import StringIO
|
|
|
+-import zlib
|
|
|
+-
|
|
|
+-from functools import wraps
|
|
|
+-
|
|
|
+-
|
|
|
+-class DMError(Exception):
|
|
|
+- "generic devicemanager exception."
|
|
|
+-
|
|
|
+- def __init__(self, msg='', fatal=False):
|
|
|
+- self.msg = msg
|
|
|
+- self.fatal = fatal
|
|
|
+-
|
|
|
+- def __str__(self):
|
|
|
+- return self.msg
|
|
|
+-
|
|
|
+-
|
|
|
+-def abstractmethod(method):
|
|
|
+- line = method.func_code.co_firstlineno
|
|
|
+- filename = method.func_code.co_filename
|
|
|
+-
|
|
|
+- @wraps(method)
|
|
|
+- def not_implemented(*args, **kwargs):
|
|
|
+- raise NotImplementedError('Abstract method %s at File "%s", line %s '
|
|
|
+- 'should be implemented by a concrete class' %
|
|
|
+- (repr(method), filename, line))
|
|
|
+- return not_implemented
|
|
|
+-
|
|
|
+-
|
|
|
+-class DeviceManager(object):
|
|
|
+- """
|
|
|
+- Represents a connection to a device. Once an implementation of this class
|
|
|
+- is successfully instantiated, you may do things like list/copy files to
|
|
|
+- the device, launch processes on the device, and install or remove
|
|
|
+- applications from the device.
|
|
|
+-
|
|
|
+- Never instantiate this class directly! Instead, instantiate an
|
|
|
+- implementation of it like DeviceManagerADB. New projects should strongly
|
|
|
+- consider using adb.py as an alternative.
|
|
|
+- """
|
|
|
+-
|
|
|
+- _logcatNeedsRoot = True
|
|
|
+- default_timeout = 300
|
|
|
+- short_timeout = 30
|
|
|
+-
|
|
|
+- def __init__(self, logLevel=None, deviceRoot=None):
|
|
|
+- try:
|
|
|
+- self._logger = mozlog.get_default_logger(component="mozdevice")
|
|
|
+- if not self._logger: # no global structured logger, fall back to reg logging
|
|
|
+- self._logger = mozlog.unstructured.getLogger("mozdevice")
|
|
|
+- if logLevel is not None:
|
|
|
+- self._logger.setLevel(logLevel)
|
|
|
+- except AttributeError:
|
|
|
+- # Structured logging doesn't work on Python 2.6
|
|
|
+- self._logger = None
|
|
|
+- self._logLevel = logLevel
|
|
|
+- self._remoteIsWin = None
|
|
|
+- self._isDeviceRootSetup = False
|
|
|
+- self._deviceRoot = deviceRoot
|
|
|
+-
|
|
|
+- def _log(self, data):
|
|
|
+- """
|
|
|
+- This helper function is called by ProcessHandler to log
|
|
|
+- the output produced by processes
|
|
|
+- """
|
|
|
+- self._logger.debug(data)
|
|
|
+-
|
|
|
+- @property
|
|
|
+- def remoteIsWin(self):
|
|
|
+- if self._remoteIsWin is None:
|
|
|
+- self._remoteIsWin = self.getInfo("os")["os"][0] == "windows"
|
|
|
+- return self._remoteIsWin
|
|
|
+-
|
|
|
+- @property
|
|
|
+- def logLevel(self):
|
|
|
+- return self._logLevel
|
|
|
+-
|
|
|
+- @logLevel.setter
|
|
|
+- def logLevel_setter(self, newLogLevel):
|
|
|
+- self._logLevel = newLogLevel
|
|
|
+- self._logger.setLevel(self._logLevel)
|
|
|
+-
|
|
|
+- @property
|
|
|
+- def debug(self):
|
|
|
+- self._logger.warning("dm.debug is deprecated. Use logLevel.")
|
|
|
+- levels = {logging.DEBUG: 5, logging.INFO: 3, logging.WARNING: 2,
|
|
|
+- logging.ERROR: 1, logging.CRITICAL: 0}
|
|
|
+- return levels[self.logLevel]
|
|
|
+-
|
|
|
+- @debug.setter
|
|
|
+- def debug_setter(self, newDebug):
|
|
|
+- self._logger.warning("dm.debug is deprecated. Use logLevel.")
|
|
|
+- newDebug = 5 if newDebug > 5 else newDebug # truncate >=5 to 5
|
|
|
+- levels = {5: logging.DEBUG, 3: logging.INFO, 2: logging.WARNING,
|
|
|
+- 1: logging.ERROR, 0: logging.CRITICAL}
|
|
|
+- self.logLevel = levels[newDebug]
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def getInfo(self, directive=None):
|
|
|
+- """
|
|
|
+- Returns a dictionary of information strings about the device.
|
|
|
+-
|
|
|
+- :param directive: information you want to get. Options are:
|
|
|
+-
|
|
|
+- - `os` - name of the os
|
|
|
+- - `id` - unique id of the device
|
|
|
+- - `uptime` - uptime of the device
|
|
|
+- - `uptimemillis` - uptime of the device in milliseconds
|
|
|
+- (NOT supported on all implementations)
|
|
|
+- - `systime` - system time of the device
|
|
|
+- - `screen` - screen resolution
|
|
|
+- - `memory` - memory stats
|
|
|
+- - `memtotal` - total memory available on the device, for example 927208 kB
|
|
|
+- - `process` - list of running processes (same as ps)
|
|
|
+- - `disk` - total, free, available bytes on disk
|
|
|
+- - `power` - power status (charge, battery temp)
|
|
|
+- - `temperature` - device temperature
|
|
|
+-
|
|
|
+- If `directive` is `None`, will return all available information
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def getCurrentTime(self):
|
|
|
+- """
|
|
|
+- Returns device time in milliseconds since the epoch.
|
|
|
+- """
|
|
|
+-
|
|
|
+- def getIP(self, interfaces=['eth0', 'wlan0']):
|
|
|
+- """
|
|
|
+- Returns the IP of the device, or None if no connection exists.
|
|
|
+- """
|
|
|
+- for interface in interfaces:
|
|
|
+- match = re.match(r"%s: ip (\S+)" % interface,
|
|
|
+- self.shellCheckOutput(['ifconfig', interface],
|
|
|
+- timeout=self.short_timeout))
|
|
|
+- if match:
|
|
|
+- return match.group(1)
|
|
|
+-
|
|
|
+- def recordLogcat(self):
|
|
|
+- """
|
|
|
+- Clears the logcat file making it easier to view specific events.
|
|
|
+- """
|
|
|
+- # TODO: spawn this off in a separate thread/process so we can collect all
|
|
|
+- # the logcat information
|
|
|
+-
|
|
|
+- # Right now this is just clearing the logcat so we can only see what
|
|
|
+- # happens after this call.
|
|
|
+- self.shellCheckOutput(['/system/bin/logcat', '-c'], root=self._logcatNeedsRoot,
|
|
|
+- timeout=self.short_timeout)
|
|
|
+-
|
|
|
+- def getLogcat(self, filterSpecs=["dalvikvm:I", "ConnectivityService:S",
|
|
|
+- "WifiMonitor:S", "WifiStateTracker:S",
|
|
|
+- "wpa_supplicant:S", "NetworkStateTracker:S"],
|
|
|
+- format="time",
|
|
|
+- filterOutRegexps=[]):
|
|
|
+- """
|
|
|
+- Returns the contents of the logcat file as a list of
|
|
|
+- '\n' terminated strings
|
|
|
+- """
|
|
|
+- cmdline = ["/system/bin/logcat", "-v", format, "-d"] + filterSpecs
|
|
|
+- output = self.shellCheckOutput(cmdline,
|
|
|
+- root=self._logcatNeedsRoot,
|
|
|
+- timeout=self.short_timeout)
|
|
|
+- lines = output.replace('\r\n', '\n').splitlines(True)
|
|
|
+-
|
|
|
+- for regex in filterOutRegexps:
|
|
|
+- lines = [line for line in lines if not re.search(regex, line)]
|
|
|
+-
|
|
|
+- return lines
|
|
|
+-
|
|
|
+- def saveScreenshot(self, filename):
|
|
|
+- """
|
|
|
+- Takes a screenshot of what's being display on the device. Uses
|
|
|
+- "screencap" on newer (Android 3.0+) devices (and some older ones with
|
|
|
+- the functionality backported). This function also works on B2G.
|
|
|
+-
|
|
|
+- Throws an exception on failure. This will always fail on devices
|
|
|
+- without the screencap utility.
|
|
|
+- """
|
|
|
+- screencap = '/system/bin/screencap'
|
|
|
+- if not self.fileExists(screencap):
|
|
|
+- raise DMError("Unable to capture screenshot on device: no screencap utility")
|
|
|
+-
|
|
|
+- with open(filename, 'w') as pngfile:
|
|
|
+- # newer versions of screencap can write directly to a png, but some
|
|
|
+- # older versions can't
|
|
|
+- tempScreenshotFile = self.deviceRoot + "/ss-dm.tmp"
|
|
|
+- self.shellCheckOutput(["sh", "-c", "%s > %s" %
|
|
|
+- (screencap, tempScreenshotFile)],
|
|
|
+- root=True)
|
|
|
+- buf = self.pullFile(tempScreenshotFile)
|
|
|
+- width = int(struct.unpack("I", buf[0:4])[0])
|
|
|
+- height = int(struct.unpack("I", buf[4:8])[0])
|
|
|
+- with open(filename, 'w') as pngfile:
|
|
|
+- pngfile.write(self._writePNG(buf[12:], width, height))
|
|
|
+- self.removeFile(tempScreenshotFile)
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def pushFile(self, localFilename, remoteFilename, retryLimit=1, createDir=True):
|
|
|
+- """
|
|
|
+- Copies localname from the host to destname on the device.
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def pushDir(self, localDirname, remoteDirname, retryLimit=1, timeout=None):
|
|
|
+- """
|
|
|
+- Push local directory from host to remote directory on the device,
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def pullFile(self, remoteFilename, offset=None, length=None):
|
|
|
+- """
|
|
|
+- Returns contents of remoteFile using the "pull" command.
|
|
|
+-
|
|
|
+- :param remoteFilename: Path to file to pull from remote device.
|
|
|
+- :param offset: Offset in bytes from which to begin reading (optional)
|
|
|
+- :param length: Number of bytes to read (optional)
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def getFile(self, remoteFilename, localFilename):
|
|
|
+- """
|
|
|
+- Copy file from remote device to local file on host.
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def getDirectory(self, remoteDirname, localDirname, checkDir=True):
|
|
|
+- """
|
|
|
+- Copy directory structure from device (remoteDirname) to host (localDirname).
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def validateFile(self, remoteFilename, localFilename):
|
|
|
+- """
|
|
|
+- Returns True if a file on the remote device has the same md5 hash as a local one.
|
|
|
+- """
|
|
|
+-
|
|
|
+- def validateDir(self, localDirname, remoteDirname):
|
|
|
+- """
|
|
|
+- Returns True if remoteDirname on device is same as localDirname on host.
|
|
|
+- """
|
|
|
+-
|
|
|
+- self._logger.info("validating directory: %s to %s" % (localDirname, remoteDirname))
|
|
|
+- for root, dirs, files in os.walk(localDirname):
|
|
|
+- parts = root.split(localDirname)
|
|
|
+- for f in files:
|
|
|
+- remoteRoot = remoteDirname + '/' + parts[1]
|
|
|
+- remoteRoot = remoteRoot.replace('/', '/')
|
|
|
+- if (parts[1] == ""):
|
|
|
+- remoteRoot = remoteDirname
|
|
|
+- remoteName = remoteRoot + '/' + f
|
|
|
+- if (self.validateFile(remoteName, os.path.join(root, f)) is not True):
|
|
|
+- return False
|
|
|
+- return True
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def mkDir(self, remoteDirname):
|
|
|
+- """
|
|
|
+- Creates a single directory on the device file system.
|
|
|
+- """
|
|
|
+-
|
|
|
+- def mkDirs(self, filename):
|
|
|
+- """
|
|
|
+- Make directory structure on the device.
|
|
|
+-
|
|
|
+- WARNING: does not create last part of the path. For example, if asked to
|
|
|
+- create `/mnt/sdcard/foo/bar/baz`, it will only create `/mnt/sdcard/foo/bar`
|
|
|
+- """
|
|
|
+- filename = posixpath.normpath(filename)
|
|
|
+- containing = posixpath.dirname(filename)
|
|
|
+- if not self.dirExists(containing):
|
|
|
+- parts = filename.split('/')
|
|
|
+- name = "/" if not self.remoteIsWin else parts.pop(0)
|
|
|
+- for part in parts[:-1]:
|
|
|
+- if part != "":
|
|
|
+- name = posixpath.join(name, part)
|
|
|
+- self.mkDir(name) # mkDir will check previous existence
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def dirExists(self, dirpath):
|
|
|
+- """
|
|
|
+- Returns whether dirpath exists and is a directory on the device file system.
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def fileExists(self, filepath):
|
|
|
+- """
|
|
|
+- Return whether filepath exists on the device file system,
|
|
|
+- regardless of file type.
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def listFiles(self, rootdir):
|
|
|
+- """
|
|
|
+- Lists files on the device rootdir.
|
|
|
+-
|
|
|
+- Returns array of filenames, ['file1', 'file2', ...]
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def removeFile(self, filename):
|
|
|
+- """
|
|
|
+- Removes filename from the device.
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def removeDir(self, remoteDirname):
|
|
|
+- """
|
|
|
+- Does a recursive delete of directory on the device: rm -Rf remoteDirname.
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def moveTree(self, source, destination):
|
|
|
+- """
|
|
|
+- Does a move of the file or directory on the device.
|
|
|
+-
|
|
|
+- :param source: Path to the original file or directory
|
|
|
+- :param destination: Path to the destination file or directory
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def copyTree(self, source, destination):
|
|
|
+- """
|
|
|
+- Does a copy of the file or directory on the device.
|
|
|
+-
|
|
|
+- :param source: Path to the original file or directory
|
|
|
+- :param destination: Path to the destination file or directory
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def chmodDir(self, remoteDirname, mask="777"):
|
|
|
+- """
|
|
|
+- Recursively changes file permissions in a directory.
|
|
|
+- """
|
|
|
+-
|
|
|
+- @property
|
|
|
+- def deviceRoot(self):
|
|
|
+- """
|
|
|
+- The device root on the device filesystem for putting temporary
|
|
|
+- testing files.
|
|
|
+- """
|
|
|
+- # derive deviceroot value if not set
|
|
|
+- if not self._deviceRoot or not self._isDeviceRootSetup:
|
|
|
+- self._deviceRoot = self._setupDeviceRoot(self._deviceRoot)
|
|
|
+- self._isDeviceRootSetup = True
|
|
|
+-
|
|
|
+- return self._deviceRoot
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def _setupDeviceRoot(self):
|
|
|
+- """
|
|
|
+- Sets up and returns a device root location that can be written to by tests.
|
|
|
+- """
|
|
|
+-
|
|
|
+- def getDeviceRoot(self):
|
|
|
+- """
|
|
|
+- Get the device root on the device filesystem for putting temporary
|
|
|
+- testing files.
|
|
|
+-
|
|
|
+- .. deprecated:: 0.38
|
|
|
+- Use the :py:attr:`deviceRoot` property instead.
|
|
|
+- """
|
|
|
+- return self.deviceRoot
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def getTempDir(self):
|
|
|
+- """
|
|
|
+- Returns a temporary directory we can use on this device, ensuring
|
|
|
+- also that it exists.
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
|
|
|
+- """
|
|
|
+- Executes shell command on device and returns exit code.
|
|
|
+-
|
|
|
+- :param cmd: Commandline list to execute
|
|
|
+- :param outputfile: File to store output
|
|
|
+- :param env: Environment to pass to exec command
|
|
|
+- :param cwd: Directory to execute command from
|
|
|
+- :param timeout: specified in seconds, defaults to 'default_timeout'
|
|
|
+- :param root: Specifies whether command requires root privileges
|
|
|
+- """
|
|
|
+-
|
|
|
+- def shellCheckOutput(self, cmd, env=None, cwd=None, timeout=None, root=False):
|
|
|
+- """
|
|
|
+- Executes shell command on device and returns output as a string. Raises if
|
|
|
+- the return code is non-zero.
|
|
|
+-
|
|
|
+- :param cmd: Commandline list to execute
|
|
|
+- :param env: Environment to pass to exec command
|
|
|
+- :param cwd: Directory to execute command from
|
|
|
+- :param timeout: specified in seconds, defaults to 'default_timeout'
|
|
|
+- :param root: Specifies whether command requires root privileges
|
|
|
+- :raises: DMError
|
|
|
+- """
|
|
|
+- buf = StringIO.StringIO()
|
|
|
+- retval = self.shell(cmd, buf, env=env, cwd=cwd, timeout=timeout, root=root)
|
|
|
+- output = str(buf.getvalue()[0:-1]).rstrip()
|
|
|
+- buf.close()
|
|
|
+- if retval != 0:
|
|
|
+- raise DMError(
|
|
|
+- "Non-zero return code for command: %s "
|
|
|
+- "(output: '%s', retval: '%s')" % (cmd, output, retval))
|
|
|
+- return output
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def getProcessList(self):
|
|
|
+- """
|
|
|
+- Returns array of tuples representing running processes on the device.
|
|
|
+-
|
|
|
+- Format of tuples is (processId, processName, userId)
|
|
|
+- """
|
|
|
+-
|
|
|
+- def processInfo(self, processName):
|
|
|
+- """
|
|
|
+- Returns information on the process with processName.
|
|
|
+- Information on process is in tuple format: (pid, process path, user)
|
|
|
+- If a process with the specified name does not exist this function will return None.
|
|
|
+- """
|
|
|
+- if not isinstance(processName, basestring):
|
|
|
+- raise TypeError("Process name %s is not a string" % processName)
|
|
|
+-
|
|
|
+- processInfo = None
|
|
|
+-
|
|
|
+- # filter out extra spaces
|
|
|
+- parts = filter(lambda x: x != '', processName.split(' '))
|
|
|
+- processName = ' '.join(parts)
|
|
|
+-
|
|
|
+- # filter out the quoted env string if it exists
|
|
|
+- # ex: '"name=value;name2=value2;etc=..." process args' -> 'process args'
|
|
|
+- parts = processName.split('"')
|
|
|
+- if (len(parts) > 2):
|
|
|
+- processName = ' '.join(parts[2:]).strip()
|
|
|
+-
|
|
|
+- pieces = processName.split(' ')
|
|
|
+- parts = pieces[0].split('/')
|
|
|
+- app = parts[-1]
|
|
|
+-
|
|
|
+- procList = self.getProcessList()
|
|
|
+- if (procList == []):
|
|
|
+- return None
|
|
|
+-
|
|
|
+- for proc in procList:
|
|
|
+- procName = proc[1].split('/')[-1]
|
|
|
+- if (procName == app):
|
|
|
+- processInfo = proc
|
|
|
+- break
|
|
|
+- return processInfo
|
|
|
+-
|
|
|
+- def processExist(self, processName):
|
|
|
+- """
|
|
|
+- Returns True if process with name processName is running on device.
|
|
|
+- """
|
|
|
+- processInfo = self.processInfo(processName)
|
|
|
+- if processInfo:
|
|
|
+- return processInfo[0]
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def killProcess(self, processName, sig=None):
|
|
|
+- """
|
|
|
+- Kills the process named processName. If sig is not None, process is
|
|
|
+- killed with the specified signal.
|
|
|
+-
|
|
|
+- :param processName: path or name of the process to kill
|
|
|
+- :param sig: signal to pass into the kill command (optional)
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def reboot(self, wait=False, ipAddr=None):
|
|
|
+- """
|
|
|
+- Reboots the device.
|
|
|
+-
|
|
|
+- :param wait: block on device to come back up before returning
|
|
|
+- :param ipAddr: deprecated; do not use
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def installApp(self, appBundlePath, destPath=None):
|
|
|
+- """
|
|
|
+- Installs an application onto the device.
|
|
|
+-
|
|
|
+- :param appBundlePath: path to the application bundle on the device
|
|
|
+- :param destPath: destination directory of where application should be
|
|
|
+- installed to (optional)
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def uninstallApp(self, appName, installPath=None):
|
|
|
+- """
|
|
|
+- Uninstalls the named application from device and DOES NOT cause a reboot.
|
|
|
+-
|
|
|
+- :param appName: the name of the application (e.g org.mozilla.fennec)
|
|
|
+- :param installPath: the path to where the application was installed (optional)
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def uninstallAppAndReboot(self, appName, installPath=None):
|
|
|
+- """
|
|
|
+- Uninstalls the named application from device and causes a reboot.
|
|
|
+-
|
|
|
+- :param appName: the name of the application (e.g org.mozilla.fennec)
|
|
|
+- :param installPath: the path to where the application was installed (optional)
|
|
|
+- """
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def updateApp(self, appBundlePath, processName=None, destPath=None,
|
|
|
+- wait=False, ipAddr=None):
|
|
|
+- """
|
|
|
+- Updates the application on the device and reboots.
|
|
|
+-
|
|
|
+- :param appBundlePath: path to the application bundle on the device
|
|
|
+- :param processName: used to end the process if the applicaiton is
|
|
|
+- currently running (optional)
|
|
|
+- :param destPath: Destination directory to where the application should
|
|
|
+- be installed (optional)
|
|
|
+- :param wait: block on device to come back up before returning
|
|
|
+- :param ipAddr: deprecated; do not use
|
|
|
+- """
|
|
|
+-
|
|
|
+- @staticmethod
|
|
|
+- def _writePNG(buf, width, height):
|
|
|
+- """
|
|
|
+- Method for writing a PNG from a buffer, used by getScreenshot on older devices,
|
|
|
+- """
|
|
|
+- # Based on: http://code.activestate.com/recipes/577443-write-a-png-image-in-native-python/
|
|
|
+- width_byte_4 = width * 4
|
|
|
+- raw_data = b"".join(b'\x00' + buf[span:span + width_byte_4]
|
|
|
+- for span in range(0, (height - 1) * width * 4, width_byte_4))
|
|
|
+-
|
|
|
+- def png_pack(png_tag, data):
|
|
|
+- chunk_head = png_tag + data
|
|
|
+- return struct.pack("!I", len(data)) \
|
|
|
+- + chunk_head \
|
|
|
+- + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
|
|
|
+- return b"".join([
|
|
|
+- b'\x89PNG\r\n\x1a\n',
|
|
|
+- png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
|
|
|
+- png_pack(b'IDAT', zlib.compress(raw_data, 9)),
|
|
|
+- png_pack(b'IEND', b'')])
|
|
|
+-
|
|
|
+- @abstractmethod
|
|
|
+- def _getRemoteHash(self, filename):
|
|
|
+- """
|
|
|
+- Return the md5 sum of a file on the device.
|
|
|
+- """
|
|
|
+-
|
|
|
+- @staticmethod
|
|
|
+- def _getLocalHash(filename):
|
|
|
+- """
|
|
|
+- Return the MD5 sum of a file on the host.
|
|
|
+- """
|
|
|
+- f = open(filename, 'rb')
|
|
|
+- if f is None:
|
|
|
+- return None
|
|
|
+-
|
|
|
+- try:
|
|
|
+- mdsum = hashlib.md5()
|
|
|
+- except Exception:
|
|
|
+- return None
|
|
|
+-
|
|
|
+- while 1:
|
|
|
+- data = f.read(1024)
|
|
|
+- if not data:
|
|
|
+- break
|
|
|
+- mdsum.update(data)
|
|
|
+-
|
|
|
+- f.close()
|
|
|
+- hexval = mdsum.hexdigest()
|
|
|
+- return hexval
|
|
|
+-
|
|
|
+- @staticmethod
|
|
|
+- def _escapedCommandLine(cmd):
|
|
|
+- """
|
|
|
+- Utility function to return escaped and quoted version of command line.
|
|
|
+- """
|
|
|
+- quotedCmd = []
|
|
|
+-
|
|
|
+- for arg in cmd:
|
|
|
+- arg.replace('&', '\&')
|
|
|
+-
|
|
|
+- needsQuoting = False
|
|
|
+- for char in [' ', '(', ')', '"', '&']:
|
|
|
+- if arg.find(char) >= 0:
|
|
|
+- needsQuoting = True
|
|
|
+- break
|
|
|
+- if needsQuoting:
|
|
|
+- arg = '\'%s\'' % arg
|
|
|
+-
|
|
|
+- quotedCmd.append(arg)
|
|
|
+-
|
|
|
+- return " ".join(quotedCmd)
|
|
|
+-
|
|
|
+-
|
|
|
+-def _pop_last_line(file_obj):
|
|
|
+- """
|
|
|
+- Utility function to get the last line from a file. Function also removes
|
|
|
+- it from the file. Intended to strip off the return code from a shell
|
|
|
+- command.
|
|
|
+- """
|
|
|
+- bytes_from_end = 1
|
|
|
+- file_obj.seek(0, 2)
|
|
|
+- length = file_obj.tell() + 1
|
|
|
+- while bytes_from_end < length:
|
|
|
+- file_obj.seek((-1) * bytes_from_end, 2)
|
|
|
+- data = file_obj.read()
|
|
|
+-
|
|
|
+- if bytes_from_end == length - 1 and len(data) == 0: # no data, return None
|
|
|
+- return None
|
|
|
+-
|
|
|
+- if data[0] == '\n' or bytes_from_end == length - 1:
|
|
|
+- # found the last line, which should have the return value
|
|
|
+- if data[0] == '\n':
|
|
|
+- data = data[1:]
|
|
|
+-
|
|
|
+- # truncate off the return code line
|
|
|
+- file_obj.truncate(length - bytes_from_end)
|
|
|
+- file_obj.seek(0, 2)
|
|
|
+- file_obj.write('\0')
|
|
|
+-
|
|
|
+- return data
|
|
|
+-
|
|
|
+- bytes_from_end += 1
|
|
|
+-
|
|
|
+- return None
|
|
|
+diff --git a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,886 +0,0 @@
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
+-# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import
|
|
|
+-
|
|
|
+-import logging
|
|
|
+-import re
|
|
|
+-import os
|
|
|
+-import tempfile
|
|
|
+-import time
|
|
|
+-import traceback
|
|
|
+-
|
|
|
+-from distutils import dir_util
|
|
|
+-
|
|
|
+-from .devicemanager import DeviceManager, DMError
|
|
|
+-from mozprocess import ProcessHandler
|
|
|
+-import mozfile
|
|
|
+-from . import version_codes
|
|
|
+-
|
|
|
+-
|
|
|
+-class DeviceManagerADB(DeviceManager):
|
|
|
+- """
|
|
|
+- Implementation of DeviceManager interface that uses the Android "adb"
|
|
|
+- utility to communicate with the device. Normally used to communicate
|
|
|
+- with a device that is directly connected with the host machine over a USB
|
|
|
+- port.
|
|
|
+- """
|
|
|
+-
|
|
|
+- _haveRootShell = None
|
|
|
+- _haveSu = None
|
|
|
+- _suModifier = None
|
|
|
+- _lsModifier = None
|
|
|
+- _useZip = False
|
|
|
+- _logcatNeedsRoot = False
|
|
|
+- _pollingInterval = 0.01
|
|
|
+- _packageName = None
|
|
|
+- _tempDir = None
|
|
|
+- _adb_version = None
|
|
|
+- _sdk_version = None
|
|
|
+- connected = False
|
|
|
+-
|
|
|
+- def __init__(self, host=None, port=5555, retryLimit=5, packageName='fennec',
|
|
|
+- adbPath=None, deviceSerial=None, deviceRoot=None,
|
|
|
+- logLevel=logging.ERROR, autoconnect=True, runAdbAsRoot=False,
|
|
|
+- serverHost=None, serverPort=None, **kwargs):
|
|
|
+- DeviceManager.__init__(self, logLevel=logLevel,
|
|
|
+- deviceRoot=deviceRoot)
|
|
|
+- self.host = host
|
|
|
+- self.port = port
|
|
|
+- self.retryLimit = retryLimit
|
|
|
+-
|
|
|
+- self._serverHost = serverHost
|
|
|
+- self._serverPort = serverPort
|
|
|
+-
|
|
|
+- # the path to adb, or 'adb' to assume that it's on the PATH
|
|
|
+- self._adbPath = adbPath or 'adb'
|
|
|
+-
|
|
|
+- # The serial number of the device to use with adb, used in cases
|
|
|
+- # where multiple devices are being managed by the same adb instance.
|
|
|
+- self._deviceSerial = deviceSerial
|
|
|
+-
|
|
|
+- # Some devices do no start adb as root, if allowed you can use
|
|
|
+- # this to reboot adbd on the device as root automatically
|
|
|
+- self._runAdbAsRoot = runAdbAsRoot
|
|
|
+-
|
|
|
+- if packageName == 'fennec':
|
|
|
+- if os.getenv('USER'):
|
|
|
+- self._packageName = 'org.mozilla.fennec_' + os.getenv('USER')
|
|
|
+- else:
|
|
|
+- self._packageName = 'org.mozilla.fennec_'
|
|
|
+- elif packageName:
|
|
|
+- self._packageName = packageName
|
|
|
+-
|
|
|
+- # verify that we can run the adb command. can't continue otherwise
|
|
|
+- self._verifyADB()
|
|
|
+-
|
|
|
+- if autoconnect:
|
|
|
+- self.connect()
|
|
|
+-
|
|
|
+- def connect(self):
|
|
|
+- if not self.connected:
|
|
|
+- # try to connect to the device over tcp/ip if we have a hostname
|
|
|
+- if self.host:
|
|
|
+- self._connectRemoteADB()
|
|
|
+-
|
|
|
+- # verify that we can connect to the device. can't continue
|
|
|
+- self._verifyDevice()
|
|
|
+-
|
|
|
+- # Note SDK version
|
|
|
+- try:
|
|
|
+- proc = self._runCmd(["shell", "getprop", "ro.build.version.sdk"],
|
|
|
+- timeout=self.short_timeout)
|
|
|
+- self._sdk_version = int(proc.output[0])
|
|
|
+- except (OSError, ValueError):
|
|
|
+- self._sdk_version = 0
|
|
|
+- self._logger.info("Detected Android sdk %d" % self._sdk_version)
|
|
|
+-
|
|
|
+- # Some commands require root to work properly, even with ADB (e.g.
|
|
|
+- # grabbing APKs out of /data). For these cases, we check whether
|
|
|
+- # we're running as root. If that isn't true, check for the
|
|
|
+- # existence of an su binary
|
|
|
+- self._checkForRoot()
|
|
|
+-
|
|
|
+- # can we use zip to speed up some file operations? (currently not
|
|
|
+- # required)
|
|
|
+- try:
|
|
|
+- self._verifyZip()
|
|
|
+- except DMError:
|
|
|
+- pass
|
|
|
+-
|
|
|
+- def __del__(self):
|
|
|
+- if self.host:
|
|
|
+- self._disconnectRemoteADB()
|
|
|
+-
|
|
|
+- def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
|
|
|
+- # FIXME: this function buffers all output of the command into memory,
|
|
|
+- # always. :(
|
|
|
+-
|
|
|
+- # If requested to run as root, check that we can actually do that
|
|
|
+- if root:
|
|
|
+- if self._haveRootShell is None and self._haveSu is None:
|
|
|
+- self._checkForRoot()
|
|
|
+- if not self._haveRootShell and not self._haveSu:
|
|
|
+- raise DMError(
|
|
|
+- "Shell command '%s' requested to run as root but root "
|
|
|
+- "is not available on this device. Root your device or "
|
|
|
+- "refactor the test/harness to not require root." %
|
|
|
+- self._escapedCommandLine(cmd))
|
|
|
+-
|
|
|
+- # Getting the return code is more complex than you'd think because adb
|
|
|
+- # doesn't actually return the return code from a process, so we have to
|
|
|
+- # capture the output to get it
|
|
|
+- if root and self._haveSu:
|
|
|
+- cmdline = "su %s \"%s\"" % (self._suModifier,
|
|
|
+- self._escapedCommandLine(cmd))
|
|
|
+- else:
|
|
|
+- cmdline = self._escapedCommandLine(cmd)
|
|
|
+- cmdline += "; echo $?"
|
|
|
+-
|
|
|
+- # prepend cwd and env to command if necessary
|
|
|
+- if cwd:
|
|
|
+- cmdline = "cd %s; %s" % (cwd, cmdline)
|
|
|
+- if env:
|
|
|
+- envstr = '; '.join(map(lambda x: 'export %s=%s' % (x[0], x[1]), env.iteritems()))
|
|
|
+- cmdline = envstr + "; " + cmdline
|
|
|
+-
|
|
|
+- # all output should be in stdout
|
|
|
+- args = [self._adbPath]
|
|
|
+- if self._serverHost is not None:
|
|
|
+- args.extend(['-H', self._serverHost])
|
|
|
+- if self._serverPort is not None:
|
|
|
+- args.extend(['-P', str(self._serverPort)])
|
|
|
+- if self._deviceSerial:
|
|
|
+- args.extend(['-s', self._deviceSerial])
|
|
|
+- args.extend(["shell", cmdline])
|
|
|
+-
|
|
|
+- def _timeout():
|
|
|
+- self._logger.error("Timeout exceeded for shell call '%s'" % ' '.join(args))
|
|
|
+-
|
|
|
+- self._logger.debug("shell - command: %s" % ' '.join(args))
|
|
|
+- proc = ProcessHandler(args, processOutputLine=self._log, onTimeout=_timeout)
|
|
|
+-
|
|
|
+- if not timeout:
|
|
|
+- # We are asserting that all commands will complete in this time unless
|
|
|
+- # otherwise specified
|
|
|
+- timeout = self.default_timeout
|
|
|
+-
|
|
|
+- timeout = int(timeout)
|
|
|
+- proc.run(timeout)
|
|
|
+- proc.wait()
|
|
|
+- output = proc.output
|
|
|
+-
|
|
|
+- if output:
|
|
|
+- lastline = output[-1]
|
|
|
+- if lastline:
|
|
|
+- m = re.search('([0-9]+)', lastline)
|
|
|
+- if m:
|
|
|
+- return_code = m.group(1)
|
|
|
+- for line in output:
|
|
|
+- outputfile.write(line + '\n')
|
|
|
+- outputfile.seek(-2, 2)
|
|
|
+- outputfile.truncate() # truncate off the return code
|
|
|
+- return int(return_code)
|
|
|
+-
|
|
|
+- return None
|
|
|
+-
|
|
|
+- def forward(self, local, remote):
|
|
|
+- """
|
|
|
+- Forward socket connections.
|
|
|
+-
|
|
|
+- Forward specs are one of:
|
|
|
+- tcp:<port>
|
|
|
+- localabstract:<unix domain socket name>
|
|
|
+- localreserved:<unix domain socket name>
|
|
|
+- localfilesystem:<unix domain socket name>
|
|
|
+- dev:<character device name>
|
|
|
+- jdwp:<process pid> (remote only)
|
|
|
+- """
|
|
|
+- if not self._checkCmd(['forward', local, remote], timeout=self.short_timeout) == 0:
|
|
|
+- raise DMError("Failed to forward socket connection.")
|
|
|
+-
|
|
|
+- def remove_forward(self, local=None):
|
|
|
+- """
|
|
|
+- Turn off forwarding of socket connection.
|
|
|
+- """
|
|
|
+- cmd = ['forward']
|
|
|
+- if local is None:
|
|
|
+- cmd.extend(['--remove-all'])
|
|
|
+- else:
|
|
|
+- cmd.extend(['--remove', local])
|
|
|
+- if not self._checkCmd(cmd, timeout=self.short_timeout) == 0:
|
|
|
+- raise DMError("Failed to remove connection forwarding.")
|
|
|
+-
|
|
|
+- def remount(self):
|
|
|
+- "Remounts the /system partition on the device read-write."
|
|
|
+- return self._checkCmd(['remount'], timeout=self.short_timeout)
|
|
|
+-
|
|
|
+- def devices(self):
|
|
|
+- "Return a list of connected devices as (serial, status) tuples."
|
|
|
+- proc = self._runCmd(['devices'])
|
|
|
+- proc.output.pop(0) # ignore first line of output
|
|
|
+- devices = []
|
|
|
+- for line in proc.output:
|
|
|
+- result = re.match('(.*?)\t(.*)', line)
|
|
|
+- if result:
|
|
|
+- devices.append((result.group(1), result.group(2)))
|
|
|
+- return devices
|
|
|
+-
|
|
|
+- def _connectRemoteADB(self):
|
|
|
+- self._checkCmd(["connect", self.host + ":" + str(self.port)])
|
|
|
+-
|
|
|
+- def _disconnectRemoteADB(self):
|
|
|
+- self._checkCmd(["disconnect", self.host + ":" + str(self.port)])
|
|
|
+-
|
|
|
+- def pushFile(self, localname, destname, retryLimit=None, createDir=True):
|
|
|
+- # you might expect us to put the file *in* the directory in this case,
|
|
|
+- # but that would be inconsistent with historical behavior.
|
|
|
+- retryLimit = retryLimit or self.retryLimit
|
|
|
+- if self.dirExists(destname):
|
|
|
+- raise DMError("Attempted to push a file (%s) to a directory (%s)!" %
|
|
|
+- (localname, destname))
|
|
|
+- if not os.access(localname, os.F_OK):
|
|
|
+- raise DMError("File not found: %s" % localname)
|
|
|
+-
|
|
|
+- proc = self._runCmd(["push", os.path.realpath(localname), destname],
|
|
|
+- retryLimit=retryLimit)
|
|
|
+- if proc.returncode != 0:
|
|
|
+- raise DMError("Error pushing file %s -> %s; output: %s" %
|
|
|
+- (localname, destname, proc.output))
|
|
|
+-
|
|
|
+- def mkDir(self, name):
|
|
|
+- result = self._runCmd(["shell", "mkdir", name], timeout=self.short_timeout).output
|
|
|
+- if len(result) and 'read-only file system' in result[0].lower():
|
|
|
+- raise DMError("Error creating directory: read only file system")
|
|
|
+-
|
|
|
+- def pushDir(self, localDir, remoteDir, retryLimit=None, timeout=None):
|
|
|
+- # adb "push" accepts a directory as an argument, but if the directory
|
|
|
+- # contains symbolic links, the links are pushed, rather than the linked
|
|
|
+- # files; we either zip/unzip or re-copy the directory into a temporary
|
|
|
+- # one to get around this limitation
|
|
|
+- retryLimit = retryLimit or self.retryLimit
|
|
|
+- if self._useZip:
|
|
|
+- self.removeDir(remoteDir)
|
|
|
+- self.mkDirs(remoteDir + "/x")
|
|
|
+- try:
|
|
|
+- localZip = tempfile.mktemp() + ".zip"
|
|
|
+- remoteZip = remoteDir + "/adbdmtmp.zip"
|
|
|
+- proc = ProcessHandler(["zip", "-r", localZip, '.'], cwd=localDir,
|
|
|
+- processOutputLine=self._log)
|
|
|
+- proc.run()
|
|
|
+- proc.wait()
|
|
|
+- self.pushFile(localZip, remoteZip, retryLimit=retryLimit, createDir=False)
|
|
|
+- mozfile.remove(localZip)
|
|
|
+- data = self._runCmd(["shell", "unzip", "-o", remoteZip,
|
|
|
+- "-d", remoteDir]).output[0]
|
|
|
+- self._checkCmd(["shell", "rm", remoteZip],
|
|
|
+- retryLimit=retryLimit, timeout=self.short_timeout)
|
|
|
+- if re.search("unzip: exiting", data) or re.search("Operation not permitted", data):
|
|
|
+- raise Exception("unzip failed, or permissions error")
|
|
|
+- except Exception:
|
|
|
+- self._logger.warning(traceback.format_exc())
|
|
|
+- self._logger.warning("zip/unzip failure: falling back to normal push")
|
|
|
+- self._useZip = False
|
|
|
+- self.pushDir(localDir, remoteDir, retryLimit=retryLimit, timeout=timeout)
|
|
|
+- else:
|
|
|
+- localDir = os.path.normpath(localDir)
|
|
|
+- remoteDir = os.path.normpath(remoteDir)
|
|
|
+- tempParent = tempfile.mkdtemp()
|
|
|
+- remoteName = os.path.basename(remoteDir)
|
|
|
+- newLocal = os.path.join(tempParent, remoteName)
|
|
|
+- dir_util.copy_tree(localDir, newLocal)
|
|
|
+- # See do_sync_push in
|
|
|
+- # https://android.googlesource.com/platform/system/core/+/master/adb/file_sync_client.cpp
|
|
|
+- # Work around change in behavior in adb 1.0.36 where if
|
|
|
+- # the remote destination directory exists, adb push will
|
|
|
+- # copy the source directory *into* the destination
|
|
|
+- # directory otherwise it will copy the source directory
|
|
|
+- # *onto* the destination directory.
|
|
|
+- if self._adb_version >= '1.0.36':
|
|
|
+- remoteDir = '/'.join(remoteDir.rstrip('/').split('/')[:-1])
|
|
|
+- try:
|
|
|
+- if self._checkCmd(["push", newLocal, remoteDir],
|
|
|
+- retryLimit=retryLimit, timeout=timeout):
|
|
|
+- raise DMError("failed to push %s (copy of %s) to %s" %
|
|
|
+- (newLocal, localDir, remoteDir))
|
|
|
+- except BaseException:
|
|
|
+- raise
|
|
|
+- finally:
|
|
|
+- mozfile.remove(tempParent)
|
|
|
+-
|
|
|
+- def dirExists(self, remotePath):
|
|
|
+- self._detectLsModifier()
|
|
|
+- data = self._runCmd(["shell", "ls", self._lsModifier, remotePath + '/'],
|
|
|
+- timeout=self.short_timeout).output
|
|
|
+-
|
|
|
+- if len(data) == 1:
|
|
|
+- res = data[0]
|
|
|
+- if "Not a directory" in res or "No such file or directory" in res:
|
|
|
+- return False
|
|
|
+- return True
|
|
|
+-
|
|
|
+- def fileExists(self, filepath):
|
|
|
+- self._detectLsModifier()
|
|
|
+- data = self._runCmd(["shell", "ls", self._lsModifier, filepath],
|
|
|
+- timeout=self.short_timeout).output
|
|
|
+- if len(data) == 1:
|
|
|
+- foundpath = data[0].decode('utf-8').rstrip()
|
|
|
+- if foundpath == filepath:
|
|
|
+- return True
|
|
|
+- return False
|
|
|
+-
|
|
|
+- def removeFile(self, filename):
|
|
|
+- if self.fileExists(filename):
|
|
|
+- self._checkCmd(["shell", "rm", filename], timeout=self.short_timeout)
|
|
|
+-
|
|
|
+- def removeDir(self, remoteDir):
|
|
|
+- if self.dirExists(remoteDir):
|
|
|
+- self._checkCmd(["shell", "rm", "-r", remoteDir], timeout=self.short_timeout)
|
|
|
+- else:
|
|
|
+- self.removeFile(remoteDir.strip())
|
|
|
+-
|
|
|
+- def moveTree(self, source, destination):
|
|
|
+- self._checkCmd(["shell", "mv", source, destination], timeout=self.short_timeout)
|
|
|
+-
|
|
|
+- def copyTree(self, source, destination):
|
|
|
+- self._checkCmd(["shell", "dd", "if=%s" % source, "of=%s" % destination])
|
|
|
+-
|
|
|
+- def listFiles(self, rootdir):
|
|
|
+- self._detectLsModifier()
|
|
|
+- data = self._runCmd(["shell", "ls", self._lsModifier, rootdir],
|
|
|
+- timeout=self.short_timeout).output
|
|
|
+- data[:] = [item.rstrip('\r\n') for item in data]
|
|
|
+- if (len(data) == 1):
|
|
|
+- if (data[0] == rootdir):
|
|
|
+- return []
|
|
|
+- if (data[0].find("No such file or directory") != -1):
|
|
|
+- return []
|
|
|
+- if (data[0].find("Not a directory") != -1):
|
|
|
+- return []
|
|
|
+- if (data[0].find("Permission denied") != -1):
|
|
|
+- return []
|
|
|
+- if (data[0].find("opendir failed") != -1):
|
|
|
+- return []
|
|
|
+- if (data[0].find("Device or resource busy") != -1):
|
|
|
+- return []
|
|
|
+- return data
|
|
|
+-
|
|
|
+- def getProcessList(self):
|
|
|
+- ret = []
|
|
|
+- p = self._runCmd(["shell", "ps"], timeout=self.short_timeout)
|
|
|
+- if not p or not p.output or len(p.output) < 1:
|
|
|
+- return ret
|
|
|
+- # first line is the headers
|
|
|
+- p.output.pop(0)
|
|
|
+- for proc in p.output:
|
|
|
+- els = proc.split()
|
|
|
+- # We need to figure out if this is "user pid name" or
|
|
|
+- # "pid user vsz stat command"
|
|
|
+- if els[1].isdigit():
|
|
|
+- ret.append(list([int(els[1]), els[len(els) - 1], els[0]]))
|
|
|
+- else:
|
|
|
+- ret.append(list([int(els[0]), els[len(els) - 1], els[1]]))
|
|
|
+- return ret
|
|
|
+-
|
|
|
+- def fireProcess(self, appname, failIfRunning=False):
|
|
|
+- """
|
|
|
+- Starts a process
|
|
|
+-
|
|
|
+- returns: pid
|
|
|
+-
|
|
|
+- DEPRECATED: Use shell() or launchApplication() for new code
|
|
|
+- """
|
|
|
+- # strip out env vars
|
|
|
+- parts = appname.split('"')
|
|
|
+- if (len(parts) > 2):
|
|
|
+- parts = parts[2:]
|
|
|
+- return self.launchProcess(parts, failIfRunning)
|
|
|
+-
|
|
|
+- def launchProcess(self, cmd, outputFile="process.txt", cwd='', env='', failIfRunning=False):
|
|
|
+- """
|
|
|
+- Launches a process, redirecting output to standard out
|
|
|
+-
|
|
|
+- WARNING: Does not work how you expect on Android! The application's
|
|
|
+- own output will be flushed elsewhere.
|
|
|
+-
|
|
|
+- DEPRECATED: Use shell() or launchApplication() for new code
|
|
|
+- """
|
|
|
+- if cmd[0] == "am":
|
|
|
+- self._checkCmd(["shell"] + cmd)
|
|
|
+- return outputFile
|
|
|
+-
|
|
|
+- acmd = ["-W"]
|
|
|
+- cmd = ' '.join(cmd).strip()
|
|
|
+- i = cmd.find(" ")
|
|
|
+- re_url = re.compile('^[http|file|chrome|about].*')
|
|
|
+- last = cmd.rfind(" ")
|
|
|
+- uri = ""
|
|
|
+- args = ""
|
|
|
+- if re_url.match(cmd[last:].strip()):
|
|
|
+- args = cmd[i:last].strip()
|
|
|
+- uri = cmd[last:].strip()
|
|
|
+- else:
|
|
|
+- args = cmd[i:].strip()
|
|
|
+- acmd.append("-n")
|
|
|
+- acmd.append(cmd[0:i] + "/org.mozilla.gecko.BrowserApp")
|
|
|
+- if args != "":
|
|
|
+- acmd.append("--es")
|
|
|
+- acmd.append("args")
|
|
|
+- acmd.append(args)
|
|
|
+- if env != '' and env is not None:
|
|
|
+- envCnt = 0
|
|
|
+- # env is expected to be a dict of environment variables
|
|
|
+- for envkey, envval in env.iteritems():
|
|
|
+- acmd.append("--es")
|
|
|
+- acmd.append("env" + str(envCnt))
|
|
|
+- acmd.append(envkey + "=" + envval)
|
|
|
+- envCnt += 1
|
|
|
+- if uri != "":
|
|
|
+- acmd.append("-d")
|
|
|
+- acmd.append(uri)
|
|
|
+-
|
|
|
+- acmd = ["shell", ' '.join(map(lambda x: '"' + x + '"', ["am", "start"] + acmd))]
|
|
|
+- self._logger.info(acmd)
|
|
|
+- self._checkCmd(acmd)
|
|
|
+- return outputFile
|
|
|
+-
|
|
|
+- def killProcess(self, appname, sig=None):
|
|
|
+- try:
|
|
|
+- self.shellCheckOutput(["am", "force-stop", appname], timeout=self.short_timeout)
|
|
|
+- except Exception:
|
|
|
+- # no problem - will kill it instead
|
|
|
+- self._logger.info("killProcess failed force-stop of %s" % appname)
|
|
|
+-
|
|
|
+- shell_args = ["shell"]
|
|
|
+- if self._sdk_version >= version_codes.N:
|
|
|
+- # Bug 1334613 - force use of root
|
|
|
+- if self._haveRootShell is None and self._haveSu is None:
|
|
|
+- self._checkForRoot()
|
|
|
+- if not self._haveRootShell and not self._haveSu:
|
|
|
+- raise DMError(
|
|
|
+- "killProcess '%s' requested to run as root but root "
|
|
|
+- "is not available on this device. Root your device or "
|
|
|
+- "refactor the test/harness to not require root." %
|
|
|
+- appname)
|
|
|
+- if not self._haveRootShell:
|
|
|
+- shell_args.extend(["su", self._suModifier])
|
|
|
+-
|
|
|
+- procs = self.getProcessList()
|
|
|
+- for (pid, name, user) in procs:
|
|
|
+- if name == appname:
|
|
|
+- args = list(shell_args)
|
|
|
+- args.append("kill")
|
|
|
+- if sig:
|
|
|
+- args.append("-%d" % sig)
|
|
|
+- args.append(str(pid))
|
|
|
+- p = self._runCmd(args, timeout=self.short_timeout)
|
|
|
+- if p.returncode != 0 and len(p.output) > 0 and \
|
|
|
+- 'No such process' not in p.output[0]:
|
|
|
+- raise DMError("Error killing process "
|
|
|
+- "'%s': %s" % (appname, p.output))
|
|
|
+-
|
|
|
+- def _runPull(self, remoteFile, localFile):
|
|
|
+- """
|
|
|
+- Pulls remoteFile from device to host
|
|
|
+- """
|
|
|
+- try:
|
|
|
+- self._runCmd(["pull", remoteFile, localFile])
|
|
|
+- except (OSError, ValueError):
|
|
|
+- raise DMError("Error pulling remote file '%s' to '%s'" % (remoteFile, localFile))
|
|
|
+-
|
|
|
+- def pullFile(self, remoteFile, offset=None, length=None):
|
|
|
+- # TODO: add debug flags and allow for printing stdout
|
|
|
+- with mozfile.NamedTemporaryFile() as tf:
|
|
|
+- self._runPull(remoteFile, tf.name)
|
|
|
+- # we need to reopen the file to get the written contents
|
|
|
+- with open(tf.name) as tf2:
|
|
|
+- # ADB pull does not support offset and length, but we can
|
|
|
+- # instead read only the requested portion of the local file
|
|
|
+- if offset is not None and length is not None:
|
|
|
+- tf2.seek(offset)
|
|
|
+- return tf2.read(length)
|
|
|
+- elif offset is not None:
|
|
|
+- tf2.seek(offset)
|
|
|
+- return tf2.read()
|
|
|
+- else:
|
|
|
+- return tf2.read()
|
|
|
+-
|
|
|
+- def getFile(self, remoteFile, localFile):
|
|
|
+- self._runPull(remoteFile, localFile)
|
|
|
+-
|
|
|
+- def getDirectory(self, remoteDir, localDir, checkDir=True):
|
|
|
+- localDir = os.path.normpath(localDir)
|
|
|
+- remoteDir = os.path.normpath(remoteDir)
|
|
|
+- copyRequired = False
|
|
|
+- originalLocal = localDir
|
|
|
+- if self._adb_version >= '1.0.36' and \
|
|
|
+- os.path.isdir(localDir) and self.dirExists(remoteDir):
|
|
|
+- # See do_sync_pull in
|
|
|
+- # https://android.googlesource.com/platform/system/core/+/master/adb/file_sync_client.cpp
|
|
|
+- # Work around change in behavior in adb 1.0.36 where if
|
|
|
+- # the local destination directory exists, adb pull will
|
|
|
+- # copy the source directory *into* the destination
|
|
|
+- # directory otherwise it will copy the source directory
|
|
|
+- # *onto* the destination directory.
|
|
|
+- #
|
|
|
+- # If the destination directory does exist, pull to its
|
|
|
+- # parent directory. If the source and destination leaf
|
|
|
+- # directory names are different, pull the source directory
|
|
|
+- # into a temporary directory and then copy the temporary
|
|
|
+- # directory onto the destination.
|
|
|
+- localName = os.path.basename(localDir)
|
|
|
+- remoteName = os.path.basename(remoteDir)
|
|
|
+- if localName != remoteName:
|
|
|
+- copyRequired = True
|
|
|
+- tempParent = tempfile.mkdtemp()
|
|
|
+- localDir = os.path.join(tempParent, remoteName)
|
|
|
+- else:
|
|
|
+- localDir = '/'.join(localDir.rstrip('/').split('/')[:-1])
|
|
|
+- self._runCmd(["pull", remoteDir, localDir]).wait()
|
|
|
+- if copyRequired:
|
|
|
+- dir_util.copy_tree(localDir, originalLocal)
|
|
|
+- mozfile.remove(tempParent)
|
|
|
+-
|
|
|
+- def validateFile(self, remoteFile, localFile):
|
|
|
+- md5Remote = self._getRemoteHash(remoteFile)
|
|
|
+- md5Local = self._getLocalHash(localFile)
|
|
|
+- if md5Remote is None or md5Local is None:
|
|
|
+- return None
|
|
|
+- return md5Remote == md5Local
|
|
|
+-
|
|
|
+- def _getRemoteHash(self, remoteFile):
|
|
|
+- """
|
|
|
+- Return the md5 sum of a file on the device
|
|
|
+- """
|
|
|
+- with tempfile.NamedTemporaryFile() as f:
|
|
|
+- self._runPull(remoteFile, f.name)
|
|
|
+-
|
|
|
+- return self._getLocalHash(f.name)
|
|
|
+-
|
|
|
+- def _setupDeviceRoot(self, deviceRoot):
|
|
|
+- # user-specified device root, create it and return it
|
|
|
+- if deviceRoot:
|
|
|
+- self.mkDir(deviceRoot)
|
|
|
+- return deviceRoot
|
|
|
+-
|
|
|
+- # we must determine the device root ourselves
|
|
|
+- paths = [('/storage/sdcard0', 'tests'),
|
|
|
+- ('/storage/sdcard1', 'tests'),
|
|
|
+- ('/storage/sdcard', 'tests'),
|
|
|
+- ('/mnt/sdcard', 'tests'),
|
|
|
+- ('/sdcard', 'tests'),
|
|
|
+- ('/data/local', 'tests')]
|
|
|
+- for (basePath, subPath) in paths:
|
|
|
+- if self.dirExists(basePath):
|
|
|
+- root = os.path.join(basePath, subPath)
|
|
|
+- try:
|
|
|
+- self.mkDir(root)
|
|
|
+- return root
|
|
|
+- except Exception:
|
|
|
+- pass
|
|
|
+-
|
|
|
+- raise DMError("Unable to set up device root using paths: [%s]"
|
|
|
+- % ", ".join(["'%s'" % os.path.join(b, s) for b, s in paths]))
|
|
|
+-
|
|
|
+- def getTempDir(self):
|
|
|
+- # Cache result to speed up operations depending
|
|
|
+- # on the temporary directory.
|
|
|
+- if not self._tempDir:
|
|
|
+- self._tempDir = "%s/tmp" % self.deviceRoot
|
|
|
+- self.mkDir(self._tempDir)
|
|
|
+-
|
|
|
+- return self._tempDir
|
|
|
+-
|
|
|
+- def reboot(self, wait=False, **kwargs):
|
|
|
+- self._checkCmd(["reboot"])
|
|
|
+- if wait:
|
|
|
+- self._checkCmd(["wait-for-device"])
|
|
|
+- if self._runAdbAsRoot:
|
|
|
+- self._adb_root()
|
|
|
+- self._checkCmd(["shell", "ls", "/sbin"], timeout=self.short_timeout)
|
|
|
+-
|
|
|
+- def updateApp(self, appBundlePath, **kwargs):
|
|
|
+- return self._runCmd(["install", "-r", appBundlePath]).output
|
|
|
+-
|
|
|
+- def getCurrentTime(self):
|
|
|
+- timestr = str(self._runCmd(["shell", "date", "+%s"], timeout=self.short_timeout).output[0])
|
|
|
+- if (not timestr or not timestr.isdigit()):
|
|
|
+- raise DMError("Unable to get current time using date (got: '%s')" % timestr)
|
|
|
+- return int(timestr) * 1000
|
|
|
+-
|
|
|
+- def getInfo(self, directive=None):
|
|
|
+- directive = directive or "all"
|
|
|
+- ret = {}
|
|
|
+- if directive == "id" or directive == "all":
|
|
|
+- ret["id"] = self._runCmd(["get-serialno"], timeout=self.short_timeout).output[0]
|
|
|
+- if directive == "os" or directive == "all":
|
|
|
+- ret["os"] = self.shellCheckOutput(
|
|
|
+- ["getprop", "ro.build.display.id"], timeout=self.short_timeout)
|
|
|
+- if directive == "uptime" or directive == "all":
|
|
|
+- uptime = self.shellCheckOutput(["uptime"], timeout=self.short_timeout)
|
|
|
+- if not uptime:
|
|
|
+- raise DMError("error getting uptime")
|
|
|
+- m = re.match("up time: ((\d+) days, )*(\d{2}):(\d{2}):(\d{2})", uptime)
|
|
|
+- if m:
|
|
|
+- uptime = "%d days %d hours %d minutes %d seconds" % tuple(
|
|
|
+- [int(g or 0) for g in m.groups()[1:]])
|
|
|
+- ret["uptime"] = uptime
|
|
|
+- if directive == "process" or directive == "all":
|
|
|
+- data = self.shellCheckOutput(["ps"], timeout=self.short_timeout)
|
|
|
+- ret["process"] = data.split('\n')
|
|
|
+- if directive == "systime" or directive == "all":
|
|
|
+- ret["systime"] = self.shellCheckOutput(["date"], timeout=self.short_timeout)
|
|
|
+- if directive == "memtotal" or directive == "all":
|
|
|
+- meminfo = {}
|
|
|
+- for line in self.pullFile("/proc/meminfo").splitlines():
|
|
|
+- key, value = line.split(":")
|
|
|
+- meminfo[key] = value.strip()
|
|
|
+- ret["memtotal"] = meminfo["MemTotal"]
|
|
|
+- if directive == "disk" or directive == "all":
|
|
|
+- data = self.shellCheckOutput(
|
|
|
+- ["df", "/data", "/system", "/sdcard"], timeout=self.short_timeout)
|
|
|
+- ret["disk"] = data.split('\n')
|
|
|
+- self._logger.debug("getInfo: %s" % ret)
|
|
|
+- return ret
|
|
|
+-
|
|
|
+- def uninstallApp(self, appName, installPath=None):
|
|
|
+- status = self._runCmd(["uninstall", appName]).output[0].strip()
|
|
|
+- if status != 'Success':
|
|
|
+- raise DMError("uninstall failed for %s. Got: %s" % (appName, status))
|
|
|
+-
|
|
|
+- def uninstallAppAndReboot(self, appName, installPath=None):
|
|
|
+- self.uninstallApp(appName)
|
|
|
+- self.reboot()
|
|
|
+-
|
|
|
+- def _runCmd(self, args, timeout=None, retryLimit=None):
|
|
|
+- """
|
|
|
+- Runs a command using adb
|
|
|
+- If timeout is specified, the process is killed after <timeout> seconds.
|
|
|
+-
|
|
|
+- returns: instance of ProcessHandler
|
|
|
+- """
|
|
|
+- retryLimit = retryLimit or self.retryLimit
|
|
|
+- finalArgs = [self._adbPath]
|
|
|
+- if self._serverHost is not None:
|
|
|
+- finalArgs.extend(['-H', self._serverHost])
|
|
|
+- if self._serverPort is not None:
|
|
|
+- finalArgs.extend(['-P', str(self._serverPort)])
|
|
|
+- if self._deviceSerial:
|
|
|
+- finalArgs.extend(['-s', self._deviceSerial])
|
|
|
+- finalArgs.extend(args)
|
|
|
+- self._logger.debug("_runCmd - command: %s" % ' '.join(finalArgs))
|
|
|
+- if not timeout:
|
|
|
+- timeout = self.default_timeout
|
|
|
+-
|
|
|
+- def _timeout():
|
|
|
+- self._logger.error("Timeout exceeded for _runCmd call '%s'" % ' '.join(finalArgs))
|
|
|
+-
|
|
|
+- retries = 0
|
|
|
+- while retries < retryLimit:
|
|
|
+- proc = ProcessHandler(finalArgs, storeOutput=True,
|
|
|
+- processOutputLine=self._log, onTimeout=_timeout)
|
|
|
+- proc.run(timeout=timeout)
|
|
|
+- proc.returncode = proc.wait()
|
|
|
+- if proc.returncode is None:
|
|
|
+- proc.kill()
|
|
|
+- retries += 1
|
|
|
+- else:
|
|
|
+- return proc
|
|
|
+-
|
|
|
+- # timeout is specified in seconds, and if no timeout is given,
|
|
|
+- # we will run until we hit the default_timeout specified in the __init__
|
|
|
+- def _checkCmd(self, args, timeout=None, retryLimit=None):
|
|
|
+- """
|
|
|
+- Runs a command using adb and waits for the command to finish.
|
|
|
+- If timeout is specified, the process is killed after <timeout> seconds.
|
|
|
+-
|
|
|
+- returns: returncode from process
|
|
|
+- """
|
|
|
+- retryLimit = retryLimit or self.retryLimit
|
|
|
+- finalArgs = [self._adbPath]
|
|
|
+- if self._serverHost is not None:
|
|
|
+- finalArgs.extend(['-H', self._serverHost])
|
|
|
+- if self._serverPort is not None:
|
|
|
+- finalArgs.extend(['-P', str(self._serverPort)])
|
|
|
+- if self._deviceSerial:
|
|
|
+- finalArgs.extend(['-s', self._deviceSerial])
|
|
|
+- finalArgs.extend(args)
|
|
|
+- self._logger.debug("_checkCmd - command: %s" % ' '.join(finalArgs))
|
|
|
+- if not timeout:
|
|
|
+- # We are asserting that all commands will complete in this
|
|
|
+- # time unless otherwise specified
|
|
|
+- timeout = self.default_timeout
|
|
|
+-
|
|
|
+- def _timeout():
|
|
|
+- self._logger.error("Timeout exceeded for _checkCmd call '%s'" % ' '.join(finalArgs))
|
|
|
+-
|
|
|
+- timeout = int(timeout)
|
|
|
+- retries = 0
|
|
|
+- while retries < retryLimit:
|
|
|
+- proc = ProcessHandler(finalArgs, processOutputLine=self._log, onTimeout=_timeout)
|
|
|
+- proc.run(timeout=timeout)
|
|
|
+- ret_code = proc.wait()
|
|
|
+- if ret_code is None:
|
|
|
+- proc.kill()
|
|
|
+- retries += 1
|
|
|
+- else:
|
|
|
+- return ret_code
|
|
|
+-
|
|
|
+- raise DMError("Timeout exceeded for _checkCmd call after %d retries." % retries)
|
|
|
+-
|
|
|
+- def chmodDir(self, remoteDir, mask="777"):
|
|
|
+- if (self.dirExists(remoteDir)):
|
|
|
+- if '/sdcard' in remoteDir:
|
|
|
+- self._logger.debug("chmod %s -- skipped (/sdcard)" % remoteDir)
|
|
|
+- else:
|
|
|
+- files = self.listFiles(remoteDir.strip())
|
|
|
+- for f in files:
|
|
|
+- remoteEntry = remoteDir.strip() + "/" + f.strip()
|
|
|
+- if (self.dirExists(remoteEntry)):
|
|
|
+- self.chmodDir(remoteEntry)
|
|
|
+- else:
|
|
|
+- self._checkCmd(["shell", "chmod", mask, remoteEntry],
|
|
|
+- timeout=self.short_timeout)
|
|
|
+- self._logger.info("chmod %s" % remoteEntry)
|
|
|
+- self._checkCmd(["shell", "chmod", mask, remoteDir], timeout=self.short_timeout)
|
|
|
+- self._logger.debug("chmod %s" % remoteDir)
|
|
|
+- else:
|
|
|
+- self._checkCmd(["shell", "chmod", mask, remoteDir.strip()], timeout=self.short_timeout)
|
|
|
+- self._logger.debug("chmod %s" % remoteDir.strip())
|
|
|
+-
|
|
|
+- def _verifyADB(self):
|
|
|
+- """
|
|
|
+- Check to see if adb itself can be executed.
|
|
|
+- """
|
|
|
+- if self._adbPath != 'adb':
|
|
|
+- if not os.access(self._adbPath, os.X_OK):
|
|
|
+- raise DMError("invalid adb path, or adb not executable: %s" % self._adbPath)
|
|
|
+-
|
|
|
+- try:
|
|
|
+- re_version = re.compile(r'Android Debug Bridge version (.*)')
|
|
|
+- proc = self._runCmd(["version"], timeout=self.short_timeout)
|
|
|
+- self._adb_version = re_version.match(proc.output[0]).group(1)
|
|
|
+- self._logger.info("Detected adb %s" % self._adb_version)
|
|
|
+- except os.error as err:
|
|
|
+- raise DMError(
|
|
|
+- "unable to execute ADB (%s): ensure Android SDK is installed "
|
|
|
+- "and adb is in your $PATH" % err)
|
|
|
+-
|
|
|
+- def _verifyDevice(self):
|
|
|
+- # If there is a device serial number, see if adb is connected to it
|
|
|
+- if self._deviceSerial:
|
|
|
+- deviceStatus = None
|
|
|
+- for line in self._runCmd(["devices"]).output:
|
|
|
+- m = re.match('(.+)?\s+(.+)$', line)
|
|
|
+- if m:
|
|
|
+- if self._deviceSerial == m.group(1):
|
|
|
+- deviceStatus = m.group(2)
|
|
|
+- if deviceStatus is None:
|
|
|
+- raise DMError("device not found: %s" % self._deviceSerial)
|
|
|
+- elif deviceStatus != "device":
|
|
|
+- raise DMError("bad status for device %s: %s" % (self._deviceSerial, deviceStatus))
|
|
|
+-
|
|
|
+- # Check to see if we can connect to device and run a simple command
|
|
|
+- if not self._checkCmd(["shell", "echo"], timeout=self.short_timeout) == 0:
|
|
|
+- raise DMError("unable to connect to device")
|
|
|
+-
|
|
|
+- def _checkForRoot(self):
|
|
|
+- self._haveRootShell = False
|
|
|
+- self._haveSu = False
|
|
|
+- # If requested to attempt to run adbd as root, do so before
|
|
|
+- # checking whether adbs is running as root.
|
|
|
+- if self._runAdbAsRoot:
|
|
|
+- self._adb_root()
|
|
|
+-
|
|
|
+- # Check whether we _are_ root by default (some development boards work
|
|
|
+- # this way, this is also the result of some relatively rare rooting
|
|
|
+- # techniques)
|
|
|
+- proc = self._runCmd(["shell", "id"], timeout=self.short_timeout)
|
|
|
+- if proc.output and 'uid=0(root)' in proc.output[0]:
|
|
|
+- self._haveRootShell = True
|
|
|
+- # if this returns true, we don't care about su
|
|
|
+- return
|
|
|
+-
|
|
|
+- # if root shell is not available, check if 'su' can be used to gain
|
|
|
+- # root
|
|
|
+- def su_id(su_modifier, timeout):
|
|
|
+- proc = self._runCmd(["shell", "su", su_modifier, "id"],
|
|
|
+- timeout=timeout)
|
|
|
+-
|
|
|
+- # wait for response for maximum of 15 seconds, in case su
|
|
|
+- # prompts for a password or triggers the Android SuperUser
|
|
|
+- # prompt
|
|
|
+- start_time = time.time()
|
|
|
+- retcode = None
|
|
|
+- while (time.time() - start_time) <= 15 and retcode is None:
|
|
|
+- retcode = proc.poll()
|
|
|
+- if retcode is None: # still not terminated, kill
|
|
|
+- proc.kill()
|
|
|
+-
|
|
|
+- if proc.output and 'uid=0(root)' in proc.output[0]:
|
|
|
+- return True
|
|
|
+- return False
|
|
|
+-
|
|
|
+- if su_id('0', self.short_timeout):
|
|
|
+- self._haveSu = True
|
|
|
+- self._suModifier = '0'
|
|
|
+- elif su_id('-c', self.short_timeout):
|
|
|
+- self._haveSu = True
|
|
|
+- self._suModifier = '-c'
|
|
|
+-
|
|
|
+- def _isUnzipAvailable(self):
|
|
|
+- data = self._runCmd(["shell", "unzip"]).output
|
|
|
+- for line in data:
|
|
|
+- if (re.search('Usage', line)):
|
|
|
+- return True
|
|
|
+- return False
|
|
|
+-
|
|
|
+- def _isLocalZipAvailable(self):
|
|
|
+- def _noOutput(line):
|
|
|
+- # suppress output from zip ProcessHandler
|
|
|
+- pass
|
|
|
+- try:
|
|
|
+- proc = ProcessHandler(["zip", "-?"], storeOutput=False, processOutputLine=_noOutput)
|
|
|
+- proc.run()
|
|
|
+- proc.wait()
|
|
|
+- except Exception:
|
|
|
+- return False
|
|
|
+- return True
|
|
|
+-
|
|
|
+- def _verifyZip(self):
|
|
|
+- # If "zip" can be run locally, and "unzip" can be run remotely, then pushDir
|
|
|
+- # can use these to push just one file per directory -- a significant
|
|
|
+- # optimization for large directories.
|
|
|
+- self._useZip = False
|
|
|
+- if (self._isUnzipAvailable() and self._isLocalZipAvailable()):
|
|
|
+- self._logger.info("will use zip to push directories")
|
|
|
+- self._useZip = True
|
|
|
+- else:
|
|
|
+- raise DMError("zip not available")
|
|
|
+-
|
|
|
+- def _adb_root(self):
|
|
|
+- """ Some devices require us to reboot adbd as root.
|
|
|
+- This function takes care of it.
|
|
|
+- """
|
|
|
+- if self.processInfo("adbd")[2] != "root":
|
|
|
+- self._checkCmd(["root"])
|
|
|
+- self._checkCmd(["wait-for-device"])
|
|
|
+- if self.processInfo("adbd")[2] != "root":
|
|
|
+- raise DMError("We tried rebooting adbd as root, however, it failed.")
|
|
|
+-
|
|
|
+- def _detectLsModifier(self):
|
|
|
+- if self._lsModifier is None:
|
|
|
+- # Check if busybox -1A is required in order to get one
|
|
|
+- # file per line.
|
|
|
+- output = self._runCmd(["shell", "ls", "-1A", "/"],
|
|
|
+- timeout=self.short_timeout).output
|
|
|
+- output = ' '.join(output)
|
|
|
+- if 'error: device not found' in output:
|
|
|
+- raise DMError(output)
|
|
|
+- if "Unknown option '-1'. Aborting." in output:
|
|
|
+- self._lsModifier = "-a"
|
|
|
+- elif "No such file or directory" in output:
|
|
|
+- self._lsModifier = "-a"
|
|
|
+- else:
|
|
|
+- self._lsModifier = "-1A"
|
|
|
+diff --git a/testing/mozbase/mozdevice/mozdevice/dmcli.py b/testing/mozbase/mozdevice/mozdevice/dmcli.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/mozdevice/dmcli.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,352 +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/.
|
|
|
+-
|
|
|
+-"""
|
|
|
+-Command-line client to control a device
|
|
|
+-"""
|
|
|
+-from __future__ import absolute_import, print_function
|
|
|
+-
|
|
|
+-import errno
|
|
|
+-import logging
|
|
|
+-import os
|
|
|
+-import posixpath
|
|
|
+-import StringIO
|
|
|
+-import sys
|
|
|
+-import mozdevice
|
|
|
+-import mozlog
|
|
|
+-import argparse
|
|
|
+-
|
|
|
+-
|
|
|
+-class DMCli(object):
|
|
|
+-
|
|
|
+- def __init__(self):
|
|
|
+- self.commands = {'deviceroot': {'function': self.deviceroot,
|
|
|
+- 'help': 'get device root directory for storing temporary '
|
|
|
+- 'files'},
|
|
|
+- 'install': {'function': self.install,
|
|
|
+- 'args': [{'name': 'file'}],
|
|
|
+- 'help': 'push this package file to the device'
|
|
|
+- ' and install it'},
|
|
|
+- 'uninstall': {'function': self.uninstall,
|
|
|
+- 'args': [{'name': 'packagename'}],
|
|
|
+- 'help': 'uninstall the named app from the device'},
|
|
|
+- 'killapp': {'function': self.kill,
|
|
|
+- 'args': [{'name': 'process_name', 'nargs': '*'}],
|
|
|
+- 'help': 'kills any processes with name(s) on device'},
|
|
|
+- 'launchapp': {'function': self.launchapp,
|
|
|
+- 'args': [{'name': 'appname'},
|
|
|
+- {'name': 'activity_name'},
|
|
|
+- {'name': '--intent',
|
|
|
+- 'action': 'store',
|
|
|
+- 'default': 'android.intent.action.VIEW'},
|
|
|
+- {'name': '--url',
|
|
|
+- 'action': 'store'},
|
|
|
+- {'name': '--no-fail-if-running',
|
|
|
+- 'action': 'store_true',
|
|
|
+- 'help': 'Don\'t fail if application is'
|
|
|
+- ' already running'}
|
|
|
+- ],
|
|
|
+- 'help': 'launches application on device'},
|
|
|
+- 'listapps': {'function': self.listapps,
|
|
|
+- 'help': 'list applications on device'},
|
|
|
+- 'push': {'function': self.push,
|
|
|
+- 'args': [{'name': 'local_file'},
|
|
|
+- {'name': 'remote_file'}
|
|
|
+- ],
|
|
|
+- 'help': 'copy file/dir to device'},
|
|
|
+- 'pull': {'function': self.pull,
|
|
|
+- 'args': [{'name': 'local_file'},
|
|
|
+- {'name': 'remote_file', 'nargs': '?'}],
|
|
|
+- 'help': 'copy file/dir from device'},
|
|
|
+- 'shell': {'function': self.shell,
|
|
|
+- 'args': [{'name': 'command', 'nargs': argparse.REMAINDER},
|
|
|
+- {'name': '--root', 'action': 'store_true',
|
|
|
+- 'help': 'Run command as root'}],
|
|
|
+- 'help': 'run shell command on device'},
|
|
|
+- 'info': {'function': self.getinfo,
|
|
|
+- 'args': [{'name': 'directive', 'nargs': '?'}],
|
|
|
+- 'help': 'get information on specified '
|
|
|
+- 'aspect of the device (if no argument '
|
|
|
+- 'given, print all available information)'
|
|
|
+- },
|
|
|
+- 'ps': {'function': self.processlist,
|
|
|
+- 'help': 'get information on running processes on device'
|
|
|
+- },
|
|
|
+- 'logcat': {'function': self.logcat,
|
|
|
+- 'help': 'get logcat from device'
|
|
|
+- },
|
|
|
+- 'ls': {'function': self.listfiles,
|
|
|
+- 'args': [{'name': 'remote_dir'}],
|
|
|
+- 'help': 'list files on device'
|
|
|
+- },
|
|
|
+- 'rm': {'function': self.removefile,
|
|
|
+- 'args': [{'name': 'remote_file'}],
|
|
|
+- 'help': 'remove file from device'
|
|
|
+- },
|
|
|
+- 'isdir': {'function': self.isdir,
|
|
|
+- 'args': [{'name': 'remote_dir'}],
|
|
|
+- 'help': 'print if remote file is a directory'
|
|
|
+- },
|
|
|
+- 'mkdir': {'function': self.mkdir,
|
|
|
+- 'args': [{'name': 'remote_dir'}],
|
|
|
+- 'help': 'makes a directory on device'
|
|
|
+- },
|
|
|
+- 'rmdir': {'function': self.rmdir,
|
|
|
+- 'args': [{'name': 'remote_dir'}],
|
|
|
+- 'help': 'recursively remove directory from device'
|
|
|
+- },
|
|
|
+- 'screencap': {'function': self.screencap,
|
|
|
+- 'args': [{'name': 'png_file'}],
|
|
|
+- 'help': 'capture screenshot of device in action'
|
|
|
+- },
|
|
|
+- 'clearlogcat': {'function': self.clearlogcat,
|
|
|
+- 'help': 'clear the logcat'
|
|
|
+- },
|
|
|
+- 'reboot': {'function': self.reboot,
|
|
|
+- 'help': 'reboot the device',
|
|
|
+- 'args': [{'name': '--wait',
|
|
|
+- 'action': 'store_true',
|
|
|
+- 'help': 'Wait for device to come back up'
|
|
|
+- ' before exiting'}]
|
|
|
+-
|
|
|
+- },
|
|
|
+- 'isfile': {'function': self.isfile,
|
|
|
+- 'args': [{'name': 'remote_file'}],
|
|
|
+- 'help': 'check whether a file exists on the device'
|
|
|
+- },
|
|
|
+- 'launchfennec': {'function': self.launchfennec,
|
|
|
+- 'args': [{'name': 'appname'},
|
|
|
+- {'name': '--intent', 'action': 'store',
|
|
|
+- 'default': 'android.intent.action.VIEW'},
|
|
|
+- {'name': '--url', 'action': 'store'},
|
|
|
+- {'name': '--extra-args', 'action': 'store'},
|
|
|
+- {'name': '--mozenv', 'action': 'store',
|
|
|
+- 'help': 'Gecko environment variables to set'
|
|
|
+- ' in "KEY1=VAL1 KEY2=VAL2" format'},
|
|
|
+- {'name': '--no-fail-if-running',
|
|
|
+- 'action': 'store_true',
|
|
|
+- 'help': 'Don\'t fail if application is '
|
|
|
+- 'already running'}
|
|
|
+- ],
|
|
|
+- 'help': 'launch fennec'
|
|
|
+- },
|
|
|
+- 'getip': {'function': self.getip,
|
|
|
+- 'args': [{'name': 'interface', 'nargs': '*'}],
|
|
|
+- 'help': 'get the ip address of the device'
|
|
|
+- }
|
|
|
+- }
|
|
|
+-
|
|
|
+- self.parser = argparse.ArgumentParser()
|
|
|
+- self.add_options(self.parser)
|
|
|
+- self.add_commands(self.parser)
|
|
|
+- mozlog.commandline.add_logging_group(self.parser)
|
|
|
+-
|
|
|
+- def run(self, args=sys.argv[1:]):
|
|
|
+- args = self.parser.parse_args()
|
|
|
+-
|
|
|
+- mozlog.commandline.setup_logging(
|
|
|
+- 'mozdevice', args, {'mach': sys.stdout})
|
|
|
+-
|
|
|
+- self.dm = self.getDevice(hwid=args.hwid,
|
|
|
+- host=args.host, port=args.port,
|
|
|
+- verbose=args.verbose)
|
|
|
+-
|
|
|
+- ret = args.func(args)
|
|
|
+- if ret is None:
|
|
|
+- ret = 0
|
|
|
+-
|
|
|
+- sys.exit(ret)
|
|
|
+-
|
|
|
+- def add_options(self, parser):
|
|
|
+- parser.add_argument("-v", "--verbose", action="store_true",
|
|
|
+- help="Verbose output from DeviceManager",
|
|
|
+- default=bool(os.environ.get('VERBOSE')))
|
|
|
+- parser.add_argument("--host", action="store",
|
|
|
+- help="Device hostname (only if using TCP/IP, "
|
|
|
+- "defaults to TEST_DEVICE environment "
|
|
|
+- "variable if present)",
|
|
|
+- default=os.environ.get('TEST_DEVICE'))
|
|
|
+- parser.add_argument("-p", "--port", action="store",
|
|
|
+- type=int,
|
|
|
+- help="Custom device port (if using "
|
|
|
+- "adb-over-tcp)", default=None)
|
|
|
+- parser.add_argument("-d", "--hwid", action="store",
|
|
|
+- help="HWID", default=None)
|
|
|
+- parser.add_argument("--package-name", action="store",
|
|
|
+- help="Packagename (if using DeviceManagerADB)",
|
|
|
+- default=None)
|
|
|
+-
|
|
|
+- def add_commands(self, parser):
|
|
|
+- subparsers = parser.add_subparsers(title="Commands", metavar="<command>")
|
|
|
+- for (commandname, commandprops) in sorted(self.commands.iteritems()):
|
|
|
+- subparser = subparsers.add_parser(commandname, help=commandprops['help'])
|
|
|
+- if commandprops.get('args'):
|
|
|
+- for arg in commandprops['args']:
|
|
|
+- # this is more elegant but doesn't work in python 2.6
|
|
|
+- # (which we still use on tbpl @ mozilla where we install
|
|
|
+- # this package)
|
|
|
+- # kwargs = { k: v for k,v in arg.items() if k is not 'name' }
|
|
|
+- kwargs = {}
|
|
|
+- for (k, v) in arg.items():
|
|
|
+- if k is not 'name':
|
|
|
+- kwargs[k] = v
|
|
|
+- subparser.add_argument(arg['name'], **kwargs)
|
|
|
+- subparser.set_defaults(func=commandprops['function'])
|
|
|
+-
|
|
|
+- def getDevice(self, hwid=None, host=None, port=None,
|
|
|
+- packagename=None, verbose=False):
|
|
|
+- '''
|
|
|
+- Returns a device with the specified parameters
|
|
|
+- '''
|
|
|
+- logLevel = logging.ERROR
|
|
|
+- if verbose:
|
|
|
+- logLevel = logging.DEBUG
|
|
|
+-
|
|
|
+- if host and not port:
|
|
|
+- port = 5555
|
|
|
+- return mozdevice.DroidADB(packageName=packagename,
|
|
|
+- host=host, port=port,
|
|
|
+- logLevel=logLevel)
|
|
|
+-
|
|
|
+- def deviceroot(self, args):
|
|
|
+- print(self.dm.deviceRoot)
|
|
|
+-
|
|
|
+- def push(self, args):
|
|
|
+- (src, dest) = (args.local_file, args.remote_file)
|
|
|
+- if os.path.isdir(src):
|
|
|
+- self.dm.pushDir(src, dest)
|
|
|
+- else:
|
|
|
+- dest_is_dir = dest[-1] == '/' or self.dm.dirExists(dest)
|
|
|
+- dest = posixpath.normpath(dest)
|
|
|
+- if dest_is_dir:
|
|
|
+- dest = posixpath.join(dest, os.path.basename(src))
|
|
|
+- self.dm.pushFile(src, dest)
|
|
|
+-
|
|
|
+- def pull(self, args):
|
|
|
+- (src, dest) = (args.local_file, args.remote_file)
|
|
|
+- if not self.dm.fileExists(src):
|
|
|
+- print('No such file or directory')
|
|
|
+- return
|
|
|
+- if not dest:
|
|
|
+- dest = posixpath.basename(src)
|
|
|
+- if self.dm.dirExists(src):
|
|
|
+- self.dm.getDirectory(src, dest)
|
|
|
+- else:
|
|
|
+- self.dm.getFile(src, dest)
|
|
|
+-
|
|
|
+- def install(self, args):
|
|
|
+- basename = os.path.basename(args.file)
|
|
|
+- app_path_on_device = posixpath.join(self.dm.deviceRoot,
|
|
|
+- basename)
|
|
|
+- self.dm.pushFile(args.file, app_path_on_device)
|
|
|
+- self.dm.installApp(app_path_on_device)
|
|
|
+-
|
|
|
+- def uninstall(self, args):
|
|
|
+- self.dm.uninstallApp(args.packagename)
|
|
|
+-
|
|
|
+- def launchapp(self, args):
|
|
|
+- self.dm.launchApplication(args.appname, args.activity_name,
|
|
|
+- args.intent, url=args.url,
|
|
|
+- failIfRunning=(not args.no_fail_if_running))
|
|
|
+-
|
|
|
+- def listapps(self, args):
|
|
|
+- for app in self.dm.getInstalledApps():
|
|
|
+- print(app)
|
|
|
+-
|
|
|
+- def stopapp(self, args):
|
|
|
+- self.dm.stopApplication(args.appname)
|
|
|
+-
|
|
|
+- def kill(self, args):
|
|
|
+- for name in args.process_name:
|
|
|
+- self.dm.killProcess(name)
|
|
|
+-
|
|
|
+- def shell(self, args):
|
|
|
+- buf = StringIO.StringIO()
|
|
|
+- self.dm.shell(args.command, buf, root=args.root)
|
|
|
+- print(str(buf.getvalue()[0:-1]).rstrip())
|
|
|
+-
|
|
|
+- def getinfo(self, args):
|
|
|
+- info = self.dm.getInfo(directive=args.directive)
|
|
|
+- for (infokey, infoitem) in sorted(info.iteritems()):
|
|
|
+- if infokey == "process":
|
|
|
+- pass # skip process list: get that through ps
|
|
|
+- elif args.directive is None:
|
|
|
+- print("%s: %s" % (infokey.upper(), infoitem))
|
|
|
+- else:
|
|
|
+- print(infoitem)
|
|
|
+-
|
|
|
+- def logcat(self, args):
|
|
|
+- print(''.join(self.dm.getLogcat()))
|
|
|
+-
|
|
|
+- def clearlogcat(self, args):
|
|
|
+- self.dm.recordLogcat()
|
|
|
+-
|
|
|
+- def reboot(self, args):
|
|
|
+- self.dm.reboot(wait=args.wait)
|
|
|
+-
|
|
|
+- def processlist(self, args):
|
|
|
+- pslist = self.dm.getProcessList()
|
|
|
+- for ps in pslist:
|
|
|
+- print(" ".join(str(i) for i in ps))
|
|
|
+-
|
|
|
+- def listfiles(self, args):
|
|
|
+- filelist = self.dm.listFiles(args.remote_dir)
|
|
|
+- for file in filelist:
|
|
|
+- print(file)
|
|
|
+-
|
|
|
+- def removefile(self, args):
|
|
|
+- self.dm.removeFile(args.remote_file)
|
|
|
+-
|
|
|
+- def isdir(self, args):
|
|
|
+- if self.dm.dirExists(args.remote_dir):
|
|
|
+- print("TRUE")
|
|
|
+- return
|
|
|
+-
|
|
|
+- print("FALSE")
|
|
|
+- return errno.ENOTDIR
|
|
|
+-
|
|
|
+- def mkdir(self, args):
|
|
|
+- self.dm.mkDir(args.remote_dir)
|
|
|
+-
|
|
|
+- def rmdir(self, args):
|
|
|
+- self.dm.removeDir(args.remote_dir)
|
|
|
+-
|
|
|
+- def screencap(self, args):
|
|
|
+- self.dm.saveScreenshot(args.png_file)
|
|
|
+-
|
|
|
+- def isfile(self, args):
|
|
|
+- if self.dm.fileExists(args.remote_file):
|
|
|
+- print("TRUE")
|
|
|
+- return
|
|
|
+- print("FALSE")
|
|
|
+- return errno.ENOENT
|
|
|
+-
|
|
|
+- def launchfennec(self, args):
|
|
|
+- mozEnv = None
|
|
|
+- if args.mozenv:
|
|
|
+- mozEnv = {}
|
|
|
+- keyvals = args.mozenv.split()
|
|
|
+- for keyval in keyvals:
|
|
|
+- (key, _, val) = keyval.partition("=")
|
|
|
+- mozEnv[key] = val
|
|
|
+- self.dm.launchFennec(args.appname, intent=args.intent,
|
|
|
+- mozEnv=mozEnv,
|
|
|
+- extraArgs=args.extra_args, url=args.url,
|
|
|
+- failIfRunning=(not args.no_fail_if_running))
|
|
|
+-
|
|
|
+- def getip(self, args):
|
|
|
+- if args.interface:
|
|
|
+- print(self.dm.getIP(args.interface))
|
|
|
+- else:
|
|
|
+- print(self.dm.getIP())
|
|
|
+-
|
|
|
+-
|
|
|
+-def cli(args=sys.argv[1:]):
|
|
|
+- # process the command line
|
|
|
+- cli = DMCli()
|
|
|
+- cli.run(args)
|
|
|
+-
|
|
|
+-
|
|
|
+-if __name__ == '__main__':
|
|
|
+- cli()
|
|
|
+diff --git a/testing/mozbase/mozdevice/mozdevice/droid.py b/testing/mozbase/mozdevice/mozdevice/droid.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/mozdevice/droid.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,198 +0,0 @@
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
+-# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import
|
|
|
+-
|
|
|
+-import StringIO
|
|
|
+-import re
|
|
|
+-import time
|
|
|
+-
|
|
|
+-from mozdevice import version_codes
|
|
|
+-
|
|
|
+-from .devicemanagerADB import DeviceManagerADB
|
|
|
+-from .devicemanager import DMError
|
|
|
+-
|
|
|
+-
|
|
|
+-class DroidMixin(object):
|
|
|
+- """Mixin to extend DeviceManager with Android-specific functionality"""
|
|
|
+-
|
|
|
+- _stopApplicationNeedsRoot = True
|
|
|
+-
|
|
|
+- def _getExtraAmStartArgs(self):
|
|
|
+- return []
|
|
|
+-
|
|
|
+- def launchApplication(self, appName, activityName, intent, url=None,
|
|
|
+- extras=None, wait=True, failIfRunning=True):
|
|
|
+- """
|
|
|
+- Launches an Android application
|
|
|
+-
|
|
|
+- :param appName: Name of application (e.g. `com.android.chrome`)
|
|
|
+- :param activityName: Name of activity to launch (e.g. `.Main`)
|
|
|
+- :param intent: Intent to launch application with
|
|
|
+- :param url: URL to open
|
|
|
+- :param extras: Dictionary of extra arguments to launch application with
|
|
|
+- :param wait: If True, wait for application to start before returning
|
|
|
+- :param failIfRunning: Raise an exception if instance of application is already running
|
|
|
+- """
|
|
|
+-
|
|
|
+- # If failIfRunning is True, we throw an exception here. Only one
|
|
|
+- # instance of an application can be running at once on Android,
|
|
|
+- # starting a new instance may not be what we want depending on what
|
|
|
+- # we want to do
|
|
|
+- if failIfRunning and self.processExist(appName):
|
|
|
+- raise DMError("Only one instance of an application may be running "
|
|
|
+- "at once")
|
|
|
+-
|
|
|
+- acmd = ["am", "start"] + self._getExtraAmStartArgs() + \
|
|
|
+- ["-W" if wait else '', "-n", "%s/%s" % (appName, activityName)]
|
|
|
+-
|
|
|
+- if intent:
|
|
|
+- acmd.extend(["-a", intent])
|
|
|
+-
|
|
|
+- if extras:
|
|
|
+- for (key, val) in extras.iteritems():
|
|
|
+- if type(val) is int:
|
|
|
+- extraTypeParam = "--ei"
|
|
|
+- elif type(val) is bool:
|
|
|
+- extraTypeParam = "--ez"
|
|
|
+- else:
|
|
|
+- extraTypeParam = "--es"
|
|
|
+- acmd.extend([extraTypeParam, str(key), str(val)])
|
|
|
+-
|
|
|
+- if url:
|
|
|
+- acmd.extend(["-d", url])
|
|
|
+-
|
|
|
+- # shell output not that interesting and debugging logs should already
|
|
|
+- # show what's going on here... so just create an empty memory buffer
|
|
|
+- # and ignore (except on error)
|
|
|
+- shellOutput = StringIO.StringIO()
|
|
|
+- if self.shell(acmd, shellOutput) == 0:
|
|
|
+- return
|
|
|
+-
|
|
|
+- shellOutput.seek(0)
|
|
|
+- raise DMError("Unable to launch application (shell output: '%s')" % shellOutput.read())
|
|
|
+-
|
|
|
+- def launchFennec(self, appName, intent="android.intent.action.VIEW",
|
|
|
+- mozEnv=None, extraArgs=None, url=None, wait=True,
|
|
|
+- failIfRunning=True):
|
|
|
+- """
|
|
|
+- Convenience method to launch Fennec on Android with various debugging
|
|
|
+- arguments
|
|
|
+-
|
|
|
+- :param appName: Name of fennec application (e.g. `org.mozilla.fennec`)
|
|
|
+- :param intent: Intent to launch application with
|
|
|
+- :param mozEnv: Mozilla specific environment to pass into application
|
|
|
+- :param extraArgs: Extra arguments to be parsed by fennec
|
|
|
+- :param url: URL to open
|
|
|
+- :param wait: If True, wait for application to start before returning
|
|
|
+- :param failIfRunning: Raise an exception if instance of application is already running
|
|
|
+- """
|
|
|
+- extras = {}
|
|
|
+-
|
|
|
+- if mozEnv:
|
|
|
+- # mozEnv is expected to be a dictionary of environment variables: Fennec
|
|
|
+- # itself will set them when launched
|
|
|
+- for (envCnt, (envkey, envval)) in enumerate(mozEnv.iteritems()):
|
|
|
+- extras["env" + str(envCnt)] = envkey + "=" + envval
|
|
|
+-
|
|
|
+- # Additional command line arguments that fennec will read and use (e.g.
|
|
|
+- # with a custom profile)
|
|
|
+- if extraArgs:
|
|
|
+- extras['args'] = " ".join(extraArgs)
|
|
|
+-
|
|
|
+- self.launchApplication(appName, "org.mozilla.gecko.BrowserApp", intent, url=url,
|
|
|
+- extras=extras,
|
|
|
+- wait=wait, failIfRunning=failIfRunning)
|
|
|
+-
|
|
|
+- def getInstalledApps(self):
|
|
|
+- """
|
|
|
+- Lists applications installed on this Android device
|
|
|
+-
|
|
|
+- Returns a list of application names in the form [ 'org.mozilla.fennec', ... ]
|
|
|
+- """
|
|
|
+- output = self.shellCheckOutput(["pm", "list", "packages", "-f"])
|
|
|
+- apps = []
|
|
|
+- for line in output.splitlines():
|
|
|
+- # lines are of form: package:/system/app/qik-tmo.apk=com.qiktmobile.android
|
|
|
+- apps.append(line.split('=')[1])
|
|
|
+-
|
|
|
+- return apps
|
|
|
+-
|
|
|
+- def stopApplication(self, appName):
|
|
|
+- """
|
|
|
+- Stops the specified application
|
|
|
+-
|
|
|
+- For Android 3.0+, we use the "am force-stop" to do this, which is
|
|
|
+- reliable and does not require root. For earlier versions of Android,
|
|
|
+- we simply try to manually kill the processes started by the app
|
|
|
+- repeatedly until none is around any more. This is less reliable and
|
|
|
+- does require root.
|
|
|
+-
|
|
|
+- :param appName: Name of application (e.g. `com.android.chrome`)
|
|
|
+- """
|
|
|
+- version = self.shellCheckOutput(["getprop", "ro.build.version.sdk"])
|
|
|
+- if int(version) >= version_codes.HONEYCOMB:
|
|
|
+- self.shellCheckOutput(["am", "force-stop", appName],
|
|
|
+- root=self._stopApplicationNeedsRoot)
|
|
|
+- else:
|
|
|
+- num_tries = 0
|
|
|
+- max_tries = 5
|
|
|
+- while self.processExist(appName):
|
|
|
+- if num_tries > max_tries:
|
|
|
+- raise DMError("Couldn't successfully kill %s after %s "
|
|
|
+- "tries" % (appName, max_tries))
|
|
|
+- self.killProcess(appName)
|
|
|
+- num_tries += 1
|
|
|
+-
|
|
|
+- # sleep for a short duration to make sure there are no
|
|
|
+- # additional processes in the process of being launched
|
|
|
+- # (this is not 100% guaranteed to work since it is inherently
|
|
|
+- # racey, but it's the best we can do)
|
|
|
+- time.sleep(1)
|
|
|
+-
|
|
|
+-
|
|
|
+-class DroidADB(DeviceManagerADB, DroidMixin):
|
|
|
+-
|
|
|
+- _stopApplicationNeedsRoot = False
|
|
|
+-
|
|
|
+- def getTopActivity(self):
|
|
|
+- package = None
|
|
|
+- data = None
|
|
|
+- try:
|
|
|
+- # Increased timeout to 60 seconds after intermittent timeouts at 30.
|
|
|
+- data = self.shellCheckOutput(
|
|
|
+- ["dumpsys", "window", "windows"], timeout=60)
|
|
|
+- except Exception:
|
|
|
+- # dumpsys seems to intermittently fail (seen on 4.3 emulator), producing
|
|
|
+- # no output.
|
|
|
+- return ""
|
|
|
+- # "dumpsys window windows" produces many lines of input. The top/foreground
|
|
|
+- # activity is indicated by something like:
|
|
|
+- # mFocusedApp=AppWindowToken{483e6db0 token=HistoryRecord{484dcad8 com.mozilla.something/.something}} # noqa
|
|
|
+- # or, on other devices:
|
|
|
+- # FocusedApplication: name='AppWindowToken{41a65340 token=ActivityRecord{418fbd68 org.mozilla.fennec_mozdev/org.mozilla.gecko.BrowserApp}}', dispatchingTimeout=5000.000ms # noqa
|
|
|
+- # Extract this line, ending in the forward slash:
|
|
|
+- m = re.search('mFocusedApp(.+)/', data)
|
|
|
+- if not m:
|
|
|
+- m = re.search('FocusedApplication(.+)/', data)
|
|
|
+- if m:
|
|
|
+- line = m.group(0)
|
|
|
+- # Extract package name: string of non-whitespace ending in forward slash
|
|
|
+- m = re.search('(\S+)/$', line)
|
|
|
+- if m:
|
|
|
+- package = m.group(1)
|
|
|
+- if not package:
|
|
|
+- # On some Android 4.4 devices, when the home screen is displayed,
|
|
|
+- # dumpsys reports "mFocusedApp=null". Guard against this case and
|
|
|
+- # others where the focused app can not be determined by returning
|
|
|
+- # an empty string.
|
|
|
+- package = ""
|
|
|
+- return package
|
|
|
+-
|
|
|
+- def getAppRoot(self, packageName):
|
|
|
+- """
|
|
|
+- Returns the root directory for the specified android application
|
|
|
+- """
|
|
|
+- # relying on convention
|
|
|
+- return '/data/data/%s' % packageName
|
|
|
+diff --git a/testing/mozbase/mozdevice/mozdevice/version_codes.py b/testing/mozbase/mozdevice/mozdevice/version_codes.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/mozdevice/version_codes.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,62 +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/.
|
|
|
+-
|
|
|
+-"""
|
|
|
+-VERSION CODES of the android releases.
|
|
|
+-
|
|
|
+-See http://developer.android.com/reference/android/os/Build.VERSION_CODES.html.
|
|
|
+-"""
|
|
|
+-from __future__ import absolute_import
|
|
|
+-
|
|
|
+-# Magic version number for a current development build, which has
|
|
|
+-# not yet turned into an official release.
|
|
|
+-CUR_DEVELOPMENT = 10000
|
|
|
+-
|
|
|
+-# October 2008: The original, first, version of Android
|
|
|
+-BASE = 1
|
|
|
+-# February 2009: First Android update, officially called 1.1
|
|
|
+-BASE_1_1 = 2
|
|
|
+-# May 2009: Android 1.5
|
|
|
+-CUPCAKE = 3
|
|
|
+-# September 2009: Android 1.6
|
|
|
+-DONUT = 4
|
|
|
+-# November 2009: Android 2.0
|
|
|
+-ECLAIR = 5
|
|
|
+-# December 2009: Android 2.0.1
|
|
|
+-ECLAIR_0_1 = 6
|
|
|
+-# January 2010: Android 2.1
|
|
|
+-ECLAIR_MR1 = 7
|
|
|
+-# June 2010: Android 2.2
|
|
|
+-FROYO = 8
|
|
|
+-# November 2010: Android 2.3
|
|
|
+-GINGERBREAD = 9
|
|
|
+-# February 2011: Android 2.3.3
|
|
|
+-GINGERBREAD_MR1 = 10
|
|
|
+-# February 2011: Android 3.0
|
|
|
+-HONEYCOMB = 11
|
|
|
+-# May 2011: Android 3.1
|
|
|
+-HONEYCOMB_MR1 = 12
|
|
|
+-# June 2011: Android 3.2
|
|
|
+-HONEYCOMB_MR2 = 13
|
|
|
+-# October 2011: Android 4.0
|
|
|
+-ICE_CREAM_SANDWICH = 14
|
|
|
+-# December 2011: Android 4.0.3
|
|
|
+-ICE_CREAM_SANDWICH_MR1 = 15
|
|
|
+-# June 2012: Android 4.1
|
|
|
+-JELLY_BEAN = 16
|
|
|
+-# November 2012: Android 4.2
|
|
|
+-JELLY_BEAN_MR1 = 17
|
|
|
+-# July 2013: Android 4.3
|
|
|
+-JELLY_BEAN_MR2 = 18
|
|
|
+-# October 2013: Android 4.4
|
|
|
+-KITKAT = 19
|
|
|
+-# Android 4.4W
|
|
|
+-KITKAT_WATCH = 20
|
|
|
+-# Lollilop
|
|
|
+-LOLLIPOP = 21
|
|
|
+-LOLLIPOP_MR1 = 22
|
|
|
+-# M
|
|
|
+-M = 23
|
|
|
+-# N
|
|
|
+-N = 24
|
|
|
+diff --git a/testing/mozbase/mozdevice/setup.py b/testing/mozbase/mozdevice/setup.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozbase/mozdevice/setup.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,40 +0,0 @@
|
|
|
+-
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
+-# You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import
|
|
|
+-
|
|
|
+-from setuptools import setup
|
|
|
+-
|
|
|
+-PACKAGE_NAME = 'mozdevice'
|
|
|
+-PACKAGE_VERSION = '0.51'
|
|
|
+-
|
|
|
+-deps = ['mozfile >= 1.0',
|
|
|
+- 'mozlog >= 3.0',
|
|
|
+- 'moznetwork >= 0.24',
|
|
|
+- 'mozprocess >= 0.19',
|
|
|
+- ]
|
|
|
+-
|
|
|
+-setup(name=PACKAGE_NAME,
|
|
|
+- version=PACKAGE_VERSION,
|
|
|
+- description="Mozilla-authored device management",
|
|
|
+- long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html",
|
|
|
+- classifiers=['Programming Language :: Python :: 2.7',
|
|
|
+- 'Programming Language :: Python :: 2 :: Only'],
|
|
|
+- # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
|
|
+- keywords='',
|
|
|
+- author='Mozilla Automation and Testing Team',
|
|
|
+- author_email='tools@lists.mozilla.org',
|
|
|
+- url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
|
|
|
+- license='MPL',
|
|
|
+- packages=['mozdevice'],
|
|
|
+- include_package_data=True,
|
|
|
+- zip_safe=False,
|
|
|
+- install_requires=deps,
|
|
|
+- entry_points="""
|
|
|
+- # -*- Entry points: -*-
|
|
|
+- [console_scripts]
|
|
|
+- dm = mozdevice.dmcli:cli
|
|
|
+- """,
|
|
|
+- )
|
|
|
+diff --git a/testing/mozbase/mozrunner/setup.py b/testing/mozbase/mozrunner/setup.py
|
|
|
+--- a/testing/mozbase/mozrunner/setup.py
|
|
|
++++ b/testing/mozbase/mozrunner/setup.py
|
|
|
+@@ -7,17 +7,16 @@ from __future__ import absolute_import
|
|
|
+ from setuptools import setup, find_packages
|
|
|
+
|
|
|
+ PACKAGE_NAME = 'mozrunner'
|
|
|
+ PACKAGE_VERSION = '7.4.0'
|
|
|
+
|
|
|
+ desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
|
|
|
+
|
|
|
+ deps = [
|
|
|
+- 'mozdevice>=1.1.6',
|
|
|
+ 'mozfile>=1.2',
|
|
|
+ 'mozinfo>=0.7,<2',
|
|
|
+ 'mozlog~=4.1.1',
|
|
|
+ 'mozprocess>=0.23,<2',
|
|
|
+ 'mozprofile~=2.1',
|
|
|
+ 'six>=1.10.0,<2',
|
|
|
+ ]
|
|
|
+
|
|
|
+diff --git a/testing/mozbase/packages.txt b/testing/mozbase/packages.txt
|
|
|
+--- a/testing/mozbase/packages.txt
|
|
|
++++ b/testing/mozbase/packages.txt
|
|
|
+@@ -1,12 +1,11 @@
|
|
|
+ manifestparser.pth:testing/mozbase/manifestparser
|
|
|
+ mozcrash.pth:testing/mozbase/mozcrash
|
|
|
+ mozdebug.pth:testing/mozbase/mozdebug
|
|
|
+-mozdevice.pth:testing/mozbase/mozdevice
|
|
|
+ mozfile.pth:testing/mozbase/mozfile
|
|
|
+ mozhttpd.pth:testing/mozbase/mozhttpd
|
|
|
+ mozinfo.pth:testing/mozbase/mozinfo
|
|
|
+ mozinstall.pth:testing/mozbase/mozinstall
|
|
|
+ mozleak.pth:testing/mozbase/mozleak
|
|
|
+ mozlog.pth:testing/mozbase/mozlog
|
|
|
+ moznetwork.pth:testing/mozbase/moznetwork
|
|
|
+ mozprocess.pth:testing/mozbase/mozprocess
|
|
|
+diff --git a/testing/mozharness/configs/android/androidarm_4_3.py b/testing/mozharness/configs/android/androidarm_4_3.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozharness/configs/android/androidarm_4_3.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,372 +0,0 @@
|
|
|
+-import os
|
|
|
+-
|
|
|
+-config = {
|
|
|
+- "buildbot_json_path": "buildprops.json",
|
|
|
+- "hostutils_manifest_path": "testing/config/tooltool-manifests/linux64/hostutils.manifest",
|
|
|
+- "robocop_package_name": "org.mozilla.roboexample.test",
|
|
|
+- "marionette_address": "localhost:2828",
|
|
|
+- "marionette_test_manifest": "unit-tests.ini",
|
|
|
+- "download_tooltool": True,
|
|
|
+- "tooltool_servers": ['http://relengapi/tooltool/'],
|
|
|
+- "tooltool_manifest_path": "testing/config/tooltool-manifests/androidarm_4_3/releng.manifest",
|
|
|
+- "tooltool_cache": "/builds/worker/tooltool_cache",
|
|
|
+- "avds_dir": "/builds/worker/workspace/build/.android",
|
|
|
+- "emulator_manifest": """
|
|
|
+- [
|
|
|
+- {
|
|
|
+- "size": 140097024,
|
|
|
+- "digest": "51781032335c09103e8509b1a558bf22a7119392cf1ea301c49c01bdf21ff0ceb37d260bc1c322cd9b903252429fb01830fc27e4632be30cd345c95bf4b1a39b",
|
|
|
+- "algorithm": "sha512",
|
|
|
+- "filename": "android-sdk_r24.0.2-linux.tgz",
|
|
|
+- "unpack": "True"
|
|
|
+- }
|
|
|
+- ] """,
|
|
|
+- "tools_manifest": """
|
|
|
+- [
|
|
|
+- {
|
|
|
+- "size": 193383673,
|
|
|
+- "digest": "6609e8b95db59c6a3ad60fc3dcfc358b2c8ec8b4dda4c2780eb439e1c5dcc5d550f2e47ce56ba14309363070078d09b5287e372f6e95686110ff8a2ef1838221",
|
|
|
+- "algorithm": "sha512",
|
|
|
+- "filename": "android-sdk18_0.r18moz1.orig.tar.gz",
|
|
|
+- "unpack": "True"
|
|
|
+- }
|
|
|
+- ] """,
|
|
|
+- "emulator_process_name": "emulator64-arm",
|
|
|
+- "emulator_extra_args": "-show-kernel -debug init,console,gles,memcheck,adbserver,adbclient,adb,avd_config,socket",
|
|
|
+- "exes": {
|
|
|
+- 'adb': '%(abs_work_dir)s/android-sdk18/platform-tools/adb',
|
|
|
+- },
|
|
|
+- "env": {
|
|
|
+- "DISPLAY": ":0.0",
|
|
|
+- "PATH": "%(PATH)s:%(abs_work_dir)s/android-sdk-linux/tools:%(abs_work_dir)s/android-sdk18/platform-tools",
|
|
|
+- "MINIDUMP_SAVEPATH": "%(abs_work_dir)s/../minidumps"
|
|
|
+- },
|
|
|
+- "default_actions": [
|
|
|
+- 'clobber',
|
|
|
+- 'read-buildbot-config',
|
|
|
+- 'setup-avds',
|
|
|
+- 'start-emulator',
|
|
|
+- 'download-and-extract',
|
|
|
+- 'create-virtualenv',
|
|
|
+- 'verify-emulator',
|
|
|
+- 'install',
|
|
|
+- 'run-tests',
|
|
|
+- ],
|
|
|
+- "emulator": {
|
|
|
+- "name": "test-1",
|
|
|
+- "device_id": "emulator-5554",
|
|
|
+- "http_port": "8854", # starting http port to use for the mochitest server
|
|
|
+- "ssl_port": "4454", # starting ssl port to use for the server
|
|
|
+- "emulator_port": 5554,
|
|
|
+- },
|
|
|
+- "suite_definitions": {
|
|
|
+- "mochitest": {
|
|
|
+- "run_filename": "runtestsremote.py",
|
|
|
+- "testsdir": "mochitest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--certificate-path=%(certs_path)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--quiet",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- "--extra-profile-file=fonts",
|
|
|
+- "--extra-profile-file=hyphenation",
|
|
|
+- "--screenshot-on-fail",
|
|
|
+- "--total-chunks=20",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "mochitest-gl": {
|
|
|
+- "run_filename": "runtestsremote.py",
|
|
|
+- "testsdir": "mochitest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--certificate-path=%(certs_path)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--quiet",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- "--screenshot-on-fail",
|
|
|
+- "--total-chunks=10",
|
|
|
+- "--subsuite=webgl",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "mochitest-chrome": {
|
|
|
+- "run_filename": "runtestsremote.py",
|
|
|
+- "testsdir": "mochitest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--certificate-path=%(certs_path)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--quiet",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- "--extra-profile-file=fonts",
|
|
|
+- "--extra-profile-file=hyphenation",
|
|
|
+- "--screenshot-on-fail",
|
|
|
+- "--flavor=chrome",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "mochitest-plain-gpu": {
|
|
|
+- "run_filename": "runtestsremote.py",
|
|
|
+- "testsdir": "mochitest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--certificate-path=%(certs_path)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--quiet",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- "--screenshot-on-fail",
|
|
|
+- "--subsuite=gpu",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "mochitest-plain-clipboard": {
|
|
|
+- "run_filename": "runtestsremote.py",
|
|
|
+- "testsdir": "mochitest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--certificate-path=%(certs_path)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--quiet",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- "--screenshot-on-fail",
|
|
|
+- "--subsuite=clipboard",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "mochitest-media": {
|
|
|
+- "run_filename": "runtestsremote.py",
|
|
|
+- "testsdir": "mochitest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--certificate-path=%(certs_path)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--quiet",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- "--screenshot-on-fail",
|
|
|
+- "--chunk-by-runtime",
|
|
|
+- "--total-chunks=2",
|
|
|
+- "--subsuite=media",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "robocop": {
|
|
|
+- "run_filename": "runrobocop.py",
|
|
|
+- "testsdir": "mochitest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--certificate-path=%(certs_path)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--quiet",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- "--total-chunks=4",
|
|
|
+- "--robocop-apk=../../robocop.apk",
|
|
|
+- "--robocop-ini=robocop.ini",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "reftest": {
|
|
|
+- "run_filename": "remotereftest.py",
|
|
|
+- "testsdir": "reftest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--ignore-window-size",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--httpd-path", "%(modules_dir)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--total-chunks=16",
|
|
|
+- "--extra-profile-file=fonts",
|
|
|
+- "--extra-profile-file=hyphenation",
|
|
|
+- "--suite=reftest",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- ],
|
|
|
+- "tests": ["tests/layout/reftests/reftest.list",],
|
|
|
+- },
|
|
|
+- "reftest-debug": {
|
|
|
+- "run_filename": "remotereftest.py",
|
|
|
+- "testsdir": "reftest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--ignore-window-size",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--httpd-path", "%(modules_dir)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--total-chunks=48",
|
|
|
+- "--extra-profile-file=fonts",
|
|
|
+- "--extra-profile-file=hyphenation",
|
|
|
+- "tests/layout/reftests/reftest.list",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "crashtest": {
|
|
|
+- "run_filename": "remotereftest.py",
|
|
|
+- "testsdir": "reftest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--ignore-window-size",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--httpd-path",
|
|
|
+- "%(modules_dir)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--total-chunks=4",
|
|
|
+- "--suite=crashtest",
|
|
|
+- ],
|
|
|
+- "tests": ["tests/testing/crashtest/crashtests.list",],
|
|
|
+- },
|
|
|
+- "crashtest-debug": {
|
|
|
+- "run_filename": "remotereftest.py",
|
|
|
+- "testsdir": "reftest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--ignore-window-size",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--httpd-path",
|
|
|
+- "%(modules_dir)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--total-chunks=10",
|
|
|
+- "tests/testing/crashtest/crashtests.list",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "jsreftest": {
|
|
|
+- "run_filename": "remotereftest.py",
|
|
|
+- "testsdir": "reftest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--ignore-window-size",
|
|
|
+- "--remote-webserver=%(remote_webserver)s", "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s", "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s", "--httpd-path", "%(modules_dir)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--total-chunks=10",
|
|
|
+- "--extra-profile-file=jsreftest/tests/user.js",
|
|
|
+- "--suite=jstestbrowser",
|
|
|
+- ],
|
|
|
+- "tests": ["../jsreftest/tests/jstests.list",],
|
|
|
+- },
|
|
|
+- "jsreftest-debug": {
|
|
|
+- "run_filename": "remotereftest.py",
|
|
|
+- "testsdir": "reftest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--ignore-window-size",
|
|
|
+- "--remote-webserver=%(remote_webserver)s", "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s", "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s", "--httpd-path", "%(modules_dir)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "../jsreftest/tests/jstests.list",
|
|
|
+- "--total-chunks=35",
|
|
|
+- "--extra-profile-file=jsreftest/tests/user.js",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "xpcshell": {
|
|
|
+- "run_filename": "remotexpcshelltests.py",
|
|
|
+- "testsdir": "xpcshell",
|
|
|
+- "install": False,
|
|
|
+- "options": [
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--testing-modules-dir=%(modules_dir)s",
|
|
|
+- "--apk=%(installer_path)s",
|
|
|
+- "--no-logfiles",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--manifest=tests/xpcshell.ini",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- "--total-chunks=3",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "cppunittest": {
|
|
|
+- "run_filename": "remotecppunittests.py",
|
|
|
+- "testsdir": "cppunittest",
|
|
|
+- "install": False,
|
|
|
+- "options": [
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--localBinDir=../bin",
|
|
|
+- "--apk=%(installer_path)s",
|
|
|
+- ".",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "marionette": {
|
|
|
+- "run_filename": os.path.join("harness", "marionette_harness", "runtests.py"),
|
|
|
+- "testsdir": "marionette",
|
|
|
+- "options": [
|
|
|
+- "--emulator",
|
|
|
+- "--app=fennec",
|
|
|
+- "--package=%(app)s",
|
|
|
+- "--address=%(address)s",
|
|
|
+- "%(test_manifest)s",
|
|
|
+- "--disable-e10s",
|
|
|
+- "--gecko-log=%(gecko_log)s",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--startup-timeout=300",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "geckoview": {
|
|
|
+- "run_filename": "rungeckoview.py",
|
|
|
+- "testsdir": "mochitest",
|
|
|
+- "options": [
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+-
|
|
|
+- }, # end suite_definitions
|
|
|
+- "download_minidump_stackwalk": True,
|
|
|
+- "default_blob_upload_servers": [
|
|
|
+- "https://blobupload.elasticbeanstalk.com",
|
|
|
+- ],
|
|
|
+-}
|
|
|
+diff --git a/testing/mozharness/configs/android/androidx86.py b/testing/mozharness/configs/android/androidx86.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/mozharness/configs/android/androidx86.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,91 +0,0 @@
|
|
|
+-import os
|
|
|
+-
|
|
|
+-config = {
|
|
|
+- "buildbot_json_path": "buildprops.json",
|
|
|
+- "hostutils_manifest_path": "testing/config/tooltool-manifests/linux64/hostutils.manifest",
|
|
|
+- "tooltool_manifest_path": "testing/config/tooltool-manifests/androidx86/releng.manifest",
|
|
|
+- "tooltool_cache": "/builds/worker/tooltool_cache",
|
|
|
+- "download_tooltool": True,
|
|
|
+- "tooltool_servers": ['http://relengapi/tooltool/'],
|
|
|
+- "avds_dir": "/builds/worker/workspace/build/.android",
|
|
|
+- "emulator_manifest": """
|
|
|
+- [
|
|
|
+- {
|
|
|
+- "size": 193383673,
|
|
|
+- "digest": "6609e8b95db59c6a3ad60fc3dcfc358b2c8ec8b4dda4c2780eb439e1c5dcc5d550f2e47ce56ba14309363070078d09b5287e372f6e95686110ff8a2ef1838221",
|
|
|
+- "algorithm": "sha512",
|
|
|
+- "filename": "android-sdk18_0.r18moz1.orig.tar.gz",
|
|
|
+- "unpack": "True"
|
|
|
+- }
|
|
|
+- ] """,
|
|
|
+- "emulator_process_name": "emulator64-x86",
|
|
|
+- "emulator_extra_args": "-show-kernel -debug init,console,gles,memcheck,adbserver,adbclient,adb,avd_config,socket -qemu -m 1024",
|
|
|
+- "exes": {
|
|
|
+- 'adb': '%(abs_work_dir)s/android-sdk18/platform-tools/adb',
|
|
|
+- },
|
|
|
+- "env": {
|
|
|
+- "DISPLAY": ":0.0",
|
|
|
+- "PATH": "%(PATH)s:%(abs_work_dir)s/android-sdk18/tools:%(abs_work_dir)s/android-sdk18/platform-tools",
|
|
|
+- "MINIDUMP_SAVEPATH": "%(abs_work_dir)s/../minidumps"
|
|
|
+- },
|
|
|
+- "default_actions": [
|
|
|
+- 'clobber',
|
|
|
+- 'read-buildbot-config',
|
|
|
+- 'setup-avds',
|
|
|
+- 'start-emulator',
|
|
|
+- 'download-and-extract',
|
|
|
+- 'create-virtualenv',
|
|
|
+- 'verify-emulator',
|
|
|
+- 'install',
|
|
|
+- 'run-tests',
|
|
|
+- ],
|
|
|
+- "emulator": {
|
|
|
+- "name": "test-1",
|
|
|
+- "device_id": "emulator-5554",
|
|
|
+- "http_port": "8854", # starting http port to use for the mochitest server
|
|
|
+- "ssl_port": "4454", # starting ssl port to use for the server
|
|
|
+- "emulator_port": 5554,
|
|
|
+- },
|
|
|
+- "suite_definitions": {
|
|
|
+- "xpcshell": {
|
|
|
+- "run_filename": "remotexpcshelltests.py",
|
|
|
+- "testsdir": "xpcshell",
|
|
|
+- "install": False,
|
|
|
+- "options": [
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--testing-modules-dir=%(modules_dir)s",
|
|
|
+- "--apk=%(installer_path)s",
|
|
|
+- "--no-logfiles",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--manifest=tests/xpcshell.ini",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- "mochitest-chrome": {
|
|
|
+- "run_filename": "runtestsremote.py",
|
|
|
+- "testsdir": "mochitest",
|
|
|
+- "options": [
|
|
|
+- "--app=%(app)s",
|
|
|
+- "--remote-webserver=%(remote_webserver)s",
|
|
|
+- "--xre-path=%(xre_path)s",
|
|
|
+- "--utility-path=%(utility_path)s",
|
|
|
+- "--http-port=%(http_port)s",
|
|
|
+- "--ssl-port=%(ssl_port)s",
|
|
|
+- "--certificate-path=%(certs_path)s",
|
|
|
+- "--symbols-path=%(symbols_path)s",
|
|
|
+- "--quiet",
|
|
|
+- "--log-raw=%(raw_log_file)s",
|
|
|
+- "--log-errorsummary=%(error_summary_file)s",
|
|
|
+- "--extra-profile-file=fonts",
|
|
|
+- "--extra-profile-file=hyphenation",
|
|
|
+- "--screenshot-on-fail",
|
|
|
+- "--flavor=chrome",
|
|
|
+- ],
|
|
|
+- },
|
|
|
+- }, # end suite_definitions
|
|
|
+- "download_minidump_stackwalk": True,
|
|
|
+- "default_blob_upload_servers": [
|
|
|
+- "https://blobupload.elasticbeanstalk.com",
|
|
|
+- ],
|
|
|
+-}
|
|
|
+diff --git a/testing/mozharness/mozharness/mozilla/mozbase.py b/testing/mozharness/mozharness/mozilla/mozbase.py
|
|
|
+--- a/testing/mozharness/mozharness/mozilla/mozbase.py
|
|
|
++++ b/testing/mozharness/mozharness/mozilla/mozbase.py
|
|
|
+@@ -27,13 +27,13 @@ class MozbaseMixin(object):
|
|
|
+ # in-tree requirements files propagate to all active trees.
|
|
|
+ mozbase_dir = os.path.join('tests', 'mozbase')
|
|
|
+ self.register_virtualenv_module(
|
|
|
+ 'manifestparser',
|
|
|
+ url=os.path.join(mozbase_dir, 'manifestdestiny')
|
|
|
+ )
|
|
|
+
|
|
|
+ for m in ('mozfile', 'mozlog', 'mozinfo', 'moznetwork', 'mozhttpd',
|
|
|
+- 'mozcrash', 'mozinstall', 'mozdevice', 'mozprofile',
|
|
|
+- 'mozprocess', 'mozrunner'):
|
|
|
++ 'mozcrash', 'mozinstall', 'mozprofile', 'mozprocess',
|
|
|
++ 'mozrunner'):
|
|
|
+ self.register_virtualenv_module(
|
|
|
+ m, url=os.path.join(mozbase_dir, m)
|
|
|
+ )
|
|
|
+diff --git a/testing/mozharness/scripts/marionette.py b/testing/mozharness/scripts/marionette.py
|
|
|
+--- a/testing/mozharness/scripts/marionette.py
|
|
|
++++ b/testing/mozharness/scripts/marionette.py
|
|
|
+@@ -206,18 +206,18 @@ class MarionetteTest(TestingMixin, Mercu
|
|
|
+ # XXX Bug 879765: Dependent modules need to be listed before parent
|
|
|
+ # modules, otherwise they will get installed from the pypi server.
|
|
|
+ # XXX Bug 908356: This block can be removed as soon as the
|
|
|
+ # in-tree requirements files propagate to all active trees.
|
|
|
+ mozbase_dir = os.path.join('tests', 'mozbase')
|
|
|
+ self.register_virtualenv_module(
|
|
|
+ 'manifestparser', os.path.join(mozbase_dir, 'manifestdestiny'))
|
|
|
+ for m in ('mozfile', 'mozlog', 'mozinfo', 'moznetwork', 'mozhttpd',
|
|
|
+- 'mozcrash', 'mozinstall', 'mozdevice', 'mozprofile',
|
|
|
+- 'mozprocess', 'mozrunner'):
|
|
|
++ 'mozcrash', 'mozinstall', 'mozprofile', 'mozprocess',
|
|
|
++ 'mozrunner'):
|
|
|
+ self.register_virtualenv_module(
|
|
|
+ m, os.path.join(mozbase_dir, m))
|
|
|
+
|
|
|
+ self.register_virtualenv_module(
|
|
|
+ 'marionette', os.path.join('tests', 'marionette'))
|
|
|
+
|
|
|
+ def _get_test_suite(self, is_emulator):
|
|
|
+ """
|
|
|
+diff --git a/testing/mozharness/test/pip-freeze.example.txt b/testing/mozharness/test/pip-freeze.example.txt
|
|
|
+--- a/testing/mozharness/test/pip-freeze.example.txt
|
|
|
++++ b/testing/mozharness/test/pip-freeze.example.txt
|
|
|
+@@ -2,17 +2,16 @@ MakeItSo==0.2.6
|
|
|
+ PyYAML==3.10
|
|
|
+ Tempita==0.5.1
|
|
|
+ WebOb==1.2b3
|
|
|
+ -e hg+http://k0s.org/mozilla/hg/configuration@35416ad140982c11eba0a2d6b96d683f53429e94#egg=configuration-dev
|
|
|
+ coverage==3.5.1
|
|
|
+ -e hg+http://k0s.org/mozilla/hg/jetperf@4645ae34d2c41a353dcdbd856b486b6d3faabb99#egg=jetperf-dev
|
|
|
+ logilab-astng==0.23.1
|
|
|
+ logilab-common==0.57.1
|
|
|
+-mozdevice==0.2
|
|
|
+ -e hg+https://hg.mozilla.org/build/mozharness@df6b7f1e14d8c472125ef7a77b8a3b40c96ae181#egg=mozharness-jetperf
|
|
|
+ mozhttpd==0.3
|
|
|
+ mozinfo==0.3.3
|
|
|
+ nose==1.1.2
|
|
|
+ pyflakes==0.5.0
|
|
|
+ pylint==0.25.1
|
|
|
+ -e hg+https://hg.mozilla.org/build/talos@ee5c0b090d808e81a8fc5ba5f96b012797b3e785#egg=talos-dev
|
|
|
+ virtualenv==1.7.1.2
|
|
|
+diff --git a/testing/mozharness/test/test_base_python.py b/testing/mozharness/test/test_base_python.py
|
|
|
+--- a/testing/mozharness/test/test_base_python.py
|
|
|
++++ b/testing/mozharness/test/test_base_python.py
|
|
|
+@@ -16,17 +16,16 @@ class TestVirtualenvMixin(unittest.TestC
|
|
|
+ # from the file
|
|
|
+ expected = {'MakeItSo': '0.2.6',
|
|
|
+ 'PyYAML': '3.10',
|
|
|
+ 'Tempita': '0.5.1',
|
|
|
+ 'WebOb': '1.2b3',
|
|
|
+ 'coverage': '3.5.1',
|
|
|
+ 'logilab-astng': '0.23.1',
|
|
|
+ 'logilab-common': '0.57.1',
|
|
|
+- 'mozdevice': '0.2',
|
|
|
+ 'mozhttpd': '0.3',
|
|
|
+ 'mozinfo': '0.3.3',
|
|
|
+ 'nose': '1.1.2',
|
|
|
+ 'pyflakes': '0.5.0',
|
|
|
+ 'pylint': '0.25.1',
|
|
|
+ 'virtualenv': '1.7.1.2',
|
|
|
+ 'wsgiref': '0.1.2'}
|
|
|
+
|
|
|
+diff --git a/testing/remotecppunittests.py b/testing/remotecppunittests.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/remotecppunittests.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,295 +0,0 @@
|
|
|
+-#!/usr/bin/env python
|
|
|
+-#
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
+-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import, print_function
|
|
|
+-import os
|
|
|
+-import sys
|
|
|
+-import subprocess
|
|
|
+-from zipfile import ZipFile
|
|
|
+-import runcppunittests as cppunittests
|
|
|
+-import mozcrash
|
|
|
+-import mozfile
|
|
|
+-import mozinfo
|
|
|
+-import mozlog
|
|
|
+-import StringIO
|
|
|
+-import posixpath
|
|
|
+-from mozdevice import devicemanagerADB
|
|
|
+-
|
|
|
+-try:
|
|
|
+- from mozbuild.base import MozbuildObject
|
|
|
+- build_obj = MozbuildObject.from_environment()
|
|
|
+-except ImportError:
|
|
|
+- build_obj = None
|
|
|
+-
|
|
|
+-
|
|
|
+-class RemoteCPPUnitTests(cppunittests.CPPUnitTests):
|
|
|
+-
|
|
|
+- def __init__(self, devmgr, options, progs):
|
|
|
+- cppunittests.CPPUnitTests.__init__(self)
|
|
|
+- self.options = options
|
|
|
+- self.device = devmgr
|
|
|
+- self.remote_test_root = self.device.deviceRoot + "/cppunittests"
|
|
|
+- self.remote_bin_dir = posixpath.join(self.remote_test_root, "b")
|
|
|
+- self.remote_tmp_dir = posixpath.join(self.remote_test_root, "tmp")
|
|
|
+- self.remote_home_dir = posixpath.join(self.remote_test_root, "h")
|
|
|
+- if options.setup:
|
|
|
+- self.setup_bin(progs)
|
|
|
+-
|
|
|
+- def setup_bin(self, progs):
|
|
|
+- if not self.device.dirExists(self.remote_test_root):
|
|
|
+- self.device.mkDir(self.remote_test_root)
|
|
|
+- if self.device.dirExists(self.remote_tmp_dir):
|
|
|
+- self.device.removeDir(self.remote_tmp_dir)
|
|
|
+- self.device.mkDir(self.remote_tmp_dir)
|
|
|
+- if self.device.dirExists(self.remote_bin_dir):
|
|
|
+- self.device.removeDir(self.remote_bin_dir)
|
|
|
+- self.device.mkDir(self.remote_bin_dir)
|
|
|
+- if self.device.dirExists(self.remote_home_dir):
|
|
|
+- self.device.removeDir(self.remote_home_dir)
|
|
|
+- self.device.mkDir(self.remote_home_dir)
|
|
|
+- self.push_libs()
|
|
|
+- self.push_progs(progs)
|
|
|
+- self.device.chmodDir(self.remote_bin_dir)
|
|
|
+-
|
|
|
+- def push_libs(self):
|
|
|
+- if self.options.local_apk:
|
|
|
+- with mozfile.TemporaryDirectory() as tmpdir:
|
|
|
+- apk_contents = ZipFile(self.options.local_apk)
|
|
|
+-
|
|
|
+- for info in apk_contents.infolist():
|
|
|
+- if info.filename.endswith(".so"):
|
|
|
+- print("Pushing %s.." % info.filename, file=sys.stderr)
|
|
|
+- remote_file = posixpath.join(
|
|
|
+- self.remote_bin_dir, os.path.basename(info.filename))
|
|
|
+- apk_contents.extract(info, tmpdir)
|
|
|
+- local_file = os.path.join(tmpdir, info.filename)
|
|
|
+- with open(local_file) as f:
|
|
|
+- # Decompress xz-compressed file.
|
|
|
+- if f.read(5)[1:] == '7zXZ':
|
|
|
+- cmd = [
|
|
|
+- 'xz', '-df', '--suffix', '.so', local_file]
|
|
|
+- subprocess.check_output(cmd)
|
|
|
+- # xz strips the ".so" file suffix.
|
|
|
+- os.rename(local_file[:-3], local_file)
|
|
|
+- self.device.pushFile(local_file, remote_file)
|
|
|
+-
|
|
|
+- elif self.options.local_lib:
|
|
|
+- for path in os.listdir(self.options.local_lib):
|
|
|
+- if path.endswith(".so"):
|
|
|
+- print("Pushing {}..".format(path), file=sys.stderr)
|
|
|
+- remote_file = posixpath.join(self.remote_bin_dir, path)
|
|
|
+- local_file = os.path.join(self.options.local_lib, path)
|
|
|
+- self.device.pushFile(local_file, remote_file)
|
|
|
+- # Additional libraries may be found in a sub-directory such as
|
|
|
+- # "lib/armeabi-v7a"
|
|
|
+- for subdir in ["assets", "lib"]:
|
|
|
+- local_arm_lib = os.path.join(self.options.local_lib, subdir)
|
|
|
+- if os.path.isdir(local_arm_lib):
|
|
|
+- for root, dirs, paths in os.walk(local_arm_lib):
|
|
|
+- for path in paths:
|
|
|
+- if path.endswith(".so"):
|
|
|
+- print("Pushing {}..".format(path), file=sys.stderr)
|
|
|
+- remote_file = posixpath.join(
|
|
|
+- self.remote_bin_dir, path)
|
|
|
+- local_file = os.path.join(root, path)
|
|
|
+- self.device.pushFile(local_file, remote_file)
|
|
|
+-
|
|
|
+- def push_progs(self, progs):
|
|
|
+- for local_file in progs:
|
|
|
+- remote_file = posixpath.join(
|
|
|
+- self.remote_bin_dir, os.path.basename(local_file))
|
|
|
+- self.device.pushFile(local_file, remote_file)
|
|
|
+-
|
|
|
+- def build_environment(self):
|
|
|
+- env = self.build_core_environment()
|
|
|
+- env['LD_LIBRARY_PATH'] = self.remote_bin_dir
|
|
|
+- env["TMPDIR"] = self.remote_tmp_dir
|
|
|
+- env["HOME"] = self.remote_home_dir
|
|
|
+- env["MOZ_XRE_DIR"] = self.remote_bin_dir
|
|
|
+- if self.options.add_env:
|
|
|
+- for envdef in self.options.add_env:
|
|
|
+- envdef_parts = envdef.split("=", 1)
|
|
|
+- if len(envdef_parts) == 2:
|
|
|
+- env[envdef_parts[0]] = envdef_parts[1]
|
|
|
+- elif len(envdef_parts) == 1:
|
|
|
+- env[envdef_parts[0]] = ""
|
|
|
+- else:
|
|
|
+- self.log.warning(
|
|
|
+- "invalid --addEnv option skipped: %s" % envdef)
|
|
|
+-
|
|
|
+- return env
|
|
|
+-
|
|
|
+- def run_one_test(self, prog, env, symbols_path=None, interactive=False,
|
|
|
+- timeout_factor=1):
|
|
|
+- """
|
|
|
+- Run a single C++ unit test program remotely.
|
|
|
+-
|
|
|
+- Arguments:
|
|
|
+- * prog: The path to the test program to run.
|
|
|
+- * env: The environment to use for running the program.
|
|
|
+- * symbols_path: A path to a directory containing Breakpad-formatted
|
|
|
+- symbol files for producing stack traces on crash.
|
|
|
+- * timeout_factor: An optional test-specific timeout multiplier.
|
|
|
+-
|
|
|
+- Return True if the program exits with a zero status, False otherwise.
|
|
|
+- """
|
|
|
+- basename = os.path.basename(prog)
|
|
|
+- remote_bin = posixpath.join(self.remote_bin_dir, basename)
|
|
|
+- self.log.test_start(basename)
|
|
|
+- buf = StringIO.StringIO()
|
|
|
+- test_timeout = cppunittests.CPPUnitTests.TEST_PROC_TIMEOUT * \
|
|
|
+- timeout_factor
|
|
|
+- returncode = self.device.shell(
|
|
|
+- [remote_bin], buf, env=env, cwd=self.remote_home_dir,
|
|
|
+- timeout=test_timeout)
|
|
|
+- self.log.process_output(basename, "\n%s" % buf.getvalue(),
|
|
|
+- command=[remote_bin])
|
|
|
+- with mozfile.TemporaryDirectory() as tempdir:
|
|
|
+- self.device.getDirectory(self.remote_home_dir, tempdir)
|
|
|
+- if mozcrash.check_for_crashes(tempdir, symbols_path,
|
|
|
+- test_name=basename):
|
|
|
+- self.log.test_end(basename, status='CRASH', expected='PASS')
|
|
|
+- return False
|
|
|
+- result = returncode == 0
|
|
|
+- if not result:
|
|
|
+- self.log.test_end(basename, status='FAIL', expected='PASS',
|
|
|
+- message=("test failed with return code %s" %
|
|
|
+- returncode))
|
|
|
+- else:
|
|
|
+- self.log.test_end(basename, status='PASS', expected='PASS')
|
|
|
+- return result
|
|
|
+-
|
|
|
+-
|
|
|
+-class RemoteCPPUnittestOptions(cppunittests.CPPUnittestOptions):
|
|
|
+-
|
|
|
+- def __init__(self):
|
|
|
+- cppunittests.CPPUnittestOptions.__init__(self)
|
|
|
+- defaults = {}
|
|
|
+-
|
|
|
+- self.add_option("--deviceIP", action="store",
|
|
|
+- type="string", dest="device_ip",
|
|
|
+- help="ip address of remote device to test")
|
|
|
+- defaults["device_ip"] = None
|
|
|
+-
|
|
|
+- self.add_option("--devicePort", action="store",
|
|
|
+- type="string", dest="device_port",
|
|
|
+- help="port of remote device to test")
|
|
|
+- defaults["device_port"] = 20701
|
|
|
+-
|
|
|
+- self.add_option("--noSetup", action="store_false",
|
|
|
+- dest="setup",
|
|
|
+- help="do not copy any files to device (to be used only if "
|
|
|
+- "device is already setup)")
|
|
|
+- defaults["setup"] = True
|
|
|
+-
|
|
|
+- self.add_option("--localLib", action="store",
|
|
|
+- type="string", dest="local_lib",
|
|
|
+- help="location of libraries to push -- preferably stripped")
|
|
|
+- defaults["local_lib"] = None
|
|
|
+-
|
|
|
+- self.add_option("--apk", action="store",
|
|
|
+- type="string", dest="local_apk",
|
|
|
+- help="local path to Fennec APK")
|
|
|
+- defaults["local_apk"] = None
|
|
|
+-
|
|
|
+- self.add_option("--localBinDir", action="store",
|
|
|
+- type="string", dest="local_bin",
|
|
|
+- help="local path to bin directory")
|
|
|
+- defaults[
|
|
|
+- "local_bin"] = build_obj.bindir if build_obj is not None else None
|
|
|
+-
|
|
|
+- self.add_option("--remoteTestRoot", action="store",
|
|
|
+- type="string", dest="remote_test_root",
|
|
|
+- help="remote directory to use as test root (eg. /data/local/tests)")
|
|
|
+- # /data/local/tests is used because it is usually not possible to set +x permissions
|
|
|
+- # on binaries on /mnt/sdcard
|
|
|
+- defaults["remote_test_root"] = "/data/local/tests"
|
|
|
+-
|
|
|
+- self.add_option("--with-b2g-emulator", action="store",
|
|
|
+- type="string", dest="with_b2g_emulator",
|
|
|
+- help="Start B2G Emulator (specify path to b2g home)")
|
|
|
+- self.add_option("--emulator", default="arm", choices=["x86", "arm"],
|
|
|
+- help="Architecture of emulator to use: x86 or arm")
|
|
|
+- self.add_option("--addEnv", action="append",
|
|
|
+- type="string", dest="add_env",
|
|
|
+- help="additional remote environment variable definitions "
|
|
|
+- "(eg. --addEnv \"somevar=something\")")
|
|
|
+- defaults["add_env"] = None
|
|
|
+-
|
|
|
+- self.set_defaults(**defaults)
|
|
|
+-
|
|
|
+-
|
|
|
+-def run_test_harness(options, args):
|
|
|
+- if options.with_b2g_emulator:
|
|
|
+- from mozrunner import B2GEmulatorRunner
|
|
|
+- runner = B2GEmulatorRunner(
|
|
|
+- arch=options.emulator, b2g_home=options.with_b2g_emulator)
|
|
|
+- runner.start()
|
|
|
+- # because we just started the emulator, we need more than the
|
|
|
+- # default number of retries here.
|
|
|
+- retryLimit = 50
|
|
|
+- else:
|
|
|
+- retryLimit = 5
|
|
|
+- try:
|
|
|
+- dm_args = {'deviceRoot': options.remote_test_root}
|
|
|
+- dm_args['retryLimit'] = retryLimit
|
|
|
+- if options.device_ip:
|
|
|
+- dm_args['host'] = options.device_ip
|
|
|
+- dm_args['port'] = options.device_port
|
|
|
+- if options.log_tbpl_level == 'debug' or options.log_mach_level == 'debug':
|
|
|
+- dm_args['logLevel'] = logging.DEBUG # noqa python 2 / 3
|
|
|
+- dm = devicemanagerADB.DeviceManagerADB(**dm_args)
|
|
|
+- except BaseException:
|
|
|
+- if options.with_b2g_emulator:
|
|
|
+- runner.cleanup()
|
|
|
+- runner.wait()
|
|
|
+- raise
|
|
|
+-
|
|
|
+- options.xre_path = os.path.abspath(options.xre_path)
|
|
|
+- cppunittests.update_mozinfo()
|
|
|
+- progs = cppunittests.extract_unittests_from_args(args,
|
|
|
+- mozinfo.info,
|
|
|
+- options.manifest_path)
|
|
|
+- tester = RemoteCPPUnitTests(dm, options, [item[0] for item in progs])
|
|
|
+- try:
|
|
|
+- result = tester.run_tests(
|
|
|
+- progs, options.xre_path, options.symbols_path)
|
|
|
+- finally:
|
|
|
+- if options.with_b2g_emulator:
|
|
|
+- runner.cleanup()
|
|
|
+- runner.wait()
|
|
|
+- return result
|
|
|
+-
|
|
|
+-
|
|
|
+-def main():
|
|
|
+- parser = RemoteCPPUnittestOptions()
|
|
|
+- mozlog.commandline.add_logging_group(parser)
|
|
|
+- options, args = parser.parse_args()
|
|
|
+- if not args:
|
|
|
+- print("""Usage: %s <test binary> [<test binary>...]""" % sys.argv[0], file=sys.stderr)
|
|
|
+- sys.exit(1)
|
|
|
+- if options.local_lib is not None and not os.path.isdir(options.local_lib):
|
|
|
+- print("""Error: --localLib directory %s not found""" % options.local_lib, file=sys.stderr)
|
|
|
+- sys.exit(1)
|
|
|
+- if options.local_apk is not None and not os.path.isfile(options.local_apk):
|
|
|
+- print("""Error: --apk file %s not found""" % options.local_apk, file=sys.stderr)
|
|
|
+- sys.exit(1)
|
|
|
+- if not options.xre_path:
|
|
|
+- print("""Error: --xre-path is required""", file=sys.stderr)
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- log = mozlog.commandline.setup_logging("remotecppunittests", options,
|
|
|
+- {"tbpl": sys.stdout})
|
|
|
+- try:
|
|
|
+- result = run_test_harness(options, args)
|
|
|
+- except Exception as e:
|
|
|
+- log.error(str(e))
|
|
|
+- result = False
|
|
|
+- sys.exit(0 if result else 1)
|
|
|
+-
|
|
|
+-
|
|
|
+-if __name__ == '__main__':
|
|
|
+- main()
|
|
|
+diff --git a/testing/testsuite-targets.mk b/testing/testsuite-targets.mk
|
|
|
+--- a/testing/testsuite-targets.mk
|
|
|
++++ b/testing/testsuite-targets.mk
|
|
|
+@@ -26,76 +26,44 @@ CHECK_TEST_ERROR = $(call check_test_err
|
|
|
+ CHECK_TEST_ERROR_RERUN = $(call check_test_error_internal,'To rerun your failures please run "make $@-rerun-failures"')
|
|
|
+ endif
|
|
|
+
|
|
|
+ # Usage: |make [EXTRA_TEST_ARGS=...] *test|.
|
|
|
+ RUN_REFTEST = rm -f ./$@.log && $(PYTHON3) _tests/reftest/runreftest.py \
|
|
|
+ --extra-profile-file=$(DIST)/plugins \
|
|
|
+ $(SYMBOLS_PATH) $(EXTRA_TEST_ARGS) $(1) | tee ./$@.log
|
|
|
+
|
|
|
+-REMOTE_REFTEST = rm -f ./$@.log && $(PYTHON3) _tests/reftest/remotereftest.py \
|
|
|
+- --ignore-window-size \
|
|
|
+- --app=$(TEST_PACKAGE_NAME) --deviceIP=${TEST_DEVICE} --xre-path=${MOZ_HOST_BIN} \
|
|
|
+- --httpd-path=_tests/modules --suite reftest \
|
|
|
+- --extra-profile-file=$(topsrcdir)/mobile/android/fonts \
|
|
|
+- $(SYMBOLS_PATH) $(EXTRA_TEST_ARGS) $(1) | tee ./$@.log
|
|
|
+-
|
|
|
+ ifeq ($(OS_ARCH),WINNT) #{
|
|
|
+ # GPU-rendered shadow layers are unsupported here
|
|
|
+ OOP_CONTENT = --setpref=layers.async-pan-zoom.enabled=true --setpref=browser.tabs.remote.autostart=true --setpref=layers.acceleration.disabled=true
|
|
|
+ GPU_RENDERING =
|
|
|
+ else
|
|
|
+ OOP_CONTENT = --setpref=layers.async-pan-zoom.enabled=true --setpref=browser.tabs.remote.autostart=true
|
|
|
+ GPU_RENDERING = --setpref=layers.acceleration.force-enabled=true
|
|
|
+ endif #}
|
|
|
+
|
|
|
+ reftest: TEST_PATH?=layout/reftests/reftest.list
|
|
|
+ reftest:
|
|
|
+ $(call RUN_REFTEST,'$(topsrcdir)/$(TEST_PATH)')
|
|
|
+ $(CHECK_TEST_ERROR)
|
|
|
+
|
|
|
+-reftest-remote: TEST_PATH?=layout/reftests/reftest.list
|
|
|
+-reftest-remote:
|
|
|
+- @if [ '${MOZ_HOST_BIN}' = '' ]; then \
|
|
|
+- echo 'environment variable MOZ_HOST_BIN must be set to a directory containing host xpcshell'; \
|
|
|
+- elif [ ! -d ${MOZ_HOST_BIN} ]; then \
|
|
|
+- echo 'MOZ_HOST_BIN does not specify a directory'; \
|
|
|
+- elif [ ! -f ${MOZ_HOST_BIN}/xpcshell ]; then \
|
|
|
+- echo 'xpcshell not found in MOZ_HOST_BIN'; \
|
|
|
+- else \
|
|
|
+- ln -s $(abspath $(topsrcdir)) _tests/reftest/tests; \
|
|
|
+- $(call REMOTE_REFTEST,'tests/$(TEST_PATH)'); \
|
|
|
+- $(CHECK_TEST_ERROR); \
|
|
|
+- fi
|
|
|
+-
|
|
|
+ crashtest: TEST_PATH?=testing/crashtest/crashtests.list
|
|
|
+ crashtest:
|
|
|
+ $(call RUN_REFTEST,'$(topsrcdir)/$(TEST_PATH)')
|
|
|
+ $(CHECK_TEST_ERROR)
|
|
|
+
|
|
|
+ jstestbrowser: TESTS_PATH?=test-stage/jsreftest/tests/
|
|
|
+ jstestbrowser:
|
|
|
+ $(MAKE) -C $(DEPTH)/config
|
|
|
+ $(MAKE) stage-jstests
|
|
|
+ $(call RUN_REFTEST,'$(DIST)/$(TESTS_PATH)/jstests.list' --extra-profile-file=$(DIST)/test-stage/jsreftest/tests/user.js)
|
|
|
+ $(CHECK_TEST_ERROR)
|
|
|
+
|
|
|
+ GARBAGE += $(addsuffix .log,$(MOCHITESTS) reftest crashtest jstestbrowser)
|
|
|
+
|
|
|
+-REMOTE_CPPUNITTESTS = \
|
|
|
+- $(PYTHON3) -u $(topsrcdir)/testing/remotecppunittests.py \
|
|
|
+- --xre-path=$(DEPTH)/dist/bin \
|
|
|
+- --localLib=$(DEPTH)/dist/fennec \
|
|
|
+- --deviceIP=${TEST_DEVICE} \
|
|
|
+- $(TEST_PATH) $(EXTRA_TEST_ARGS)
|
|
|
+-
|
|
|
+-# Usage: |make [TEST_PATH=...] [EXTRA_TEST_ARGS=...] cppunittests-remote|.
|
|
|
+-cppunittests-remote:
|
|
|
+- $(call REMOTE_CPPUNITTESTS);
|
|
|
+-
|
|
|
+ jetpack-tests:
|
|
|
+ cd $(topsrcdir)/addon-sdk/source && $(PYTHON) bin/cfx -b $(abspath $(browser_path)) --parseable testpkgs
|
|
|
+
|
|
|
+ # Package up the tests and test harnesses
|
|
|
+ include $(topsrcdir)/toolkit/mozapps/installer/package-name.mk
|
|
|
+
|
|
|
+ PKG_STAGE = $(DIST)/test-stage
|
|
|
+
|
|
|
+diff --git a/testing/tools/mach_test_package_bootstrap.py b/testing/tools/mach_test_package_bootstrap.py
|
|
|
+--- a/testing/tools/mach_test_package_bootstrap.py
|
|
|
++++ b/testing/tools/mach_test_package_bootstrap.py
|
|
|
+@@ -13,17 +13,16 @@ import types
|
|
|
+
|
|
|
+ SEARCH_PATHS = [
|
|
|
+ 'marionette/client',
|
|
|
+ 'marionette/harness',
|
|
|
+ 'mochitest',
|
|
|
+ 'mozbase/manifestparser',
|
|
|
+ 'mozbase/mozcrash',
|
|
|
+ 'mozbase/mozdebug',
|
|
|
+- 'mozbase/mozdevice',
|
|
|
+ 'mozbase/mozfile',
|
|
|
+ 'mozbase/mozhttpd',
|
|
|
+ 'mozbase/mozinfo',
|
|
|
+ 'mozbase/mozinstall',
|
|
|
+ 'mozbase/mozleak',
|
|
|
+ 'mozbase/mozlog',
|
|
|
+ 'mozbase/moznetwork',
|
|
|
+ 'mozbase/mozprocess',
|
|
|
+diff --git a/testing/xpcshell/mach_commands.py b/testing/xpcshell/mach_commands.py
|
|
|
+--- a/testing/xpcshell/mach_commands.py
|
|
|
++++ b/testing/xpcshell/mach_commands.py
|
|
|
+@@ -135,94 +135,18 @@ class XPCShellRunner(MozbuildObject):
|
|
|
+ self.log_manager.disable_unstructured()
|
|
|
+
|
|
|
+ if not result and not xpcshell.sequential:
|
|
|
+ print("Tests were run in parallel. Try running with --sequential "
|
|
|
+ "to make sure the failures were not caused by this.")
|
|
|
+ return int(not result)
|
|
|
+
|
|
|
+
|
|
|
+-class AndroidXPCShellRunner(MozbuildObject):
|
|
|
+- """Get specified DeviceManager"""
|
|
|
+- def get_devicemanager(self, ip, port, remote_test_root):
|
|
|
+- import mozdevice
|
|
|
+- dm = None
|
|
|
+- if ip:
|
|
|
+- dm = mozdevice.DroidADB(ip, port, packageName=None, deviceRoot=remote_test_root)
|
|
|
+- else:
|
|
|
+- dm = mozdevice.DroidADB(packageName=None, deviceRoot=remote_test_root)
|
|
|
+- return dm
|
|
|
+-
|
|
|
+- """Run Android xpcshell tests."""
|
|
|
+- def run_test(self, **kwargs):
|
|
|
+- # TODO Bug 794506 remove once mach integrates with virtualenv.
|
|
|
+- build_path = os.path.join(self.topobjdir, 'build')
|
|
|
+- if build_path not in sys.path:
|
|
|
+- sys.path.append(build_path)
|
|
|
+-
|
|
|
+- import remotexpcshelltests
|
|
|
+-
|
|
|
+- dm = self.get_devicemanager(kwargs["deviceIP"], kwargs["devicePort"],
|
|
|
+- kwargs["remoteTestRoot"])
|
|
|
+-
|
|
|
+- log = kwargs.pop("log")
|
|
|
+- self.log_manager.enable_unstructured()
|
|
|
+-
|
|
|
+- if kwargs["xpcshell"] is None:
|
|
|
+- kwargs["xpcshell"] = "xpcshell"
|
|
|
+-
|
|
|
+- if not kwargs["objdir"]:
|
|
|
+- kwargs["objdir"] = self.topobjdir
|
|
|
+-
|
|
|
+- if not kwargs["localLib"]:
|
|
|
+- kwargs["localLib"] = os.path.join(self.topobjdir, 'dist/fennec')
|
|
|
+-
|
|
|
+- if not kwargs["localBin"]:
|
|
|
+- kwargs["localBin"] = os.path.join(self.topobjdir, 'dist/bin')
|
|
|
+-
|
|
|
+- if not kwargs["testingModulesDir"]:
|
|
|
+- kwargs["testingModulesDir"] = os.path.join(self.topobjdir, '_tests/modules')
|
|
|
+-
|
|
|
+- if not kwargs["mozInfo"]:
|
|
|
+- kwargs["mozInfo"] = os.path.join(self.topobjdir, 'mozinfo.json')
|
|
|
+-
|
|
|
+- if not kwargs["manifest"]:
|
|
|
+- kwargs["manifest"] = os.path.join(self.topobjdir, '_tests/xpcshell/xpcshell.ini')
|
|
|
+-
|
|
|
+- if not kwargs["symbolsPath"]:
|
|
|
+- kwargs["symbolsPath"] = os.path.join(self.distdir, 'crashreporter-symbols')
|
|
|
+-
|
|
|
+- if not kwargs["localAPK"]:
|
|
|
+- for file_name in os.listdir(os.path.join(kwargs["objdir"], "dist")):
|
|
|
+- if file_name.endswith(".apk") and file_name.startswith("fennec"):
|
|
|
+- kwargs["localAPK"] = os.path.join(kwargs["objdir"], "dist", file_name)
|
|
|
+- print ("using APK: %s" % kwargs["localAPK"])
|
|
|
+- break
|
|
|
+- else:
|
|
|
+- raise Exception("APK not found in objdir. You must specify an APK.")
|
|
|
+-
|
|
|
+- if not kwargs["sequential"]:
|
|
|
+- kwargs["sequential"] = True
|
|
|
+-
|
|
|
+- xpcshell = remotexpcshelltests.XPCShellRemote(dm, kwargs, log)
|
|
|
+-
|
|
|
+- result = xpcshell.runTests(kwargs, testClass=remotexpcshelltests.RemoteXPCShellTestThread,
|
|
|
+- mobileArgs=xpcshell.mobileArgs)
|
|
|
+-
|
|
|
+- self.log_manager.disable_unstructured()
|
|
|
+-
|
|
|
+- return int(not result)
|
|
|
+-
|
|
|
+-
|
|
|
+ def get_parser():
|
|
|
+- build_obj = MozbuildObject.from_environment(cwd=here)
|
|
|
+- if conditions.is_android(build_obj):
|
|
|
+- return parser_remote()
|
|
|
+- else:
|
|
|
+- return parser_desktop()
|
|
|
++ return parser_desktop()
|
|
|
+
|
|
|
+
|
|
|
+ @CommandProvider
|
|
|
+ class MachCommands(MachCommandBase):
|
|
|
+ @Command('xpcshell-test', category='testing',
|
|
|
+ description='Run XPCOM Shell tests (API direct unit testing)',
|
|
|
+ conditions=[lambda *args: True],
|
|
|
+ parser=get_parser)
|
|
|
+@@ -247,21 +171,16 @@ class MachCommands(MachCommandBase):
|
|
|
+ params['log'] = structured.commandline.setup_logging("XPCShellTests",
|
|
|
+ params,
|
|
|
+ {"mach": sys.stdout},
|
|
|
+ {"verbose": True})
|
|
|
+
|
|
|
+ if not params['threadCount']:
|
|
|
+ params['threadCount'] = int((cpu_count() * 3) / 2)
|
|
|
+
|
|
|
+- if conditions.is_android(self):
|
|
|
+- from mozrunner.devices.android_device import verify_android_device
|
|
|
+- verify_android_device(self)
|
|
|
+- xpcshell = self._spawn(AndroidXPCShellRunner)
|
|
|
+- else:
|
|
|
+- xpcshell = self._spawn(XPCShellRunner)
|
|
|
++ xpcshell = self._spawn(XPCShellRunner)
|
|
|
+ xpcshell.cwd = self._mach_context.cwd
|
|
|
+
|
|
|
+ try:
|
|
|
+ return xpcshell.run_test(**params)
|
|
|
+ except InvalidTestPathError as e:
|
|
|
+ print(e.message)
|
|
|
+ return 1
|
|
|
+diff --git a/testing/xpcshell/remotexpcshelltests.py b/testing/xpcshell/remotexpcshelltests.py
|
|
|
+deleted file mode 100644
|
|
|
+--- a/testing/xpcshell/remotexpcshelltests.py
|
|
|
++++ /dev/null
|
|
|
+@@ -1,618 +0,0 @@
|
|
|
+-#!/usr/bin/env python
|
|
|
+-#
|
|
|
+-# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
+-# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
+-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
+-
|
|
|
+-from __future__ import absolute_import, print_function
|
|
|
+-
|
|
|
+-import logging
|
|
|
+-import posixpath
|
|
|
+-import os
|
|
|
+-import sys
|
|
|
+-import subprocess
|
|
|
+-import runxpcshelltests as xpcshell
|
|
|
+-import tempfile
|
|
|
+-import time
|
|
|
+-from argparse import Namespace
|
|
|
+-from zipfile import ZipFile
|
|
|
+-from mozlog import commandline
|
|
|
+-import shutil
|
|
|
+-import mozdevice
|
|
|
+-import mozfile
|
|
|
+-import mozinfo
|
|
|
+-
|
|
|
+-from xpcshellcommandline import parser_remote
|
|
|
+-
|
|
|
+-here = os.path.dirname(os.path.abspath(__file__))
|
|
|
+-
|
|
|
+-
|
|
|
+-def remoteJoin(path1, path2):
|
|
|
+- return posixpath.join(path1, path2)
|
|
|
+-
|
|
|
+-
|
|
|
+-class RemoteXPCShellTestThread(xpcshell.XPCShellTestThread):
|
|
|
+- def __init__(self, *args, **kwargs):
|
|
|
+- xpcshell.XPCShellTestThread.__init__(self, *args, **kwargs)
|
|
|
+-
|
|
|
+- self.shellReturnCode = None
|
|
|
+- # embed the mobile params from the harness into the TestThread
|
|
|
+- mobileArgs = kwargs.get('mobileArgs')
|
|
|
+- for key in mobileArgs:
|
|
|
+- setattr(self, key, mobileArgs[key])
|
|
|
+-
|
|
|
+- def buildCmdTestFile(self, name):
|
|
|
+- remoteDir = self.remoteForLocal(os.path.dirname(name))
|
|
|
+- if remoteDir == self.remoteHere:
|
|
|
+- remoteName = os.path.basename(name)
|
|
|
+- else:
|
|
|
+- remoteName = remoteJoin(remoteDir, os.path.basename(name))
|
|
|
+- return ['-e', 'const _TEST_FILE = ["%s"];' %
|
|
|
+- remoteName.replace('\\', '/')]
|
|
|
+-
|
|
|
+- def remoteForLocal(self, local):
|
|
|
+- for mapping in self.pathMapping:
|
|
|
+- if (os.path.abspath(mapping.local) == os.path.abspath(local)):
|
|
|
+- return mapping.remote
|
|
|
+- return local
|
|
|
+-
|
|
|
+- def setupTempDir(self):
|
|
|
+- # make sure the temp dir exists
|
|
|
+- self.clearRemoteDir(self.remoteTmpDir)
|
|
|
+- # env var is set in buildEnvironment
|
|
|
+- return self.remoteTmpDir
|
|
|
+-
|
|
|
+- def setupProfileDir(self):
|
|
|
+- self.clearRemoteDir(self.profileDir)
|
|
|
+- if self.interactive or self.singleFile:
|
|
|
+- self.log.info("profile dir is %s" % self.profileDir)
|
|
|
+- return self.profileDir
|
|
|
+-
|
|
|
+- def setupMozinfoJS(self):
|
|
|
+- local = tempfile.mktemp()
|
|
|
+- mozinfo.output_to_file(local)
|
|
|
+- mozInfoJSPath = remoteJoin(self.profileDir, "mozinfo.json")
|
|
|
+- self.device.pushFile(local, mozInfoJSPath)
|
|
|
+- os.remove(local)
|
|
|
+- return mozInfoJSPath
|
|
|
+-
|
|
|
+- def logCommand(self, name, completeCmd, testdir):
|
|
|
+- self.log.info("%s | full command: %r" % (name, completeCmd))
|
|
|
+- self.log.info("%s | current directory: %r" % (name, self.remoteHere))
|
|
|
+- self.log.info("%s | environment: %s" % (name, self.env))
|
|
|
+-
|
|
|
+- def getHeadFiles(self, test):
|
|
|
+- """Override parent method to find files on remote device.
|
|
|
+-
|
|
|
+- Obtains lists of head- files. Returns a list of head files.
|
|
|
+- """
|
|
|
+- def sanitize_list(s, kind):
|
|
|
+- for f in s.strip().split(' '):
|
|
|
+- f = f.strip()
|
|
|
+- if len(f) < 1:
|
|
|
+- continue
|
|
|
+-
|
|
|
+- path = remoteJoin(self.remoteHere, f)
|
|
|
+-
|
|
|
+- # skip check for file existence: the convenience of discovering
|
|
|
+- # a missing file does not justify the time cost of the round trip
|
|
|
+- # to the device
|
|
|
+- yield path
|
|
|
+-
|
|
|
+- self.remoteHere = self.remoteForLocal(test['here'])
|
|
|
+-
|
|
|
+- headlist = test.get('head', '')
|
|
|
+- return list(sanitize_list(headlist, 'head'))
|
|
|
+-
|
|
|
+- def buildXpcsCmd(self):
|
|
|
+- # change base class' paths to remote paths and use base class to build command
|
|
|
+- self.xpcshell = remoteJoin(self.remoteBinDir, "xpcw")
|
|
|
+- self.headJSPath = remoteJoin(self.remoteScriptsDir, 'head.js')
|
|
|
+- self.httpdJSPath = remoteJoin(self.remoteComponentsDir, 'httpd.js')
|
|
|
+- self.httpdManifest = remoteJoin(self.remoteComponentsDir, 'httpd.manifest')
|
|
|
+- self.testingModulesDir = self.remoteModulesDir
|
|
|
+- self.testharnessdir = self.remoteScriptsDir
|
|
|
+- xpcshell.XPCShellTestThread.buildXpcsCmd(self)
|
|
|
+- # remove "-g <dir> -a <dir>" and add "--greomni <apk>"
|
|
|
+- del(self.xpcsCmd[1:5])
|
|
|
+- if self.options['localAPK']:
|
|
|
+- self.xpcsCmd.insert(3, '--greomni')
|
|
|
+- self.xpcsCmd.insert(4, self.remoteAPK)
|
|
|
+-
|
|
|
+- if self.remoteDebugger:
|
|
|
+- # for example, "/data/local/gdbserver" "localhost:12345"
|
|
|
+- self.xpcsCmd = [
|
|
|
+- self.remoteDebugger,
|
|
|
+- self.remoteDebuggerArgs,
|
|
|
+- self.xpcsCmd]
|
|
|
+-
|
|
|
+- def killTimeout(self, proc):
|
|
|
+- self.kill(proc)
|
|
|
+-
|
|
|
+- def launchProcess(self, cmd, stdout, stderr, env, cwd, timeout=None):
|
|
|
+- self.timedout = False
|
|
|
+- cmd.insert(1, self.remoteHere)
|
|
|
+- outputFile = "xpcshelloutput"
|
|
|
+- with open(outputFile, 'w+') as f:
|
|
|
+- try:
|
|
|
+- self.shellReturnCode = self.device.shell(cmd, f, timeout=timeout+10)
|
|
|
+- except mozdevice.DMError as e:
|
|
|
+- if self.timedout:
|
|
|
+- # If the test timed out, there is a good chance the device
|
|
|
+- # manager also timed out and raised DMError.
|
|
|
+- # Ignore the DMError to simplify the error report.
|
|
|
+- self.shellReturnCode = None
|
|
|
+- pass
|
|
|
+- else:
|
|
|
+- raise e
|
|
|
+- # The device manager may have timed out waiting for xpcshell.
|
|
|
+- # Guard against an accumulation of hung processes by killing
|
|
|
+- # them here. Note also that IPC tests may spawn new instances
|
|
|
+- # of xpcshell.
|
|
|
+- self.device.killProcess("xpcshell")
|
|
|
+- return outputFile
|
|
|
+-
|
|
|
+- def checkForCrashes(self,
|
|
|
+- dump_directory,
|
|
|
+- symbols_path,
|
|
|
+- test_name=None):
|
|
|
+- if not self.device.dirExists(self.remoteMinidumpDir):
|
|
|
+- # 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" %
|
|
|
+- self.remoteMinidumpDir)
|
|
|
+- # Whilst no crash was found, the run should still display as a failure
|
|
|
+- return True
|
|
|
+- with mozfile.TemporaryDirectory() as dumpDir:
|
|
|
+- self.device.getDirectory(self.remoteMinidumpDir, dumpDir)
|
|
|
+- crashed = xpcshell.XPCShellTestThread.checkForCrashes(
|
|
|
+- self, dumpDir, symbols_path, test_name)
|
|
|
+- self.clearRemoteDir(self.remoteMinidumpDir)
|
|
|
+- return crashed
|
|
|
+-
|
|
|
+- def communicate(self, proc):
|
|
|
+- f = open(proc, "r")
|
|
|
+- contents = f.read()
|
|
|
+- f.close()
|
|
|
+- os.remove(proc)
|
|
|
+- return contents, ""
|
|
|
+-
|
|
|
+- def poll(self, proc):
|
|
|
+- if self.device.processExist("xpcshell") is None:
|
|
|
+- return self.getReturnCode(proc)
|
|
|
+- # Process is still running
|
|
|
+- return None
|
|
|
+-
|
|
|
+- def kill(self, proc):
|
|
|
+- return self.device.killProcess("xpcshell", True)
|
|
|
+-
|
|
|
+- def getReturnCode(self, proc):
|
|
|
+- if self.shellReturnCode is not None:
|
|
|
+- return self.shellReturnCode
|
|
|
+- else:
|
|
|
+- return -1
|
|
|
+-
|
|
|
+- def removeDir(self, dirname):
|
|
|
+- self.device.removeDir(dirname)
|
|
|
+-
|
|
|
+- def clearRemoteDir(self, remoteDir):
|
|
|
+- out = ""
|
|
|
+- try:
|
|
|
+- out = self.device.shellCheckOutput([self.remoteClearDirScript, remoteDir])
|
|
|
+- except mozdevice.DMError:
|
|
|
+- self.log.info("unable to delete %s: '%s'" % (remoteDir, str(out)))
|
|
|
+- self.log.info("retrying after 10 seconds...")
|
|
|
+- time.sleep(10)
|
|
|
+- try:
|
|
|
+- out = self.device.shellCheckOutput([self.remoteClearDirScript, remoteDir])
|
|
|
+- except mozdevice.DMError:
|
|
|
+- self.log.error("failed to delete %s: '%s'" % (remoteDir, str(out)))
|
|
|
+-
|
|
|
+- # TODO: consider creating a separate log dir. We don't have the test file structure,
|
|
|
+- # so we use filename.log. Would rather see ./logs/filename.log
|
|
|
+- def createLogFile(self, test, stdout):
|
|
|
+- try:
|
|
|
+- f = None
|
|
|
+- filename = test.replace('\\', '/').split('/')[-1] + ".log"
|
|
|
+- f = open(filename, "w")
|
|
|
+- f.write(stdout)
|
|
|
+-
|
|
|
+- finally:
|
|
|
+- if f is not None:
|
|
|
+- f.close()
|
|
|
+-
|
|
|
+-
|
|
|
+-# A specialization of XPCShellTests that runs tests on an Android device
|
|
|
+-# 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.
|
|
|
+- 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
|
|
|
+- # be executable. Since +x permissions cannot usually be set on /mnt/sdcard,
|
|
|
+- # and the test root may be on /mnt/sdcard, remoteBinDir is set to be on
|
|
|
+- # /data/local, always.
|
|
|
+- self.remoteBinDir = "/data/local/xpcb"
|
|
|
+- # Terse directory names are used here ("c" for the components directory)
|
|
|
+- # to minimize the length of the command line used to execute
|
|
|
+- # xpcshell on the remote device. adb has a limit to the number
|
|
|
+- # of characters used in a shell command, and the xpcshell command
|
|
|
+- # line can be quite complex.
|
|
|
+- self.remoteTmpDir = remoteJoin(self.remoteTestRoot, "tmp")
|
|
|
+- self.remoteScriptsDir = self.remoteTestRoot
|
|
|
+- self.remoteComponentsDir = remoteJoin(self.remoteTestRoot, "c")
|
|
|
+- self.remoteModulesDir = remoteJoin(self.remoteTestRoot, "m")
|
|
|
+- self.remoteMinidumpDir = remoteJoin(self.remoteTestRoot, "minidumps")
|
|
|
+- self.remoteClearDirScript = remoteJoin(self.remoteBinDir, "cleardir")
|
|
|
+- self.profileDir = remoteJoin(self.remoteTestRoot, "p")
|
|
|
+- self.remoteDebugger = options['debugger']
|
|
|
+- self.remoteDebuggerArgs = options['debuggerArgs']
|
|
|
+- self.testingModulesDir = options['testingModulesDir']
|
|
|
+-
|
|
|
+- self.env = {}
|
|
|
+-
|
|
|
+- if options['objdir']:
|
|
|
+- self.xpcDir = os.path.join(options['objdir'], "_tests/xpcshell")
|
|
|
+- elif os.path.isdir(os.path.join(here, 'tests')):
|
|
|
+- self.xpcDir = os.path.join(here, 'tests')
|
|
|
+- else:
|
|
|
+- print("Couldn't find local xpcshell test directory", file=sys.stderr)
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- if options['localAPK']:
|
|
|
+- self.localAPKContents = ZipFile(options['localAPK'])
|
|
|
+- if options['setup']:
|
|
|
+- self.setupTestDir()
|
|
|
+- self.setupUtilities()
|
|
|
+- self.setupModules()
|
|
|
+- self.setupMinidumpDir()
|
|
|
+- self.remoteAPK = None
|
|
|
+- if options['localAPK']:
|
|
|
+- self.remoteAPK = remoteJoin(self.remoteBinDir, os.path.basename(options['localAPK']))
|
|
|
+- self.setAppRoot()
|
|
|
+-
|
|
|
+- # data that needs to be passed to the RemoteXPCShellTestThread
|
|
|
+- self.mobileArgs = {
|
|
|
+- 'device': self.device,
|
|
|
+- 'remoteBinDir': self.remoteBinDir,
|
|
|
+- 'remoteScriptsDir': self.remoteScriptsDir,
|
|
|
+- 'remoteComponentsDir': self.remoteComponentsDir,
|
|
|
+- 'remoteModulesDir': self.remoteModulesDir,
|
|
|
+- 'options': self.options,
|
|
|
+- 'remoteDebugger': self.remoteDebugger,
|
|
|
+- 'pathMapping': self.pathMapping,
|
|
|
+- 'profileDir': self.profileDir,
|
|
|
+- 'remoteTmpDir': self.remoteTmpDir,
|
|
|
+- 'remoteMinidumpDir': self.remoteMinidumpDir,
|
|
|
+- 'remoteClearDirScript': self.remoteClearDirScript,
|
|
|
+- }
|
|
|
+- if self.remoteAPK:
|
|
|
+- self.mobileArgs['remoteAPK'] = self.remoteAPK
|
|
|
+-
|
|
|
+- def setLD_LIBRARY_PATH(self):
|
|
|
+- self.env["LD_LIBRARY_PATH"] = self.remoteBinDir
|
|
|
+-
|
|
|
+- def pushWrapper(self):
|
|
|
+- # Rather than executing xpcshell directly, this wrapper script is
|
|
|
+- # used. By setting environment variables and the cwd in the script,
|
|
|
+- # the length of the per-test command line is shortened. This is
|
|
|
+- # often important when using ADB, as there is a limit to the length
|
|
|
+- # of the ADB command line.
|
|
|
+- localWrapper = tempfile.mktemp()
|
|
|
+- f = open(localWrapper, "w")
|
|
|
+- f.write("#!/system/bin/sh\n")
|
|
|
+- for envkey, envval in self.env.iteritems():
|
|
|
+- f.write("export %s=%s\n" % (envkey, envval))
|
|
|
+- f.writelines([
|
|
|
+- "cd $1\n",
|
|
|
+- "echo xpcw: cd $1\n",
|
|
|
+- "shift\n",
|
|
|
+- "echo xpcw: xpcshell \"$@\"\n",
|
|
|
+- "%s/xpcshell \"$@\"\n" % self.remoteBinDir])
|
|
|
+- f.close()
|
|
|
+- remoteWrapper = remoteJoin(self.remoteBinDir, "xpcw")
|
|
|
+- self.device.pushFile(localWrapper, remoteWrapper)
|
|
|
+- os.remove(localWrapper)
|
|
|
+-
|
|
|
+- # Removing and re-creating a directory is a common operation which
|
|
|
+- # can be implemented more efficiently with a shell script.
|
|
|
+- localWrapper = tempfile.mktemp()
|
|
|
+- f = open(localWrapper, "w")
|
|
|
+- # The directory may not exist initially, so rm may fail. 'rm -f' is not
|
|
|
+- # supported on some Androids. Similarly, 'test' and 'if [ -d ]' are not
|
|
|
+- # universally available, so we just ignore errors from rm.
|
|
|
+- f.writelines([
|
|
|
+- "#!/system/bin/sh\n",
|
|
|
+- "rm -r \"$1\"\n",
|
|
|
+- "mkdir \"$1\"\n"])
|
|
|
+- f.close()
|
|
|
+- self.device.pushFile(localWrapper, self.remoteClearDirScript)
|
|
|
+- os.remove(localWrapper)
|
|
|
+-
|
|
|
+- self.device.chmodDir(self.remoteBinDir)
|
|
|
+-
|
|
|
+- def buildEnvironment(self):
|
|
|
+- self.buildCoreEnvironment()
|
|
|
+- self.setLD_LIBRARY_PATH()
|
|
|
+- if self.options['localAPK'] and self.appRoot:
|
|
|
+- self.env["GRE_HOME"] = self.appRoot
|
|
|
+- self.env["XPCSHELL_TEST_PROFILE_DIR"] = self.profileDir
|
|
|
+- self.env["TMPDIR"] = self.remoteTmpDir
|
|
|
+- self.env["HOME"] = self.profileDir
|
|
|
+- self.env["XPCSHELL_TEST_TEMP_DIR"] = self.remoteTmpDir
|
|
|
+- self.env["XPCSHELL_MINIDUMP_DIR"] = self.remoteMinidumpDir
|
|
|
+- if self.options['setup']:
|
|
|
+- self.pushWrapper()
|
|
|
+-
|
|
|
+- def setAppRoot(self):
|
|
|
+- # Determine the application root directory associated with the package
|
|
|
+- # name used by the Fennec APK.
|
|
|
+- self.appRoot = None
|
|
|
+- packageName = None
|
|
|
+- if self.options['localAPK']:
|
|
|
+- try:
|
|
|
+- packageName = self.localAPKContents.read("package-name.txt")
|
|
|
+- if packageName:
|
|
|
+- self.appRoot = self.device.getAppRoot(packageName.strip())
|
|
|
+- except Exception as detail:
|
|
|
+- print("unable to determine app root: " + str(detail))
|
|
|
+- pass
|
|
|
+- return None
|
|
|
+-
|
|
|
+- def setupUtilities(self):
|
|
|
+- if (not self.device.dirExists(self.remoteBinDir)):
|
|
|
+- # device.mkDir may fail here where shellCheckOutput may succeed -- see bug 817235
|
|
|
+- try:
|
|
|
+- self.device.shellCheckOutput(["mkdir", self.remoteBinDir])
|
|
|
+- except mozdevice.DMError:
|
|
|
+- # Might get a permission error; try again as root, if available
|
|
|
+- self.device.shellCheckOutput(["mkdir", self.remoteBinDir], root=True)
|
|
|
+- self.device.shellCheckOutput(["chmod", "777", self.remoteBinDir], root=True)
|
|
|
+-
|
|
|
+- remotePrefDir = remoteJoin(self.remoteBinDir, "defaults/pref")
|
|
|
+- if (self.device.dirExists(self.remoteTmpDir)):
|
|
|
+- self.device.removeDir(self.remoteTmpDir)
|
|
|
+- self.device.mkDir(self.remoteTmpDir)
|
|
|
+- if (not self.device.dirExists(remotePrefDir)):
|
|
|
+- self.device.mkDirs(remoteJoin(remotePrefDir, "extra"))
|
|
|
+- if (not self.device.dirExists(self.remoteScriptsDir)):
|
|
|
+- self.device.mkDir(self.remoteScriptsDir)
|
|
|
+- if (not self.device.dirExists(self.remoteComponentsDir)):
|
|
|
+- self.device.mkDir(self.remoteComponentsDir)
|
|
|
+-
|
|
|
+- local = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'head.js')
|
|
|
+- remoteFile = remoteJoin(self.remoteScriptsDir, "head.js")
|
|
|
+- self.device.pushFile(local, remoteFile)
|
|
|
+-
|
|
|
+- # The xpcshell binary is required for all tests. Additional binaries
|
|
|
+- # are required for some tests. This list should be similar to
|
|
|
+- # TEST_HARNESS_BINS in testing/mochitest/Makefile.in.
|
|
|
+- binaries = ["xpcshell",
|
|
|
+- "ssltunnel",
|
|
|
+- "certutil",
|
|
|
+- "pk12util",
|
|
|
+- "BadCertServer",
|
|
|
+- "OCSPStaplingServer",
|
|
|
+- "GenerateOCSPResponse",
|
|
|
+- "SymantecSanctionsServer"]
|
|
|
+- for fname in binaries:
|
|
|
+- local = os.path.join(self.localBin, fname)
|
|
|
+- if os.path.isfile(local):
|
|
|
+- print("Pushing %s.." % fname, file=sys.stderr)
|
|
|
+- remoteFile = remoteJoin(self.remoteBinDir, fname)
|
|
|
+- self.device.pushFile(local, remoteFile)
|
|
|
+- else:
|
|
|
+- print("*** Expected binary %s not found in %s!" %
|
|
|
+- (fname, self.localBin), file=sys.stderr)
|
|
|
+-
|
|
|
+- local = os.path.join(self.localBin, "components/httpd.js")
|
|
|
+- remoteFile = remoteJoin(self.remoteComponentsDir, "httpd.js")
|
|
|
+- self.device.pushFile(local, remoteFile)
|
|
|
+-
|
|
|
+- local = os.path.join(self.localBin, "components/httpd.manifest")
|
|
|
+- remoteFile = remoteJoin(self.remoteComponentsDir, "httpd.manifest")
|
|
|
+- self.device.pushFile(local, remoteFile)
|
|
|
+-
|
|
|
+- local = os.path.join(self.localBin, "components/test_necko.xpt")
|
|
|
+- remoteFile = remoteJoin(self.remoteComponentsDir, "test_necko.xpt")
|
|
|
+- self.device.pushFile(local, remoteFile)
|
|
|
+-
|
|
|
+- if self.options['localAPK']:
|
|
|
+- remoteFile = remoteJoin(self.remoteBinDir, os.path.basename(self.options['localAPK']))
|
|
|
+- self.device.pushFile(self.options['localAPK'], remoteFile)
|
|
|
+-
|
|
|
+- self.pushLibs()
|
|
|
+-
|
|
|
+- def pushLibs(self):
|
|
|
+- elfhack = os.path.join(self.localBin, 'elfhack')
|
|
|
+- if not os.path.exists(elfhack):
|
|
|
+- elfhack = None
|
|
|
+- pushed_libs_count = 0
|
|
|
+- if self.options['localAPK']:
|
|
|
+- try:
|
|
|
+- dir = tempfile.mkdtemp()
|
|
|
+- for info in self.localAPKContents.infolist():
|
|
|
+- if info.filename.endswith(".so"):
|
|
|
+- print("Pushing %s.." % info.filename, file=sys.stderr)
|
|
|
+- remoteFile = remoteJoin(self.remoteBinDir, os.path.basename(info.filename))
|
|
|
+- self.localAPKContents.extract(info, dir)
|
|
|
+- localFile = os.path.join(dir, info.filename)
|
|
|
+- with open(localFile) as f:
|
|
|
+- # Decompress xz-compressed file.
|
|
|
+- if f.read(5)[1:] == '7zXZ':
|
|
|
+- cmd = ['xz', '-df', '--suffix', '.so', localFile]
|
|
|
+- subprocess.check_output(cmd)
|
|
|
+- # xz strips the ".so" file suffix.
|
|
|
+- os.rename(localFile[:-3], localFile)
|
|
|
+- # elfhack -r should provide better crash reports
|
|
|
+- if elfhack:
|
|
|
+- cmd = [elfhack, '-r', localFile]
|
|
|
+- subprocess.check_output(cmd)
|
|
|
+- self.device.pushFile(localFile, remoteFile)
|
|
|
+- pushed_libs_count += 1
|
|
|
+- finally:
|
|
|
+- shutil.rmtree(dir)
|
|
|
+- return pushed_libs_count
|
|
|
+-
|
|
|
+- for file in os.listdir(self.localLib):
|
|
|
+- if (file.endswith(".so")):
|
|
|
+- print("Pushing %s.." % file, file=sys.stderr)
|
|
|
+- if 'libxul' in file:
|
|
|
+- print("This is a big file, it could take a while.", file=sys.stderr)
|
|
|
+- localFile = os.path.join(self.localLib, file)
|
|
|
+- remoteFile = remoteJoin(self.remoteBinDir, file)
|
|
|
+- self.device.pushFile(localFile, remoteFile)
|
|
|
+- pushed_libs_count += 1
|
|
|
+-
|
|
|
+- # Additional libraries may be found in a sub-directory such as "lib/armeabi-v7a"
|
|
|
+- localArmLib = os.path.join(self.localLib, "lib")
|
|
|
+- if os.path.exists(localArmLib):
|
|
|
+- for root, dirs, files in os.walk(localArmLib):
|
|
|
+- for file in files:
|
|
|
+- if (file.endswith(".so")):
|
|
|
+- print("Pushing %s.." % file, file=sys.stderr)
|
|
|
+- localFile = os.path.join(root, file)
|
|
|
+- remoteFile = remoteJoin(self.remoteBinDir, file)
|
|
|
+- self.device.pushFile(localFile, remoteFile)
|
|
|
+- pushed_libs_count += 1
|
|
|
+-
|
|
|
+- return pushed_libs_count
|
|
|
+-
|
|
|
+- def setupModules(self):
|
|
|
+- if self.testingModulesDir:
|
|
|
+- self.device.pushDir(self.testingModulesDir, self.remoteModulesDir)
|
|
|
+-
|
|
|
+- def setupTestDir(self):
|
|
|
+- print('pushing %s' % self.xpcDir)
|
|
|
+- try:
|
|
|
+- # The tests directory can be quite large: 5000 files and growing!
|
|
|
+- # Sometimes - like on a low-end aws instance running an emulator - the push
|
|
|
+- # may exceed the default 5 minute timeout, so we increase it here to 10 minutes.
|
|
|
+- self.device.pushDir(self.xpcDir, self.remoteScriptsDir, timeout=600, retryLimit=10)
|
|
|
+- except TypeError:
|
|
|
+- # Foopies have an older mozdevice ver without retryLimit
|
|
|
+- self.device.pushDir(self.xpcDir, self.remoteScriptsDir)
|
|
|
+-
|
|
|
+- def setupMinidumpDir(self):
|
|
|
+- if self.device.dirExists(self.remoteMinidumpDir):
|
|
|
+- self.device.removeDir(self.remoteMinidumpDir)
|
|
|
+- self.device.mkDir(self.remoteMinidumpDir)
|
|
|
+-
|
|
|
+- def buildTestList(self, test_tags=None, test_paths=None, verify=False):
|
|
|
+- xpcshell.XPCShellTests.buildTestList(
|
|
|
+- self, test_tags=test_tags, test_paths=test_paths, verify=verify)
|
|
|
+- uniqueTestPaths = set([])
|
|
|
+- for test in self.alltests:
|
|
|
+- uniqueTestPaths.add(test['here'])
|
|
|
+- for testdir in uniqueTestPaths:
|
|
|
+- abbrevTestDir = os.path.relpath(testdir, self.xpcDir)
|
|
|
+- remoteScriptDir = remoteJoin(self.remoteScriptsDir, abbrevTestDir)
|
|
|
+- self.pathMapping.append(PathMapping(testdir, remoteScriptDir))
|
|
|
+-
|
|
|
+-
|
|
|
+-def verifyRemoteOptions(parser, options):
|
|
|
+- if isinstance(options, Namespace):
|
|
|
+- options = vars(options)
|
|
|
+-
|
|
|
+- if options['localLib'] is None:
|
|
|
+- if options['localAPK'] and options['objdir']:
|
|
|
+- for path in ['dist/fennec', 'fennec/lib']:
|
|
|
+- options['localLib'] = os.path.join(options['objdir'], path)
|
|
|
+- if os.path.isdir(options['localLib']):
|
|
|
+- break
|
|
|
+- else:
|
|
|
+- parser.error("Couldn't find local library dir, specify --local-lib-dir")
|
|
|
+- elif options['objdir']:
|
|
|
+- options['localLib'] = os.path.join(options['objdir'], 'dist/bin')
|
|
|
+- elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')):
|
|
|
+- # assume tests are being run from a tests.zip
|
|
|
+- options['localLib'] = os.path.abspath(os.path.join(here, '..', 'bin'))
|
|
|
+- else:
|
|
|
+- parser.error("Couldn't find local library dir, specify --local-lib-dir")
|
|
|
+-
|
|
|
+- if options['localBin'] is None:
|
|
|
+- if options['objdir']:
|
|
|
+- for path in ['dist/bin', 'bin']:
|
|
|
+- options['localBin'] = os.path.join(options['objdir'], path)
|
|
|
+- if os.path.isdir(options['localBin']):
|
|
|
+- break
|
|
|
+- else:
|
|
|
+- parser.error("Couldn't find local binary dir, specify --local-bin-dir")
|
|
|
+- elif os.path.isfile(os.path.join(here, '..', 'bin', 'xpcshell')):
|
|
|
+- # assume tests are being run from a tests.zip
|
|
|
+- options['localBin'] = os.path.abspath(os.path.join(here, '..', 'bin'))
|
|
|
+- else:
|
|
|
+- parser.error("Couldn't find local binary dir, specify --local-bin-dir")
|
|
|
+- return options
|
|
|
+-
|
|
|
+-
|
|
|
+-class PathMapping:
|
|
|
+-
|
|
|
+- def __init__(self, localDir, remoteDir):
|
|
|
+- self.local = localDir
|
|
|
+- self.remote = remoteDir
|
|
|
+-
|
|
|
+-
|
|
|
+-def main():
|
|
|
+- if sys.version_info < (2, 7):
|
|
|
+- print("Error: You must use python version 2.7 or newer but less than 3.0", file=sys.stderr)
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- parser = parser_remote()
|
|
|
+- options = parser.parse_args()
|
|
|
+- if not options.localAPK:
|
|
|
+- for file in os.listdir(os.path.join(options.objdir, "dist")):
|
|
|
+- if (file.endswith(".apk") and file.startswith("fennec")):
|
|
|
+- options.localAPK = os.path.join(options.objdir, "dist")
|
|
|
+- options.localAPK = os.path.join(options.localAPK, file)
|
|
|
+- print("using APK: " + options.localAPK, file=sys.stderr)
|
|
|
+- break
|
|
|
+- else:
|
|
|
+- print("Error: please specify an APK", file=sys.stderr)
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- options = verifyRemoteOptions(parser, options)
|
|
|
+- log = commandline.setup_logging("Remote XPCShell",
|
|
|
+- options,
|
|
|
+- {"tbpl": sys.stdout})
|
|
|
+-
|
|
|
+- dm_args = {'deviceRoot': options['remoteTestRoot']}
|
|
|
+- if options['deviceIP']:
|
|
|
+- dm_args['host'] = options['deviceIP']
|
|
|
+- dm_args['port'] = options['devicePort']
|
|
|
+- if options['log_tbpl_level'] == 'debug' or options['log_mach_level'] == 'debug':
|
|
|
+- dm_args['logLevel'] = logging.DEBUG
|
|
|
+- dm = mozdevice.DroidADB(**dm_args)
|
|
|
+-
|
|
|
+- if options['interactive'] and not options['testPath']:
|
|
|
+- print("Error: You must specify a test filename in interactive mode!", file=sys.stderr)
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+- if options['xpcshell'] is None:
|
|
|
+- options['xpcshell'] = "xpcshell"
|
|
|
+-
|
|
|
+- xpcsh = XPCShellRemote(dm, options, log)
|
|
|
+-
|
|
|
+- # we don't run concurrent tests on mobile
|
|
|
+- options['sequential'] = True
|
|
|
+-
|
|
|
+- if not xpcsh.runTests(options,
|
|
|
+- testClass=RemoteXPCShellTestThread,
|
|
|
+- mobileArgs=xpcsh.mobileArgs):
|
|
|
+- sys.exit(1)
|
|
|
+-
|
|
|
+-
|
|
|
+-if __name__ == '__main__':
|
|
|
+- main()
|