|
@@ -2,9 +2,2443 @@
|
|
|
# User Geoff Brown <gbrown@mozilla.com>
|
|
|
# Date 1524760988 21600
|
|
|
# Node ID ad8b8a0eb0d2e98f3dbe51979d16d3200d95fc6a
|
|
|
-# Parent 7bc9cdc13e72d98c1700debfea3e37aee4ab2354
|
|
|
+# Parent fe3937113be1a54c091bf6db9046db6380c12fd7
|
|
|
Bug 1440714 - Remove DeviceManagerADB and Droid classes from mozdevice; r=bc
|
|
|
|
|
|
+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
|
|
|
+--- a/testing/mozbase/mozdevice/mozdevice/__init__.py
|
|
|
++++ b/testing/mozbase/mozdevice/mozdevice/__init__.py
|
|
|
+@@ -3,15 +3,11 @@
|
|
|
+ # 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']
|
|
|
++__all__ = ['ADBError', 'ADBRootError', 'ADBTimeoutError',
|
|
|
++ 'ADBProcess', 'ADBCommand', 'ADBHost', 'ADBDevice', 'ADBAndroid', 'ADBB2G']
|
|
|
+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/setup.py b/testing/mozbase/mozdevice/setup.py
|
|
|
+--- a/testing/mozbase/mozdevice/setup.py
|
|
|
++++ b/testing/mozbase/mozdevice/setup.py
|
|
|
+@@ -29,12 +29,10 @@ setup(name=PACKAGE_NAME,
|
|
|
+ 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/moztest/moztest/output/base.py b/testing/mozbase/moztest/moztest/output/base.py
|
|
|
+--- a/testing/mozbase/moztest/moztest/output/base.py
|
|
|
++++ b/testing/mozbase/moztest/moztest/output/base.py
|
|
|
+@@ -6,17 +6,16 @@ from __future__ import absolute_import,
|
|
|
+
|
|
|
+ from contextlib import closing
|
|
|
+ from StringIO import StringIO
|
|
|
+
|
|
|
+ try:
|
|
|
+ from abc import abstractmethod
|
|
|
+ except ImportError:
|
|
|
+ # abc is python 2.6+
|
|
|
+- # from https://github.com/mozilla/mozbase/blob/master/mozdevice/mozdevice/devicemanager.py
|
|
|
+ def abstractmethod(method):
|
|
|
+ line = method.func_code.co_firstlineno
|
|
|
+ filename = method.func_code.co_filename
|
|
|
+
|
|
|
+ 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))
|
|
|
diff --git a/testing/xpcshell/runxpcshelltests.py b/testing/xpcshell/runxpcshelltests.py
|
|
|
--- a/testing/xpcshell/runxpcshelltests.py
|
|
|
+++ b/testing/xpcshell/runxpcshelltests.py
|