summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozdevice
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /testing/mozbase/mozdevice
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/mozbase/mozdevice')
-rw-r--r--testing/mozbase/mozdevice/adb_tests/test_device_running_adb_as_root.py48
-rw-r--r--testing/mozbase/mozdevice/adb_tests/test_devicemanagerADB.py219
-rwxr-xr-xtesting/mozbase/mozdevice/mozdevice/Zeroconf.py1560
-rw-r--r--testing/mozbase/mozdevice/mozdevice/__init__.py15
-rw-r--r--testing/mozbase/mozdevice/mozdevice/adb.py2271
-rw-r--r--testing/mozbase/mozdevice/mozdevice/adb_android.py493
-rw-r--r--testing/mozbase/mozdevice/mozdevice/adb_b2g.py122
-rw-r--r--testing/mozbase/mozdevice/mozdevice/devicemanager.py674
-rw-r--r--testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py893
-rw-r--r--testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py975
-rw-r--r--testing/mozbase/mozdevice/mozdevice/dmcli.py382
-rw-r--r--testing/mozbase/mozdevice/mozdevice/droid.py263
-rw-r--r--testing/mozbase/mozdevice/mozdevice/sutini.py126
-rw-r--r--testing/mozbase/mozdevice/mozdevice/version_codes.py61
-rw-r--r--testing/mozbase/mozdevice/setup.py36
-rw-r--r--testing/mozbase/mozdevice/sut_tests/README.md15
-rw-r--r--testing/mozbase/mozdevice/sut_tests/dmunit.py55
-rw-r--r--testing/mozbase/mozdevice/sut_tests/genfiles.py85
-rw-r--r--testing/mozbase/mozdevice/sut_tests/runtests.py96
-rwxr-xr-xtesting/mozbase/mozdevice/sut_tests/setup-tools.sh10
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test-files/mytext.txt177
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test-files/smalltext.txt1
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test-files/test_script.sh1
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_datachannel.py53
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_exec.py24
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_exec_env.py32
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_fileExists.py37
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_getdir.py51
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_info.py20
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_prompt.py30
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_ps.py27
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_pull.py34
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_push1.py38
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_push2.py39
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_pushbinary.py19
-rw-r--r--testing/mozbase/mozdevice/sut_tests/test_pushsmalltext.py19
-rw-r--r--testing/mozbase/mozdevice/tests/droidsut_launch.py36
-rw-r--r--testing/mozbase/mozdevice/tests/manifest.ini23
-rw-r--r--testing/mozbase/mozdevice/tests/sut.py89
-rw-r--r--testing/mozbase/mozdevice/tests/sut_app.py20
-rw-r--r--testing/mozbase/mozdevice/tests/sut_basic.py73
-rw-r--r--testing/mozbase/mozdevice/tests/sut_chmod.py22
-rw-r--r--testing/mozbase/mozdevice/tests/sut_copytree.py67
-rw-r--r--testing/mozbase/mozdevice/tests/sut_fileExists.py29
-rw-r--r--testing/mozbase/mozdevice/tests/sut_fileMethods.py72
-rw-r--r--testing/mozbase/mozdevice/tests/sut_info.py49
-rw-r--r--testing/mozbase/mozdevice/tests/sut_ip.py37
-rw-r--r--testing/mozbase/mozdevice/tests/sut_kill.py24
-rw-r--r--testing/mozbase/mozdevice/tests/sut_list.py22
-rw-r--r--testing/mozbase/mozdevice/tests/sut_logcat.py52
-rw-r--r--testing/mozbase/mozdevice/tests/sut_mkdir.py78
-rw-r--r--testing/mozbase/mozdevice/tests/sut_movetree.py65
-rw-r--r--testing/mozbase/mozdevice/tests/sut_ps.py50
-rw-r--r--testing/mozbase/mozdevice/tests/sut_pull.py47
-rw-r--r--testing/mozbase/mozdevice/tests/sut_push.py88
-rw-r--r--testing/mozbase/mozdevice/tests/sut_remove.py24
-rw-r--r--testing/mozbase/mozdevice/tests/sut_time.py18
-rw-r--r--testing/mozbase/mozdevice/tests/sut_unpackfile.py23
58 files changed, 10009 insertions, 0 deletions
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
new file mode 100644
index 000000000..2b223bb04
--- /dev/null
+++ b/testing/mozbase/mozdevice/adb_tests/test_device_running_adb_as_root.py
@@ -0,0 +1,48 @@
+"""
+ 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.
+"""
+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
new file mode 100644
index 000000000..495e449a4
--- /dev/null
+++ b/testing/mozbase/mozdevice/adb_tests/test_devicemanagerADB.py
@@ -0,0 +1,219 @@
+"""
+ 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()
+"""
+
+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/Zeroconf.py b/testing/mozbase/mozdevice/mozdevice/Zeroconf.py
new file mode 100755
index 000000000..54a5d5359
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/Zeroconf.py
@@ -0,0 +1,1560 @@
+""" Multicast DNS Service Discovery for Python, v0.12
+ Copyright (C) 2003, Paul Scott-Murphy
+
+ This module provides a framework for the use of DNS Service Discovery
+ using IP multicast. It has been tested against the JRendezvous
+ implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
+ and against the mDNSResponder from Mac OS X 10.3.8.
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+"""
+
+"""0.12 update - allow selection of binding interface
+ typo fix - Thanks A. M. Kuchlingi
+ removed all use of word 'Rendezvous' - this is an API change"""
+
+"""0.11 update - correction to comments for addListener method
+ support for new record types seen from OS X
+ - IPv6 address
+ - hostinfo
+ ignore unknown DNS record types
+ fixes to name decoding
+ works alongside other processes using port 5353 (e.g. on Mac OS X)
+ tested against Mac OS X 10.3.2's mDNSResponder
+ corrections to removal of list entries for service browser"""
+
+"""0.10 update - Jonathon Paisley contributed these corrections:
+ always multicast replies, even when query is unicast
+ correct a pointer encoding problem
+ can now write records in any order
+ traceback shown on failure
+ better TXT record parsing
+ server is now separate from name
+ can cancel a service browser
+
+ modified some unit tests to accommodate these changes"""
+
+"""0.09 update - remove all records on service unregistration
+ fix DOS security problem with readName"""
+
+"""0.08 update - changed licensing to LGPL"""
+
+"""0.07 update - faster shutdown on engine
+ pointer encoding of outgoing names
+ ServiceBrowser now works
+ new unit tests"""
+
+"""0.06 update - small improvements with unit tests
+ added defined exception types
+ new style objects
+ fixed hostname/interface problem
+ fixed socket timeout problem
+ fixed addServiceListener() typo bug
+ using select() for socket reads
+ tested on Debian unstable with Python 2.2.2"""
+
+"""0.05 update - ensure case insensitivty on domain names
+ support for unicast DNS queries"""
+
+"""0.04 update - added some unit tests
+ added __ne__ adjuncts where required
+ ensure names end in '.local.'
+ timeout on receiving socket for clean shutdown"""
+
+__author__ = "Paul Scott-Murphy"
+__email__ = "paul at scott dash murphy dot com"
+__version__ = "0.12"
+
+import string
+import time
+import struct
+import socket
+import threading
+import select
+import traceback
+
+__all__ = ["Zeroconf", "ServiceInfo", "ServiceBrowser"]
+
+# hook for threads
+
+globals()['_GLOBAL_DONE'] = 0
+
+# Some timing constants
+
+_UNREGISTER_TIME = 125
+_CHECK_TIME = 175
+_REGISTER_TIME = 225
+_LISTENER_TIME = 200
+_BROWSER_TIME = 500
+
+# Some DNS constants
+
+_MDNS_ADDR = '224.0.0.251'
+_MDNS_PORT = 5353;
+_DNS_PORT = 53;
+_DNS_TTL = 60 * 60; # one hour default TTL
+
+_MAX_MSG_TYPICAL = 1460 # unused
+_MAX_MSG_ABSOLUTE = 8972
+
+_FLAGS_QR_MASK = 0x8000 # query response mask
+_FLAGS_QR_QUERY = 0x0000 # query
+_FLAGS_QR_RESPONSE = 0x8000 # response
+
+_FLAGS_AA = 0x0400 # Authorative answer
+_FLAGS_TC = 0x0200 # Truncated
+_FLAGS_RD = 0x0100 # Recursion desired
+_FLAGS_RA = 0x8000 # Recursion available
+
+_FLAGS_Z = 0x0040 # Zero
+_FLAGS_AD = 0x0020 # Authentic data
+_FLAGS_CD = 0x0010 # Checking disabled
+
+_CLASS_IN = 1
+_CLASS_CS = 2
+_CLASS_CH = 3
+_CLASS_HS = 4
+_CLASS_NONE = 254
+_CLASS_ANY = 255
+_CLASS_MASK = 0x7FFF
+_CLASS_UNIQUE = 0x8000
+
+_TYPE_A = 1
+_TYPE_NS = 2
+_TYPE_MD = 3
+_TYPE_MF = 4
+_TYPE_CNAME = 5
+_TYPE_SOA = 6
+_TYPE_MB = 7
+_TYPE_MG = 8
+_TYPE_MR = 9
+_TYPE_NULL = 10
+_TYPE_WKS = 11
+_TYPE_PTR = 12
+_TYPE_HINFO = 13
+_TYPE_MINFO = 14
+_TYPE_MX = 15
+_TYPE_TXT = 16
+_TYPE_AAAA = 28
+_TYPE_SRV = 33
+_TYPE_ANY = 255
+
+# Mapping constants to names
+
+_CLASSES = { _CLASS_IN : "in",
+ _CLASS_CS : "cs",
+ _CLASS_CH : "ch",
+ _CLASS_HS : "hs",
+ _CLASS_NONE : "none",
+ _CLASS_ANY : "any" }
+
+_TYPES = { _TYPE_A : "a",
+ _TYPE_NS : "ns",
+ _TYPE_MD : "md",
+ _TYPE_MF : "mf",
+ _TYPE_CNAME : "cname",
+ _TYPE_SOA : "soa",
+ _TYPE_MB : "mb",
+ _TYPE_MG : "mg",
+ _TYPE_MR : "mr",
+ _TYPE_NULL : "null",
+ _TYPE_WKS : "wks",
+ _TYPE_PTR : "ptr",
+ _TYPE_HINFO : "hinfo",
+ _TYPE_MINFO : "minfo",
+ _TYPE_MX : "mx",
+ _TYPE_TXT : "txt",
+ _TYPE_AAAA : "quada",
+ _TYPE_SRV : "srv",
+ _TYPE_ANY : "any" }
+
+# utility functions
+
+def currentTimeMillis():
+ """Current system time in milliseconds"""
+ return time.time() * 1000
+
+# Exceptions
+
+class NonLocalNameException(Exception):
+ pass
+
+class NonUniqueNameException(Exception):
+ pass
+
+class NamePartTooLongException(Exception):
+ pass
+
+class AbstractMethodException(Exception):
+ pass
+
+class BadTypeInNameException(Exception):
+ pass
+
+# implementation classes
+
+class DNSEntry(object):
+ """A DNS entry"""
+
+ def __init__(self, name, type, clazz):
+ self.key = string.lower(name)
+ self.name = name
+ self.type = type
+ self.clazz = clazz & _CLASS_MASK
+ self.unique = (clazz & _CLASS_UNIQUE) != 0
+
+ def __eq__(self, other):
+ """Equality test on name, type, and class"""
+ if isinstance(other, DNSEntry):
+ return self.name == other.name and self.type == other.type and self.clazz == other.clazz
+ return 0
+
+ def __ne__(self, other):
+ """Non-equality test"""
+ return not self.__eq__(other)
+
+ def getClazz(self, clazz):
+ """Class accessor"""
+ try:
+ return _CLASSES[clazz]
+ except:
+ return "?(%s)" % (clazz)
+
+ def getType(self, type):
+ """Type accessor"""
+ try:
+ return _TYPES[type]
+ except:
+ return "?(%s)" % (type)
+
+ def toString(self, hdr, other):
+ """String representation with additional information"""
+ result = "%s[%s,%s" % (hdr, self.getType(self.type), self.getClazz(self.clazz))
+ if self.unique:
+ result += "-unique,"
+ else:
+ result += ","
+ result += self.name
+ if other is not None:
+ result += ",%s]" % (other)
+ else:
+ result += "]"
+ return result
+
+class DNSQuestion(DNSEntry):
+ """A DNS question entry"""
+
+ def __init__(self, name, type, clazz):
+ if not name.endswith(".local."):
+ raise NonLocalNameException
+ DNSEntry.__init__(self, name, type, clazz)
+
+ def answeredBy(self, rec):
+ """Returns true if the question is answered by the record"""
+ return self.clazz == rec.clazz and (self.type == rec.type or self.type == _TYPE_ANY) and self.name == rec.name
+
+ def __repr__(self):
+ """String representation"""
+ return DNSEntry.toString(self, "question", None)
+
+
+class DNSRecord(DNSEntry):
+ """A DNS record - like a DNS entry, but has a TTL"""
+
+ def __init__(self, name, type, clazz, ttl):
+ DNSEntry.__init__(self, name, type, clazz)
+ self.ttl = ttl
+ self.created = currentTimeMillis()
+
+ def __eq__(self, other):
+ """Tests equality as per DNSRecord"""
+ if isinstance(other, DNSRecord):
+ return DNSEntry.__eq__(self, other)
+ return 0
+
+ def suppressedBy(self, msg):
+ """Returns true if any answer in a message can suffice for the
+ information held in this record."""
+ for record in msg.answers:
+ if self.suppressedByAnswer(record):
+ return 1
+ return 0
+
+ def suppressedByAnswer(self, other):
+ """Returns true if another record has same name, type and class,
+ and if its TTL is at least half of this record's."""
+ if self == other and other.ttl > (self.ttl / 2):
+ return 1
+ return 0
+
+ def getExpirationTime(self, percent):
+ """Returns the time at which this record will have expired
+ by a certain percentage."""
+ return self.created + (percent * self.ttl * 10)
+
+ def getRemainingTTL(self, now):
+ """Returns the remaining TTL in seconds."""
+ return max(0, (self.getExpirationTime(100) - now) / 1000)
+
+ def isExpired(self, now):
+ """Returns true if this record has expired."""
+ return self.getExpirationTime(100) <= now
+
+ def isStale(self, now):
+ """Returns true if this record is at least half way expired."""
+ return self.getExpirationTime(50) <= now
+
+ def resetTTL(self, other):
+ """Sets this record's TTL and created time to that of
+ another record."""
+ self.created = other.created
+ self.ttl = other.ttl
+
+ def write(self, out):
+ """Abstract method"""
+ raise AbstractMethodException
+
+ def toString(self, other):
+ """String representation with addtional information"""
+ arg = "%s/%s,%s" % (self.ttl, self.getRemainingTTL(currentTimeMillis()), other)
+ return DNSEntry.toString(self, "record", arg)
+
+class DNSAddress(DNSRecord):
+ """A DNS address record"""
+
+ def __init__(self, name, type, clazz, ttl, address):
+ DNSRecord.__init__(self, name, type, clazz, ttl)
+ self.address = address
+
+ def write(self, out):
+ """Used in constructing an outgoing packet"""
+ out.writeString(self.address, len(self.address))
+
+ def __eq__(self, other):
+ """Tests equality on address"""
+ if isinstance(other, DNSAddress):
+ return self.address == other.address
+ return 0
+
+ def __repr__(self):
+ """String representation"""
+ try:
+ return socket.inet_ntoa(self.address)
+ except:
+ return self.address
+
+class DNSHinfo(DNSRecord):
+ """A DNS host information record"""
+
+ def __init__(self, name, type, clazz, ttl, cpu, os):
+ DNSRecord.__init__(self, name, type, clazz, ttl)
+ self.cpu = cpu
+ self.os = os
+
+ def write(self, out):
+ """Used in constructing an outgoing packet"""
+ out.writeString(self.cpu, len(self.cpu))
+ out.writeString(self.os, len(self.os))
+
+ def __eq__(self, other):
+ """Tests equality on cpu and os"""
+ if isinstance(other, DNSHinfo):
+ return self.cpu == other.cpu and self.os == other.os
+ return 0
+
+ def __repr__(self):
+ """String representation"""
+ return self.cpu + " " + self.os
+
+class DNSPointer(DNSRecord):
+ """A DNS pointer record"""
+
+ def __init__(self, name, type, clazz, ttl, alias):
+ DNSRecord.__init__(self, name, type, clazz, ttl)
+ self.alias = alias
+
+ def write(self, out):
+ """Used in constructing an outgoing packet"""
+ out.writeName(self.alias)
+
+ def __eq__(self, other):
+ """Tests equality on alias"""
+ if isinstance(other, DNSPointer):
+ return self.alias == other.alias
+ return 0
+
+ def __repr__(self):
+ """String representation"""
+ return self.toString(self.alias)
+
+class DNSText(DNSRecord):
+ """A DNS text record"""
+
+ def __init__(self, name, type, clazz, ttl, text):
+ DNSRecord.__init__(self, name, type, clazz, ttl)
+ self.text = text
+
+ def write(self, out):
+ """Used in constructing an outgoing packet"""
+ out.writeString(self.text, len(self.text))
+
+ def __eq__(self, other):
+ """Tests equality on text"""
+ if isinstance(other, DNSText):
+ return self.text == other.text
+ return 0
+
+ def __repr__(self):
+ """String representation"""
+ if len(self.text) > 10:
+ return self.toString(self.text[:7] + "...")
+ else:
+ return self.toString(self.text)
+
+class DNSService(DNSRecord):
+ """A DNS service record"""
+
+ def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
+ DNSRecord.__init__(self, name, type, clazz, ttl)
+ self.priority = priority
+ self.weight = weight
+ self.port = port
+ self.server = server
+
+ def write(self, out):
+ """Used in constructing an outgoing packet"""
+ out.writeShort(self.priority)
+ out.writeShort(self.weight)
+ out.writeShort(self.port)
+ out.writeName(self.server)
+
+ def __eq__(self, other):
+ """Tests equality on priority, weight, port and server"""
+ if isinstance(other, DNSService):
+ return self.priority == other.priority and self.weight == other.weight and self.port == other.port and self.server == other.server
+ return 0
+
+ def __repr__(self):
+ """String representation"""
+ return self.toString("%s:%s" % (self.server, self.port))
+
+class DNSIncoming(object):
+ """Object representation of an incoming DNS packet"""
+
+ def __init__(self, data):
+ """Constructor from string holding bytes of packet"""
+ self.offset = 0
+ self.data = data
+ self.questions = []
+ self.answers = []
+ self.numQuestions = 0
+ self.numAnswers = 0
+ self.numAuthorities = 0
+ self.numAdditionals = 0
+
+ self.readHeader()
+ self.readQuestions()
+ self.readOthers()
+
+ def readHeader(self):
+ """Reads header portion of packet"""
+ format = '!HHHHHH'
+ length = struct.calcsize(format)
+ info = struct.unpack(format, self.data[self.offset:self.offset+length])
+ self.offset += length
+
+ self.id = info[0]
+ self.flags = info[1]
+ self.numQuestions = info[2]
+ self.numAnswers = info[3]
+ self.numAuthorities = info[4]
+ self.numAdditionals = info[5]
+
+ def readQuestions(self):
+ """Reads questions section of packet"""
+ format = '!HH'
+ length = struct.calcsize(format)
+ for i in range(0, self.numQuestions):
+ name = self.readName()
+ info = struct.unpack(format, self.data[self.offset:self.offset+length])
+ self.offset += length
+
+ question = DNSQuestion(name, info[0], info[1])
+ self.questions.append(question)
+
+ def readInt(self):
+ """Reads an integer from the packet"""
+ format = '!I'
+ length = struct.calcsize(format)
+ info = struct.unpack(format, self.data[self.offset:self.offset+length])
+ self.offset += length
+ return info[0]
+
+ def readCharacterString(self):
+ """Reads a character string from the packet"""
+ length = ord(self.data[self.offset])
+ self.offset += 1
+ return self.readString(length)
+
+ def readString(self, len):
+ """Reads a string of a given length from the packet"""
+ format = '!' + str(len) + 's'
+ length = struct.calcsize(format)
+ info = struct.unpack(format, self.data[self.offset:self.offset+length])
+ self.offset += length
+ return info[0]
+
+ def readUnsignedShort(self):
+ """Reads an unsigned short from the packet"""
+ format = '!H'
+ length = struct.calcsize(format)
+ info = struct.unpack(format, self.data[self.offset:self.offset+length])
+ self.offset += length
+ return info[0]
+
+ def readOthers(self):
+ """Reads the answers, authorities and additionals section of the packet"""
+ format = '!HHiH'
+ length = struct.calcsize(format)
+ n = self.numAnswers + self.numAuthorities + self.numAdditionals
+ for i in range(0, n):
+ domain = self.readName()
+ info = struct.unpack(format, self.data[self.offset:self.offset+length])
+ self.offset += length
+
+ rec = None
+ if info[0] == _TYPE_A:
+ rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(4))
+ elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
+ rec = DNSPointer(domain, info[0], info[1], info[2], self.readName())
+ elif info[0] == _TYPE_TXT:
+ rec = DNSText(domain, info[0], info[1], info[2], self.readString(info[3]))
+ elif info[0] == _TYPE_SRV:
+ rec = DNSService(domain, info[0], info[1], info[2], self.readUnsignedShort(), self.readUnsignedShort(), self.readUnsignedShort(), self.readName())
+ elif info[0] == _TYPE_HINFO:
+ rec = DNSHinfo(domain, info[0], info[1], info[2], self.readCharacterString(), self.readCharacterString())
+ elif info[0] == _TYPE_AAAA:
+ rec = DNSAddress(domain, info[0], info[1], info[2], self.readString(16))
+ else:
+ # Try to ignore types we don't know about
+ # this may mean the rest of the name is
+ # unable to be parsed, and may show errors
+ # so this is left for debugging. New types
+ # encountered need to be parsed properly.
+ #
+ #print "UNKNOWN TYPE = " + str(info[0])
+ #raise BadTypeInNameException
+ pass
+
+ if rec is not None:
+ self.answers.append(rec)
+
+ def isQuery(self):
+ """Returns true if this is a query"""
+ return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
+
+ def isResponse(self):
+ """Returns true if this is a response"""
+ return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
+
+ def readUTF(self, offset, len):
+ """Reads a UTF-8 string of a given length from the packet"""
+ result = self.data[offset:offset+len].decode('utf-8')
+ return result
+
+ def readName(self):
+ """Reads a domain name from the packet"""
+ result = ''
+ off = self.offset
+ next = -1
+ first = off
+
+ while 1:
+ len = ord(self.data[off])
+ off += 1
+ if len == 0:
+ break
+ t = len & 0xC0
+ if t == 0x00:
+ result = ''.join((result, self.readUTF(off, len) + '.'))
+ off += len
+ elif t == 0xC0:
+ if next < 0:
+ next = off + 1
+ off = ((len & 0x3F) << 8) | ord(self.data[off])
+ if off >= first:
+ raise "Bad domain name (circular) at " + str(off)
+ first = off
+ else:
+ raise "Bad domain name at " + str(off)
+
+ if next >= 0:
+ self.offset = next
+ else:
+ self.offset = off
+
+ return result
+
+
+class DNSOutgoing(object):
+ """Object representation of an outgoing packet"""
+
+ def __init__(self, flags, multicast = 1):
+ self.finished = 0
+ self.id = 0
+ self.multicast = multicast
+ self.flags = flags
+ self.names = {}
+ self.data = []
+ self.size = 12
+
+ self.questions = []
+ self.answers = []
+ self.authorities = []
+ self.additionals = []
+
+ def addQuestion(self, record):
+ """Adds a question"""
+ self.questions.append(record)
+
+ def addAnswer(self, inp, record):
+ """Adds an answer"""
+ if not record.suppressedBy(inp):
+ self.addAnswerAtTime(record, 0)
+
+ def addAnswerAtTime(self, record, now):
+ """Adds an answer if if does not expire by a certain time"""
+ if record is not None:
+ if now == 0 or not record.isExpired(now):
+ self.answers.append((record, now))
+
+ def addAuthorativeAnswer(self, record):
+ """Adds an authoritative answer"""
+ self.authorities.append(record)
+
+ def addAdditionalAnswer(self, record):
+ """Adds an additional answer"""
+ self.additionals.append(record)
+
+ def writeByte(self, value):
+ """Writes a single byte to the packet"""
+ format = '!c'
+ self.data.append(struct.pack(format, chr(value)))
+ self.size += 1
+
+ def insertShort(self, index, value):
+ """Inserts an unsigned short in a certain position in the packet"""
+ format = '!H'
+ self.data.insert(index, struct.pack(format, value))
+ self.size += 2
+
+ def writeShort(self, value):
+ """Writes an unsigned short to the packet"""
+ format = '!H'
+ self.data.append(struct.pack(format, value))
+ self.size += 2
+
+ def writeInt(self, value):
+ """Writes an unsigned integer to the packet"""
+ format = '!I'
+ self.data.append(struct.pack(format, value))
+ self.size += 4
+
+ def writeString(self, value, length):
+ """Writes a string to the packet"""
+ format = '!' + str(length) + 's'
+ self.data.append(struct.pack(format, value))
+ self.size += length
+
+ def writeUTF(self, s):
+ """Writes a UTF-8 string of a given length to the packet"""
+ utfstr = s.encode('utf-8')
+ length = len(utfstr)
+ if length > 64:
+ raise NamePartTooLongException
+ self.writeByte(length)
+ self.writeString(utfstr, length)
+
+ def writeName(self, name):
+ """Writes a domain name to the packet"""
+
+ try:
+ # Find existing instance of this name in packet
+ #
+ index = self.names[name]
+ except KeyError:
+ # No record of this name already, so write it
+ # out as normal, recording the location of the name
+ # for future pointers to it.
+ #
+ self.names[name] = self.size
+ parts = name.split('.')
+ if parts[-1] == '':
+ parts = parts[:-1]
+ for part in parts:
+ self.writeUTF(part)
+ self.writeByte(0)
+ return
+
+ # An index was found, so write a pointer to it
+ #
+ self.writeByte((index >> 8) | 0xC0)
+ self.writeByte(index)
+
+ def writeQuestion(self, question):
+ """Writes a question to the packet"""
+ self.writeName(question.name)
+ self.writeShort(question.type)
+ self.writeShort(question.clazz)
+
+ def writeRecord(self, record, now):
+ """Writes a record (answer, authoritative answer, additional) to
+ the packet"""
+ self.writeName(record.name)
+ self.writeShort(record.type)
+ if record.unique and self.multicast:
+ self.writeShort(record.clazz | _CLASS_UNIQUE)
+ else:
+ self.writeShort(record.clazz)
+ if now == 0:
+ self.writeInt(record.ttl)
+ else:
+ self.writeInt(record.getRemainingTTL(now))
+ index = len(self.data)
+ # Adjust size for the short we will write before this record
+ #
+ self.size += 2
+ record.write(self)
+ self.size -= 2
+
+ length = len(''.join(self.data[index:]))
+ self.insertShort(index, length) # Here is the short we adjusted for
+
+ def packet(self):
+ """Returns a string containing the packet's bytes
+
+ No further parts should be added to the packet once this
+ is done."""
+ if not self.finished:
+ self.finished = 1
+ for question in self.questions:
+ self.writeQuestion(question)
+ for answer, time in self.answers:
+ self.writeRecord(answer, time)
+ for authority in self.authorities:
+ self.writeRecord(authority, 0)
+ for additional in self.additionals:
+ self.writeRecord(additional, 0)
+
+ self.insertShort(0, len(self.additionals))
+ self.insertShort(0, len(self.authorities))
+ self.insertShort(0, len(self.answers))
+ self.insertShort(0, len(self.questions))
+ self.insertShort(0, self.flags)
+ if self.multicast:
+ self.insertShort(0, 0)
+ else:
+ self.insertShort(0, self.id)
+ return ''.join(self.data)
+
+
+class DNSCache(object):
+ """A cache of DNS entries"""
+
+ def __init__(self):
+ self.cache = {}
+
+ def add(self, entry):
+ """Adds an entry"""
+ try:
+ list = self.cache[entry.key]
+ except:
+ list = self.cache[entry.key] = []
+ list.append(entry)
+
+ def remove(self, entry):
+ """Removes an entry"""
+ try:
+ list = self.cache[entry.key]
+ list.remove(entry)
+ except:
+ pass
+
+ def get(self, entry):
+ """Gets an entry by key. Will return None if there is no
+ matching entry."""
+ try:
+ list = self.cache[entry.key]
+ return list[list.index(entry)]
+ except:
+ return None
+
+ def getByDetails(self, name, type, clazz):
+ """Gets an entry by details. Will return None if there is
+ no matching entry."""
+ entry = DNSEntry(name, type, clazz)
+ return self.get(entry)
+
+ def entriesWithName(self, name):
+ """Returns a list of entries whose key matches the name."""
+ try:
+ return self.cache[name]
+ except:
+ return []
+
+ def entries(self):
+ """Returns a list of all entries"""
+ def add(x, y): return x+y
+ try:
+ return reduce(add, self.cache.values())
+ except:
+ return []
+
+
+class Engine(threading.Thread):
+ """An engine wraps read access to sockets, allowing objects that
+ need to receive data from sockets to be called back when the
+ sockets are ready.
+
+ A reader needs a handle_read() method, which is called when the socket
+ it is interested in is ready for reading.
+
+ Writers are not implemented here, because we only send short
+ packets.
+ """
+
+ def __init__(self, zeroconf):
+ threading.Thread.__init__(self)
+ self.zeroconf = zeroconf
+ self.readers = {} # maps socket to reader
+ self.timeout = 5
+ self.condition = threading.Condition()
+ self.daemon = True
+ self.start()
+
+ def run(self):
+ while not globals()['_GLOBAL_DONE']:
+ rs = self.getReaders()
+ if len(rs) == 0:
+ # No sockets to manage, but we wait for the timeout
+ # or addition of a socket
+ #
+ self.condition.acquire()
+ self.condition.wait(self.timeout)
+ self.condition.release()
+ else:
+ try:
+ rr, wr, er = select.select(rs, [], [], self.timeout)
+ for socket in rr:
+ try:
+ self.readers[socket].handle_read()
+ except:
+ # Ignore errors that occur on shutdown
+ pass
+ except:
+ pass
+
+ def getReaders(self):
+ result = []
+ self.condition.acquire()
+ result = self.readers.keys()
+ self.condition.release()
+ return result
+
+ def addReader(self, reader, socket):
+ self.condition.acquire()
+ self.readers[socket] = reader
+ self.condition.notify()
+ self.condition.release()
+
+ def delReader(self, socket):
+ self.condition.acquire()
+ del(self.readers[socket])
+ self.condition.notify()
+ self.condition.release()
+
+ def notify(self):
+ self.condition.acquire()
+ self.condition.notify()
+ self.condition.release()
+
+class Listener(object):
+ """A Listener is used by this module to listen on the multicast
+ group to which DNS messages are sent, allowing the implementation
+ to cache information as it arrives.
+
+ It requires registration with an Engine object in order to have
+ the read() method called when a socket is availble for reading."""
+
+ def __init__(self, zeroconf):
+ self.zeroconf = zeroconf
+ self.zeroconf.engine.addReader(self, self.zeroconf.socket)
+
+ def handle_read(self):
+ data, (addr, port) = self.zeroconf.socket.recvfrom(_MAX_MSG_ABSOLUTE)
+ self.data = data
+ msg = DNSIncoming(data)
+ if msg.isQuery():
+ # Always multicast responses
+ #
+ if port == _MDNS_PORT:
+ self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
+ # If it's not a multicast query, reply via unicast
+ # and multicast
+ #
+ elif port == _DNS_PORT:
+ self.zeroconf.handleQuery(msg, addr, port)
+ self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
+ else:
+ self.zeroconf.handleResponse(msg)
+
+
+class Reaper(threading.Thread):
+ """A Reaper is used by this module to remove cache entries that
+ have expired."""
+
+ def __init__(self, zeroconf):
+ threading.Thread.__init__(self)
+ self.daemon = True
+ self.zeroconf = zeroconf
+ self.start()
+
+ def run(self):
+ while 1:
+ self.zeroconf.wait(10 * 1000)
+ if globals()['_GLOBAL_DONE']:
+ return
+ now = currentTimeMillis()
+ for record in self.zeroconf.cache.entries():
+ if record.isExpired(now):
+ self.zeroconf.updateRecord(now, record)
+ self.zeroconf.cache.remove(record)
+
+
+class ServiceBrowser(threading.Thread):
+ """Used to browse for a service of a specific type.
+
+ The listener object will have its addService() and
+ removeService() methods called when this browser
+ discovers changes in the services availability."""
+
+ def __init__(self, zeroconf, type, listener):
+ """Creates a browser for a specific type"""
+ threading.Thread.__init__(self)
+ self.zeroconf = zeroconf
+ self.type = type
+ self.listener = listener
+ self.services = {}
+ self.nextTime = currentTimeMillis()
+ self.delay = _BROWSER_TIME
+ self.list = []
+ self.daemon = True
+
+ self.done = 0
+
+ self.zeroconf.addListener(self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
+ self.start()
+
+ def updateRecord(self, zeroconf, now, record):
+ """Callback invoked by Zeroconf when new information arrives.
+
+ Updates information required by browser in the Zeroconf cache."""
+ if record.type == _TYPE_PTR and record.name == self.type:
+ expired = record.isExpired(now)
+ try:
+ oldrecord = self.services[record.alias.lower()]
+ if not expired:
+ oldrecord.resetTTL(record)
+ else:
+ del(self.services[record.alias.lower()])
+ callback = lambda x: self.listener.removeService(x, self.type, record.alias)
+ self.list.append(callback)
+ return
+ except:
+ if not expired:
+ self.services[record.alias.lower()] = record
+ callback = lambda x: self.listener.addService(x, self.type, record.alias)
+ self.list.append(callback)
+
+ expires = record.getExpirationTime(75)
+ if expires < self.nextTime:
+ self.nextTime = expires
+
+ def cancel(self):
+ self.done = 1
+ self.zeroconf.notifyAll()
+
+ def run(self):
+ while 1:
+ event = None
+ now = currentTimeMillis()
+ if len(self.list) == 0 and self.nextTime > now:
+ self.zeroconf.wait(self.nextTime - now)
+ if globals()['_GLOBAL_DONE'] or self.done:
+ return
+ now = currentTimeMillis()
+
+ if self.nextTime <= now:
+ out = DNSOutgoing(_FLAGS_QR_QUERY)
+ out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
+ for record in self.services.values():
+ if not record.isExpired(now):
+ out.addAnswerAtTime(record, now)
+ self.zeroconf.send(out)
+ self.nextTime = now + self.delay
+ self.delay = min(20 * 1000, self.delay * 2)
+
+ if len(self.list) > 0:
+ event = self.list.pop(0)
+
+ if event is not None:
+ event(self.zeroconf)
+
+
+class ServiceInfo(object):
+ """Service information"""
+
+ def __init__(self, type, name, address=None, port=None, weight=0, priority=0, properties=None, server=None):
+ """Create a service description.
+
+ type: fully qualified service type name
+ name: fully qualified service name
+ address: IP address as unsigned short, network byte order
+ port: port that the service runs on
+ weight: weight of the service
+ priority: priority of the service
+ properties: dictionary of properties (or a string holding the bytes for the text field)
+ server: fully qualified name for service host (defaults to name)"""
+
+ if not name.endswith(type):
+ raise BadTypeInNameException
+ self.type = type
+ self.name = name
+ self.address = address
+ self.port = port
+ self.weight = weight
+ self.priority = priority
+ if server:
+ self.server = server
+ else:
+ self.server = name
+ self.setProperties(properties)
+
+ def setProperties(self, properties):
+ """Sets properties and text of this info from a dictionary"""
+ if isinstance(properties, dict):
+ self.properties = properties
+ list = []
+ result = ''
+ for key in properties:
+ value = properties[key]
+ if value is None:
+ suffix = ''.encode('utf-8')
+ elif isinstance(value, str):
+ suffix = value.encode('utf-8')
+ elif isinstance(value, int):
+ if value:
+ suffix = 'true'
+ else:
+ suffix = 'false'
+ else:
+ suffix = ''.encode('utf-8')
+ list.append('='.join((key, suffix)))
+ for item in list:
+ result = ''.join((result, struct.pack('!c', chr(len(item))), item))
+ self.text = result
+ else:
+ self.text = properties
+
+ def setText(self, text):
+ """Sets properties and text given a text field"""
+ self.text = text
+ try:
+ result = {}
+ end = len(text)
+ index = 0
+ strs = []
+ while index < end:
+ length = ord(text[index])
+ index += 1
+ strs.append(text[index:index+length])
+ index += length
+
+ for s in strs:
+ eindex = s.find('=')
+ if eindex == -1:
+ # No equals sign at all
+ key = s
+ value = 0
+ else:
+ key = s[:eindex]
+ value = s[eindex+1:]
+ if value == 'true':
+ value = 1
+ elif value == 'false' or not value:
+ value = 0
+
+ # Only update non-existent properties
+ if key and result.get(key) == None:
+ result[key] = value
+
+ self.properties = result
+ except:
+ traceback.print_exc()
+ self.properties = None
+
+ def getType(self):
+ """Type accessor"""
+ return self.type
+
+ def getName(self):
+ """Name accessor"""
+ if self.type is not None and self.name.endswith("." + self.type):
+ return self.name[:len(self.name) - len(self.type) - 1]
+ return self.name
+
+ def getAddress(self):
+ """Address accessor"""
+ return self.address
+
+ def getPort(self):
+ """Port accessor"""
+ return self.port
+
+ def getPriority(self):
+ """Pirority accessor"""
+ return self.priority
+
+ def getWeight(self):
+ """Weight accessor"""
+ return self.weight
+
+ def getProperties(self):
+ """Properties accessor"""
+ return self.properties
+
+ def getText(self):
+ """Text accessor"""
+ return self.text
+
+ def getServer(self):
+ """Server accessor"""
+ return self.server
+
+ def updateRecord(self, zeroconf, now, record):
+ """Updates service information from a DNS record"""
+ if record is not None and not record.isExpired(now):
+ if record.type == _TYPE_A:
+ if record.name == self.name:
+ self.address = record.address
+ elif record.type == _TYPE_SRV:
+ if record.name == self.name:
+ self.server = record.server
+ self.port = record.port
+ self.weight = record.weight
+ self.priority = record.priority
+ self.address = None
+ self.updateRecord(zeroconf, now, zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN))
+ elif record.type == _TYPE_TXT:
+ if record.name == self.name:
+ self.setText(record.text)
+
+ def request(self, zeroconf, timeout):
+ """Returns true if the service could be discovered on the
+ network, and updates this object with details discovered.
+ """
+ now = currentTimeMillis()
+ delay = _LISTENER_TIME
+ next = now + delay
+ last = now + timeout
+ result = 0
+ try:
+ zeroconf.addListener(self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN))
+ while self.server is None or self.address is None or self.text is None:
+ if last <= now:
+ return 0
+ if next <= now:
+ out = DNSOutgoing(_FLAGS_QR_QUERY)
+ out.addQuestion(DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN))
+ out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_SRV, _CLASS_IN), now)
+ out.addQuestion(DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN))
+ out.addAnswerAtTime(zeroconf.cache.getByDetails(self.name, _TYPE_TXT, _CLASS_IN), now)
+ if self.server is not None:
+ out.addQuestion(DNSQuestion(self.server, _TYPE_A, _CLASS_IN))
+ out.addAnswerAtTime(zeroconf.cache.getByDetails(self.server, _TYPE_A, _CLASS_IN), now)
+ zeroconf.send(out)
+ next = now + delay
+ delay = delay * 2
+
+ zeroconf.wait(min(next, last) - now)
+ now = currentTimeMillis()
+ result = 1
+ finally:
+ zeroconf.removeListener(self)
+
+ return result
+
+ def __eq__(self, other):
+ """Tests equality of service name"""
+ if isinstance(other, ServiceInfo):
+ return other.name == self.name
+ return 0
+
+ def __ne__(self, other):
+ """Non-equality test"""
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ """String representation"""
+ result = "service[%s,%s:%s," % (self.name, socket.inet_ntoa(self.getAddress()), self.port)
+ if self.text is None:
+ result += "None"
+ else:
+ if len(self.text) < 20:
+ result += self.text
+ else:
+ result += self.text[:17] + "..."
+ result += "]"
+ return result
+
+
+class Zeroconf(object):
+ """Implementation of Zeroconf Multicast DNS Service Discovery
+
+ Supports registration, unregistration, queries and browsing.
+ """
+ def __init__(self, bindaddress=None):
+ """Creates an instance of the Zeroconf class, establishing
+ multicast communications, listening and reaping threads."""
+ globals()['_GLOBAL_DONE'] = 0
+ if bindaddress is None:
+ self.intf = socket.gethostbyname(socket.gethostname())
+ else:
+ self.intf = bindaddress
+ self.group = ('', _MDNS_PORT)
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ try:
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+ except:
+ # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
+ # multicast UDP sockets (p 731, "TCP/IP Illustrated,
+ # Volume 2"), but some BSD-derived systems require
+ # SO_REUSEPORT to be specified explicity. Also, not all
+ # versions of Python have SO_REUSEPORT available. So
+ # if you're on a BSD-based system, and haven't upgraded
+ # to Python 2.3 yet, you may find this library doesn't
+ # work as expected.
+ #
+ pass
+ self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 255)
+ self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
+ try:
+ self.socket.bind(self.group)
+ except:
+ # Some versions of linux raise an exception even though
+ # the SO_REUSE* options have been set, so ignore it
+ #
+ pass
+ self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(self.intf) + socket.inet_aton('0.0.0.0'))
+ self.socket.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
+
+ self.listeners = []
+ self.browsers = []
+ self.services = {}
+
+ self.cache = DNSCache()
+
+ self.condition = threading.Condition()
+
+ self.engine = Engine(self)
+ self.listener = Listener(self)
+ self.reaper = Reaper(self)
+
+ def isLoopback(self):
+ return self.intf.startswith("127.0.0.1")
+
+ def isLinklocal(self):
+ return self.intf.startswith("169.254.")
+
+ def wait(self, timeout):
+ """Calling thread waits for a given number of milliseconds or
+ until notified."""
+ self.condition.acquire()
+ self.condition.wait(timeout/1000)
+ self.condition.release()
+
+ def notifyAll(self):
+ """Notifies all waiting threads"""
+ self.condition.acquire()
+ self.condition.notifyAll()
+ self.condition.release()
+
+ def getServiceInfo(self, type, name, timeout=3000):
+ """Returns network's service information for a particular
+ name and type, or None if no service matches by the timeout,
+ which defaults to 3 seconds."""
+ info = ServiceInfo(type, name)
+ if info.request(self, timeout):
+ return info
+ return None
+
+ def addServiceListener(self, type, listener):
+ """Adds a listener for a particular service type. This object
+ will then have its updateRecord method called when information
+ arrives for that type."""
+ self.removeServiceListener(listener)
+ self.browsers.append(ServiceBrowser(self, type, listener))
+
+ def removeServiceListener(self, listener):
+ """Removes a listener from the set that is currently listening."""
+ for browser in self.browsers:
+ if browser.listener == listener:
+ browser.cancel()
+ del(browser)
+
+ def registerService(self, info, ttl=_DNS_TTL):
+ """Registers service information to the network with a default TTL
+ of 60 seconds. Zeroconf will then respond to requests for
+ information for that service. The name of the service may be
+ changed if needed to make it unique on the network."""
+ self.checkService(info)
+ self.services[info.name.lower()] = info
+ now = currentTimeMillis()
+ nextTime = now
+ i = 0
+ while i < 3:
+ if now < nextTime:
+ self.wait(nextTime - now)
+ now = currentTimeMillis()
+ continue
+ out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
+ out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0)
+ out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, ttl, info.priority, info.weight, info.port, info.server), 0)
+ out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0)
+ if info.address:
+ out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, ttl, info.address), 0)
+ self.send(out)
+ i += 1
+ nextTime += _REGISTER_TIME
+
+ def unregisterService(self, info):
+ """Unregister a service."""
+ try:
+ del(self.services[info.name.lower()])
+ except:
+ pass
+ now = currentTimeMillis()
+ nextTime = now
+ i = 0
+ while i < 3:
+ if now < nextTime:
+ self.wait(nextTime - now)
+ now = currentTimeMillis()
+ continue
+ out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
+ out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
+ out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.name), 0)
+ out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
+ if info.address:
+ out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
+ self.send(out)
+ i += 1
+ nextTime += _UNREGISTER_TIME
+
+ def unregisterAllServices(self):
+ """Unregister all registered services."""
+ if len(self.services) > 0:
+ now = currentTimeMillis()
+ nextTime = now
+ i = 0
+ while i < 3:
+ if now < nextTime:
+ self.wait(nextTime - now)
+ now = currentTimeMillis()
+ continue
+ out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
+ for info in self.services.values():
+ out.addAnswerAtTime(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0)
+ out.addAnswerAtTime(DNSService(info.name, _TYPE_SRV, _CLASS_IN, 0, info.priority, info.weight, info.port, info.server), 0)
+ out.addAnswerAtTime(DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0)
+ if info.address:
+ out.addAnswerAtTime(DNSAddress(info.server, _TYPE_A, _CLASS_IN, 0, info.address), 0)
+ self.send(out)
+ i += 1
+ nextTime += _UNREGISTER_TIME
+
+ def checkService(self, info):
+ """Checks the network for a unique service name, modifying the
+ ServiceInfo passed in if it is not unique."""
+ now = currentTimeMillis()
+ nextTime = now
+ i = 0
+ while i < 3:
+ for record in self.cache.entriesWithName(info.type):
+ if record.type == _TYPE_PTR and not record.isExpired(now) and record.alias == info.name:
+ if (info.name.find('.') < 0):
+ info.name = info.name + ".[" + info.address + ":" + info.port + "]." + info.type
+ self.checkService(info)
+ return
+ raise NonUniqueNameException
+ if now < nextTime:
+ self.wait(nextTime - now)
+ now = currentTimeMillis()
+ continue
+ out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
+ self.debug = out
+ out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
+ out.addAuthorativeAnswer(DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name))
+ self.send(out)
+ i += 1
+ nextTime += _CHECK_TIME
+
+ def addListener(self, listener, question):
+ """Adds a listener for a given question. The listener will have
+ its updateRecord method called when information is available to
+ answer the question."""
+ now = currentTimeMillis()
+ self.listeners.append(listener)
+ if question is not None:
+ for record in self.cache.entriesWithName(question.name):
+ if question.answeredBy(record) and not record.isExpired(now):
+ listener.updateRecord(self, now, record)
+ self.notifyAll()
+
+ def removeListener(self, listener):
+ """Removes a listener."""
+ try:
+ self.listeners.remove(listener)
+ self.notifyAll()
+ except:
+ pass
+
+ def updateRecord(self, now, rec):
+ """Used to notify listeners of new information that has updated
+ a record."""
+ for listener in self.listeners:
+ listener.updateRecord(self, now, rec)
+ self.notifyAll()
+
+ def handleResponse(self, msg):
+ """Deal with incoming response packets. All answers
+ are held in the cache, and listeners are notified."""
+ now = currentTimeMillis()
+ for record in msg.answers:
+ expired = record.isExpired(now)
+ if record in self.cache.entries():
+ if expired:
+ self.cache.remove(record)
+ else:
+ entry = self.cache.get(record)
+ if entry is not None:
+ entry.resetTTL(record)
+ record = entry
+ else:
+ self.cache.add(record)
+
+ self.updateRecord(now, record)
+
+ def handleQuery(self, msg, addr, port):
+ """Deal with incoming query packets. Provides a response if
+ possible."""
+ out = None
+
+ # Support unicast client responses
+ #
+ if port != _MDNS_PORT:
+ out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
+ for question in msg.questions:
+ out.addQuestion(question)
+
+ for question in msg.questions:
+ if question.type == _TYPE_PTR:
+ for service in self.services.values():
+ if question.name == service.type:
+ if out is None:
+ out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
+ out.addAnswer(msg, DNSPointer(service.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, service.name))
+ else:
+ try:
+ if out is None:
+ out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
+
+ # Answer A record queries for any service addresses we know
+ if question.type == _TYPE_A or question.type == _TYPE_ANY:
+ for service in self.services.values():
+ if service.server == question.name.lower():
+ out.addAnswer(msg, DNSAddress(question.name, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
+
+ service = self.services.get(question.name.lower(), None)
+ if not service: continue
+
+ if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
+ out.addAnswer(msg, DNSService(question.name, _TYPE_SRV, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.priority, service.weight, service.port, service.server))
+ if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
+ out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text))
+ if question.type == _TYPE_SRV:
+ out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address))
+ except:
+ traceback.print_exc()
+
+ if out is not None and out.answers:
+ out.id = msg.id
+ self.send(out, addr, port)
+
+ def send(self, out, addr = _MDNS_ADDR, port = _MDNS_PORT):
+ """Sends an outgoing packet."""
+ # This is a quick test to see if we can parse the packets we generate
+ #temp = DNSIncoming(out.packet())
+ try:
+ bytes_sent = self.socket.sendto(out.packet(), 0, (addr, port))
+ except:
+ # Ignore this, it may be a temporary loss of network connection
+ pass
+
+ def close(self):
+ """Ends the background threads, and prevent this instance from
+ servicing further queries."""
+ if globals()['_GLOBAL_DONE'] == 0:
+ globals()['_GLOBAL_DONE'] = 1
+ self.notifyAll()
+ self.engine.notify()
+ self.unregisterAllServices()
+ self.socket.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'))
+ self.socket.close()
+
+# Test a few module features, including service registration, service
+# query (for Zoe), and service unregistration.
+
+if __name__ == '__main__':
+ print "Multicast DNS Service Discovery for Python, version", __version__
+ r = Zeroconf()
+ print "1. Testing registration of a service..."
+ desc = {'version':'0.10','a':'test value', 'b':'another value'}
+ info = ServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local.", socket.inet_aton("127.0.0.1"), 1234, 0, 0, desc)
+ print " Registering service..."
+ r.registerService(info)
+ print " Registration done."
+ print "2. Testing query of service information..."
+ print " Getting ZOE service:", str(r.getServiceInfo("_http._tcp.local.", "ZOE._http._tcp.local."))
+ print " Query done."
+ print "3. Testing query of own service..."
+ print " Getting self:", str(r.getServiceInfo("_http._tcp.local.", "My Service Name._http._tcp.local."))
+ print " Query done."
+ print "4. Testing unregister of service information..."
+ r.unregisterService(info)
+ print " Unregister done."
+ r.close()
diff --git a/testing/mozbase/mozdevice/mozdevice/__init__.py b/testing/mozbase/mozdevice/mozdevice/__init__.py
new file mode 100644
index 000000000..2493f75db
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/__init__.py
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from adb import ADBError, ADBRootError, ADBTimeoutError, ADBProcess, ADBCommand, ADBHost, ADBDevice
+from adb_android import ADBAndroid
+from adb_b2g import ADBB2G
+from devicemanager import DeviceManager, DMError, ZeroconfListener
+from devicemanagerADB import DeviceManagerADB
+from devicemanagerSUT import DeviceManagerSUT
+from droid import DroidADB, DroidSUT, DroidConnectByHWID
+
+__all__ = ['ADBError', 'ADBRootError', 'ADBTimeoutError', 'ADBProcess', 'ADBCommand', 'ADBHost',
+ 'ADBDevice', 'ADBAndroid', 'ADBB2G', 'DeviceManager', 'DMError', 'ZeroconfListener',
+ 'DeviceManagerADB', 'DeviceManagerSUT', 'DroidADB', 'DroidSUT', 'DroidConnectByHWID']
diff --git a/testing/mozbase/mozdevice/mozdevice/adb.py b/testing/mozbase/mozdevice/mozdevice/adb.py
new file mode 100644
index 000000000..5958937d9
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/adb.py
@@ -0,0 +1,2271 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import posixpath
+import re
+import shutil
+import subprocess
+import tempfile
+import time
+import traceback
+
+from abc import ABCMeta, abstractmethod
+from distutils import dir_util
+
+
+class ADBProcess(object):
+ """ADBProcess encapsulates the data related to executing the adb process."""
+
+ def __init__(self, args):
+ #: command argument argument list.
+ self.args = args
+ #: Temporary file handle to be used for stdout.
+ self.stdout_file = tempfile.TemporaryFile()
+ #: boolean indicating if the command timed out.
+ self.timedout = None
+ #: exitcode of the process.
+ self.exitcode = None
+ #: subprocess Process object used to execute the command.
+ self.proc = subprocess.Popen(args,
+ stdout=self.stdout_file,
+ stderr=subprocess.STDOUT)
+
+ @property
+ def stdout(self):
+ """Return the contents of stdout."""
+ if not self.stdout_file or self.stdout_file.closed:
+ content = ""
+ else:
+ self.stdout_file.seek(0, os.SEEK_SET)
+ content = self.stdout_file.read().rstrip()
+ return content
+
+ def __str__(self):
+ return ('args: %s, exitcode: %s, stdout: %s' % (
+ ' '.join(self.args), self.exitcode, self.stdout))
+
+# ADBError, ADBRootError, and ADBTimeoutError are treated
+# differently in order that unhandled ADBRootErrors and
+# ADBTimeoutErrors can be handled distinctly from ADBErrors.
+
+
+class ADBError(Exception):
+ """ADBError is raised in situations where a command executed on a
+ device either exited with a non-zero exitcode or when an
+ unexpected error condition has occurred. Generally, ADBErrors can
+ be handled and the device can continue to be used.
+ """
+ pass
+
+
+class ADBListDevicesError(ADBError):
+ """ADBListDevicesError is raised when errors are found listing the
+ devices, typically not any permissions.
+
+ The devices information is stocked with the *devices* member.
+ """
+
+ def __init__(self, msg, devices):
+ ADBError.__init__(self, msg)
+ self.devices = devices
+
+
+class ADBRootError(Exception):
+ """ADBRootError is raised when a shell command is to be executed as
+ root but the device does not support it. This error is fatal since
+ there is no recovery possible by the script. You must either root
+ your device or change your scripts to not require running as root.
+ """
+ pass
+
+
+class ADBTimeoutError(Exception):
+ """ADBTimeoutError is raised when either a host command or shell
+ command takes longer than the specified timeout to execute. The
+ timeout value is set in the ADBCommand constructor and is 300 seconds by
+ default. This error is typically fatal since the host is having
+ problems communicating with the device. You may be able to recover
+ by rebooting, but this is not guaranteed.
+
+ Recovery options are:
+
+ * Killing and restarting the adb server via
+ ::
+
+ adb kill-server; adb start-server
+
+ * Rebooting the device manually.
+ * Rebooting the host.
+ """
+ pass
+
+
+class ADBCommand(object):
+ """ADBCommand provides a basic interface to adb commands
+ which is used to provide the 'command' methods for the
+ classes ADBHost and ADBDevice.
+
+ ADBCommand should only be used as the base class for other
+ classes and should not be instantiated directly. To enforce this
+ restriction calling ADBCommand's constructor will raise a
+ NonImplementedError exception.
+
+ ::
+
+ from mozdevice import ADBCommand
+
+ try:
+ adbcommand = ADBCommand()
+ except NotImplementedError:
+ print "ADBCommand can not be instantiated."
+ """
+
+ def __init__(self,
+ adb='adb',
+ adb_host=None,
+ adb_port=None,
+ logger_name='adb',
+ timeout=300,
+ verbose=False):
+ """Initializes the ADBCommand object.
+
+ :param str adb: path to adb executable. Defaults to 'adb'.
+ :param adb_host: host of the adb server.
+ :type adb_host: str or None
+ :param adb_port: port of the adb server.
+ :type adb_port: integer or None
+ :param str logger_name: logging logger name. Defaults to 'adb'.
+
+ :raises: * ADBError
+ * ADBTimeoutError
+ """
+ if self.__class__ == ADBCommand:
+ raise NotImplementedError
+
+ self._logger = self._get_logger(logger_name)
+ self._verbose = verbose
+ self._adb_path = adb
+ self._adb_host = adb_host
+ self._adb_port = adb_port
+ self._timeout = timeout
+ self._polling_interval = 0.1
+ self._adb_version = ''
+
+ self._logger.debug("%s: %s" % (self.__class__.__name__,
+ self.__dict__))
+
+ # catch early a missing or non executable adb command
+ # and get the adb version while we are at it.
+ try:
+ output = subprocess.Popen([adb, 'version'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE).communicate()
+ re_version = re.compile(r'Android Debug Bridge version (.*)')
+ self._adb_version = re_version.match(output[0]).group(1)
+ except Exception as exc:
+ raise ADBError('%s: %s is not executable.' % (exc, adb))
+
+ def _get_logger(self, logger_name):
+ logger = None
+ try:
+ import mozlog
+ logger = mozlog.get_default_logger(logger_name)
+ except ImportError:
+ pass
+
+ if logger is None:
+ import logging
+ logger = logging.getLogger(logger_name)
+ return logger
+
+ # Host Command methods
+
+ def command(self, cmds, device_serial=None, timeout=None):
+ """Executes an adb command on the host.
+
+ :param list cmds: The command and its arguments to be
+ executed.
+ :param device_serial: The device's
+ serial number if the adb command is to be executed against
+ a specific device.
+ :type device_serial: str or None
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError. This timeout is per adb call. The
+ total time spent may exceed this value. If it is not
+ specified, the value set in the ADBCommand constructor is used.
+ :type timeout: integer or None
+ :returns: :class:`mozdevice.ADBProcess`
+
+ command() provides a low level interface for executing
+ commands on the host via adb.
+
+ command() executes on the host in such a fashion that stdout
+ of the adb process is a file handle on the host and
+ the exit code is available as the exit code of the adb
+ process.
+
+ The caller provides a list containing commands, as well as a
+ timeout period in seconds.
+
+ A subprocess is spawned to execute adb with stdout and stderr
+ directed to a temporary file. If the process takes longer than
+ the specified timeout, the process is terminated.
+
+ It is the caller's responsibilty to clean up by closing
+ the stdout temporary file.
+ """
+ args = [self._adb_path]
+ if self._adb_host:
+ args.extend(['-H', self._adb_host])
+ if self._adb_port:
+ args.extend(['-P', str(self._adb_port)])
+ if device_serial:
+ args.extend(['-s', device_serial, 'wait-for-device'])
+ args.extend(cmds)
+
+ adb_process = ADBProcess(args)
+
+ if timeout is None:
+ timeout = self._timeout
+
+ start_time = time.time()
+ adb_process.exitcode = adb_process.proc.poll()
+ while ((time.time() - start_time) <= timeout and
+ adb_process.exitcode is None):
+ time.sleep(self._polling_interval)
+ adb_process.exitcode = adb_process.proc.poll()
+ if adb_process.exitcode is None:
+ adb_process.proc.kill()
+ adb_process.timedout = True
+ adb_process.exitcode = adb_process.proc.poll()
+
+ adb_process.stdout_file.seek(0, os.SEEK_SET)
+
+ return adb_process
+
+ def command_output(self, cmds, device_serial=None, timeout=None):
+ """Executes an adb command on the host returning stdout.
+
+ :param list cmds: The command and its arguments to be
+ executed.
+ :param device_serial: The device's
+ serial number if the adb command is to be executed against
+ a specific device.
+ :type device_serial: str or None
+ :param timeout: The maximum time in seconds
+ for any spawned adb process to complete before throwing
+ an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBCommand constructor is used.
+ :type timeout: integer or None
+ :returns: string - content of stdout.
+
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ adb_process = None
+ try:
+ # Need to force the use of the ADBCommand class's command
+ # since ADBDevice will redefine command and call its
+ # own version otherwise.
+ adb_process = ADBCommand.command(self, cmds,
+ device_serial=device_serial,
+ timeout=timeout)
+ if adb_process.timedout:
+ raise ADBTimeoutError("%s" % adb_process)
+ elif adb_process.exitcode:
+ raise ADBError("%s" % adb_process)
+ output = adb_process.stdout_file.read().rstrip()
+ if self._verbose:
+ self._logger.debug('command_output: %s, '
+ 'timeout: %s, '
+ 'timedout: %s, '
+ 'exitcode: %s, output: %s' %
+ (' '.join(adb_process.args),
+ timeout,
+ adb_process.timedout,
+ adb_process.exitcode,
+ output))
+
+ return output
+ finally:
+ if adb_process and isinstance(adb_process.stdout_file, file):
+ adb_process.stdout_file.close()
+
+
+class ADBHost(ADBCommand):
+ """ADBHost provides a basic interface to adb host commands
+ which do not target a specific device.
+
+ ::
+
+ from mozdevice import ADBHost
+
+ adbhost = ADBHost()
+ adbhost.start_server()
+ """
+
+ def __init__(self,
+ adb='adb',
+ adb_host=None,
+ adb_port=None,
+ logger_name='adb',
+ timeout=300,
+ verbose=False):
+ """Initializes the ADBHost object.
+
+ :param str adb: path to adb executable. Defaults to 'adb'.
+ :param adb_host: host of the adb server.
+ :type adb_host: str or None
+ :param adb_port: port of the adb server.
+ :type adb_port: integer or None
+ :param str logger_name: logging logger name. Defaults to 'adb'.
+
+ :raises: * ADBError
+ * ADBTimeoutError
+ """
+ ADBCommand.__init__(self, adb=adb, adb_host=adb_host,
+ adb_port=adb_port, logger_name=logger_name,
+ timeout=timeout, verbose=verbose)
+
+ def command(self, cmds, timeout=None):
+ """Executes an adb command on the host.
+
+ :param list cmds: The command and its arguments to be
+ executed.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError. This timeout is per adb call. The
+ total time spent may exceed this value. If it is not
+ specified, the value set in the ADBHost constructor is used.
+ :type timeout: integer or None
+ :returns: :class:`mozdevice.ADBProcess`
+
+ command() provides a low level interface for executing
+ commands on the host via adb.
+
+ command() executes on the host in such a fashion that stdout
+ of the adb process is a file handle on the host and
+ the exit code is available as the exit code of the adb
+ process.
+
+ The caller provides a list containing commands, as well as a
+ timeout period in seconds.
+
+ A subprocess is spawned to execute adb with stdout and stderr
+ directed to a temporary file. If the process takes longer than
+ the specified timeout, the process is terminated.
+
+ It is the caller's responsibilty to clean up by closing
+ the stdout temporary file.
+ """
+ return ADBCommand.command(self, cmds, timeout=timeout)
+
+ def command_output(self, cmds, timeout=None):
+ """Executes an adb command on the host returning stdout.
+
+ :param list cmds: The command and its arguments to be
+ executed.
+ :param timeout: The maximum time in seconds
+ for any spawned adb process to complete before throwing
+ an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBHost constructor is used.
+ :type timeout: integer or None
+ :returns: string - content of stdout.
+
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ return ADBCommand.command_output(self, cmds, timeout=timeout)
+
+ def start_server(self, timeout=None):
+ """Starts the adb server.
+
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError. This timeout is per adb call. The
+ total time spent may exceed this value. If it is not
+ specified, the value set in the ADBHost constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+
+ Attempting to use start_server with any adb_host value other than None
+ will fail with an ADBError exception.
+
+ You will need to start the server on the remote host via the command:
+
+ .. code-block:: shell
+
+ adb -a fork-server server
+
+ If you wish the remote adb server to restart automatically, you can
+ enclose the command in a loop as in:
+
+ .. code-block:: shell
+
+ while true; do
+ adb -a fork-server server
+ done
+ """
+ self.command_output(["start-server"], timeout=timeout)
+
+ def kill_server(self, timeout=None):
+ """Kills the adb server.
+
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError. This timeout is per adb call. The
+ total time spent may exceed this value. If it is not
+ specified, the value set in the ADBHost constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ self.command_output(["kill-server"], timeout=timeout)
+
+ def devices(self, timeout=None):
+ """Executes adb devices -l and returns a list of objects describing attached devices.
+
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError. This timeout is per adb call. The
+ total time spent may exceed this value. If it is not
+ specified, the value set in the ADBHost constructor is used.
+ :type timeout: integer or None
+ :returns: an object contain
+ :raises: * ADBTimeoutError
+ * ADBListDevicesError
+ * ADBError
+
+ The output of adb devices -l ::
+
+ $ adb devices -l
+ List of devices attached
+ b313b945 device usb:1-7 product:d2vzw model:SCH_I535 device:d2vzw
+
+ is parsed and placed into an object as in
+
+ [{'device_serial': 'b313b945', 'state': 'device', 'product': 'd2vzw',
+ 'usb': '1-7', 'device': 'd2vzw', 'model': 'SCH_I535' }]
+ """
+ # b313b945 device usb:1-7 product:d2vzw model:SCH_I535 device:d2vzw
+ # from Android system/core/adb/transport.c statename()
+ re_device_info = re.compile(
+ r"([^\s]+)\s+(offline|bootloader|device|host|recovery|sideload|"
+ "no permissions|unauthorized|unknown)")
+ devices = []
+ lines = self.command_output(["devices", "-l"], timeout=timeout).split('\n')
+ for line in lines:
+ if line == 'List of devices attached ':
+ continue
+ match = re_device_info.match(line)
+ if match:
+ device = {
+ 'device_serial': match.group(1),
+ 'state': match.group(2)
+ }
+ remainder = line[match.end(2):].strip()
+ if remainder:
+ try:
+ device.update(dict([j.split(':')
+ for j in remainder.split(' ')]))
+ except ValueError:
+ self._logger.warning('devices: Unable to parse '
+ 'remainder for device %s' % line)
+ devices.append(device)
+ for device in devices:
+ if device['state'] == 'no permissions':
+ raise ADBListDevicesError(
+ "No permissions to detect devices. You should restart the"
+ " adb server as root:\n"
+ "\n# adb kill-server\n# adb start-server\n"
+ "\nor maybe configure your udev rules.",
+ devices)
+ return devices
+
+
+class ADBDevice(ADBCommand):
+ """ADBDevice is an abstract base class which provides methods which
+ can be used to interact with the associated Android or B2G based
+ device. It must be used via one of the concrete implementations in
+ :class:`ADBAndroid` or :class:`ADBB2G`.
+ """
+ __metaclass__ = ABCMeta
+
+ def __init__(self,
+ device=None,
+ adb='adb',
+ adb_host=None,
+ adb_port=None,
+ test_root='',
+ logger_name='adb',
+ timeout=300,
+ verbose=False,
+ device_ready_retry_wait=20,
+ device_ready_retry_attempts=3):
+ """Initializes the ADBDevice object.
+
+ :param device: When a string is passed, it is interpreted as the
+ device serial number. This form is not compatible with
+ devices containing a ":" in the serial; in this case
+ ValueError will be raised.
+ When a dictionary is passed it must have one or both of
+ the keys "device_serial" and "usb". This is compatible
+ with the dictionaries in the list returned by
+ ADBHost.devices(). If the value of device_serial is a
+ valid serial not containing a ":" it will be used to
+ identify the device, otherwise the value of the usb key,
+ prefixed with "usb:" is used.
+ If None is passed and there is exactly one device attached
+ to the host, that device is used. If there is more than one
+ device attached, ValueError is raised. If no device is
+ attached the constructor will block until a device is
+ attached or the timeout is reached.
+ :type device: dict, str or None
+ :param adb_host: host of the adb server to connect to.
+ :type adb_host: str or None
+ :param adb_port: port of the adb server to connect to.
+ :type adb_port: integer or None
+ :param str logger_name: logging logger name. Defaults to 'adb'.
+ :param integer device_ready_retry_wait: number of seconds to wait
+ between attempts to check if the device is ready after a
+ reboot.
+ :param integer device_ready_retry_attempts: number of attempts when
+ checking if a device is ready.
+
+ :raises: * ADBError
+ * ADBTimeoutError
+ * ValueError
+ """
+ ADBCommand.__init__(self, adb=adb, adb_host=adb_host,
+ adb_port=adb_port, logger_name=logger_name,
+ timeout=timeout, verbose=verbose)
+ self._device_serial = self._get_device_serial(device)
+ self._initial_test_root = test_root
+ self._test_root = None
+ self._device_ready_retry_wait = device_ready_retry_wait
+ self._device_ready_retry_attempts = device_ready_retry_attempts
+ self._have_root_shell = False
+ self._have_su = False
+ self._have_android_su = False
+
+ # Catch exceptions due to the potential for segfaults
+ # calling su when using an improperly rooted device.
+
+ # Note this check to see if adbd is running is performed on
+ # the device in the state it exists in when the ADBDevice is
+ # initialized. It may be the case that it has been manipulated
+ # since its last boot and that its current state does not
+ # match the state the device will have immediately after a
+ # reboot. For example, if adb root was called manually prior
+ # to ADBDevice being initialized, then self._have_root_shell
+ # will not reflect the state of the device after it has been
+ # rebooted again. Therefore this check will need to be
+ # performed again after a reboot.
+
+ self._check_adb_root(timeout=timeout)
+
+ uid = 'uid=0'
+ # Do we have a 'Superuser' sh like su?
+ try:
+ if self.shell_output("su -c id", timeout=timeout).find(uid) != -1:
+ self._have_su = True
+ self._logger.info("su -c supported")
+ except ADBError:
+ self._logger.debug("Check for su -c failed")
+
+ # Do we have Android's su?
+ try:
+ if self.shell_output("su 0 id", timeout=timeout).find(uid) != -1:
+ self._have_android_su = True
+ self._logger.info("su 0 supported")
+ except ADBError:
+ self._logger.debug("Check for su 0 failed")
+
+ self._mkdir_p = None
+ # Force the use of /system/bin/ls or /system/xbin/ls in case
+ # there is /sbin/ls which embeds ansi escape codes to colorize
+ # the output. Detect if we are using busybox ls. We want each
+ # entry on a single line and we don't want . or ..
+ if self.shell_bool("/system/bin/ls /data/local/tmp", timeout=timeout):
+ self._ls = "/system/bin/ls"
+ elif self.shell_bool("/system/xbin/ls /data/local/tmp", timeout=timeout):
+ self._ls = "/system/xbin/ls"
+ else:
+ raise ADBError("ADBDevice.__init__: ls not found")
+ try:
+ self.shell_output("%s -1A /data/local/tmp" % self._ls, timeout=timeout)
+ self._ls += " -1A"
+ except ADBError:
+ self._ls += " -a"
+
+ self._logger.info("%s supported" % self._ls)
+
+ # Do we have cp?
+ self._have_cp = self.shell_bool("type cp", timeout=timeout)
+ self._logger.info("Native cp support: %s" % self._have_cp)
+
+ # Do we have chmod -R?
+ try:
+ self._chmod_R = False
+ re_recurse = re.compile(r'[-]R')
+ chmod_output = self.shell_output("chmod --help", timeout=timeout)
+ match = re_recurse.search(chmod_output)
+ if match:
+ self._chmod_R = True
+ except (ADBError, ADBTimeoutError) as e:
+ self._logger.debug('Check chmod -R: %s' % e)
+ match = re_recurse.search(e.message)
+ if match:
+ self._chmod_R = True
+ self._logger.info("Native chmod -R support: %s" % self._chmod_R)
+
+ self._logger.debug("ADBDevice: %s" % self.__dict__)
+
+ def _get_device_serial(self, device):
+ if device is None:
+ devices = ADBHost(adb=self._adb_path, adb_host=self._adb_host,
+ adb_port=self._adb_port).devices()
+ if len(devices) > 1:
+ raise ValueError("ADBDevice called with multiple devices "
+ "attached and no device specified")
+ elif len(devices) == 0:
+ # We could error here, but this way we'll wait-for-device before we next
+ # run a command, which seems more friendly
+ return
+ device = devices[0]
+
+ def is_valid_serial(serial):
+ return ":" not in serial or serial.startswith("usb:")
+
+ if isinstance(device, (str, unicode)):
+ # Treat this as a device serial
+ if not is_valid_serial(device):
+ raise ValueError("Device serials containing ':' characters are "
+ "invalid. Pass the output from "
+ "ADBHost.devices() for the device instead")
+ return device
+
+ serial = device.get("device_serial")
+ if serial is not None and is_valid_serial(serial):
+ return serial
+ usb = device.get("usb")
+ if usb is not None:
+ return "usb:%s" % usb
+
+ raise ValueError("Unable to get device serial")
+
+ def _check_adb_root(self, timeout=None):
+ self._have_root_shell = False
+ uid = 'uid=0'
+ # Is shell already running as root?
+ try:
+ if self.shell_output("id", timeout=timeout).find(uid) != -1:
+ self._have_root_shell = True
+ self._logger.info("adbd running as root")
+ except ADBError:
+ self._logger.debug("Check for root shell failed")
+
+ # Do we need to run adb root to get a root shell?
+ try:
+ if (not self._have_root_shell and self.command_output(
+ ["root"],
+ timeout=timeout).find("cannot run as root") == -1):
+ self._have_root_shell = True
+ self._logger.info("adbd restarted as root")
+ except ADBError:
+ self._logger.debug("Check for root adbd failed")
+
+ @staticmethod
+ def _escape_command_line(cmd):
+ """Utility function to return escaped and quoted version of command
+ line.
+ """
+ quoted_cmd = []
+
+ for arg in cmd:
+ arg.replace('&', r'\&')
+
+ needs_quoting = False
+ for char in [' ', '(', ')', '"', '&']:
+ if arg.find(char) >= 0:
+ needs_quoting = True
+ break
+ if needs_quoting:
+ arg = "'%s'" % arg
+
+ quoted_cmd.append(arg)
+
+ return " ".join(quoted_cmd)
+
+ @staticmethod
+ def _get_exitcode(file_obj):
+ """Get the exitcode from the last line of the file_obj for shell
+ commands.
+ """
+ file_obj.seek(0, os.SEEK_END)
+
+ line = ''
+ length = file_obj.tell()
+ offset = 1
+ while length - offset >= 0:
+ file_obj.seek(-offset, os.SEEK_END)
+ char = file_obj.read(1)
+ if not char:
+ break
+ if char != '\r' and char != '\n':
+ line = char + line
+ elif line:
+ # we have collected everything up to the beginning of the line
+ break
+ offset += 1
+
+ match = re.match(r'rc=([0-9]+)', line)
+ if match:
+ exitcode = int(match.group(1))
+ file_obj.seek(-1, os.SEEK_CUR)
+ file_obj.truncate()
+ else:
+ exitcode = None
+
+ return exitcode
+
+ @property
+ def test_root(self):
+ """
+ The test_root property returns the directory on the device where
+ temporary test files are stored.
+
+ The first time test_root it is called it determines and caches a value
+ for the test root on the device. It determines the appropriate test
+ root by attempting to create a 'dummy' directory on each of a list of
+ directories and returning the first successful directory as the
+ test_root value.
+
+ The default list of directories checked by test_root are:
+
+ - /storage/sdcard0/tests
+ - /storage/sdcard1/tests
+ - /sdcard/tests
+ - /mnt/sdcard/tests
+ - /data/local/tests
+
+ You may override the default list by providing a test_root argument to
+ the :class:`ADBDevice` constructor which will then be used when
+ attempting to create the 'dummy' directory.
+
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ * ADBError
+ """
+ if self._test_root is not None:
+ return self._test_root
+
+ if self._initial_test_root:
+ paths = [self._initial_test_root]
+ else:
+ paths = ['/storage/sdcard0/tests',
+ '/storage/sdcard1/tests',
+ '/sdcard/tests',
+ '/mnt/sdcard/tests',
+ '/data/local/tests']
+
+ max_attempts = 3
+ for attempt in range(1, max_attempts + 1):
+ for test_root in paths:
+ self._logger.debug("Setting test root to %s attempt %d of %d" %
+ (test_root, attempt, max_attempts))
+
+ if self._try_test_root(test_root):
+ self._test_root = test_root
+ return self._test_root
+
+ self._logger.debug('_setup_test_root: '
+ 'Attempt %d of %d failed to set test_root to %s' %
+ (attempt, max_attempts, test_root))
+
+ if attempt != max_attempts:
+ time.sleep(20)
+
+ raise ADBError("Unable to set up test root using paths: [%s]"
+ % ", ".join(paths))
+
+ def _try_test_root(self, test_root):
+ base_path, sub_path = posixpath.split(test_root)
+ if not self.is_dir(base_path):
+ return False
+
+ try:
+ dummy_dir = posixpath.join(test_root, 'dummy')
+ if self.is_dir(dummy_dir):
+ self.rm(dummy_dir, recursive=True)
+ self.mkdir(dummy_dir, parents=True)
+ except ADBError:
+ self._logger.debug("%s is not writable" % test_root)
+ return False
+
+ return True
+
+ # Host Command methods
+
+ def command(self, cmds, timeout=None):
+ """Executes an adb command on the host against the device.
+
+ :param list cmds: The command and its arguments to be
+ executed.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError. This timeout is per adb call. The
+ total time spent may exceed this value. If it is not
+ specified, the value set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :returns: :class:`mozdevice.ADBProcess`
+
+ command() provides a low level interface for executing
+ commands for a specific device on the host via adb.
+
+ command() executes on the host in such a fashion that stdout
+ of the adb process are file handles on the host and
+ the exit code is available as the exit code of the adb
+ process.
+
+ For executing shell commands on the device, use
+ ADBDevice.shell(). The caller provides a list containing
+ commands, as well as a timeout period in seconds.
+
+ A subprocess is spawned to execute adb for the device with
+ stdout and stderr directed to a temporary file. If the process
+ takes longer than the specified timeout, the process is
+ terminated.
+
+ It is the caller's responsibilty to clean up by closing
+ the stdout temporary file.
+ """
+
+ return ADBCommand.command(self, cmds,
+ device_serial=self._device_serial,
+ timeout=timeout)
+
+ def command_output(self, cmds, timeout=None):
+ """Executes an adb command on the host against the device returning
+ stdout.
+
+ :param list cmds: The command and its arguments to be executed.
+ :param timeout: The maximum time in seconds
+ for any spawned adb process to complete before throwing
+ an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :returns: string - content of stdout.
+
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ return ADBCommand.command_output(self, cmds,
+ device_serial=self._device_serial,
+ timeout=timeout)
+
+ # Port forwarding methods
+
+ def _validate_port(self, port, is_local=True):
+ """Validate a port forwarding specifier. Raises ValueError on failure.
+
+ :param str port: The port specifier to validate
+ :param bool is_local: Flag indicating whether the port represents a local port.
+ """
+ prefixes = ["tcp", "localabstract", "localreserved", "localfilesystem", "dev"]
+
+ if not is_local:
+ prefixes += ["jdwp"]
+
+ parts = port.split(":", 1)
+ if len(parts) != 2 or parts[0] not in prefixes:
+ raise ValueError("Invalid forward specifier %s" % port)
+
+ def forward(self, local, remote, allow_rebind=True, timeout=None):
+ """Forward a local port to a specific port on the device.
+
+ Ports are specified in the form:
+ tcp:<port>
+ localabstract:<unix domain socket name>
+ localreserved:<unix domain socket name>
+ localfilesystem:<unix domain socket name>
+ dev:<character device name>
+ jdwp:<process pid> (remote only)
+
+ :param str local: Local port to forward
+ :param str remote: Remote port to which to forward
+ :param bool allow_rebind: Don't error if the local port is already forwarded
+ :param timeout: The maximum time in seconds
+ for any spawned adb process to complete before throwing
+ an ADBTimeoutError. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :raises: * ValueError
+ * ADBTimeoutError
+ * ADBError
+ """
+
+ for port, is_local in [(local, True), (remote, False)]:
+ self._validate_port(port, is_local=is_local)
+
+ cmd = ["forward", local, remote]
+ if not allow_rebind:
+ cmd.insert(1, "--no-rebind")
+ self.command_output(cmd, timeout=timeout)
+
+ def list_forwards(self, timeout=None):
+ """Return a list of tuples specifying active forwards
+
+ Return values are of the form (device, local, remote).
+
+ :param timeout: The maximum time in seconds
+ for any spawned adb process to complete before throwing
+ an ADBTimeoutError. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ forwards = self.command_output(["forward", "--list"], timeout=timeout)
+ return [tuple(line.split(" ")) for line in forwards.split("\n") if line.strip()]
+
+ def remove_forwards(self, local=None, timeout=None):
+ """Remove existing port forwards.
+
+ :param local: local port specifier as for ADBDevice.forward. If local
+ is not specified removes all forwards.
+ :type local: str or None
+ :param timeout: The maximum time in seconds
+ for any spawned adb process to complete before throwing
+ an ADBTimeoutError. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :raises: * ValueError
+ * ADBTimeoutError
+ * ADBError
+ """
+ cmd = ["forward"]
+ if local is None:
+ cmd.extend(["--remove-all"])
+ else:
+ self._validate_port(local, is_local=True)
+ cmd.extend(["--remove", local])
+
+ self.command_output(cmd, timeout=timeout)
+
+ # Device Shell methods
+
+ def shell(self, cmd, env=None, cwd=None, timeout=None, root=False):
+ """Executes a shell command on the device.
+
+ :param str cmd: The command to be executed.
+ :param env: Contains the environment variables and
+ their values.
+ :type env: dict or None
+ :param cwd: The directory from which to execute.
+ :type cwd: str or None
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError. This timeout is per adb call. The
+ total time spent may exceed this value. If it is not
+ specified, the value set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :returns: :class:`mozdevice.ADBProcess`
+ :raises: ADBRootError
+
+ shell() provides a low level interface for executing commands
+ on the device via adb shell.
+
+ shell() executes on the host in such as fashion that stdout
+ contains the stdout and stderr of the host abd process
+ combined with the stdout and stderr of the shell command
+ on the device. The exit code of shell() is the exit code of
+ the adb command if it was non-zero or the extracted exit code
+ from the output of the shell command executed on the
+ device.
+
+ The caller provides a flag indicating if the command is to be
+ executed as root, a string for any requested working
+ directory, a hash defining the environment, a string
+ containing shell commands, as well as a timeout period in
+ seconds.
+
+ The command line to be executed is created to set the current
+ directory, set the required environment variables, optionally
+ execute the command using su and to output the return code of
+ the command to stdout. The command list is created as a
+ command sequence separated by && which will terminate the
+ command sequence on the first command which returns a non-zero
+ exit code.
+
+ A subprocess is spawned to execute adb shell for the device
+ with stdout and stderr directed to a temporary file. If the
+ process takes longer than the specified timeout, the process
+ is terminated. The return code is extracted from the stdout
+ and is then removed from the file.
+
+ It is the caller's responsibilty to clean up by closing
+ the stdout temporary files.
+
+ """
+ if root and not self._have_root_shell:
+ # If root was requested and we do not already have a root
+ # shell, then use the appropriate version of su to invoke
+ # the shell cmd. Prefer Android's su version since it may
+ # falsely report support for su -c.
+ if self._have_android_su:
+ cmd = "su 0 %s" % cmd
+ elif self._have_su:
+ cmd = "su -c \"%s\"" % cmd
+ else:
+ raise ADBRootError('Can not run command %s as root!' % cmd)
+
+ # prepend cwd and env to command if necessary
+ if cwd:
+ cmd = "cd %s && %s" % (cwd, cmd)
+ if env:
+ envstr = '&& '.join(map(lambda x: 'export %s=%s' %
+ (x[0], x[1]), env.iteritems()))
+ cmd = envstr + "&& " + cmd
+ cmd += "; echo rc=$?"
+
+ args = [self._adb_path]
+ if self._adb_host:
+ args.extend(['-H', self._adb_host])
+ if self._adb_port:
+ args.extend(['-P', str(self._adb_port)])
+ if self._device_serial:
+ args.extend(['-s', self._device_serial])
+ args.extend(["wait-for-device", "shell", cmd])
+ adb_process = ADBProcess(args)
+
+ if timeout is None:
+ timeout = self._timeout
+
+ start_time = time.time()
+ exitcode = adb_process.proc.poll()
+ while ((time.time() - start_time) <= timeout) and exitcode is None:
+ time.sleep(self._polling_interval)
+ exitcode = adb_process.proc.poll()
+ if exitcode is None:
+ adb_process.proc.kill()
+ adb_process.timedout = True
+ adb_process.exitcode = adb_process.proc.poll()
+ elif exitcode == 0:
+ adb_process.exitcode = self._get_exitcode(adb_process.stdout_file)
+ else:
+ adb_process.exitcode = exitcode
+
+ adb_process.stdout_file.seek(0, os.SEEK_SET)
+
+ return adb_process
+
+ def shell_bool(self, cmd, env=None, cwd=None, timeout=None, root=False):
+ """Executes a shell command on the device returning True on success
+ and False on failure.
+
+ :param str cmd: The command to be executed.
+ :param env: Contains the environment variables and
+ their values.
+ :type env: dict or None
+ :param cwd: The directory from which to execute.
+ :type cwd: str or None
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :returns: boolean
+
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ """
+ adb_process = None
+ try:
+ adb_process = self.shell(cmd, env=env, cwd=cwd,
+ timeout=timeout, root=root)
+ if adb_process.timedout:
+ raise ADBTimeoutError("%s" % adb_process)
+ return adb_process.exitcode == 0
+ finally:
+ if adb_process:
+ adb_process.stdout_file.close()
+
+ def shell_output(self, cmd, env=None, cwd=None, timeout=None, root=False):
+ """Executes an adb shell on the device returning stdout.
+
+ :param str cmd: The command to be executed.
+ :param env: Contains the environment variables and their values.
+ :type env: dict or None
+ :param cwd: The directory from which to execute.
+ :type cwd: str or None
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError. This timeout is per
+ adb call. The total time spent may exceed this
+ value. If it is not specified, the value set
+ in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command
+ should be executed as root.
+ :returns: string - content of stdout.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ * ADBError
+ """
+ adb_process = None
+ try:
+ adb_process = self.shell(cmd, env=env, cwd=cwd,
+ timeout=timeout, root=root)
+ if adb_process.timedout:
+ raise ADBTimeoutError("%s" % adb_process)
+ elif adb_process.exitcode:
+ raise ADBError("%s" % adb_process)
+ output = adb_process.stdout_file.read().rstrip()
+ if self._verbose:
+ self._logger.debug('shell_output: %s, '
+ 'timeout: %s, '
+ 'root: %s, '
+ 'timedout: %s, '
+ 'exitcode: %s, '
+ 'output: %s' %
+ (' '.join(adb_process.args),
+ timeout,
+ root,
+ adb_process.timedout,
+ adb_process.exitcode,
+ output))
+
+ return output
+ finally:
+ if adb_process and isinstance(adb_process.stdout_file, file):
+ adb_process.stdout_file.close()
+
+ # Informational methods
+
+ def _get_logcat_buffer_args(self, buffers):
+ valid_buffers = set(['radio', 'main', 'events'])
+ invalid_buffers = set(buffers).difference(valid_buffers)
+ if invalid_buffers:
+ raise ADBError('Invalid logcat buffers %s not in %s ' % (
+ list(invalid_buffers), list(valid_buffers)))
+ args = []
+ for b in buffers:
+ args.extend(['-b', b])
+ return args
+
+ def clear_logcat(self, timeout=None, buffers=[]):
+ """Clears logcat via adb logcat -c.
+
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError. This timeout is per
+ adb call. The total time spent may exceed this
+ value. If it is not specified, the value set
+ in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param list buffers: Log buffers to clear. Valid buffers are
+ "radio", "events", and "main". Defaults to "main".
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ buffers = self._get_logcat_buffer_args(buffers)
+ cmds = ["logcat", "-c"] + buffers
+ self.command_output(cmds, timeout=timeout)
+ self.shell_output("log logcat cleared", timeout=timeout)
+
+ def get_logcat(self,
+ filter_specs=[
+ "dalvikvm:I",
+ "ConnectivityService:S",
+ "WifiMonitor:S",
+ "WifiStateTracker:S",
+ "wpa_supplicant:S",
+ "NetworkStateTracker:S"],
+ format="time",
+ filter_out_regexps=[],
+ timeout=None,
+ buffers=[]):
+ """Returns the contents of the logcat file as a list of strings.
+
+ :param list filter_specs: Optional logcat messages to
+ be included.
+ :param str format: Optional logcat format.
+ :param list filterOutRexps: Optional logcat messages to be
+ excluded.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param list buffers: Log buffers to retrieve. Valid buffers are
+ "radio", "events", and "main". Defaults to "main".
+ :returns: list of lines logcat output.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ buffers = self._get_logcat_buffer_args(buffers)
+ cmds = ["logcat", "-v", format, "-d"] + buffers + filter_specs
+ lines = self.command_output(cmds, timeout=timeout).split('\r')
+
+ for regex in filter_out_regexps:
+ lines = [line for line in lines if not re.search(regex, line)]
+
+ return lines
+
+ def get_prop(self, prop, timeout=None):
+ """Gets value of a property from the device via adb shell getprop.
+
+ :param str prop: The propery name.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :returns: string value of property.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ output = self.shell_output('getprop %s' % prop, timeout=timeout)
+ return output
+
+ def get_state(self, timeout=None):
+ """Returns the device's state via adb get-state.
+
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before throwing
+ an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :returns: string value of adb get-state.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ output = self.command_output(["get-state"], timeout=timeout).strip()
+ return output
+
+ def get_ip_address(self, interfaces=None, timeout=None):
+ """Returns the device's ip address, or None if it doesn't have one
+
+ :param interfaces: Interfaces to allow, or None to allow any
+ non-loopback interface.
+ :type interfaces: list or None
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before throwing
+ an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :returns: string ip address of the device or None if it could not
+ be found.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ if not interfaces:
+ interfaces = ["wlan0", "eth0"]
+ wifi_interface = self.shell_output('getprop wifi.interface', timeout=timeout)
+ self._logger.debug('get_ip_address: wifi_interface: %s' % wifi_interface)
+ if wifi_interface and wifi_interface not in interfaces:
+ interfaces = interfaces.append(wifi_interface)
+
+ # ifconfig interface
+ # can return two different formats:
+ # eth0: ip 192.168.1.139 mask 255.255.255.0 flags [up broadcast running multicast]
+ # or
+ # wlan0 Link encap:Ethernet HWaddr 00:9A:CD:B8:39:65
+ # inet addr:192.168.1.38 Bcast:192.168.1.255 Mask:255.255.255.0
+ # inet6 addr: fe80::29a:cdff:feb8:3965/64 Scope: Link
+ # UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
+ # RX packets:180 errors:0 dropped:0 overruns:0 frame:0
+ # TX packets:218 errors:0 dropped:0 overruns:0 carrier:0
+ # collisions:0 txqueuelen:1000
+ # RX bytes:84577 TX bytes:31202
+
+ re1_ip = re.compile(r'(\w+): ip ([0-9.]+) mask.*')
+ # re1_ip will match output of the first format
+ # with group 1 returning the interface and group 2 returing the ip address.
+
+ # re2_interface will match the interface line in the second format
+ # while re2_ip will match the inet addr line of the second format.
+ re2_interface = re.compile(r'(\w+)\s+Link')
+ re2_ip = re.compile(r'\s+inet addr:([0-9.]+)')
+
+ matched_interface = None
+ matched_ip = None
+ re_bad_addr = re.compile(r'127.0.0.1|0.0.0.0')
+
+ self._logger.debug('get_ip_address: ifconfig')
+ for interface in interfaces:
+ try:
+ output = self.shell_output('ifconfig %s' % interface,
+ timeout=timeout)
+ except ADBError:
+ output = ''
+
+ for line in output.split("\n"):
+ if not matched_interface:
+ match = re1_ip.match(line)
+ if match:
+ matched_interface, matched_ip = match.groups()
+ else:
+ match = re2_interface.match(line)
+ if match:
+ matched_interface = match.group(1)
+ else:
+ match = re2_ip.match(line)
+ if match:
+ matched_ip = match.group(1)
+
+ if matched_ip:
+ if not re_bad_addr.match(matched_ip):
+ self._logger.debug('get_ip_address: found: %s %s' %
+ (matched_interface, matched_ip))
+ return matched_ip
+ matched_interface = None
+ matched_ip = None
+
+ self._logger.debug('get_ip_address: netcfg')
+ # Fall back on netcfg if ifconfig does not work.
+ # $ adb shell netcfg
+ # lo UP 127.0.0.1/8 0x00000049 00:00:00:00:00:00
+ # dummy0 DOWN 0.0.0.0/0 0x00000082 8e:cd:67:48:b7:c2
+ # rmnet0 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
+ # rmnet1 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
+ # rmnet2 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
+ # rmnet3 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
+ # rmnet4 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
+ # rmnet5 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
+ # rmnet6 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
+ # rmnet7 DOWN 0.0.0.0/0 0x00000000 00:00:00:00:00:00
+ # sit0 DOWN 0.0.0.0/0 0x00000080 00:00:00:00:00:00
+ # vip0 DOWN 0.0.0.0/0 0x00001012 00:01:00:00:00:01
+ # wlan0 UP 192.168.1.157/24 0x00001043 38:aa:3c:1c:f6:94
+
+ re3_netcfg = re.compile(r'(\w+)\s+UP\s+([1-9]\d{0,2}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
+ try:
+ output = self.shell_output('netcfg', timeout=timeout)
+ except ADBError:
+ output = ''
+ for line in output.split("\n"):
+ match = re3_netcfg.search(line)
+ if match:
+ matched_interface, matched_ip = match.groups()
+ if matched_interface == "lo" or re_bad_addr.match(matched_ip):
+ matched_interface = None
+ matched_ip = None
+ elif matched_ip and matched_interface in interfaces:
+ self._logger.debug('get_ip_address: found: %s %s' %
+ (matched_interface, matched_ip))
+ return matched_ip
+ self._logger.debug('get_ip_address: not found')
+ return matched_ip
+
+ # File management methods
+
+ def remount(self, timeout=None):
+ """Remount /system/ in read/write mode
+
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before throwing
+ an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError"""
+
+ rv = self.command_output(["remount"], timeout=timeout)
+ if not rv.startswith("remount succeeded"):
+ raise ADBError("Unable to remount device")
+
+ def chmod(self, path, recursive=False, mask="777", timeout=None, root=False):
+ """Recursively changes the permissions of a directory on the
+ device.
+
+ :param str path: The directory name on the device.
+ :param bool recursive: Flag specifying if the command should be
+ executed recursively.
+ :param str mask: The octal permissions.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before throwing
+ an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ * ADBError
+ """
+ # Note that on some tests such as webappstartup, an error
+ # occurs during recursive calls to chmod where a "No such file
+ # or directory" error will occur for the
+ # /data/data/org.mozilla.fennec/files/mozilla/*.webapp0/lock
+ # which is a symbolic link to a socket: lock ->
+ # 127.0.0.1:+<port>. On Linux, chmod -R ignores symbolic
+ # links but it appear Android's version does not. We ignore
+ # this type of error, but pass on any other errors that are
+ # detected.
+ path = posixpath.normpath(path.strip())
+ self._logger.debug('chmod: path=%s, recursive=%s, mask=%s, root=%s' %
+ (path, recursive, mask, root))
+ if not recursive:
+ self.shell_output("chmod %s %s" % (mask, path),
+ timeout=timeout, root=root)
+ return
+
+ if self._chmod_R:
+ try:
+ self.shell_output("chmod -R %s %s" % (mask, path),
+ timeout=timeout, root=root)
+ except ADBError as e:
+ if e.message.find('No such file or directory') == -1:
+ raise
+ self._logger.warning('chmod -R %s %s: Ignoring Error: %s' %
+ (mask, path, e.message))
+ return
+ # Obtain a list of the directories and files which match path
+ # and construct a shell script which explictly calls chmod on
+ # each of them.
+ entries = self.ls(path, recursive=recursive, timeout=timeout,
+ root=root)
+ tmpf = None
+ chmodsh = None
+ try:
+ tmpf = tempfile.NamedTemporaryFile(delete=False)
+ for entry in entries:
+ tmpf.write('chmod %s %s\n' % (mask, entry))
+ tmpf.close()
+ chmodsh = '/data/local/tmp/%s' % os.path.basename(tmpf.name)
+ self.push(tmpf.name, chmodsh)
+ self.shell_output('chmod 777 %s' % chmodsh, timeout=timeout,
+ root=root)
+ self.shell_output('sh -c %s' % chmodsh, timeout=timeout,
+ root=root)
+ finally:
+ if tmpf:
+ os.unlink(tmpf.name)
+ if chmodsh:
+ self.rm(chmodsh, timeout=timeout, root=root)
+
+ def exists(self, path, timeout=None, root=False):
+ """Returns True if the path exists on the device.
+
+ :param str path: The directory name on the device.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should be
+ executed as root.
+ :returns: boolean - True if path exists.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ """
+ path = posixpath.normpath(path)
+ return self.shell_bool('ls -a %s' % path, timeout=timeout, root=root)
+
+ def is_dir(self, path, timeout=None, root=False):
+ """Returns True if path is an existing directory on the device.
+
+ :param str path: The path on the device.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :returns: boolean - True if path exists on the device and is a
+ directory.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ """
+ path = posixpath.normpath(path)
+ return self.shell_bool('ls -a %s/' % path, timeout=timeout, root=root)
+
+ def is_file(self, path, timeout=None, root=False):
+ """Returns True if path is an existing file on the device.
+
+ :param str path: The file name on the device.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :returns: boolean - True if path exists on the device and is a
+ file.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ """
+ path = posixpath.normpath(path)
+ return (
+ self.exists(path, timeout=timeout, root=root) and
+ not self.is_dir(path, timeout=timeout, root=root))
+
+ def list_files(self, path, timeout=None, root=False):
+ """Return a list of files/directories contained in a directory
+ on the device.
+
+ :param str path: The directory name on the device.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :returns: list of files/directories contained in the directory.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ """
+ path = posixpath.normpath(path.strip())
+ data = []
+ if self.is_dir(path, timeout=timeout, root=root):
+ try:
+ data = self.shell_output("%s %s" % (self._ls, path),
+ timeout=timeout,
+ root=root).split('\r\n')
+ self._logger.debug('list_files: data: %s' % data)
+ except ADBError:
+ self._logger.error('Ignoring exception in ADBDevice.list_files\n%s' %
+ traceback.format_exc())
+ data[:] = [item for item in data if item]
+ self._logger.debug('list_files: %s' % data)
+ return data
+
+ def ls(self, path, recursive=False, timeout=None, root=False):
+ """Return a list of matching files/directories on the device.
+
+ The ls method emulates the behavior of the ls shell command.
+ It differs from the list_files method by supporting wild cards
+ and returning matches even if the path is not a directory and
+ by allowing a recursive listing.
+
+ ls /sdcard always returns /sdcard and not the contents of the
+ sdcard path. The ls method makes the behavior consistent with
+ others paths by adjusting /sdcard to /sdcard/. Note this is
+ also the case of other sdcard related paths such as
+ /storage/emulated/legacy but no adjustment is made in those
+ cases.
+
+ The ls method works around a Nexus 4 bug which prevents
+ recursive listing of directories on the sdcard unless the path
+ ends with "/*" by adjusting sdcard paths ending in "/" to end
+ with "/*". This adjustment is only made on official Nexus 4
+ builds with property ro.product.model "Nexus 4". Note that
+ this will fail to return any "hidden" files or directories
+ which begin with ".".
+
+ :param str path: The directory name on the device.
+ :param bool recursive: Flag specifying if a recursive listing
+ is to be returned. If recursive is False, the returned
+ matches will be relative to the path. If recursive is True,
+ the returned matches will be absolute paths.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :returns: list of files/directories contained in the directory.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ """
+ path = posixpath.normpath(path.strip())
+ parent = ''
+ entries = {}
+
+ if path == '/sdcard':
+ path += '/'
+
+ # Android 2.3 and later all appear to support ls -R however
+ # Nexus 4 does not perform a recursive search on the sdcard
+ # unless the path is a directory with * wild card.
+ if not recursive:
+ recursive_flag = ''
+ else:
+ recursive_flag = '-R'
+ if path.startswith('/sdcard') and path.endswith('/'):
+ model = self.shell_output('getprop ro.product.model',
+ timeout=timeout,
+ root=root)
+ if model == 'Nexus 4':
+ path += '*'
+ lines = self.shell_output('%s %s %s' % (self._ls, recursive_flag, path),
+ timeout=timeout,
+ root=root).split('\r\n')
+ for line in lines:
+ line = line.strip()
+ if not line:
+ parent = ''
+ continue
+ if line.endswith(':'): # This is a directory
+ parent = line.replace(':', '/')
+ entry = parent
+ # Remove earlier entry which is marked as a file.
+ if parent[:-1] in entries:
+ del entries[parent[:-1]]
+ elif parent:
+ entry = "%s%s" % (parent, line)
+ else:
+ entry = line
+ entries[entry] = 1
+ entry_list = entries.keys()
+ entry_list.sort()
+ return entry_list
+
+ def mkdir(self, path, parents=False, timeout=None, root=False):
+ """Create a directory on the device.
+
+ :param str path: The directory name on the device
+ to be created.
+ :param bool parents: Flag indicating if the parent directories are
+ also to be created. Think mkdir -p path.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ * ADBError
+ """
+ path = posixpath.normpath(path)
+ if parents:
+ if self._mkdir_p is None or self._mkdir_p:
+ # Use shell_bool to catch the possible
+ # non-zero exitcode if -p is not supported.
+ if self.shell_bool('mkdir -p %s' % path, timeout=timeout,
+ root=root):
+ self._mkdir_p = True
+ return
+ # mkdir -p is not supported. create the parent
+ # directories individually.
+ if not self.is_dir(posixpath.dirname(path), root=root):
+ parts = path.split('/')
+ name = "/"
+ for part in parts[:-1]:
+ if part != "":
+ name = posixpath.join(name, part)
+ if not self.is_dir(name, root=root):
+ # Use shell_output to allow any non-zero
+ # exitcode to raise an ADBError.
+ self.shell_output('mkdir %s' % name,
+ timeout=timeout, root=root)
+
+ # If parents is True and the directory does exist, we don't
+ # need to do anything. Otherwise we call mkdir. If the
+ # directory already exists or if it is a file instead of a
+ # directory, mkdir will fail and we will raise an ADBError.
+ if not parents or not self.is_dir(path, root=root):
+ self.shell_output('mkdir %s' % path, timeout=timeout, root=root)
+ if not self.is_dir(path, timeout=timeout, root=root):
+ raise ADBError('mkdir %s Failed' % path)
+
+ def push(self, local, remote, timeout=None):
+ """Pushes a file or directory to the device.
+
+ :param str local: The name of the local file or
+ directory name.
+ :param str remote: The name of the remote file or
+ directory name.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ # remove trailing /
+ local = os.path.normpath(local)
+ remote = os.path.normpath(remote)
+ copy_required = False
+ if self._adb_version >= '1.0.36' and \
+ os.path.isdir(local) and self.is_dir(remote):
+ # 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 the destination directory does exist, push to its
+ # parent directory. If the source and destination leaf
+ # directory names are different, copy the source directory
+ # to a temporary directory with the same leaf name as the
+ # destination so that when we push to the parent, the
+ # source is copied onto the destination directory.
+ local_name = os.path.basename(local)
+ remote_name = os.path.basename(remote)
+ if local_name != remote_name:
+ copy_required = True
+ temp_parent = tempfile.mkdtemp()
+ new_local = os.path.join(temp_parent, remote_name)
+ dir_util.copy_tree(local, new_local)
+ local = new_local
+ remote = '/'.join(remote.rstrip('/').split('/')[:-1])
+ try:
+ self.command_output(["push", local, remote], timeout=timeout)
+ except:
+ raise
+ finally:
+ if copy_required:
+ shutil.rmtree(temp_parent)
+
+ def pull(self, remote, local, timeout=None):
+ """Pulls a file or directory from the device.
+
+ :param str remote: The path of the remote file or
+ directory.
+ :param str local: The path of the local file or
+ directory name.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ # remove trailing /
+ local = os.path.normpath(local)
+ remote = os.path.normpath(remote)
+ copy_required = False
+ original_local = local
+ if self._adb_version >= '1.0.36' and \
+ os.path.isdir(local) and self.is_dir(remote):
+ # See do_sync_pull in
+ # https://android.googlesource.com/platform/system/core/+/master/adb/file_sync_client.cpp
+ # Work around change in behavior in adb 1.0.36 where if
+ # the local destination directory exists, adb pull will
+ # copy the source directory *into* the destination
+ # directory otherwise it will copy the source directory
+ # *onto* the destination directory.
+ #
+ # If the destination directory does exist, pull to its
+ # parent directory. If the source and destination leaf
+ # directory names are different, pull the source directory
+ # into a temporary directory and then copy the temporary
+ # directory onto the destination.
+ local_name = os.path.basename(local)
+ remote_name = os.path.basename(remote)
+ if local_name != remote_name:
+ copy_required = True
+ temp_parent = tempfile.mkdtemp()
+ local = os.path.join(temp_parent, remote_name)
+ else:
+ local = '/'.join(local.rstrip('/').split('/')[:-1])
+ try:
+ self.command_output(["pull", remote, local], timeout=timeout)
+ except:
+ raise
+ finally:
+ if copy_required:
+ dir_util.copy_tree(local, original_local)
+ shutil.rmtree(temp_parent)
+
+ def rm(self, path, recursive=False, force=False, timeout=None, root=False):
+ """Delete files or directories on the device.
+
+ :param str path: The path of the remote file or directory.
+ :param bool recursive: Flag specifying if the command is
+ to be applied recursively to the target. Default is False.
+ :param bool force: Flag which if True will not raise an
+ error when attempting to delete a non-existent file. Default
+ is False.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ * ADBError
+ """
+ cmd = "rm"
+ if recursive:
+ cmd += " -r"
+ try:
+ self.shell_output("%s %s" % (cmd, path), timeout=timeout, root=root)
+ if self.is_file(path, timeout=timeout, root=root):
+ raise ADBError('rm("%s") failed to remove file.' % path)
+ except ADBError as e:
+ if not force and 'No such file or directory' in e.message:
+ raise
+
+ def rmdir(self, path, timeout=None, root=False):
+ """Delete empty directory on the device.
+
+ :param str path: The directory name on the device.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ * ADBError
+ """
+ self.shell_output("rmdir %s" % path, timeout=timeout, root=root)
+ if self.is_dir(path, timeout=timeout, root=root):
+ raise ADBError('rmdir("%s") failed to remove directory.' % path)
+
+ # Process management methods
+
+ def get_process_list(self, timeout=None):
+ """Returns list of tuples (pid, name, user) for running
+ processes on device.
+
+ :param timeout: The maximum time
+ in seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified,
+ the value set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :returns: list of (pid, name, user) tuples for running processes
+ on the device.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ adb_process = None
+ try:
+ adb_process = self.shell("ps", timeout=timeout)
+ if adb_process.timedout:
+ raise ADBTimeoutError("%s" % adb_process)
+ elif adb_process.exitcode:
+ raise ADBError("%s" % adb_process)
+ # first line is the headers
+ header = adb_process.stdout_file.readline()
+ pid_i = -1
+ user_i = -1
+ els = header.split()
+ for i in range(len(els)):
+ item = els[i].lower()
+ if item == 'user':
+ user_i = i
+ elif item == 'pid':
+ pid_i = i
+ if user_i == -1 or pid_i == -1:
+ self._logger.error('get_process_list: %s' % header)
+ raise ADBError('get_process_list: Unknown format: %s: %s' % (
+ header, adb_process))
+ ret = []
+ line = adb_process.stdout_file.readline()
+ while line:
+ els = line.split()
+ try:
+ ret.append([int(els[pid_i]), els[-1], els[user_i]])
+ except ValueError:
+ self._logger.error('get_process_list: %s %s\n%s' % (
+ header, line, traceback.format_exc()))
+ raise ADBError('get_process_list: %s: %s: %s' % (
+ header, line, adb_process))
+ line = adb_process.stdout_file.readline()
+ self._logger.debug('get_process_list: %s' % ret)
+ return ret
+ finally:
+ if adb_process and isinstance(adb_process.stdout_file, file):
+ adb_process.stdout_file.close()
+
+ def kill(self, pids, sig=None, attempts=3, wait=5,
+ timeout=None, root=False):
+ """Kills processes on the device given a list of process ids.
+
+ :param list pids: process ids to be killed.
+ :param sig: signal to be sent to the process.
+ :type sig: integer or None
+ :param integer attempts: number of attempts to try to
+ kill the processes.
+ :param integer wait: number of seconds to wait after each attempt.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ * ADBError
+ """
+ pid_list = [str(pid) for pid in pids]
+ for attempt in range(attempts):
+ args = ["kill"]
+ if sig:
+ args.append("-%d" % sig)
+ args.extend(pid_list)
+ try:
+ self.shell_output(' '.join(args), timeout=timeout, root=root)
+ except ADBError as e:
+ if 'No such process' not in e.message:
+ raise
+ pid_set = set(pid_list)
+ current_pid_set = set([str(proc[0]) for proc in
+ self.get_process_list(timeout=timeout)])
+ pid_list = list(pid_set.intersection(current_pid_set))
+ if not pid_list:
+ break
+ self._logger.debug("Attempt %d of %d to kill processes %s failed" %
+ (attempt + 1, attempts, pid_list))
+ time.sleep(wait)
+
+ if pid_list:
+ raise ADBError('kill: processes %s not killed' % pid_list)
+
+ def pkill(self, appname, sig=None, attempts=3, wait=5,
+ timeout=None, root=False):
+ """Kills a processes on the device matching a name.
+
+ :param str appname: The app name of the process to
+ be killed. Note that only the first 75 characters of the
+ process name are significant.
+ :param sig: optional signal to be sent to the process.
+ :type sig: integer or None
+ :param integer attempts: number of attempts to try to
+ kill the processes.
+ :param integer wait: number of seconds to wait after each attempt.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should
+ be executed as root.
+
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ * ADBError
+ """
+ procs = self.get_process_list(timeout=timeout)
+ # limit the comparion to the first 75 characters due to a
+ # limitation in processname length in android.
+ pids = [proc[0] for proc in procs if proc[1] == appname[:75]]
+ if not pids:
+ return
+
+ try:
+ self.kill(pids, sig, attempts=attempts, wait=wait,
+ timeout=timeout, root=root)
+ except ADBError as e:
+ if self.process_exist(appname, timeout=timeout):
+ raise e
+
+ def process_exist(self, process_name, timeout=None):
+ """Returns True if process with name process_name is running on
+ device.
+
+ :param str process_name: The name of the process
+ to check. Note that only the first 75 characters of the
+ process name are significant.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :returns: boolean - True if process exists.
+
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ if not isinstance(process_name, basestring):
+ raise ADBError("Process name %s is not a string" % process_name)
+
+ # Filter out extra spaces.
+ parts = [x for x in process_name.split(' ') if x != '']
+ process_name = ' '.join(parts)
+
+ # Filter out the quoted env string if it exists
+ # ex: '"name=value;name2=value2;etc=..." process args' -> 'process args'
+ parts = process_name.split('"')
+ if len(parts) > 2:
+ process_name = ' '.join(parts[2:]).strip()
+
+ pieces = process_name.split(' ')
+ parts = pieces[0].split('/')
+ app = parts[-1]
+
+ proc_list = self.get_process_list(timeout=timeout)
+ if not proc_list:
+ return False
+
+ for proc in proc_list:
+ proc_name = proc[1].split('/')[-1]
+ # limit the comparion to the first 75 characters due to a
+ # limitation in processname length in android.
+ if proc_name == app[:75]:
+ return True
+ return False
+
+ def cp(self, source, destination, recursive=False, timeout=None,
+ root=False):
+ """Copies a file or directory on the device.
+
+ :param source: string containing the path of the source file or
+ directory.
+ :param destination: string containing the path of the destination file
+ or directory.
+ :param recursive: optional boolean indicating if a recursive copy is to
+ be performed. Required if the source is a directory. Defaults to
+ False. Think cp -R source destination.
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ * ADBError
+ """
+ source = posixpath.normpath(source)
+ destination = posixpath.normpath(destination)
+ if self._have_cp:
+ r = '-R' if recursive else ''
+ self.shell_output('cp %s %s %s' % (r, source, destination),
+ timeout=timeout, root=root)
+ return
+
+ # Emulate cp behavior depending on if source and destination
+ # already exists and whether they are a directory or file.
+ if not self.exists(source, timeout=timeout, root=root):
+ raise ADBError("cp: can't stat '%s': No such file or directory" %
+ source)
+
+ if self.is_file(source, timeout=timeout, root=root):
+ if self.is_dir(destination, timeout=timeout, root=root):
+ # Copy the source file into the destination directory
+ destination = posixpath.join(destination,
+ posixpath.basename(source))
+ self.shell_output('dd if=%s of=%s' % (source, destination),
+ timeout=timeout, root=root)
+ return
+
+ if self.is_file(destination, timeout=timeout, root=root):
+ raise ADBError('cp: %s: Not a directory' % destination)
+
+ if not recursive:
+ raise ADBError("cp: omitting directory '%s'" % source)
+
+ if self.is_dir(destination, timeout=timeout, root=root):
+ # Copy the source directory into the destination directory.
+ destination_dir = posixpath.join(destination,
+ posixpath.basename(source))
+ else:
+ # Copy the contents of the source directory into the
+ # destination directory.
+ destination_dir = destination
+
+ try:
+ # Do not create parent directories since cp does not.
+ self.mkdir(destination_dir, timeout=timeout, root=root)
+ except ADBError as e:
+ if 'File exists' not in e.message:
+ raise
+
+ for i in self.list_files(source, timeout=timeout, root=root):
+ self.cp(posixpath.join(source, i),
+ posixpath.join(destination_dir, i),
+ recursive=recursive,
+ timeout=timeout, root=root)
+
+ def mv(self, source, destination, timeout=None, root=False):
+ """Moves a file or directory on the device.
+
+ :param source: string containing the path of the source file or
+ directory.
+ :param destination: string containing the path of the destination file
+ or directory.
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :raises: * ADBTimeoutError
+ * ADBRootError
+ * ADBError
+ """
+ source = posixpath.normpath(source)
+ destination = posixpath.normpath(destination)
+ self.shell_output('mv %s %s' % (source, destination), timeout=timeout,
+ root=root)
+
+ def reboot(self, timeout=None):
+ """Reboots the device.
+
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :raises: * ADBTimeoutError
+ * ADBError
+
+ reboot() reboots the device, issues an adb wait-for-device in order to
+ wait for the device to complete rebooting, then calls is_device_ready()
+ to determine if the device has completed booting.
+ """
+ self.command_output(["reboot"], timeout=timeout)
+ # command_output automatically inserts a 'wait-for-device'
+ # argument to adb. Issuing an empty command is the same as adb
+ # -s <device> wait-for-device. We don't send an explicit
+ # 'wait-for-device' since that would add duplicate
+ # 'wait-for-device' arguments which is an error in newer
+ # versions of adb.
+ self.command_output([], timeout=timeout)
+ self._check_adb_root(timeout=timeout)
+ return self.is_device_ready(timeout=timeout)
+
+ @abstractmethod
+ def is_device_ready(self, timeout=None):
+ """Abstract class that returns True if the device is ready.
+
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ return
+
+ @abstractmethod
+ def get_battery_percentage(self, timeout=None):
+ """Abstract class that returns the battery charge as a percentage.
+
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :returns: battery charge as a percentage.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ return
+
+ def get_info(self, directive=None, timeout=None):
+ """
+ Returns a dictionary of information strings about the device.
+
+ :param directive: information you want to get. Options are:
+ - `battery` - battery charge as a percentage
+ - `disk` - total, free, available bytes on disk
+ - `id` - unique id of the device
+ - `os` - name of the os
+ - `process` - list of running processes (same as ps)
+ - `systime` - system time of the device
+ - `uptime` - uptime of the device
+
+ If `directive` is `None`, will return all available information
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ directives = ['battery', 'disk', 'id', 'os', 'process', 'systime',
+ 'uptime']
+
+ if directive in directives:
+ directives = [directive]
+
+ info = {}
+ if 'battery' in directives:
+ info['battery'] = self.get_battery_percentage(timeout=timeout)
+ if 'disk' in directives:
+ info['disk'] = self.shell_output('df /data /system /sdcard',
+ timeout=timeout).splitlines()
+ if 'id' in directives:
+ info['id'] = self.command_output(['get-serialno'], timeout=timeout)
+ if 'os' in directives:
+ info['os'] = self.shell_output('getprop ro.build.display.id',
+ timeout=timeout)
+ if 'process' in directives:
+ ps = self.shell_output('ps', timeout=timeout)
+ info['process'] = ps.splitlines()
+ if 'systime' in directives:
+ info['systime'] = self.shell_output('date', timeout=timeout)
+ if 'uptime' in directives:
+ uptime = self.shell_output('uptime', timeout=timeout)
+ if uptime:
+ m = re.match(r'up time: ((\d+) days, )*(\d{2}):(\d{2}):(\d{2})',
+ uptime)
+ if m:
+ uptime = '%d days %d hours %d minutes %d seconds' % tuple(
+ [int(g or 0) for g in m.groups()[1:]])
+ info['uptime'] = uptime
+ return info
diff --git a/testing/mozbase/mozdevice/mozdevice/adb_android.py b/testing/mozbase/mozdevice/mozdevice/adb_android.py
new file mode 100644
index 000000000..bf5fffc0e
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/adb_android.py
@@ -0,0 +1,493 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import re
+import time
+
+from abc import ABCMeta
+
+import version_codes
+
+from adb import ADBDevice, ADBError
+
+
+class ADBAndroid(ADBDevice):
+ """ADBAndroid implements :class:`ADBDevice` providing Android-specific
+ functionality.
+
+ ::
+
+ from mozdevice import ADBAndroid
+
+ adbdevice = ADBAndroid()
+ print adbdevice.list_files("/mnt/sdcard")
+ if adbdevice.process_exist("org.mozilla.fennec"):
+ print "Fennec is running"
+ """
+ __metaclass__ = ABCMeta
+
+ def __init__(self,
+ device=None,
+ adb='adb',
+ adb_host=None,
+ adb_port=None,
+ test_root='',
+ logger_name='adb',
+ timeout=300,
+ verbose=False,
+ device_ready_retry_wait=20,
+ device_ready_retry_attempts=3):
+ """Initializes the ADBAndroid object.
+
+ :param device: When a string is passed, it is interpreted as the
+ device serial number. This form is not compatible with
+ devices containing a ":" in the serial; in this case
+ ValueError will be raised.
+ When a dictionary is passed it must have one or both of
+ the keys "device_serial" and "usb". This is compatible
+ with the dictionaries in the list returned by
+ ADBHost.devices(). If the value of device_serial is a
+ valid serial not containing a ":" it will be used to
+ identify the device, otherwise the value of the usb key,
+ prefixed with "usb:" is used.
+ If None is passed and there is exactly one device attached
+ to the host, that device is used. If there is more than one
+ device attached, ValueError is raised. If no device is
+ attached the constructor will block until a device is
+ attached or the timeout is reached.
+ :type device: dict, str or None
+ :param adb_host: host of the adb server to connect to.
+ :type adb_host: str or None
+ :param adb_port: port of the adb server to connect to.
+ :type adb_port: integer or None
+ :param str logger_name: logging logger name. Defaults to 'adb'.
+ :param integer device_ready_retry_wait: number of seconds to wait
+ between attempts to check if the device is ready after a
+ reboot.
+ :param integer device_ready_retry_attempts: number of attempts when
+ checking if a device is ready.
+
+ :raises: * ADBError
+ * ADBTimeoutError
+ * ValueError
+ """
+ ADBDevice.__init__(self, device=device, adb=adb,
+ adb_host=adb_host, adb_port=adb_port,
+ test_root=test_root,
+ logger_name=logger_name, timeout=timeout,
+ verbose=verbose,
+ device_ready_retry_wait=device_ready_retry_wait,
+ device_ready_retry_attempts=device_ready_retry_attempts)
+ # https://source.android.com/devices/tech/security/selinux/index.html
+ # setenforce
+ # usage: setenforce [ Enforcing | Permissive | 1 | 0 ]
+ # getenforce returns either Enforcing or Permissive
+
+ try:
+ self.selinux = True
+ if self.shell_output('getenforce', timeout=timeout) != 'Permissive':
+ self._logger.info('Setting SELinux Permissive Mode')
+ self.shell_output("setenforce Permissive", timeout=timeout, root=True)
+ except ADBError:
+ self.selinux = False
+
+ self.version = int(self.shell_output("getprop ro.build.version.sdk",
+ timeout=timeout))
+
+ def reboot(self, timeout=None):
+ """Reboots the device.
+
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :raises: * ADBTimeoutError
+ * ADBError
+
+ reboot() reboots the device, issues an adb wait-for-device in order to
+ wait for the device to complete rebooting, then calls is_device_ready()
+ to determine if the device has completed booting.
+
+ If the device supports running adbd as root, adbd will be
+ restarted running as root. Then, if the device supports
+ SELinux, setenforce Permissive will be called to change
+ SELinux to permissive. This must be done after adbd is
+ restarted in order for the SELinux Permissive setting to
+ persist.
+
+ """
+ ready = ADBDevice.reboot(self, timeout=timeout)
+ self._check_adb_root(timeout=timeout)
+ return ready
+
+ # Informational methods
+
+ def get_battery_percentage(self, timeout=None):
+ """Returns the battery charge as a percentage.
+
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :type timeout: integer or None
+ :returns: battery charge as a percentage.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ level = None
+ scale = None
+ percentage = 0
+ cmd = "dumpsys battery"
+ re_parameter = re.compile(r'\s+(\w+):\s+(\d+)')
+ lines = self.shell_output(cmd, timeout=timeout).split('\r')
+ for line in lines:
+ match = re_parameter.match(line)
+ if match:
+ parameter = match.group(1)
+ value = match.group(2)
+ if parameter == 'level':
+ level = float(value)
+ elif parameter == 'scale':
+ scale = float(value)
+ if parameter is not None and scale is not None:
+ percentage = 100.0 * level / scale
+ break
+ return percentage
+
+ # System control methods
+
+ def is_device_ready(self, timeout=None):
+ """Checks if a device is ready for testing.
+
+ This method uses the android only package manager to check for
+ readiness.
+
+ :param timeout: The maximum time
+ in seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ # command_output automatically inserts a 'wait-for-device'
+ # argument to adb. Issuing an empty command is the same as adb
+ # -s <device> wait-for-device. We don't send an explicit
+ # 'wait-for-device' since that would add duplicate
+ # 'wait-for-device' arguments which is an error in newer
+ # versions of adb.
+ self.command_output([], timeout=timeout)
+ pm_error_string = "Error: Could not access the Package Manager"
+ pm_list_commands = ["packages", "permission-groups", "permissions",
+ "instrumentation", "features", "libraries"]
+ ready_path = os.path.join(self.test_root, "ready")
+ for attempt in range(self._device_ready_retry_attempts):
+ failure = 'Unknown failure'
+ success = True
+ try:
+ state = self.get_state(timeout=timeout)
+ if state != 'device':
+ failure = "Device state: %s" % state
+ success = False
+ else:
+ if (self.selinux and self.shell_output('getenforce',
+ timeout=timeout) != 'Permissive'):
+ self._logger.info('Setting SELinux Permissive Mode')
+ self.shell_output("setenforce Permissive", timeout=timeout, root=True)
+ if self.is_dir(ready_path, timeout=timeout, root=True):
+ self.rmdir(ready_path, timeout=timeout, root=True)
+ self.mkdir(ready_path, timeout=timeout, root=True)
+ self.rmdir(ready_path, timeout=timeout, root=True)
+ # Invoke the pm list commands to see if it is up and
+ # running.
+ for pm_list_cmd in pm_list_commands:
+ data = self.shell_output("pm list %s" % pm_list_cmd,
+ timeout=timeout)
+ if pm_error_string in data:
+ failure = data
+ success = False
+ break
+ except ADBError as e:
+ success = False
+ failure = e.message
+
+ if not success:
+ self._logger.debug('Attempt %s of %s device not ready: %s' % (
+ attempt + 1, self._device_ready_retry_attempts,
+ failure))
+ time.sleep(self._device_ready_retry_wait)
+
+ return success
+
+ def power_on(self, timeout=None):
+ """Sets the device's power stayon value.
+
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ try:
+ self.shell_output('svc power stayon true',
+ timeout=timeout,
+ root=True)
+ except ADBError as e:
+ # Executing this via adb shell errors, but not interactively.
+ # Any other exitcode is a real error.
+ if 'exitcode: 137' not in e.message:
+ raise
+ self._logger.warning('Unable to set power stayon true: %s' % e)
+
+ # Application management methods
+
+ def install_app(self, apk_path, timeout=None):
+ """Installs an app on the device.
+
+ :param str apk_path: The apk file name to be installed.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ cmd = ["install"]
+ if self.version >= version_codes.M:
+ cmd.append("-g")
+ cmd.append(apk_path)
+ data = self.command_output(cmd, timeout=timeout)
+ if data.find('Success') == -1:
+ raise ADBError("install failed for %s. Got: %s" %
+ (apk_path, data))
+
+ def is_app_installed(self, app_name, timeout=None):
+ """Returns True if an app is installed on the device.
+
+ :param str app_name: The name of the app to be checked.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ pm_error_string = 'Error: Could not access the Package Manager'
+ data = self.shell_output("pm list package %s" % app_name, timeout=timeout)
+ if pm_error_string in data:
+ raise ADBError(pm_error_string)
+ if app_name not in data:
+ return False
+ return True
+
+ def launch_application(self, app_name, activity_name, intent, url=None,
+ extras=None, wait=True, fail_if_running=True,
+ timeout=None):
+ """Launches an Android application
+
+ :param str app_name: Name of application (e.g. `com.android.chrome`)
+ :param str activity_name: Name of activity to launch (e.g. `.Main`)
+ :param str intent: Intent to launch application with
+ :param url: URL to open
+ :type url: str or None
+ :param extras: Extra arguments for application.
+ :type extras: dict or None
+ :param bool wait: If True, wait for application to start before
+ returning.
+ :param bool fail_if_running: Raise an exception if instance of
+ application is already running.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ # If fail_if_running is True, we throw an exception here. Only one
+ # instance of an application can be running at once on Android,
+ # starting a new instance may not be what we want depending on what
+ # we want to do
+ if fail_if_running and self.process_exist(app_name, timeout=timeout):
+ raise ADBError("Only one instance of an application may be running "
+ "at once")
+
+ acmd = ["am", "start"] + \
+ ["-W" if wait else '', "-n", "%s/%s" % (app_name, activity_name)]
+
+ if intent:
+ acmd.extend(["-a", intent])
+
+ if extras:
+ for (key, val) in extras.iteritems():
+ if isinstance(val, int):
+ extra_type_param = "--ei"
+ elif isinstance(val, bool):
+ extra_type_param = "--ez"
+ else:
+ extra_type_param = "--es"
+ acmd.extend([extra_type_param, str(key), str(val)])
+
+ if url:
+ acmd.extend(["-d", url])
+
+ cmd = self._escape_command_line(acmd)
+ self.shell_output(cmd, timeout=timeout)
+
+ def launch_fennec(self, app_name, intent="android.intent.action.VIEW",
+ moz_env=None, extra_args=None, url=None, wait=True,
+ fail_if_running=True, timeout=None):
+ """Convenience method to launch Fennec on Android with various
+ debugging arguments
+
+ :param str app_name: Name of fennec application (e.g.
+ `org.mozilla.fennec`)
+ :param str intent: Intent to launch application.
+ :param moz_env: Mozilla specific environment to pass into
+ application.
+ :type moz_env: str or None
+ :param extra_args: Extra arguments to be parsed by fennec.
+ :type extra_args: str or None
+ :param url: URL to open
+ :type url: str or None
+ :param bool wait: If True, wait for application to start before
+ returning.
+ :param bool fail_if_running: Raise an exception if instance of
+ application is already running.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ extras = {}
+
+ if moz_env:
+ # moz_env is expected to be a dictionary of environment variables:
+ # Fennec itself will set them when launched
+ for (env_count, (env_key, env_val)) in enumerate(moz_env.iteritems()):
+ extras["env" + str(env_count)] = env_key + "=" + env_val
+
+ # Additional command line arguments that fennec will read and use (e.g.
+ # with a custom profile)
+ if extra_args:
+ extras['args'] = " ".join(extra_args)
+
+ self.launch_application(app_name, "org.mozilla.gecko.BrowserApp",
+ intent, url=url, extras=extras,
+ wait=wait, fail_if_running=fail_if_running,
+ timeout=timeout)
+
+ def stop_application(self, app_name, timeout=None, root=False):
+ """Stops the specified application
+
+ For Android 3.0+, we use the "am force-stop" to do this, which
+ is reliable and does not require root. For earlier versions of
+ Android, we simply try to manually kill the processes started
+ by the app repeatedly until none is around any more. This is
+ less reliable and does require root.
+
+ :param str app_name: Name of application (e.g. `com.android.chrome`)
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :param bool root: Flag specifying if the command should be
+ executed as root.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ if self.version >= version_codes.HONEYCOMB:
+ self.shell_output("am force-stop %s" % app_name,
+ timeout=timeout, root=root)
+ else:
+ num_tries = 0
+ max_tries = 5
+ while self.process_exist(app_name, timeout=timeout):
+ if num_tries > max_tries:
+ raise ADBError("Couldn't successfully kill %s after %s "
+ "tries" % (app_name, max_tries))
+ self.pkill(app_name, timeout=timeout, root=root)
+ num_tries += 1
+
+ # sleep for a short duration to make sure there are no
+ # additional processes in the process of being launched
+ # (this is not 100% guaranteed to work since it is inherently
+ # racey, but it's the best we can do)
+ time.sleep(1)
+
+ def uninstall_app(self, app_name, reboot=False, timeout=None):
+ """Uninstalls an app on the device.
+
+ :param str app_name: The name of the app to be
+ uninstalled.
+ :param bool reboot: Flag indicating that the device should
+ be rebooted after the app is uninstalled. No reboot occurs
+ if the app is not installed.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ if self.is_app_installed(app_name, timeout=timeout):
+ data = self.command_output(["uninstall", app_name], timeout=timeout)
+ if data.find('Success') == -1:
+ self._logger.debug('uninstall_app failed: %s' % data)
+ raise ADBError("uninstall failed for %s. Got: %s" % (app_name, data))
+ if reboot:
+ self.reboot(timeout=timeout)
+
+ def update_app(self, apk_path, timeout=None):
+ """Updates an app on the device and reboots.
+
+ :param str apk_path: The apk file name to be
+ updated.
+ :param timeout: The maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :type timeout: integer or None
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ cmd = ["install", "-r"]
+ if self.version >= version_codes.M:
+ cmd.append("-g")
+ cmd.append(apk_path)
+ output = self.command_output(cmd, timeout=timeout)
+ self.reboot(timeout=timeout)
+ return output
diff --git a/testing/mozbase/mozdevice/mozdevice/adb_b2g.py b/testing/mozbase/mozdevice/mozdevice/adb_b2g.py
new file mode 100644
index 000000000..3280e6172
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/adb_b2g.py
@@ -0,0 +1,122 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import traceback
+
+import mozfile
+
+from adb import ADBDevice, ADBError
+
+
+class ADBB2G(ADBDevice):
+ """ADBB2G implements :class:`ADBDevice` providing B2G-specific
+ functionality.
+
+ ::
+
+ from mozdevice import ADBB2G
+
+ adbdevice = ADBB2G()
+ print adbdevice.list_files("/mnt/sdcard")
+ if adbdevice.process_exist("b2g"):
+ print "B2G is running"
+ """
+
+ def get_battery_percentage(self, timeout=None):
+ """Returns the battery charge as a percentage.
+
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :returns: battery charge as a percentage.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ with mozfile.NamedTemporaryFile() as tf:
+ self.pull('/sys/class/power_supply/battery/capacity', tf.name,
+ timeout=timeout)
+ try:
+ with open(tf.name) as tf2:
+ return tf2.read().splitlines()[0]
+ except Exception as e:
+ raise ADBError(traceback.format_exception_only(
+ type(e), e)[0].strip())
+
+ def get_memory_total(self, timeout=None):
+ """Returns the total memory available with units.
+
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADBDevice constructor is used.
+ :returns: memory total with units.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ meminfo = {}
+ with mozfile.NamedTemporaryFile() as tf:
+ self.pull('/proc/meminfo', tf.name, timeout=timeout)
+ try:
+ with open(tf.name) as tf2:
+ for line in tf2.read().splitlines():
+ key, value = line.split(':')
+ meminfo[key] = value.strip()
+ except Exception as e:
+ raise ADBError(traceback.format_exception_only(
+ type(e), e)[0].strip())
+ return meminfo['MemTotal']
+
+ def get_info(self, directive=None, timeout=None):
+ """
+ Returns a dictionary of information strings about the device.
+
+ :param directive: information you want to get. Options are:
+ - `battery` - battery charge as a percentage
+ - `disk` - total, free, available bytes on disk
+ - `id` - unique id of the device
+ - `memtotal` - total memory available on the device
+ - `os` - name of the os
+ - `process` - list of running processes (same as ps)
+ - `systime` - system time of the device
+ - `uptime` - uptime of the device
+
+ If `directive` is `None`, will return all available information
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ info = super(ADBB2G, self).get_info(directive=directive,
+ timeout=timeout)
+
+ directives = ['memtotal']
+ if directive in directives:
+ directives = [directive]
+
+ if 'memtotal' in directives:
+ info['memtotal'] = self.get_memory_total(timeout=timeout)
+ return info
+
+ def is_device_ready(self, timeout=None):
+ """Returns True if the device is ready.
+
+ :param timeout: optional integer specifying the maximum time in
+ seconds for any spawned adb process to complete before
+ throwing an ADBTimeoutError.
+ This timeout is per adb call. The total time spent
+ may exceed this value. If it is not specified, the value
+ set in the ADB constructor is used.
+ :raises: * ADBTimeoutError
+ * ADBError
+ """
+ return self.shell_bool('ls /sbin', timeout=timeout)
diff --git a/testing/mozbase/mozdevice/mozdevice/devicemanager.py b/testing/mozbase/mozdevice/mozdevice/devicemanager.py
new file mode 100644
index 000000000..de87735ef
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanager.py
@@ -0,0 +1,674 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import 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 or DeviceManagerSUT.
+ """
+
+ _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: if specified, try to make the device connect to this
+ specific IP address after rebooting (only works with
+ SUT; if None, we try to determine a reasonable address
+ ourselves)
+ """
+
+ @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: if specified, try to make the device connect to this
+ specific IP address after rebooting (only works with
+ SUT; if None and wait is True, we try to determine a
+ reasonable address ourselves)
+ """
+
+ @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:
+ 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 (shared between ADB and
+ SUT device managers). 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
+
+
+class ZeroconfListener(object):
+
+ def __init__(self, hwid, evt):
+ self.hwid = hwid
+ self.evt = evt
+
+ # Format is 'SUTAgent [hwid:015d2bc2825ff206] [ip:10_242_29_221]._sutagent._tcp.local.'
+ def addService(self, zeroconf, type, name):
+ # print "Found _sutagent service broadcast:", name
+ if not name.startswith("SUTAgent"):
+ return
+
+ sutname = name.split('.')[0]
+ m = re.search('\[hwid:([^\]]*)\]', sutname)
+ if m is None:
+ return
+
+ hwid = m.group(1)
+
+ m = re.search('\[ip:([0-9_]*)\]', sutname)
+ if m is None:
+ return
+
+ ip = m.group(1).replace("_", ".")
+
+ if self.hwid == hwid:
+ self.ip = ip
+ self.evt.set()
+
+ def removeService(self, zeroconf, type, name):
+ pass
diff --git a/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
new file mode 100644
index 000000000..74d0a8d23
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py
@@ -0,0 +1,893 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import logging
+import 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
+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 different behaviour from devicemanagerSUT. Throw
+ # an exception so we have the same behaviour between the two
+ # implementations
+ 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:
+ 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)
+ copyRequired = False
+ if self._adb_version >= '1.0.36' and \
+ os.path.isdir(localDir) and self.dirExists(remoteDir):
+ # 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 the destination directory does exist, push to its
+ # parent directory. If the source and destination leaf
+ # directory names are different, copy the source directory
+ # to a temporary directory with the same leaf name as the
+ # destination so that when we push to the parent, the
+ # source is copied onto the destination directory.
+ localName = os.path.basename(localDir)
+ remoteName = os.path.basename(remoteDir)
+ if localName != remoteName:
+ copyRequired = True
+ tempParent = tempfile.mkdtemp()
+ newLocal = os.path.join(tempParent, remoteName)
+ dir_util.copy_tree(localDir, newLocal)
+ localDir = newLocal
+ remoteDir = '/'.join(remoteDir.rstrip('/').split('/')[:-1])
+ try:
+ self._checkCmd(["push", localDir, remoteDir],
+ retryLimit=retryLimit, timeout=timeout)
+ except:
+ raise
+ finally:
+ if copyRequired:
+ 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(" ")
+ # SUT identifies the URL by looking for :\\ -- another strategy to consider
+ 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):
+ 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:
+ 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:
+ 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/devicemanagerSUT.py b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
new file mode 100644
index 000000000..7816a3fdd
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py
@@ -0,0 +1,975 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import datetime
+import logging
+import moznetwork
+import select
+import socket
+import time
+import os
+import re
+import posixpath
+import subprocess
+import StringIO
+from devicemanager import DeviceManager, DMError, _pop_last_line
+import errno
+from distutils.version import StrictVersion
+
+
+class DeviceManagerSUT(DeviceManager):
+ """
+ Implementation of DeviceManager interface that speaks to a device over
+ TCP/IP using the "system under test" protocol. A software agent such as
+ Negatus (http://github.com/mozilla/Negatus) or the Mozilla Android SUTAgent
+ app must be present and listening for connections for this to work.
+ """
+
+ _base_prompt = '$>'
+ _base_prompt_re = '\$\>'
+ _prompt_sep = '\x00'
+ _prompt_regex = '.*(' + _base_prompt_re + _prompt_sep + ')'
+ _agentErrorRE = re.compile('^##AGENT-WARNING##\ ?(.*)')
+
+ reboot_timeout = 600
+ reboot_settling_time = 60
+
+ def __init__(self, host, port=20701, retryLimit=5, deviceRoot=None,
+ logLevel=logging.ERROR, **kwargs):
+ DeviceManager.__init__(self, logLevel=logLevel,
+ deviceRoot=deviceRoot)
+ self.host = host
+ self.port = port
+ self.retryLimit = retryLimit
+ self._sock = None
+ self._everConnected = False
+
+ # Get version
+ verstring = self._runCmds([{'cmd': 'ver'}])
+ ver_re = re.match('(\S+) Version (\S+)', verstring)
+ self.agentProductName = ver_re.group(1)
+ self.agentVersion = ver_re.group(2)
+
+ def _cmdNeedsResponse(self, cmd):
+ """ Not all commands need a response from the agent:
+ * rebt obviously doesn't get a response
+ * uninstall performs a reboot to ensure starting in a clean state and
+ so also doesn't look for a response
+ """
+ noResponseCmds = [re.compile('^rebt'),
+ re.compile('^uninst .*$'),
+ re.compile('^pull .*$')]
+
+ for c in noResponseCmds:
+ if (c.match(cmd)):
+ return False
+
+ # If the command is not in our list, then it gets a response
+ return True
+
+ def _stripPrompt(self, data):
+ """
+ take a data blob and strip instances of the prompt '$>\x00'
+ """
+ promptre = re.compile(self._prompt_regex + '.*')
+ retVal = []
+ lines = data.split('\n')
+ for line in lines:
+ foundPrompt = False
+ try:
+ while (promptre.match(line)):
+ foundPrompt = True
+ pieces = line.split(self._prompt_sep)
+ index = pieces.index('$>')
+ pieces.pop(index)
+ line = self._prompt_sep.join(pieces)
+ except(ValueError):
+ pass
+
+ # we don't want to append lines that are blank after stripping the
+ # prompt (those are basically "prompts")
+ if not foundPrompt or line:
+ retVal.append(line)
+
+ return '\n'.join(retVal)
+
+ def _shouldCmdCloseSocket(self, cmd):
+ """
+ Some commands need to close the socket after they are sent:
+ * rebt
+ * uninst
+ * quit
+ """
+ socketClosingCmds = [re.compile('^quit.*'),
+ re.compile('^rebt.*'),
+ re.compile('^uninst .*$')]
+
+ for c in socketClosingCmds:
+ if (c.match(cmd)):
+ return True
+ return False
+
+ def _sendCmds(self, cmdlist, outputfile, timeout=None, retryLimit=None):
+ """
+ Wrapper for _doCmds that loops up to retryLimit iterations
+ """
+ # this allows us to move the retry logic outside of the _doCmds() to make it
+ # easier for debugging in the future.
+ # note that since cmdlist is a list of commands, they will all be retried if
+ # one fails. this is necessary in particular for pushFile(), where we don't want
+ # to accidentally send extra data if a failure occurs during data transmission.
+
+ retryLimit = retryLimit or self.retryLimit
+ retries = 0
+ while retries < retryLimit:
+ try:
+ self._doCmds(cmdlist, outputfile, timeout)
+ return
+ except DMError as err:
+ # re-raise error if it's fatal (i.e. the device got the command but
+ # couldn't execute it). retry otherwise
+ if err.fatal:
+ raise err
+ self._logger.debug(err)
+ retries += 1
+ # if we lost the connection or failed to establish one, wait a bit
+ if retries < retryLimit and not self._sock:
+ sleep_time = 5 * retries
+ self._logger.info('Could not connect; sleeping for %d seconds.' % sleep_time)
+ time.sleep(sleep_time)
+
+ raise DMError("Remote Device Error: unable to connect to %s after %s attempts" %
+ (self.host, retryLimit))
+
+ def _runCmds(self, cmdlist, timeout=None, retryLimit=None):
+ """
+ Similar to _sendCmds, but just returns any output as a string instead of
+ writing to a file
+ """
+ retryLimit = retryLimit or self.retryLimit
+ outputfile = StringIO.StringIO()
+ self._sendCmds(cmdlist, outputfile, timeout, retryLimit=retryLimit)
+ outputfile.seek(0)
+ return outputfile.read()
+
+ def _doCmds(self, cmdlist, outputfile, timeout):
+ promptre = re.compile(self._prompt_regex + '$')
+ shouldCloseSocket = False
+
+ if not timeout:
+ # We are asserting that all commands will complete in this time unless
+ # otherwise specified
+ timeout = self.default_timeout
+
+ if not self._sock:
+ try:
+ if self._everConnected:
+ self._logger.info("reconnecting socket")
+ self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ except socket.error as msg:
+ self._sock = None
+ raise DMError("Automation Error: unable to create socket: " + str(msg))
+
+ try:
+ self._sock.settimeout(float(timeout))
+ self._sock.connect((self.host, int(self.port)))
+ self._everConnected = True
+ except socket.error as msg:
+ self._sock = None
+ raise DMError("Remote Device Error: Unable to connect socket: " + str(msg))
+
+ # consume prompt
+ try:
+ self._sock.recv(1024)
+ except socket.error as msg:
+ self._sock.close()
+ self._sock = None
+ raise DMError(
+ "Remote Device Error: Did not get prompt after connecting: " + str(msg),
+ fatal=True)
+
+ # future recv() timeouts are handled by select() calls
+ self._sock.settimeout(None)
+
+ for cmd in cmdlist:
+ cmdline = '%s\r\n' % cmd['cmd']
+
+ try:
+ sent = self._sock.send(cmdline)
+ if sent != len(cmdline):
+ raise DMError("Remote Device Error: our cmd was %s bytes and we "
+ "only sent %s" % (len(cmdline), sent))
+ if cmd.get('data'):
+ totalsent = 0
+ while totalsent < len(cmd['data']):
+ sent = self._sock.send(cmd['data'][totalsent:])
+ self._logger.debug("sent %s bytes of data payload" % sent)
+ if sent == 0:
+ raise DMError("Socket connection broken when sending data")
+ totalsent += sent
+
+ self._logger.debug("sent cmd: %s" % cmd['cmd'])
+ except socket.error as msg:
+ self._sock.close()
+ self._sock = None
+ self._logger.error("Remote Device Error: Error sending data"
+ " to socket. cmd=%s; err=%s" % (cmd['cmd'], msg))
+ return False
+
+ # Check if the command should close the socket
+ shouldCloseSocket = self._shouldCmdCloseSocket(cmd['cmd'])
+
+ # Handle responses from commands
+ if self._cmdNeedsResponse(cmd['cmd']):
+ foundPrompt = False
+ data = ""
+ timer = 0
+ select_timeout = 1
+ commandFailed = False
+
+ while not foundPrompt:
+ socketClosed = False
+ errStr = ''
+ temp = ''
+ self._logger.debug("recv'ing...")
+
+ # Get our response
+ try:
+ # Wait up to a second for socket to become ready for reading...
+ if select.select([self._sock], [], [], select_timeout)[0]:
+ temp = self._sock.recv(1024)
+ self._logger.debug(u"response: %s" % temp.decode('utf8', 'replace'))
+ timer = 0
+ if not temp:
+ socketClosed = True
+ errStr = 'connection closed'
+ timer += select_timeout
+ if timer > timeout:
+ self._sock.close()
+ self._sock = None
+ raise DMError("Automation Error: Timeout in command %s" %
+ cmd['cmd'], fatal=True)
+ except socket.error as err:
+ socketClosed = True
+ errStr = str(err)
+ # This error shows up with we have our tegra rebooted.
+ if err[0] == errno.ECONNRESET:
+ errStr += ' - possible reboot'
+
+ if socketClosed:
+ self._sock.close()
+ self._sock = None
+ raise DMError(
+ "Automation Error: Error receiving data from socket. "
+ "cmd=%s; err=%s" % (cmd, errStr))
+
+ data += temp
+
+ # If something goes wrong in the agent it will send back a string that
+ # starts with '##AGENT-WARNING##'
+ if not commandFailed:
+ errorMatch = self._agentErrorRE.match(data)
+ if errorMatch:
+ # We still need to consume the prompt, so raise an error after
+ # draining the rest of the buffer.
+ commandFailed = True
+
+ for line in data.splitlines():
+ if promptre.match(line):
+ foundPrompt = True
+ data = self._stripPrompt(data)
+ break
+
+ # periodically flush data to output file to make sure it doesn't get
+ # too big/unwieldly
+ if len(data) > 1024:
+ outputfile.write(data[0:1024])
+ data = data[1024:]
+
+ if commandFailed:
+ raise DMError("Automation Error: Error processing command '%s'; err='%s'" %
+ (cmd['cmd'], errorMatch.group(1)), fatal=True)
+
+ # Write any remaining data to outputfile
+ outputfile.write(data)
+
+ if shouldCloseSocket:
+ try:
+ self._sock.close()
+ self._sock = None
+ except:
+ self._sock = None
+ raise DMError("Automation Error: Error closing socket")
+
+ def _setupDeviceRoot(self, deviceRoot):
+ if not deviceRoot:
+ deviceRoot = "%s/tests" % self._runCmds(
+ [{'cmd': 'testroot'}]).strip()
+ self.mkDir(deviceRoot)
+
+ return deviceRoot
+
+ def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
+ cmdline = self._escapedCommandLine(cmd)
+ if env:
+ cmdline = '%s %s' % (self._formatEnvString(env), cmdline)
+
+ # execcwd/execcwdsu currently unsupported in Negatus; see bug 824127.
+ if cwd and self.agentProductName == 'SUTAgentNegatus':
+ raise DMError("Negatus does not support execcwd/execcwdsu")
+
+ haveExecSu = (self.agentProductName == 'SUTAgentNegatus' or
+ StrictVersion(self.agentVersion) >= StrictVersion('1.13'))
+
+ # Depending on agent version we send one of the following commands here:
+ # * exec (run as normal user)
+ # * execsu (run as privileged user)
+ # * execcwd (run as normal user from specified directory)
+ # * execcwdsu (run as privileged user from specified directory)
+
+ cmd = "exec"
+ if cwd:
+ cmd += "cwd"
+ if root and haveExecSu:
+ cmd += "su"
+
+ if cwd:
+ self._sendCmds([{'cmd': '%s %s %s' % (cmd, cwd, cmdline)}], outputfile, timeout)
+ else:
+ if (not root) or haveExecSu:
+ self._sendCmds([{'cmd': '%s %s' % (cmd, cmdline)}], outputfile, timeout)
+ else:
+ # need to manually inject su -c for backwards compatibility (this may
+ # not work on ICS or above!!)
+ # (FIXME: this backwards compatibility code is really ugly and should
+ # be deprecated at some point in the future)
+ self._sendCmds([{'cmd': '%s su -c "%s"' % (cmd, cmdline)}], outputfile,
+ timeout)
+
+ # dig through the output to get the return code
+ lastline = _pop_last_line(outputfile)
+ if lastline:
+ m = re.search('return code \[([0-9]+)\]', lastline)
+ if m:
+ return int(m.group(1))
+
+ # woops, we couldn't find an end of line/return value
+ raise DMError(
+ "Automation Error: Error finding end of line/return value when running '%s'" % cmdline)
+
+ def pushFile(self, localname, destname, retryLimit=None, createDir=True):
+ retryLimit = retryLimit or self.retryLimit
+ if createDir:
+ self.mkDirs(destname)
+
+ try:
+ filesize = os.path.getsize(localname)
+ with open(localname, 'rb') as f:
+ remoteHash = self._runCmds([{'cmd': 'push ' + destname + ' ' + str(filesize),
+ 'data': f.read()}], retryLimit=retryLimit).strip()
+ except OSError:
+ raise DMError("DeviceManager: Error reading file to push")
+
+ self._logger.debug("push returned: %s" % remoteHash)
+
+ localHash = self._getLocalHash(localname)
+
+ if localHash != remoteHash:
+ raise DMError("Automation Error: Push File failed to Validate! (localhash: %s, "
+ "remotehash: %s)" % (localHash, remoteHash))
+
+ def mkDir(self, name):
+ if not self.dirExists(name):
+ self._runCmds([{'cmd': 'mkdr ' + name}])
+
+ def pushDir(self, localDir, remoteDir, retryLimit=None, timeout=None):
+ retryLimit = retryLimit or self.retryLimit
+ self._logger.info("pushing directory: %s to %s" % (localDir, remoteDir))
+
+ existentDirectories = []
+ for root, dirs, files in os.walk(localDir, followlinks=True):
+ _, subpath = root.split(localDir)
+ subpath = subpath.lstrip('/')
+ remoteRoot = posixpath.join(remoteDir, subpath)
+ for f in files:
+ remoteName = posixpath.join(remoteRoot, f)
+
+ if subpath == "":
+ remoteRoot = remoteDir
+
+ parent = os.path.dirname(remoteName)
+ if parent not in existentDirectories:
+ self.mkDirs(remoteName)
+ existentDirectories.append(parent)
+
+ self.pushFile(os.path.join(root, f), remoteName,
+ retryLimit=retryLimit, createDir=False)
+
+ def dirExists(self, remotePath):
+ ret = self._runCmds([{'cmd': 'isdir ' + remotePath}]).strip()
+ if not ret:
+ raise DMError('Automation Error: DeviceManager isdir returned null')
+
+ return ret == 'TRUE'
+
+ def fileExists(self, filepath):
+ # Because we always have / style paths we make this a lot easier with some
+ # assumptions
+ filepath = posixpath.normpath(filepath)
+ # / should always exist but we can use this to check for things like
+ # having access to the filesystem
+ if filepath == '/':
+ return self.dirExists(filepath)
+ (containingpath, filename) = posixpath.split(filepath)
+ return filename in self.listFiles(containingpath)
+
+ def listFiles(self, rootdir):
+ rootdir = posixpath.normpath(rootdir)
+ if not self.dirExists(rootdir):
+ return []
+ data = self._runCmds([{'cmd': 'cd ' + rootdir}, {'cmd': 'ls'}])
+
+ files = filter(lambda x: x, data.splitlines())
+ if len(files) == 1 and files[0] == '<empty>':
+ # special case on the agent: empty directories return just the
+ # string "<empty>"
+ return []
+ return files
+
+ def removeFile(self, filename):
+ self._logger.info("removing file: " + filename)
+ if self.fileExists(filename):
+ self._runCmds([{'cmd': 'rm ' + filename}])
+
+ def removeDir(self, remoteDir):
+ if self.dirExists(remoteDir):
+ self._runCmds([{'cmd': 'rmdr ' + remoteDir}])
+
+ def moveTree(self, source, destination):
+ self._runCmds([{'cmd': 'mv %s %s' % (source, destination)}])
+
+ def copyTree(self, source, destination):
+ self._runCmds([{'cmd': 'dd if=%s of=%s' % (source, destination)}])
+
+ def getProcessList(self):
+ data = self._runCmds([{'cmd': 'ps'}])
+
+ processTuples = []
+ for line in data.splitlines():
+ if line:
+ pidproc = line.strip().split()
+ try:
+ if (len(pidproc) == 2):
+ processTuples += [[pidproc[0], pidproc[1]]]
+ elif (len(pidproc) == 3):
+ # android returns <userID> <procID> <procName>
+ processTuples += [[int(pidproc[1]), pidproc[2], int(pidproc[0])]]
+ else:
+ # unexpected format
+ raise ValueError
+ except ValueError:
+ self._logger.error("Unable to parse process list (bug 805969)")
+ self._logger.error("Line: %s\nFull output of process list:\n%s" % (line, data))
+ raise DMError("Invalid process line: %s" % line)
+
+ return processTuples
+
+ def fireProcess(self, appname, failIfRunning=False, maxWaitTime=30):
+ """
+ Starts a process
+
+ returns: pid
+
+ DEPRECATED: Use shell() or launchApplication() for new code
+ """
+ if not appname:
+ raise DMError("Automation Error: fireProcess called with no command to run")
+
+ self._logger.info("FIRE PROC: '%s'" % appname)
+
+ if (self.processExist(appname) is None):
+ self._logger.warning("process %s appears to be running already\n" % appname)
+ if (failIfRunning):
+ raise DMError("Automation Error: Process is already running")
+
+ self._runCmds([{'cmd': 'exec ' + appname}])
+
+ # The 'exec' command may wait for the process to start and end, so checking
+ # for the process here may result in process = None.
+ # The normal case is to launch the process and return right away
+ # There is one case with robotium (am instrument) where exec returns at the end
+ pid = None
+ waited = 0
+ while pid is None and waited < maxWaitTime:
+ pid = self.processExist(appname)
+ if pid:
+ break
+ time.sleep(1)
+ waited += 1
+
+ self._logger.debug("got pid: %s for process: %s" % (pid, appname))
+ return pid
+
+ def launchProcess(self, cmd, outputFile="process.txt", cwd='', env='', failIfRunning=False):
+ """
+ Launches a process, redirecting output to standard out
+
+ Returns output filename
+
+ 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 not cmd:
+ self._logger.warning("launchProcess called without command to run")
+ return None
+
+ if cmd[0] == 'am' and hasattr(self, '_getExtraAmStartArgs'):
+ cmd = cmd[:2] + self._getExtraAmStartArgs() + cmd[2:]
+
+ cmdline = subprocess.list2cmdline(cmd)
+ if outputFile == "process.txt" or outputFile is None:
+ outputFile += "%s/process.txt" % self.deviceRoot
+ cmdline += " > " + outputFile
+
+ # Prepend our env to the command
+ cmdline = '%s %s' % (self._formatEnvString(env), cmdline)
+
+ # fireProcess may trigger an exception, but we won't handle it
+ if cmd[0] == "am":
+ # Robocop tests spawn "am instrument". sutAgent's exec ensures that
+ # am has started before returning, so there is no point in having
+ # fireProcess wait for it to start. Also, since "am" does not show
+ # up in the process list while the test is running, waiting for it
+ # in fireProcess is difficult.
+ self.fireProcess(cmdline, failIfRunning, 0)
+ else:
+ self.fireProcess(cmdline, failIfRunning)
+ return outputFile
+
+ def killProcess(self, appname, sig=None):
+ if sig:
+ pid = self.processExist(appname)
+ if pid and pid > 0:
+ try:
+ self.shellCheckOutput(['kill', '-%d' % sig, str(pid)],
+ root=True)
+ except DMError as err:
+ self._logger.warning("unable to kill -%d %s (pid %s)" %
+ (sig, appname, str(pid)))
+ self._logger.debug(err)
+ raise err
+ else:
+ self._logger.warning("unable to kill -%d %s -- not running?" %
+ (sig, appname))
+ else:
+ retries = 0
+ while retries < self.retryLimit:
+ try:
+ if self.processExist(appname):
+ self._runCmds([{'cmd': 'kill ' + appname}])
+ return
+ except DMError as err:
+ retries += 1
+ self._logger.warning("try %d of %d failed to kill %s" %
+ (retries, self.retryLimit, appname))
+ self._logger.debug(err)
+ if retries >= self.retryLimit:
+ raise err
+
+ def getTempDir(self):
+ return self._runCmds([{'cmd': 'tmpd'}]).strip()
+
+ def pullFile(self, remoteFile, offset=None, length=None):
+ # The "pull" command is different from other commands in that DeviceManager
+ # has to read a certain number of bytes instead of just reading to the
+ # next prompt. This is more robust than the "cat" command, which will be
+ # confused if the prompt string exists within the file being catted.
+ # However it means we can't use the response-handling logic in sendCMD().
+
+ def err(error_msg):
+ err_str = 'DeviceManager: pull unsuccessful: %s' % error_msg
+ self._logger.error(err_str)
+ self._sock = None
+ raise DMError(err_str)
+
+ # FIXME: We could possibly move these socket-reading functions up to
+ # the class level if we wanted to refactor sendCMD(). For now they are
+ # only used to pull files.
+
+ def uread(to_recv, error_msg):
+ """ unbuffered read """
+ try:
+ data = ""
+ if select.select([self._sock], [], [], self.default_timeout)[0]:
+ data = self._sock.recv(to_recv)
+ if not data:
+ # timed out waiting for response or error response
+ err(error_msg)
+
+ return data
+ except:
+ err(error_msg)
+
+ def read_until_char(c, buf, error_msg):
+ """ read until 'c' is found; buffer rest """
+ while c not in buf:
+ data = uread(1024, error_msg)
+ buf += data
+ return buf.partition(c)
+
+ def read_exact(total_to_recv, buf, error_msg):
+ """ read exact number of 'total_to_recv' bytes """
+ while len(buf) < total_to_recv:
+ to_recv = min(total_to_recv - len(buf), 1024)
+ data = uread(to_recv, error_msg)
+ buf += data
+ return buf
+
+ prompt = self._base_prompt + self._prompt_sep
+ buf = ''
+
+ # expected return value:
+ # <filename>,<filesize>\n<filedata>
+ # or, if error,
+ # <filename>,-1\n<error message>
+
+ # just send the command first, we read the response inline below
+ if offset is not None and length is not None:
+ cmd = 'pull %s %d %d' % (remoteFile, offset, length)
+ elif offset is not None:
+ cmd = 'pull %s %d' % (remoteFile, offset)
+ else:
+ cmd = 'pull %s' % remoteFile
+
+ self._runCmds([{'cmd': cmd}])
+
+ # read metadata; buffer the rest
+ metadata, sep, buf = read_until_char('\n', buf, 'could not find metadata')
+ if not metadata:
+ return None
+ self._logger.debug('metadata: %s' % metadata)
+
+ filename, sep, filesizestr = metadata.partition(',')
+ if sep == '':
+ err('could not find file size in returned metadata')
+ try:
+ filesize = int(filesizestr)
+ except ValueError:
+ err('invalid file size in returned metadata')
+
+ if filesize == -1:
+ # read error message
+ error_str, sep, buf = read_until_char('\n', buf, 'could not find error message')
+ if not error_str:
+ err("blank error message")
+ # prompt should follow
+ read_exact(len(prompt), buf, 'could not find prompt')
+ # failures are expected, so don't use "Remote Device Error" or we'll RETRY
+ raise DMError("DeviceManager: pulling file '%s' unsuccessful: %s" %
+ (remoteFile, error_str))
+
+ # read file data
+ total_to_recv = filesize + len(prompt)
+ buf = read_exact(total_to_recv, buf, 'could not get all file data')
+ if buf[-len(prompt):] != prompt:
+ err('no prompt found after file data--DeviceManager may be out of sync with agent')
+ return buf
+ return buf[:-len(prompt)]
+
+ def getFile(self, remoteFile, localFile):
+ data = self.pullFile(remoteFile)
+
+ fhandle = open(localFile, 'wb')
+ fhandle.write(data)
+ fhandle.close()
+ if not self.validateFile(remoteFile, localFile):
+ raise DMError("Automation Error: Failed to validate file when downloading %s" %
+ remoteFile)
+
+ def getDirectory(self, remoteDir, localDir, checkDir=True):
+ self._logger.info("getting files in '%s'" % remoteDir)
+ if checkDir and not self.dirExists(remoteDir):
+ raise DMError("Automation Error: Error getting directory: %s not a directory" %
+ remoteDir)
+
+ filelist = self.listFiles(remoteDir)
+ self._logger.debug(filelist)
+ if not os.path.exists(localDir):
+ os.makedirs(localDir)
+
+ for f in filelist:
+ if f == '.' or f == '..':
+ continue
+ remotePath = remoteDir + '/' + f
+ localPath = os.path.join(localDir, f)
+ if self.dirExists(remotePath):
+ self.getDirectory(remotePath, localPath, False)
+ else:
+ self.getFile(remotePath, localPath)
+
+ def validateFile(self, remoteFile, localFile):
+ remoteHash = self._getRemoteHash(remoteFile)
+ localHash = self._getLocalHash(localFile)
+
+ if (remoteHash is None):
+ return False
+
+ if (remoteHash == localHash):
+ return True
+
+ return False
+
+ def _getRemoteHash(self, filename):
+ data = self._runCmds([{'cmd': 'hash ' + filename}]).strip()
+ self._logger.debug("remote hash returned: '%s'" % data)
+ return data
+
+ def unpackFile(self, filePath, destDir=None):
+ """
+ Unzips a bundle to a location on the device
+
+ If destDir is not specified, the bundle is extracted in the same directory
+ """
+ # if no destDir is passed in just set it to filePath's folder
+ if not destDir:
+ destDir = posixpath.dirname(filePath)
+
+ if destDir[-1] != '/':
+ destDir += '/'
+
+ self._runCmds([{'cmd': 'unzp %s %s' % (filePath, destDir)}])
+
+ def _getRebootServerSocket(self, ipAddr):
+ serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ serverSocket.settimeout(60.0)
+ serverSocket.bind((ipAddr, 0))
+ serverSocket.listen(1)
+ self._logger.debug('Created reboot callback server at %s:%d' %
+ serverSocket.getsockname())
+ return serverSocket
+
+ def _waitForRebootPing(self, serverSocket):
+ conn = None
+ data = None
+ startTime = datetime.datetime.now()
+ waitTime = datetime.timedelta(seconds=self.reboot_timeout)
+ while not data and datetime.datetime.now() - startTime < waitTime:
+ self._logger.info("Waiting for reboot callback ping from device...")
+ try:
+ if not conn:
+ conn, _ = serverSocket.accept()
+ # Receiving any data is good enough.
+ data = conn.recv(1024)
+ if data:
+ self._logger.info("Received reboot callback ping from device!")
+ conn.sendall('OK')
+ conn.close()
+ except socket.timeout:
+ pass
+ except socket.error as e:
+ if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK:
+ raise
+
+ if not data:
+ raise DMError('Timed out waiting for reboot callback.')
+
+ self._logger.info("Sleeping for %s seconds to wait for device "
+ "to 'settle'" % self.reboot_settling_time)
+ time.sleep(self.reboot_settling_time)
+
+ def reboot(self, ipAddr=None, port=30000, wait=False):
+ # port ^^^ is here for backwards compatibility only, we now
+ # determine a port automatically and safely
+ wait = (wait or ipAddr)
+
+ cmd = 'rebt'
+
+ self._logger.info("Rebooting device")
+
+ # if we're waiting, create a listening server and pass information on
+ # it to the device before rebooting (we do this instead of just polling
+ # to make sure the device actually rebooted -- yes, there are probably
+ # simpler ways of doing this like polling uptime, but this is what we're
+ # doing for now)
+ if wait:
+ if not ipAddr:
+ ipAddr = moznetwork.get_ip()
+ serverSocket = self._getRebootServerSocket(ipAddr)
+ # The update.info command tells the SUTAgent to send a TCP message
+ # after restarting.
+ destname = '/data/data/com.mozilla.SUTAgentAndroid/files/update.info'
+ data = "%s,%s\rrebooting\r" % serverSocket.getsockname()
+ self._runCmds([{'cmd': 'push %s %s' % (destname, len(data)),
+ 'data': data}])
+ cmd += " %s %s" % serverSocket.getsockname()
+
+ # actually reboot device
+ self._runCmds([{'cmd': cmd}])
+ # if we're waiting, wait for a callback ping from the agent before
+ # continuing (and throw an exception if we don't receive said ping)
+ if wait:
+ self._waitForRebootPing(serverSocket)
+
+ def getInfo(self, directive=None):
+ data = None
+ result = {}
+ collapseSpaces = re.compile(' +')
+
+ directives = ['os', 'id', 'uptime', 'uptimemillis', 'systime', 'screen',
+ 'rotation', 'memory', 'process', 'disk', 'power', 'sutuserinfo',
+ 'temperature']
+ if (directive in directives):
+ directives = [directive]
+
+ for d in directives:
+ data = self._runCmds([{'cmd': 'info ' + d}])
+
+ data = collapseSpaces.sub(' ', data)
+ result[d] = data.split('\n')
+
+ # Get rid of any 0 length members of the arrays
+ for k, v in result.iteritems():
+ result[k] = filter(lambda x: x != '', result[k])
+
+ # Format the process output
+ if 'process' in result:
+ proclist = []
+ for l in result['process']:
+ if l:
+ proclist.append(l.split('\t'))
+ result['process'] = proclist
+
+ self._logger.debug("results: %s" % result)
+ return result
+
+ def installApp(self, appBundlePath, destPath=None):
+ cmd = 'inst ' + appBundlePath
+ if destPath:
+ cmd += ' ' + destPath
+
+ data = self._runCmds([{'cmd': cmd}])
+
+ if 'installation complete [0]' not in data:
+ raise DMError("Remove Device Error: Error installing app. Error message: %s" % data)
+
+ def uninstallApp(self, appName, installPath=None):
+ cmd = 'uninstall ' + appName
+ if installPath:
+ cmd += ' ' + installPath
+ data = self._runCmds([{'cmd': cmd}])
+
+ status = data.split('\n')[0].strip()
+ self._logger.debug("uninstallApp: '%s'" % status)
+ if status == 'Success':
+ return
+ raise DMError("Remote Device Error: uninstall failed for %s" % appName)
+
+ def uninstallAppAndReboot(self, appName, installPath=None):
+ cmd = 'uninst ' + appName
+ if installPath:
+ cmd += ' ' + installPath
+ data = self._runCmds([{'cmd': cmd}])
+
+ self._logger.debug("uninstallAppAndReboot: %s" % data)
+ return
+
+ def updateApp(self, appBundlePath, processName=None, destPath=None,
+ ipAddr=None, port=30000, wait=False):
+ # port ^^^ is here for backwards compatibility only, we now
+ # determine a port automatically and safely
+ wait = (wait or ipAddr)
+
+ cmd = 'updt '
+ if processName is None:
+ # Then we pass '' for processName
+ cmd += "'' " + appBundlePath
+ else:
+ cmd += processName + ' ' + appBundlePath
+
+ if destPath:
+ cmd += " " + destPath
+
+ if wait:
+ if not ipAddr:
+ ipAddr = moznetwork.get_ip()
+ serverSocket = self._getRebootServerSocket(ipAddr)
+ cmd += " %s %s" % serverSocket.getsockname()
+
+ self._logger.debug("updateApp using command: " % cmd)
+
+ self._runCmds([{'cmd': cmd}])
+
+ if wait:
+ self._waitForRebootPing(serverSocket)
+
+ def getCurrentTime(self):
+ return int(self._runCmds([{'cmd': 'clok'}]).strip())
+
+ def _formatEnvString(self, env):
+ """
+ Returns a properly formatted env string for the agent.
+
+ Input - env, which is either None, '', or a dict
+ Output - a quoted string of the form: '"envvar1=val1,envvar2=val2..."'
+ If env is None or '' return '' (empty quoted string)
+ """
+ if (env is None or env == ''):
+ return ''
+
+ retVal = '"%s"' % ','.join(map(lambda x: '%s=%s' % (x[0], x[1]), env.iteritems()))
+ if (retVal == '""'):
+ return ''
+
+ return retVal
+
+ def adjustResolution(self, width=1680, height=1050, type='hdmi'):
+ """
+ Adjust the screen resolution on the device, REBOOT REQUIRED
+
+ NOTE: this only works on a tegra ATM
+
+ supported resolutions: 640x480, 800x600, 1024x768, 1152x864, 1200x1024, 1440x900,
+ 1680x1050, 1920x1080
+ """
+ if self.getInfo('os')['os'][0].split()[0] != 'harmony-eng':
+ self._logger.warning("unable to adjust screen resolution on non Tegra device")
+ return False
+
+ results = self.getInfo('screen')
+ parts = results['screen'][0].split(':')
+ self._logger.debug("we have a current resolution of %s, %s" %
+ (parts[1].split()[0], parts[2].split()[0]))
+
+ # verify screen type is valid, and set it to the proper value
+ # (https://bugzilla.mozilla.org/show_bug.cgi?id=632895#c4)
+ screentype = -1
+ if (type == 'hdmi'):
+ screentype = 5
+ elif (type == 'vga' or type == 'crt'):
+ screentype = 3
+ else:
+ return False
+
+ # verify we have numbers
+ if not (isinstance(width, int) and isinstance(height, int)):
+ return False
+
+ if (width < 100 or width > 9999):
+ return False
+
+ if (height < 100 or height > 9999):
+ return False
+
+ self._logger.debug("adjusting screen resolution to %s, %s and rebooting" % (width, height))
+
+ self._runCmds(
+ [{'cmd': "exec setprop persist.tegra.dpy%s.mode.width %s" % (screentype, width)}])
+ self._runCmds(
+ [{'cmd': "exec setprop persist.tegra.dpy%s.mode.height %s" % (screentype, height)}])
+
+ def chmodDir(self, remoteDir, **kwargs):
+ self._runCmds([{'cmd': "chmod " + remoteDir}])
diff --git a/testing/mozbase/mozdevice/mozdevice/dmcli.py b/testing/mozbase/mozdevice/mozdevice/dmcli.py
new file mode 100644
index 000000000..7ba65e842
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/dmcli.py
@@ -0,0 +1,382 @@
+# 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
+"""
+
+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'
+ },
+ 'sutver': {'function': self.sutver,
+ 'help': 'SUTAgent\'s product name and version (SUT only)'
+ },
+ '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})
+
+ if args.dmtype == "sut" and not args.host and not args.hwid:
+ self.parser.error("Must specify device ip in TEST_DEVICE or "
+ "with --host option with SUT")
+
+ self.dm = self.getDevice(dmtype=args.dmtype, 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 SUTAgent or "
+ "adb-over-tcp)", default=None)
+ parser.add_argument("-m", "--dmtype", action="store",
+ help="DeviceManager type (adb or sut, defaults "
+ "to DM_TRANS environment variable, if "
+ "present, or adb)",
+ default=os.environ.get('DM_TRANS', 'adb'))
+ 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, dmtype="adb", 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 hwid:
+ return mozdevice.DroidConnectByHWID(hwid, logLevel=logLevel)
+
+ if dmtype == "adb":
+ if host and not port:
+ port = 5555
+ return mozdevice.DroidADB(packageName=packagename,
+ host=host, port=port,
+ logLevel=logLevel)
+ elif dmtype == "sut":
+ if not host:
+ self.parser.error("Must specify host with SUT!")
+ if not port:
+ port = 20701
+ return mozdevice.DroidSUT(host=host, port=port,
+ logLevel=logLevel)
+ else:
+ self.parser.error("Unknown device manager type: %s" % type)
+
+ 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 sutver(self, args):
+ if args.dmtype == 'sut':
+ print '%s Version %s' % (self.dm.agentProductName,
+ self.dm.agentVersion)
+ else:
+ print 'Must use SUT transport to get SUT version.'
+
+ 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
new file mode 100644
index 000000000..f06a619c4
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/droid.py
@@ -0,0 +1,263 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import StringIO
+import moznetwork
+import re
+import threading
+import time
+
+import version_codes
+
+from Zeroconf import Zeroconf, ServiceBrowser
+from devicemanager import ZeroconfListener
+from devicemanagerADB import DeviceManagerADB
+from devicemanagerSUT import DeviceManagerSUT
+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:
+ data = self.shellCheckOutput(
+ ["dumpsys", "window", "windows"], timeout=self.short_timeout)
+ except:
+ # 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.SUTAgentAndroid/.SUTAgentAndroid}} # 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 -- same as sutagent.
+ package = ""
+ return package
+
+ def getAppRoot(self, packageName):
+ """
+ Returns the root directory for the specified android application
+ """
+ # relying on convention
+ return '/data/data/%s' % packageName
+
+
+class DroidSUT(DeviceManagerSUT, DroidMixin):
+
+ def _getExtraAmStartArgs(self):
+ # in versions of android in jellybean and beyond, the agent may run as
+ # a different process than the one that started the app. In this case,
+ # we need to get back the original user serial number and then pass
+ # that to the 'am start' command line
+ if not hasattr(self, '_userSerial'):
+ infoDict = self.getInfo(directive="sutuserinfo")
+ if infoDict.get('sutuserinfo') and \
+ len(infoDict['sutuserinfo']) > 0:
+ userSerialString = infoDict['sutuserinfo'][0]
+ # user serial always an integer, see:
+ # http://developer.android.com/reference/android/os/UserManager.html#getSerialNumberForUser%28android.os.UserHandle%29
+ m = re.match('User Serial:([0-9]+)', userSerialString)
+ if m:
+ self._userSerial = m.group(1)
+ else:
+ self._userSerial = None
+ else:
+ self._userSerial = None
+
+ if self._userSerial is not None:
+ return ["--user", self._userSerial]
+
+ return []
+
+ def getTopActivity(self):
+ return self._runCmds([{'cmd': "activity"}]).strip()
+
+ def getAppRoot(self, packageName):
+ return self._runCmds([{'cmd': 'getapproot %s' % packageName}]).strip()
+
+
+def DroidConnectByHWID(hwid, timeout=30, **kwargs):
+ """Try to connect to the given device by waiting for it to show up using
+ mDNS with the given timeout."""
+ zc = Zeroconf(moznetwork.get_ip())
+
+ evt = threading.Event()
+ listener = ZeroconfListener(hwid, evt)
+ sb = ServiceBrowser(zc, "_sutagent._tcp.local.", listener)
+ foundIP = None
+ if evt.wait(timeout):
+ # we found the hwid
+ foundIP = listener.ip
+ sb.cancel()
+ zc.close()
+
+ if foundIP is not None:
+ return DroidSUT(foundIP, **kwargs)
+ print "Connected via SUT to %s [at %s]" % (hwid, foundIP)
+
+ # try connecting via adb
+ try:
+ sut = DroidADB(deviceSerial=hwid, **kwargs)
+ except:
+ return None
+
+ print "Connected via ADB to %s" % (hwid)
+ return sut
diff --git a/testing/mozbase/mozdevice/mozdevice/sutini.py b/testing/mozbase/mozdevice/mozdevice/sutini.py
new file mode 100644
index 000000000..7dd5e54c4
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/sutini.py
@@ -0,0 +1,126 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import ConfigParser
+import StringIO
+import os
+import sys
+import tempfile
+
+from mozdevice.droid import DroidSUT
+from mozdevice.devicemanager import DMError
+
+USAGE = '%s <host>'
+INI_PATH_JAVA = '/data/data/com.mozilla.SUTAgentAndroid/files/SUTAgent.ini'
+INI_PATH_NEGATUS = '/data/local/SUTAgent.ini'
+SCHEMA = {'Registration Server': (('IPAddr', ''),
+ ('PORT', '28001'),
+ ('HARDWARE', ''),
+ ('POOL', '')),
+ 'Network Settings': (('SSID', ''),
+ ('AUTH', ''),
+ ('ENCR', ''),
+ ('EAP', ''))}
+
+
+def get_cfg(d, ini_path):
+ cfg = ConfigParser.RawConfigParser()
+ try:
+ cfg.readfp(StringIO.StringIO(d.pullFile(ini_path)), 'SUTAgent.ini')
+ except DMError:
+ # assume this is due to a missing file...
+ pass
+ return cfg
+
+
+def put_cfg(d, cfg, ini_path):
+ print 'Writing modified SUTAgent.ini...'
+ t = tempfile.NamedTemporaryFile(delete=False)
+ cfg.write(t)
+ t.close()
+ try:
+ d.pushFile(t.name, ini_path)
+ except DMError, e:
+ print e
+ else:
+ print 'Done.'
+ finally:
+ os.unlink(t.name)
+
+
+def set_opt(cfg, s, o, dflt):
+ prompt = ' %s' % o
+ try:
+ curval = cfg.get(s, o)
+ except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+ curval = ''
+ if curval:
+ dflt = curval
+ prompt += ': '
+ if dflt:
+ prompt += '[%s] ' % dflt
+ newval = raw_input(prompt)
+ if not newval:
+ newval = dflt
+ if newval == curval:
+ return False
+ cfg.set(s, o, newval)
+ return True
+
+
+def bool_query(prompt, dflt):
+ while True:
+ i = raw_input('%s [%s] ' % (prompt, 'y' if dflt else 'n')).lower()
+ if not i or i[0] in ('y', 'n'):
+ break
+ print 'Enter y or n.'
+ return (not i and dflt) or (i and i[0] == 'y')
+
+
+def edit_sect(cfg, sect, opts):
+ changed_vals = False
+ if bool_query('Edit section %s?' % sect, False):
+ if not cfg.has_section(sect):
+ cfg.add_section(sect)
+ print '%s settings:' % sect
+ for opt, dflt in opts:
+ changed_vals |= set_opt(cfg, sect, opt, dflt)
+ print
+ else:
+ if cfg.has_section(sect) and bool_query('Delete section %s?' % sect,
+ False):
+ cfg.remove_section(sect)
+ changed_vals = True
+ return changed_vals
+
+
+def main():
+ try:
+ host = sys.argv[1]
+ except IndexError:
+ print USAGE % sys.argv[0]
+ sys.exit(1)
+ try:
+ d = DroidSUT(host, retryLimit=1)
+ except DMError, e:
+ print e
+ sys.exit(1)
+ # check if using Negatus and change path accordingly
+ ini_path = INI_PATH_JAVA
+ if 'Negatus' in d.agentProductName:
+ ini_path = INI_PATH_NEGATUS
+ cfg = get_cfg(d, ini_path)
+ if not cfg.sections():
+ print 'Empty or missing ini file.'
+ changed_vals = False
+ for sect, opts in SCHEMA.iteritems():
+ changed_vals |= edit_sect(cfg, sect, opts)
+ if changed_vals:
+ put_cfg(d, cfg, ini_path)
+ else:
+ print 'No changes.'
+
+
+if __name__ == '__main__':
+ main()
diff --git a/testing/mozbase/mozdevice/mozdevice/version_codes.py b/testing/mozbase/mozdevice/mozdevice/version_codes.py
new file mode 100644
index 000000000..6602d837a
--- /dev/null
+++ b/testing/mozbase/mozdevice/mozdevice/version_codes.py
@@ -0,0 +1,61 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+VERSION CODES of the android releases.
+
+See http://developer.android.com/reference/android/os/Build.VERSION_CODES.html.
+"""
+
+# Magic version number for a current development build, which has
+# not yet turned into an official release.
+CUR_DEVELOPMENT = 10000
+
+# October 2008: The original, first, version of Android
+BASE = 1
+# February 2009: First Android update, officially called 1.1
+BASE_1_1 = 2
+# May 2009: Android 1.5
+CUPCAKE = 3
+# September 2009: Android 1.6
+DONUT = 4
+# November 2009: Android 2.0
+ECLAIR = 5
+# December 2009: Android 2.0.1
+ECLAIR_0_1 = 6
+# January 2010: Android 2.1
+ECLAIR_MR1 = 7
+# June 2010: Android 2.2
+FROYO = 8
+# November 2010: Android 2.3
+GINGERBREAD = 9
+# February 2011: Android 2.3.3
+GINGERBREAD_MR1 = 10
+# February 2011: Android 3.0
+HONEYCOMB = 11
+# May 2011: Android 3.1
+HONEYCOMB_MR1 = 12
+# June 2011: Android 3.2
+HONEYCOMB_MR2 = 13
+# October 2011: Android 4.0
+ICE_CREAM_SANDWICH = 14
+# December 2011: Android 4.0.3
+ICE_CREAM_SANDWICH_MR1 = 15
+# June 2012: Android 4.1
+JELLY_BEAN = 16
+# November 2012: Android 4.2
+JELLY_BEAN_MR1 = 17
+# July 2013: Android 4.3
+JELLY_BEAN_MR2 = 18
+# October 2013: Android 4.4
+KITKAT = 19
+# Android 4.4W
+KITKAT_WATCH = 20
+# Lollilop
+LOLLIPOP = 21
+LOLLIPOP_MR1 = 22
+# M
+M = 23
+# N
+N = 24
diff --git a/testing/mozbase/mozdevice/setup.py b/testing/mozbase/mozdevice/setup.py
new file mode 100644
index 000000000..477227f28
--- /dev/null
+++ b/testing/mozbase/mozdevice/setup.py
@@ -0,0 +1,36 @@
+# 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 setuptools import setup
+
+PACKAGE_NAME = 'mozdevice'
+PACKAGE_VERSION = '0.48'
+
+deps = ['mozfile >= 1.0',
+ 'mozlog >= 3.0',
+ 'moznetwork >= 0.24',
+ 'mozprocess >= 0.19',
+ ]
+
+setup(name=PACKAGE_NAME,
+ version=PACKAGE_VERSION,
+ description="Mozilla-authored device management",
+ long_description="see http://mozbase.readthedocs.org/",
+ classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+ keywords='',
+ author='Mozilla Automation and Testing Team',
+ author_email='tools@lists.mozilla.org',
+ url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
+ license='MPL',
+ packages=['mozdevice'],
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=deps,
+ entry_points="""
+ # -*- Entry points: -*-
+ [console_scripts]
+ dm = mozdevice.dmcli:cli
+ sutini = mozdevice.sutini:main
+ """,
+ )
diff --git a/testing/mozbase/mozdevice/sut_tests/README.md b/testing/mozbase/mozdevice/sut_tests/README.md
new file mode 100644
index 000000000..ffc100f45
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/README.md
@@ -0,0 +1,15 @@
+# SUT Agent tests
+
+* In order to run these tests you need to have a phone running SUT Agent
+connected.
+
+* Make sure you can reach the device's TCP 20700 and 20701 ports. Doing
+*adb forward tcp:20700 tcp:20700 && adb forward tcp:20701 tcp:20701* will
+forward your localhost 20700 and 20701 ports to the ones on the device.
+
+* You might need some common tools like cp. Use the `setup-tools.sh` script
+to install them. It requires `$ADB` to point to the `adb` binary on the system.
+
+* Make sure the SUTAgent on the device is running.
+
+* Run: python runtests.py
diff --git a/testing/mozbase/mozdevice/sut_tests/dmunit.py b/testing/mozbase/mozdevice/sut_tests/dmunit.py
new file mode 100644
index 000000000..b4a3d0b9b
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/dmunit.py
@@ -0,0 +1,55 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import logging
+import types
+import unittest
+
+from mozdevice import devicemanager
+from mozdevice import devicemanagerSUT
+
+ip = ''
+port = 0
+heartbeat_port = 0
+log_level = logging.ERROR
+
+
+class DeviceManagerTestCase(unittest.TestCase):
+ """DeviceManager tests should subclass this.
+ """
+
+ """Set to False in your derived class if this test
+ should not be run on the Python agent.
+ """
+ runs_on_test_device = True
+
+ def _setUp(self):
+ """ Override this if you want set-up code in your test."""
+ return
+
+ def setUp(self):
+ self.dm = devicemanagerSUT.DeviceManagerSUT(host=ip, port=port,
+ logLevel=log_level)
+ self.dmerror = devicemanager.DMError
+ self._setUp()
+
+
+class DeviceManagerTestLoader(unittest.TestLoader):
+
+ def __init__(self, isTestDevice=False):
+ self.isTestDevice = isTestDevice
+
+ def loadTestsFromModuleName(self, module_name):
+ """Loads tests from modules unless the SUT is a test device and
+ the test case has runs_on_test_device set to False
+ """
+ tests = []
+ module = __import__(module_name)
+ for name in dir(module):
+ obj = getattr(module, name)
+ if (isinstance(obj, (type, types.ClassType)) and
+ issubclass(obj, unittest.TestCase)) and \
+ (not self.isTestDevice or obj.runs_on_test_device):
+ tests.append(self.loadTestsFromTestCase(obj))
+ return self.suiteClass(tests)
diff --git a/testing/mozbase/mozdevice/sut_tests/genfiles.py b/testing/mozbase/mozdevice/sut_tests/genfiles.py
new file mode 100644
index 000000000..5ab8d6349
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/genfiles.py
@@ -0,0 +1,85 @@
+# 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 random import randint
+from zipfile import ZipFile
+import os
+import shutil
+
+
+def gen_binary_file(path, size):
+ with open(path, 'wb') as f:
+ for i in xrange(size):
+ byte = '%c' % randint(0, 255)
+ f.write(byte)
+
+
+def gen_zip(path, files, stripped_prefix=''):
+ with ZipFile(path, 'w') as z:
+ for f in files:
+ new_name = f.replace(stripped_prefix, '')
+ z.write(f, new_name)
+
+
+def mkdir(path, *args):
+ try:
+ os.mkdir(path, *args)
+ except OSError:
+ pass
+
+
+def gen_folder_structure():
+ root = 'test-files'
+ prefix = os.path.join(root, 'push2')
+ mkdir(prefix)
+
+ gen_binary_file(os.path.join(prefix, 'file4.bin'), 59036)
+ mkdir(os.path.join(prefix, 'sub1'))
+ shutil.copyfile(os.path.join(root, 'mytext.txt'),
+ os.path.join(prefix, 'sub1', 'file1.txt'))
+ mkdir(os.path.join(prefix, 'sub1', 'sub1.1'))
+ shutil.copyfile(os.path.join(root, 'mytext.txt'),
+ os.path.join(prefix, 'sub1', 'sub1.1', 'file2.txt'))
+ mkdir(os.path.join(prefix, 'sub2'))
+ shutil.copyfile(os.path.join(root, 'mytext.txt'),
+ os.path.join(prefix, 'sub2', 'file3.txt'))
+
+
+def gen_test_files():
+ gen_folder_structure()
+ flist = [
+ os.path.join('test-files', 'push2'),
+ os.path.join('test-files', 'push2', 'file4.bin'),
+ os.path.join('test-files', 'push2', 'sub1'),
+ os.path.join('test-files', 'push2', 'sub1', 'file1.txt'),
+ os.path.join('test-files', 'push2', 'sub1', 'sub1.1'),
+ os.path.join('test-files', 'push2', 'sub1', 'sub1.1', 'file2.txt'),
+ os.path.join('test-files', 'push2', 'sub2'),
+ os.path.join('test-files', 'push2', 'sub2', 'file3.txt')
+ ]
+ gen_zip(os.path.join('test-files', 'mybinary.zip'),
+ flist, stripped_prefix=('test-files' + os.path.sep))
+ gen_zip(os.path.join('test-files', 'mytext.zip'),
+ [os.path.join('test-files', 'mytext.txt')])
+
+
+def clean_test_files():
+ ds = [os.path.join('test-files', d) for d in ('push1', 'push2')]
+ for d in ds:
+ try:
+ shutil.rmtree(d)
+ except OSError:
+ pass
+
+ fs = [os.path.join('test-files', f) for f in ('mybinary.zip', 'mytext.zip')]
+ for f in fs:
+ try:
+ os.remove(f)
+ except OSError:
+ pass
+
+
+if __name__ == '__main__':
+ gen_test_files()
diff --git a/testing/mozbase/mozdevice/sut_tests/runtests.py b/testing/mozbase/mozdevice/sut_tests/runtests.py
new file mode 100644
index 000000000..fffc306e3
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/runtests.py
@@ -0,0 +1,96 @@
+# 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 optparse import OptionParser
+import logging
+import os
+import re
+import sys
+import unittest
+
+import dmunit
+import genfiles
+
+
+def main(ip, port, heartbeat_port, scripts, directory, isTestDevice, verbose):
+ dmunit.ip = ip
+ dmunit.port = port
+ dmunit.heartbeat_port = heartbeat_port
+ if verbose:
+ dmunit.log_level = logging.DEBUG
+
+ suite = unittest.TestSuite()
+
+ genfiles.gen_test_files()
+
+ if scripts:
+ # Ensure the user didn't include the .py on the name of the test file
+ # (and get rid of it if they did)
+ scripts = map(lambda x: x.split('.')[0], scripts)
+ else:
+ # Go through the directory and pick up everything
+ # named test_*.py and run it
+ testfile = re.compile('^test_.*\.py$')
+ files = os.listdir(directory)
+
+ for f in files:
+ if testfile.match(f):
+ scripts.append(f.split('.')[0])
+
+ testLoader = dmunit.DeviceManagerTestLoader(isTestDevice)
+ for s in scripts:
+ suite.addTest(testLoader.loadTestsFromModuleName(s))
+ unittest.TextTestRunner(verbosity=2).run(suite)
+
+ genfiles.clean_test_files()
+
+
+if __name__ == "__main__":
+
+ default_ip = '127.0.0.1'
+ default_port = 20701
+
+ env_ip, _, env_port = os.getenv('TEST_DEVICE', '').partition(':')
+ if env_port:
+ try:
+ env_port = int(env_port)
+ except ValueError:
+ print >> sys.stderr, "Port in TEST_DEVICE should be an integer."
+ sys.exit(1)
+
+ # Deal with the options
+ parser = OptionParser()
+ parser.add_option("--ip", action="store", type="string", dest="ip",
+ help="IP address for device running SUTAgent, defaults "
+ "to what's provided in $TEST_DEVICE or 127.0.0.1",
+ default=(env_ip or default_ip))
+
+ parser.add_option("--port", action="store", type="int", dest="port",
+ help="Port of SUTAgent on device, defaults to "
+ "what's provided in $TEST_DEVICE or 20701",
+ default=(env_port or default_port))
+
+ parser.add_option("--heartbeat", action="store", type="int",
+ dest="heartbeat_port", help="Port for heartbeat/data "
+ "channel, defaults to 20700", default=20700)
+
+ parser.add_option("--script", action="append", type="string",
+ dest="scripts", help="Name of test script to run, "
+ "can be specified multiple times", default=[])
+
+ parser.add_option("--directory", action="store", type="string", dest="dir",
+ help="Directory to look for tests in, defaults to "
+ "current directory", default=os.getcwd())
+
+ parser.add_option("--testDevice", action="store_true", dest="isTestDevice",
+ help="Specifies that the device is a local test agent",
+ default=False)
+
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ help="Verbose DeviceManager output", default=False)
+
+ (options, args) = parser.parse_args()
+
+ main(options.ip, options.port, options.heartbeat_port, options.scripts,
+ options.dir, options.isTestDevice, options.verbose)
diff --git a/testing/mozbase/mozdevice/sut_tests/setup-tools.sh b/testing/mozbase/mozdevice/sut_tests/setup-tools.sh
new file mode 100755
index 000000000..e50a71d0b
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/setup-tools.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+if [ ! -f busybox-armv6l ]
+then
+ wget http://busybox.net/downloads/binaries/1.19.0/busybox-armv6l
+fi
+$ADB remount
+$ADB push busybox-armv6l /system/bin/busybox
+
+$ADB shell 'cd /system/bin; chmod 555 busybox; for x in `./busybox --list`; do ln -s ./busybox $x; done'
diff --git a/testing/mozbase/mozdevice/sut_tests/test-files/mytext.txt b/testing/mozbase/mozdevice/sut_tests/test-files/mytext.txt
new file mode 100644
index 000000000..74cb65fa8
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test-files/mytext.txt
@@ -0,0 +1,177 @@
+this is a file with 71K bytes of text in it
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc placerat, mi sit amet laoreet sollicitudin, neque urna bibendum eros, nec adipiscing tellus ipsum id risus. Sed aliquam ligula nec nibh sollicitudin venenatis. Praesent faucibus tortor vel felis egestas pellentesque. Cras viverra, dui viverra vulputate ornare, eros nunc volutpat nisl, sed sodales turpis orci quis diam. Donec eu sem mi. Mauris dictum blandit mauris quis ultricies. Sed faucibus erat vel velit viverra adipiscing. Donec placerat mattis venenatis. Suspendisse placerat sagittis risus et dapibus. Vivamus diam nisi, elementum ac mollis nec, porta ut sapien. Curabitur ac dolor ligula, vel sollicitudin sapien. Nullam blandit ligula nisl. Proin faucibus, ipsum sit amet molestie tincidunt, tellus neque accumsan lectus, a congue felis odio eu nunc. Pellentesque mauris sapien, varius ut scelerisque et, dictum sed magna. In faucibus tristique erat, a malesuada justo tincidunt sed.
+
+Morbi quis iaculis elit. Praesent nec diam mi, eu auctor neque. Phasellus fringilla turpis a metus imperdiet laoreet et ut augue. Mauris imperdiet scelerisque arcu quis sollicitudin. Nulla mauris dui, ultricies at vulputate quis, pharetra in erat. Donec mollis ipsum quis purus fermentum commodo. Nunc nec orci sem, quis rhoncus mauris. Sed iaculis tempus quam, non consectetur nisl tincidunt vitae. Nulla aliquam sodales auctor. Donec placerat venenatis facilisis. In sollicitudin arcu tincidunt lorem molestie bibendum. Phasellus rutrum ante vitae lorem iaculis eget porta odio pretium.
+
+Duis id mauris ante, eget ullamcorper justo. Integer vitae felis nisi, eget blandit tortor. Vivamus ligula odio, adipiscing sit amet tincidunt id, pretium sed massa. Suspendisse massa felis, viverra non adipiscing quis, dictum eget metus. In porta, tortor a imperdiet sodales, nulla mi mollis ipsum, quis venenatis nunc ipsum sit amet libero. Aenean sed leo eros. Curabitur varius egestas tempor. Nullam vitae convallis nunc. Phasellus molestie volutpat purus ut commodo. Phasellus eget lacus sem. Maecenas ligula magna, lacinia mollis molestie vitae, fringilla ac turpis. Sed ut nunc id nunc fringilla consectetur at et neque.
+
+Aliquam erat volutpat. Nullam lacinia, neque id luctus consectetur, nisl justo porta justo, eu scelerisque ligula ligula sed purus. Cras faucibus porttitor nisi at vulputate. Integer iaculis urna ut sapien iaculis ac malesuada quam congue. Mauris volutpat tristique est, vitae vehicula nisi imperdiet tincidunt. Curabitur semper, tellus sed cursus placerat, mi nulla dapibus odio, quis adipiscing arcu eros eu quam. Nullam fermentum dictum tellus non pretium. Sed dignissim enim a odio varius pellentesque. Nullam at lacinia mi. Nam et sem non risus suscipit pharetra vel et nisl. Cras porta lorem quis diam tempus nec dapibus velit sodales. Suspendisse laoreet hendrerit fringilla.
+
+Phasellus velit quam, malesuada eget rhoncus in, hendrerit sed nibh. Quisque nisl erat, pulvinar vitae condimentum sed, vehicula sit amet elit. Nulla eget mauris est, vel lacinia eros. Maecenas feugiat tortor ac nulla porta bibendum. Phasellus commodo ultrices rhoncus. Ut nec lacus in mauris semper congue. Vivamus rhoncus dolor a nulla accumsan semper. Donec vestibulum dictum blandit. Donec lobortis, purus a cursus faucibus, enim nisl fermentum odio, sed sagittis odio quam quis elit. Sed eget varius augue. Quisque a erat dolor, sit amet porttitor eros. Curabitur libero orci, dignissim vel egestas ut, laoreet sit amet augue. Curabitur porta consectetur felis. Etiam sit amet enim dolor, quis lacinia libero. Nunc vel vulputate turpis. Nulla elit nunc, dignissim sed hendrerit vitae, laoreet et urna. Donec massa est, porta eget lobortis sed, dictum vel arcu. Curabitur nec sem neque.
+
+Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In ipsum risus, blandit ac porta non, imperdiet ac erat. Sed libero nisi, gravida quis dignissim vel, mattis quis sem. Ut pretium vulputate augue, a varius mi vehicula at. Ut cursus interdum lobortis. Duis ac sagittis lacus. Suspendisse pulvinar feugiat mi id vestibulum. Integer aliquet augue vitae augue tincidunt pharetra. Duis interdum nunc pellentesque nisl malesuada volutpat. Nam molestie pulvinar felis, quis volutpat urna commodo in. Donec sed adipiscing risus. Mauris nec orci ac eros lacinia euismod sed sed dui. Mauris vel est eget mi bibendum venenatis nec id enim. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ac orci varius mi aliquam sodales. Cras dapibus lorem et erat tincidunt non consectetur risus commodo. Aenean tincidunt varius orci eu placerat. Sed in euismod justo.
+
+Pellentesque auctor porta magna, vitae volutpat est pharetra id. Phasellus at mi nibh, vitae eleifend mi. Sed egestas orci lacus. Mauris suscipit nunc non diam mattis rutrum. Etiam pretium, mi et ultricies molestie, ante nibh posuere dolor, a fermentum diam massa eget purus. Aliquam erat volutpat. Nam accumsan dapibus quam, vitae dictum est bibendum ut. Sed at vehicula mi. Phasellus vitae ipsum a quam cursus euismod sit amet et turpis. Nam ultricies molestie massa, a consectetur ipsum aliquet sit amet. Pellentesque non orci mauris. Suspendisse congue venenatis est convallis laoreet. Aenean nulla est, bibendum id adipiscing quis, fermentum quis nisi. Nam lectus ante, sodales sodales ultrices a, vehicula ac ligula. Phasellus feugiat tempor lectus, id interdum turpis mollis eu. Suspendisse potenti. Sed euismod tempus ipsum, et iaculis felis consequat sed. Mauris bibendum, eros a semper pharetra, nunc urna commodo lacus, quis placerat dui urna semper libero. Mauris turpis metus, mattis id dignissim eget, sollicitudin nec lacus.
+
+Donec massa dui, laoreet dignissim interdum sit amet, semper vel ligula. Maecenas ut eros est, quis hendrerit purus. In sit amet mattis quam. Curabitur sit amet turpis ac ipsum gravida pulvinar sit amet ut libero. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum bibendum massa eu nisi fermentum varius. Mauris sollicitudin ultrices nunc, eget facilisis est imperdiet sit amet. Nam elementum magna eget nisi commodo tincidunt. Aliquam erat volutpat. Curabitur in mauris nunc, at eleifend lectus. Integer tincidunt vestibulum lectus, ut porttitor magna dapibus a. Vivamus erat massa, pretium sed tincidunt ac, tincidunt hendrerit ligula. Praesent purus eros, euismod at commodo eu, bibendum eu turpis.
+
+Sed tempor ultrices tortor, et imperdiet est porttitor a. Vestibulum sodales mauris sed urna pellentesque eleifend. Ut euismod tristique nulla eu fermentum. Ut eu dui non purus varius mollis in vel enim. Maecenas ut congue nulla. Suspendisse ultrices sollicitudin molestie. Aliquam vel pulvinar metus. Nulla varius adipiscing metus, ac commodo ante dapibus ac. Phasellus sit amet ligula sed elit scelerisque molestie sit amet ac quam. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Pellentesque sollicitudin libero a quam rutrum egestas ac quis arcu. Etiam mattis massa vel erat mattis ut elementum diam cursus. Fusce bibendum lorem in erat auctor posuere. Ut non mi sed neque sodales vulputate. Donec lacinia, lacus nec hendrerit luctus, dolor nisi dignissim turpis, at rhoncus dui nisi nec elit. Integer laoreet, justo ut pellentesque iaculis, diam turpis scelerisque quam, sit amet semper purus lacus at erat. Sed sollicitudin consectetur eros at ultricies.
+
+Nam in dolor massa. Vivamus semper, quam sed bibendum pellentesque, lectus purus auctor dui, eget mollis tellus urna luctus nisi. Duis felis tellus, dapibus sed sollicitudin commodo, ornare id metus. Aliquam rhoncus pulvinar elit sit amet fermentum. Curabitur ut ligula augue, nec rhoncus orci. Proin ipsum elit, tristique semper rhoncus sit amet, ultrices vel orci. Integer mattis hendrerit blandit. Curabitur tempor quam eget nunc rutrum nec porta elit elementum. Morbi at accumsan libero. Etiam vestibulum facilisis augue vitae feugiat. Vivamus in quam arcu, vel ornare purus. Pellentesque non augue sit amet metus imperdiet accumsan. Suspendisse condimentum vulputate congue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec consequat enim ac est iaculis dictum. Vivamus rhoncus, urna sit amet tempor ornare, nulla sem eleifend mi, eu pretium justo sapien a nulla. Nulla facilisi. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean sed mattis turpis.
+
+Nulla in magna scelerisque sem imperdiet tempus. Aenean adipiscing pretium sem, eget vulputate turpis pretium vitae. Etiam id enim a mauris faucibus facilisis faucibus vel enim. Phasellus blandit mi nec nibh rhoncus nec sollicitudin mi semper. Maecenas euismod dui sit amet dolor dictum dignissim. Mauris ac quam urna, quis posuere lacus. Sed velit elit, dapibus hendrerit sagittis at, pulvinar ac velit. Quisque in nulla vel massa posuere feugiat sed quis enim. Donec erat eros, adipiscing at fringilla sed, ornare id nisl. Duis eleifend consectetur tincidunt. Donec enim augue, mollis sed commodo mattis, luctus ac libero. Vestibulum erat ante, lacinia ac porttitor quis, vulputate et ligula. Nunc nisl orci, eleifend et laoreet eu, egestas et est. Nulla nulla purus, euismod nec porttitor quis, volutpat id diam. Nunc ut nisl eget orci venenatis mattis. In eget nisi nibh. Integer erat mauris, interdum nec mattis in, pulvinar vitae orci. Duis dictum tortor in elit aliquet commodo. Vestibulum venenatis auctor faucibus. Nulla adipiscing nisi eu lectus ornare ultrices.
+
+Curabitur placerat ante a odio dapibus placerat. Praesent ante quam, rutrum quis dignissim vulputate, dignissim vitae elit. Curabitur et nibh ante. Sed luctus bibendum pulvinar. Ut vel justo eros. Maecenas faucibus ornare consequat. Mauris non interdum elit. Mauris tortor magna, tempor quis rutrum ac, congue ut sem. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed semper interdum quam eu semper. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Mauris enim velit, mattis at dictum eget, ornare vel erat. Quisque non tincidunt lectus. Vestibulum auctor scelerisque erat eget adipiscing. Mauris ac metus purus, sit amet dignissim felis.
+
+Curabitur vitae quam sagittis massa aliquet facilisis id tempor justo. Aenean vulputate libero nec odio porta in rhoncus massa interdum. Maecenas consectetur suscipit consectetur. Proin a mauris sit amet ante sollicitudin auctor id ac libero. Vivamus hendrerit porta augue, ac pretium nibh cursus at. Aliquam varius nulla porta quam pellentesque scelerisque eget a felis. Maecenas elit quam, tempor vel dignissim nec, aliquam ac justo. Curabitur scelerisque cursus orci, sit amet scelerisque dolor consectetur vel. Integer tellus tortor, laoreet laoreet consequat id, vehicula nec neque. Sed sit amet ante sed magna faucibus luctus et vel nisi.
+
+Curabitur placerat viverra urna et auctor. Proin ac lacus urna, vitae sagittis erat. In ut tellus ipsum, rutrum auctor orci. Sed dolor nibh, laoreet egestas egestas non, eleifend eu lectus. Aenean lorem leo, rhoncus sit amet fermentum in, porta vel leo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Praesent lorem orci, congue nec consectetur eu, ullamcorper non nulla. Duis sed augue libero. Suspendisse potenti. Nunc id neque massa.
+
+Etiam odio magna, congue ut tristique non, dignissim nec est. Sed id purus velit. Vivamus dui dui, rutrum sit amet imperdiet non, pharetra cursus ante. Curabitur aliquet dapibus massa, non molestie orci aliquet tincidunt. Aenean in varius risus. Nullam faucibus sapien odio. Integer id est erat. Nam iaculis purus a ipsum sagittis in vestibulum lectus pulvinar. Nulla ultricies nisi a nibh gravida eget vestibulum tellus auctor. Suspendisse ut dolor elementum mi iaculis dignissim eu eleifend tellus. Sed pretium mi ligula. Integer vitae sem sit amet nunc dignissim rutrum nec eleifend felis. Aenean blandit fermentum lectus quis dignissim. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis congue sem a est accumsan sit amet facilisis erat dapibus. Mauris id lectus ipsum. Sed velit metus, ultrices rhoncus porta non, consectetur id ligula.
+
+Fusce eu odio volutpat sem pellentesque laoreet. Integer a justo ante, sed elementum elit. Donec sed mattis arcu. Vivamus imperdiet sodales ante, eget tincidunt turpis imperdiet et. Donec mi ante, tincidunt nec adipiscing sit amet, sodales vel arcu. Cras eu libero arcu. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aliquam ac dui justo. Nam nisi ipsum, dignissim id fermentum at, accumsan ut quam. Quisque non est quis nibh iaculis gravida nec id velit. Cras elementum tincidunt mattis. Mauris odio erat, sodales ut egestas nec, semper eget enim. Mauris quis tincidunt quam.
+
+Nullam vestibulum ligula imperdiet nunc tincidunt feugiat imperdiet neque sodales. Praesent lacinia sollicitudin pulvinar. Donec ipsum augue, interdum et commodo vitae, lobortis nec ipsum. Nulla ac diam sed ipsum venenatis malesuada at eu odio. Vivamus in urna sed sapien mollis convallis eget eu massa. Proin viverra dolor vitae sem porta vulputate. Donec velit leo, ullamcorper dictum rhoncus in, euismod et ante. Morbi at leo venenatis felis faucibus elementum a a elit. Integer aliquet tempor neque ac bibendum. In fermentum commodo faucibus. In hac habitasse platea dictumst. Nam pulvinar gravida metus in rhoncus. Praesent lobortis ornare libero quis faucibus. Donec a erat ligula. Praesent quis sapien sit amet urna adipiscing sagittis.
+
+Praesent eget libero sed massa ornare congue eget eu lorem. Nunc porta magna ut massa dignissim ultricies. Duis eu arcu quis purus consequat egestas vitae a ipsum. In nunc sapien, venenatis et commodo sollicitudin, facilisis rhoncus risus. Nullam aliquam, orci eu vestibulum sagittis, nulla risus dictum dui, non luctus diam arcu in massa. Maecenas risus lacus, adipiscing sed laoreet sed, ornare sit amet quam. Nam convallis euismod sagittis. Fusce justo mauris, laoreet lobortis gravida semper, tincidunt pellentesque nisl. Sed sit amet turpis in nisi molestie sagittis eget sit amet nulla. Donec eget semper mauris. Aenean nec odio a nibh faucibus dapibus. Donec imperdiet tortor non elit congue varius. Morbi libero enim, tincidunt at bibendum vitae, dapibus ac ante. Proin eu metus quis turpis bibendum molestie. Nulla malesuada magna quis ante mollis ultrices. Suspendisse vel nibh at risus porttitor mattis. Nulla laoreet consequat viverra. Ut scelerisque faucibus mauris sed vestibulum. In pulvinar massa in magna dapibus ullamcorper. Quisque in ante sapien, nec ullamcorper tortor.
+
+Etiam in ipsum urna, eu feugiat nibh. In sed eros ligula, eget interdum lorem. Cras ut malesuada purus. Suspendisse vel odio quam. Vivamus eu rutrum quam. Integer nec luctus est. Mauris aliquam est ac neque convallis placerat. Sed massa ante, sagittis a tincidunt semper, interdum eget mauris. Sed a ligula sed justo facilisis sagittis vel eu ipsum. Quisque aliquam vestibulum nisl quis commodo.
+
+Morbi id rutrum mi. Curabitur a est quis mauris accumsan egestas a vulputate urna. Nunc eleifend lacus non lacus tincidunt vitae commodo odio mattis. Cras accumsan blandit odio, vitae mattis est egestas eget. Integer condimentum sem in lectus euismod consectetur. Donec est lectus, posuere sit amet ornare non, ullamcorper vel dolor. Vestibulum luctus consectetur scelerisque. Duis suscipit congue mi id venenatis. Quisque eu mauris venenatis dolor condimentum gravida a a leo. Aenean et massa est. Sed arcu ligula, sagittis in luctus in, condimentum a nisl. In placerat interdum felis, eu luctus dolor rutrum sed. Nam commodo, urna a adipiscing scelerisque, turpis arcu adipiscing metus, at blandit nulla elit quis sapien. Quisque sodales tincidunt odio, quis sodales erat bibendum condimentum. Ut semper dolor in ipsum tincidunt convallis. Phasellus molestie nulla id ipsum semper ultrices. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris aliquam semper neque at sagittis. Curabitur luctus tristique facilisis. Donec scelerisque ante non tortor fringilla eleifend non in felis.
+
+Maecenas nec ipsum eget odio ornare egestas non non tortor. Vestibulum elementum ultrices ipsum, nec elementum augue dapibus vitae. Fusce hendrerit erat eget libero porttitor sit amet venenatis neque mollis. Donec lorem quam, egestas sed rutrum pharetra, ultrices quis quam. Phasellus iaculis risus eget leo suscipit eu consectetur libero bibendum. Nulla euismod, est sit amet tristique tincidunt, nisi turpis sagittis justo, ornare elementum nibh turpis at ipsum. Mauris id velit risus, in lacinia libero. Integer at urna eu sapien luctus sollicitudin. Vestibulum vitae varius est. Curabitur eget quam urna, cursus egestas orci.
+
+Sed eu felis nisi. Nullam nisi lacus, imperdiet sed accumsan sed, pretium ac dolor. Curabitur feugiat tristique velit, id fermentum velit blandit lobortis. Phasellus ac arcu vel lacus ultricies aliquet. Morbi aliquet pulvinar convallis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin tincidunt commodo tortor, vitae semper velit consequat ac. Suspendisse ac sollicitudin elit. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Quisque imperdiet, mi sed ultrices ullamcorper, eros justo malesuada urna, ac dapibus turpis leo sed sem. Nulla commodo consectetur libero a scelerisque. Maecenas in tortor sem, vitae rhoncus magna. Nulla nec nisl nisl, eget iaculis felis. Phasellus placerat consectetur erat, non porta tellus egestas nec. Praesent gravida pharetra arcu. Nullam bibendum congue eleifend.
+
+Nam risus dolor, mollis in suscipit vel, egestas eget augue. Donec et nulla mi. Vestibulum nunc mauris, volutpat eget lacinia ut, consequat non justo. Etiam bibendum elit quis ipsum volutpat sit amet convallis erat feugiat. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed non turpis elit, in laoreet sapien. Quisque ac elit id odio luctus pharetra. Phasellus sit amet est nec orci vestibulum varius. Cras ut justo a velit accumsan scelerisque. Proin lacus odio, convallis in semper egestas, ullamcorper sit amet erat. Proin ornare mollis pharetra. Phasellus convallis, sapien a placerat scelerisque, magna ante lobortis massa, ut semper nibh turpis a nibh.
+
+Vestibulum risus mauris, auctor eu aliquam quis, pretium vel massa. Nunc imperdiet magna quis nisi facilisis euismod. Nunc aliquam, felis quis mollis aliquam, mi arcu commodo eros, sit amet convallis nunc magna non magna. Suspendisse accumsan tortor non metus convallis pharetra. In vitae mi sed leo ornare viverra. Donec a massa at sem euismod scelerisque id a sapien. Nam nec purus purus, quis lacinia sem. Sed laoreet erat quis tortor feugiat at mattis lacus sollicitudin. In hac habitasse platea dictumst. Vivamus tristique rhoncus eros a hendrerit. Etiam semper dapibus tortor, quis porta purus ullamcorper eget. In iaculis elit ut neque varius at consequat tellus accumsan.
+
+Praesent ut ipsum nec nulla consequat laoreet. Quisque viverra rutrum bibendum. Vivamus vitae bibendum augue. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nulla hendrerit condimentum lacinia. Donec sed bibendum lectus. Ut venenatis tincidunt neque et fermentum. Mauris fermentum, est at molestie luctus, nunc lorem sodales dolor, ut facilisis massa risus ut sem. Vestibulum nec nisi sed lacus imperdiet ornare. Duis sed lobortis nisi. In urna ipsum, posuere fringilla adipiscing eu, euismod a purus. Proin bibendum feugiat adipiscing. Morbi neque turpis, ullamcorper at feugiat ac, condimentum ut ante. Proin eget orci mauris, nec congue dolor.
+
+Sed quis dolor massa, sed fermentum eros. Fusce et scelerisque tortor. Donec bibendum vestibulum neque, id tristique leo eleifend non. Ut vel lacinia orci. Etiam lacus erat, varius viverra accumsan sit amet, imperdiet at sapien. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum erat lacus, hendrerit consectetur vulputate id, mattis eu nunc. Morbi lacinia bibendum eros, sit amet luctus nisl lobortis lobortis. Nullam sit amet nisl vel justo ornare bibendum eu quis nunc. Morbi faucibus dictum quam, sed suscipit est auctor ac. Sed egestas ultricies sem a pharetra. Phasellus sagittis ornare lorem eu aliquam. Praesent vitae lectus ut dui consectetur varius. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nam ac orci id metus tincidunt congue vel ut mi. Nunc auctor tristique enim quis sodales.
+
+Aenean quis tempor libero. In sed quam purus. Nam in velit erat. Ut ullamcorper nunc ut nibh facilisis non imperdiet enim interdum. Praesent et mi nulla, quis facilisis lacus. Nulla luctus, velit vulputate egestas aliquet, arcu dolor vulputate tellus, eu auctor ipsum tortor ut lorem. In sed nulla auctor elit adipiscing laoreet. Mauris id pretium velit. Vestibulum aliquet bibendum laoreet. Duis convallis, leo vitae tincidunt fringilla, massa eros porta lorem, a convallis sem massa sit amet libero. Nam ligula leo, porta non hendrerit a, luctus pellentesque tortor. Nulla fermentum mi lacinia est pellentesque sed rhoncus nisl tristique. Curabitur venenatis neque id magna egestas eget dictum nisi volutpat.
+
+Ut sagittis fringilla arcu, ut condimentum metus tempor et. Duis elit neque, varius quis consectetur et, vulputate egestas odio. Curabitur molestie congue nibh, pulvinar tincidunt elit tempus ut. Quisque nec magna lacus. Quisque eu justo lacus. Maecenas tempus porttitor consequat. Ut vulputate lacinia tempus. Praesent dignissim iaculis orci ac euismod. Proin porttitor lorem auctor erat placerat quis tincidunt tellus posuere. Nam ultrices sapien ultrices urna aliquet convallis. Aenean auctor fringilla vestibulum.
+
+Proin eros nisl, viverra placerat eleifend a, facilisis et augue. Duis commodo tincidunt molestie. Nullam malesuada ligula eget libero tincidunt viverra. Ut euismod sem in turpis posuere rhoncus. Donec luctus, eros quis ultricies eleifend, lacus ligula porttitor magna, sit amet lobortis enim turpis non orci. Nunc odio nisi, luctus id euismod non, hendrerit quis dolor. Proin tristique sem semper massa porttitor fringilla. Curabitur a felis tellus. Donec tempus, libero at ornare commodo, risus sapien venenatis mi, sit amet fringilla diam enim at arcu. Suspendisse potenti. Phasellus auctor, lorem sed pulvinar ornare, eros nunc tincidunt dui, semper interdum lorem purus nec turpis. Sed egestas, orci non varius dapibus, nulla felis rutrum tortor, a vehicula nisi magna et magna. Donec aliquam rhoncus arcu ac volutpat.
+
+Quisque leo risus, egestas eu posuere eget, malesuada quis erat. Donec vel nisi quis erat vestibulum consectetur. Donec mi mi, dictum vel posuere ac, pharetra non justo. Vivamus rhoncus mollis odio, eu fermentum turpis blandit a. Pellentesque ornare consequat odio, non sodales massa sollicitudin ac. Vestibulum euismod nisi non augue commodo vitae laoreet justo tempor. Vestibulum at arcu ac elit tincidunt vehicula pretium eget magna. Nullam non eros eros. Morbi sed diam ut leo viverra gravida a sit amet sem. Duis ultricies tellus in nisi vulputate rhoncus. Praesent molestie eros et ligula sodales ut euismod arcu egestas. Cras ullamcorper dapibus erat id luctus. Maecenas pretium rutrum mauris, ac rhoncus lacus commodo eu. Duis ut diam quis neque accumsan laoreet in eu tellus. Curabitur sit amet ligula nibh. Vestibulum vitae semper leo. Sed volutpat turpis dictum justo luctus quis gravida tortor volutpat. Proin velit dolor, tempor quis iaculis eu, congue vitae nisl. Vestibulum porttitor, risus id consequat suscipit, ipsum leo luctus tellus, sed sollicitudin nulla orci eget arcu.
+
+Fusce et urna sed erat porttitor condimentum convallis et ipsum. Integer sagittis arcu sit amet dolor interdum eu tincidunt sapien sodales. Sed ut elementum ipsum. Aliquam erat volutpat. Fusce vel enim velit. Duis sit amet gravida quam. Sed iaculis aliquet erat sed semper. Sed in ipsum nisi. Suspendisse blandit urna ac lectus congue hendrerit. Donec sapien enim, auctor quis suscipit id, interdum a nunc. Etiam erat velit, hendrerit eget tincidunt ut, pellentesque in lectus. Integer vitae lacus eget est tempor dapibus. Duis in velit augue. In accumsan ipsum eu nibh commodo id consequat lectus condimentum. Integer volutpat condimentum posuere.
+
+Proin at turpis sapien, vel bibendum odio. Etiam scelerisque, nulla vel dapibus dapibus, neque nunc fringilla libero, nec malesuada elit erat eget turpis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec eu mi nisi. Mauris quis dolor libero. Etiam non libero mauris. Nam posuere tortor vel dolor aliquam eu porttitor nisi convallis. Sed eu ante nec diam hendrerit aliquet. Suspendisse fermentum, augue ut lobortis viverra, turpis mi tristique felis, a facilisis est nisi vitae nisl. Nunc sit amet semper tortor. Duis et enim in nulla aliquet fermentum. Etiam ultrices facilisis justo, quis molestie enim convallis ut. Donec congue, eros quis rhoncus interdum, nisl orci porta nisl, posuere tincidunt est tellus nec magna. Suspendisse interdum, lorem nec dictum dignissim, justo dui imperdiet felis, laoreet ultricies lacus elit eu libero. Sed quis urna nec nisi condimentum tristique pulvinar id orci. Vivamus a leo nec libero hendrerit imperdiet. Sed gravida interdum urna, ac dictum odio dictum id. Vestibulum vel varius dolor.
+
+Nulla consequat condimentum eros nec mollis. Donec eget ornare eros. Etiam consequat accumsan aliquet. Quisque non leo nibh. Mauris convallis congue hendrerit. Aliquam nec augue at risus ornare viverra at id felis. Nullam ac turpis ut nisl semper rhoncus quis sit amet justo. Aliquam laoreet arcu vitae odio consequat condimentum. Aliquam erat volutpat. Sed consectetur ipsum nec justo tempor ullamcorper. Donec ac sapien lectus. Suspendisse ut velit eget massa dapibus tincidunt vel eget enim. Etiam quis quam vel lectus tincidunt viverra eget eget risus.
+
+Nulla pulvinar, odio eu hendrerit egestas, nisl nunc gravida mi, non adipiscing tortor mauris a lectus. Sed sapien mi, porttitor vel consectetur ut, viverra ut ipsum. Duis id velit vel ipsum vestibulum sodales. Nunc lorem mi, mollis nec malesuada nec, ornare faucibus nunc. Vestibulum gravida pulvinar eros quis blandit. Nulla facilisi. Curabitur consectetur condimentum justo sed faucibus. Vestibulum neque urna, tincidunt in adipiscing a, interdum a orci. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec libero neque, fringilla quis bibendum vel, tincidunt eget metus. Integer tristique, lectus quis rhoncus iaculis, enim dui adipiscing massa, sit amet blandit risus orci eu magna. Fusce ultricies tellus quis massa tempus at laoreet turpis dapibus. Donec sit amet massa viverra purus tincidunt scelerisque. Nunc ut leo nec tellus imperdiet vulputate tincidunt sed nisi. Suspendisse potenti. Sed a nisi nunc. Ut tortor quam, vestibulum et ultrices id, mattis non lacus.
+
+Nullam tincidunt quam quis erat rutrum eget tempor diam vestibulum. Morbi dapibus, quam sed placerat blandit, mi enim dictum nulla, sit amet sollicitudin lectus ante eu sem. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed in elit id nisi aliquet mollis. Cras non lorem risus. Ut libero elit, ornare id placerat ut, sodales at lectus. Nunc orci turpis, tempus vitae pellentesque id, sodales et sem. Aliquam erat volutpat. Sed sit amet tellus condimentum magna cursus consectetur non sed arcu. Vivamus in consectetur massa. Aliquam vitae nibh nec lacus volutpat sodales. Quisque est arcu, porttitor a pharetra ac, laoreet nec nibh. Nunc ullamcorper adipiscing libero a dictum. Vivamus vulputate egestas arcu non viverra. Phasellus eget libero in ipsum fringilla dapibus. Quisque vehicula rhoncus lorem vel dictum. Sed molestie lorem ac tellus ultrices a varius dui faucibus. Integer quis quam libero. Sed fringilla aliquet lacus, non porttitor erat ultricies eu.
+
+Fusce bibendum euismod porta. Praesent libero nunc, dapibus ac aliquam fringilla, ornare quis eros. Vivamus tincidunt arcu vitae felis varius nec facilisis elit fermentum. In quis quam eget mauris porta faucibus. Fusce nec erat eu lectus pellentesque tempus. Morbi a justo a ante pulvinar ultricies ac tincidunt turpis. Etiam malesuada ultrices nibh quis bibendum. Quisque lacus dui, mattis id lobortis sit amet, fermentum id nisl. Donec fermentum nisi ac metus consectetur semper. Duis condimentum ipsum sit amet arcu adipiscing cursus. Nulla vulputate risus vel elit adipiscing sed pretium mauris venenatis. Vestibulum tincidunt, sapien at dapibus rutrum, urna nisi sollicitudin orci, ut condimentum lectus tellus ut lacus. Sed in nisl et urna placerat vestibulum. Ut fringilla suscipit iaculis. In in eros eget neque suscipit mollis quis ut libero. Pellentesque hendrerit consectetur tellus. Nulla a purus ut dolor volutpat ultrices.
+
+Pellentesque at laoreet libero. Quisque pretium tempus placerat. Proin egestas rhoncus est, eu vehicula justo gravida eu. Sed sem velit, sodales tincidunt gravida vitae, rhoncus vel neque. Proin quis quam ut turpis rhoncus suscipit quis vitae tellus. Phasellus non scelerisque nisl. Vestibulum lectus odio, tristique vitae rhoncus id, dapibus vitae magna. Vestibulum aliquet magna in turpis eleifend in dapibus augue lacinia. Ut risus mi, dictum at mollis eu, feugiat a massa. Nam in velit urna. Aliquam imperdiet porta eros a suscipit. Nullam ante quam, congue ut lacinia vel, laoreet vitae felis. Mauris commodo ultricies lobortis. Donec id varius augue. Vivamus convallis, nulla eget aliquam varius, ligula quam rhoncus augue, vel rutrum diam odio in felis. Nulla facilisi. Duis pretium magna nulla, id pretium mi.
+
+Sed elit odio, semper non semper vel, dapibus eu metus. Ut quis nibh vel leo laoreet egestas vitae id odio. Nunc nec egestas nisl. Vivamus tristique pulvinar leo ullamcorper convallis. Praesent elementum condimentum consectetur. Etiam dui nisi, convallis vel fringilla ac, dignissim vel velit. Fusce magna quam, malesuada at vehicula quis, luctus vel tortor. Vivamus viverra consectetur velit, quis bibendum dolor hendrerit nec. Mauris pretium laoreet eleifend. Donec in ligula a enim fringilla pellentesque vitae sed magna. Integer vitae odio et arcu tempor molestie.
+
+In lacus quam, placerat nec accumsan ut, faucibus eget tellus. Maecenas cursus risus enim. Pellentesque quis lorem orci, id dictum velit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed justo arcu, tristique vitae mollis id, dictum non enim. Proin gravida fringilla est eu elementum. Donec ac nulla sapien, et volutpat lectus. Mauris eget quam vel dolor aliquet pretium eu nec dolor. Phasellus auctor nunc ut risus aliquet eu consequat urna rutrum. Integer porta lacus vel metus hendrerit sed fermentum arcu hendrerit. Morbi nibh arcu, tristique ut hendrerit in, rhoncus eget elit.
+
+Morbi tincidunt lectus ut metus aliquam adipiscing. Phasellus eros purus, laoreet non rhoncus nec, aliquet sed justo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Quisque leo nunc, feugiat ut consequat in, condimentum sit amet urna. Nam et dui sed orci pellentesque feugiat. Aliquam erat volutpat. Aliquam rhoncus sollicitudin orci. Ut blandit dignissim est, a dapibus erat tincidunt vel. Fusce dignissim vehicula lorem non suscipit. Vivamus gravida accumsan est nec consectetur. Etiam congue diam non nisi ornare semper. Maecenas pretium vestibulum velit. Suspendisse at tincidunt quam. In vitae sagittis est. Duis convallis sollicitudin nunc quis posuere. Quisque et augue eget metus commodo pulvinar. Pellentesque et velit eget massa scelerisque sagittis. Aenean tortor magna, auctor sed sodales et, vestibulum sit amet leo. Vestibulum id ligula vel nisi faucibus cursus.
+
+Quisque hendrerit, lorem vel ultricies adipiscing, massa ligula consectetur odio, eu eleifend sem eros varius magna. Mauris metus arcu, hendrerit et fringilla sit amet, vehicula vel leo. Pellentesque eu tellus in nulla sollicitudin tempus. Sed dapibus cursus facilisis. Cras id lectus turpis, et iaculis felis. Nulla dignissim dui non sem posuere posuere. Ut id arcu sit amet quam tristique malesuada. Curabitur ut posuere urna. Vivamus aliquet pretium leo, id sollicitudin nulla tempor eget. Aliquam commodo enim lacus, quis hendrerit lacus. Praesent tortor felis, semper vel aliquet eget, aliquet a ante. Nullam ullamcorper arcu nibh, a facilisis neque. Nunc rutrum posuere sagittis. Donec eleifend aliquam vulputate. Curabitur eget dapibus ipsum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras mollis laoreet nunc, ut suscipit tellus laoreet semper.
+
+Phasellus libero enim, malesuada ut rutrum a, sollicitudin sed elit. Ut suscipit imperdiet nibh, vel gravida mauris fringilla non. Pellentesque sagittis libero id nulla adipiscing vitae iaculis justo consequat. In hac habitasse platea dictumst. Sed venenatis cursus est, et iaculis nisi convallis vel. Etiam non elementum mi. Etiam semper faucibus orci. Nullam tincidunt, lorem commodo sodales placerat, est velit interdum nulla, ut rutrum lectus massa malesuada elit. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris faucibus odio vel tellus ornare vitae lacinia libero lobortis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Cras porttitor, tortor vel adipiscing ornare, nunc elit lobortis nisl, eu vehicula sapien purus id diam. Sed blandit bibendum facilisis. Pellentesque ornare auctor commodo. In et aliquet magna. Fusce molestie sem eget orci semper sollicitudin. Donec placerat tristique urna, a varius velit sagittis eget. Aliquam vitae rutrum orci. Vivamus ac lobortis dui. Integer ornare lobortis sem vel convallis.
+
+Praesent ornare aliquet arcu, sed lacinia dui convallis quis. Suspendisse nec arcu lectus. Suspendisse potenti. Curabitur scelerisque quam id lacus vehicula ut tristique eros viverra. Mauris et mi ac massa auctor pharetra a eget enim. Sed vel dui sem, ut pulvinar risus. Etiam ac ipsum ipsum, eu venenatis odio. Proin lacinia eleifend risus sed hendrerit. Quisque velit nunc, sodales vitae venenatis vitae, lacinia porta neque. Donec nec vestibulum massa. Duis blandit, sapien in congue pharetra, dolor felis pharetra velit, semper vulputate metus massa ac leo. Etiam dictum neque sed lectus condimentum euismod. Maecenas vel magna ultrices lorem fermentum feugiat. Proin pulvinar ornare libero, aliquet tincidunt neque laoreet vitae. Mauris adipiscing convallis massa, quis pellentesque nulla rhoncus quis. Etiam viverra condimentum commodo. Nulla feugiat molestie ipsum sed pretium. Aenean rhoncus imperdiet urna, quis fringilla justo commodo sed. Aliquam erat volutpat. Morbi sed sem nulla.
+
+Integer scelerisque leo eu massa porta non tincidunt velit dictum. Ut ac ligula ipsum. Phasellus vehicula gravida felis, ac commodo lacus mattis ac. Nam bibendum enim eget diam mattis pharetra. Suspendisse malesuada arcu lacus. Nulla elementum arcu a nulla aliquam eu vestibulum dui pulvinar. Duis a facilisis risus. Nam ac dui nibh, eu porttitor mauris. Integer sollicitudin egestas dui, mollis laoreet mauris molestie ac. Aliquam egestas auctor neque, vitae aliquet dolor tincidunt blandit. Suspendisse laoreet orci at augue dapibus suscipit. In hac habitasse platea dictumst. Phasellus egestas ornare sem ac tincidunt. Suspendisse condimentum sem non augue tincidunt vulputate. Mauris cursus quam vel tortor dapibus eu ultricies mauris viverra. Nulla elit dolor, placerat sit amet facilisis non, fringilla in felis.
+
+Proin consequat diam non quam accumsan faucibus. Sed malesuada, dui quis placerat sagittis, sapien libero molestie libero, a sodales tortor neque non elit. Nulla et sodales ante. Donec tempor, tortor ut congue pulvinar, mi elit tempus risus, a pharetra libero quam a augue. Nulla facilisi. Quisque feugiat tortor a arcu dictum tincidunt. Nulla tincidunt tincidunt tortor, ac suscipit eros bibendum pharetra. Ut dignissim sollicitudin massa, et porttitor ligula vulputate a. Integer condimentum dapibus diam in tempor. Pellentesque molestie fringilla rhoncus. Donec eget laoreet libero. Suspendisse vulputate sapien eu sapien faucibus egestas.
+
+Integer nec erat dui, at eleifend arcu. Cras mauris est, cursus vel euismod sed, suscipit quis lorem. Donec neque sem, laoreet suscipit scelerisque a, volutpat at lectus. Pellentesque non felis erat, sed pulvinar nisl. In congue sollicitudin metus sodales convallis. Fusce venenatis risus ut velit adipiscing vestibulum eu sed augue. Proin metus turpis, sodales at faucibus vel, fringilla sodales ligula. Sed fringilla magna sed diam lacinia adipiscing. Maecenas nibh nibh, consequat vel malesuada sed, vestibulum nec felis. Quisque tempus lobortis dui ut euismod. Nulla facilisi. Ut adipiscing purus quis purus pellentesque eu viverra nunc placerat. Nullam nec dignissim diam. Fusce non dignissim massa. Donec condimentum, orci iaculis vulputate elementum, lectus nunc luctus augue, sit amet suscipit nulla odio at massa. Aliquam eu turpis nec massa feugiat condimentum a ac lectus. Nunc lectus ligula, feugiat vel bibendum et, tempor quis mi. Curabitur molestie, urna quis fringilla consequat, ipsum erat sodales turpis, ut laoreet velit risus vitae libero.
+
+Aliquam erat volutpat. Suspendisse tincidunt accumsan eros in posuere. Morbi non ullamcorper augue. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui felis, feugiat semper venenatis vitae, lobortis nec ipsum. In lacinia mauris in massa ornare vel congue ipsum lacinia. Maecenas rhoncus vulputate enim, ut porttitor purus gravida id. Nunc urna ligula, pulvinar eu lacinia nec, scelerisque at nibh. Nam accumsan leo est. Pellentesque congue fermentum nisl ac semper. Sed eget blandit urna. Nullam interdum, risus id hendrerit ultrices, turpis erat vestibulum turpis, quis vehicula mauris sem sit amet est. Mauris et lorem metus, id rutrum nisl. Donec blandit dapibus neque, hendrerit fringilla diam tempus sed. Integer vestibulum, felis quis pulvinar adipiscing, ipsum risus convallis lorem, ac pulvinar lacus nunc sed felis.
+
+Vestibulum vitae tristique orci. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam erat volutpat. Maecenas non eros quis nulla adipiscing rutrum eu at mi. Suspendisse laoreet nulla vitae nunc venenatis vitae adipiscing felis pharetra. Integer viverra vehicula risus, vitae dictum massa tempor a. Sed id leo neque, nec consectetur tellus. Donec fermentum eros vitae magna vulputate ac volutpat ligula suscipit. Curabitur mi orci, molestie tristique bibendum egestas, blandit vel arcu. Sed molestie ullamcorper nisl nec dignissim. Fusce consectetur suscipit mauris at ullamcorper. In massa diam, feugiat in euismod id, tincidunt id libero. Donec adipiscing, tellus id vehicula hendrerit, justo mauris sagittis odio, eget placerat felis ante et enim.
+
+Curabitur posuere fermentum arcu id fringilla. Maecenas et purus ipsum. Maecenas auctor, velit a ullamcorper eleifend, arcu tellus adipiscing turpis, ac malesuada ante lorem eu massa. Aenean libero velit, mollis sed imperdiet in, fringilla eu lectus. Cras ullamcorper lobortis massa non volutpat. Nunc sapien lorem, posuere posuere mattis at, rutrum et dolor. Vivamus dignissim consequat nisi in viverra. Maecenas nec diam quis urna ultrices rutrum feugiat quis urna. Cras sed leo mauris. Vestibulum vitae odio ut nunc posuere lobortis. Ut felis eros, posuere at porttitor sit amet, tincidunt in justo. Nullam turpis magna, egestas ac sodales ut, cursus in eros. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus bibendum nulla nec augue vehicula laoreet. Morbi vitae nunc ac lorem pharetra pellentesque sit amet ut sapien. Maecenas placerat nunc ultricies felis consequat varius. Duis scelerisque ultrices dolor in commodo. Sed sagittis, enim quis pulvinar volutpat, lectus ante tempor arcu, id fringilla velit risus id nibh. Vivamus ac risus ut lorem dapibus ullamcorper.
+
+Vestibulum blandit lacus mattis eros cursus hendrerit. Quisque nibh arcu, condimentum ut imperdiet eget, interdum sed magna. Cras sem mauris, sagittis at dapibus sit amet, vulputate et felis. Suspendisse gravida tincidunt pellentesque. Fusce aliquet, augue eu porttitor ultricies, diam quam lacinia eros, sed consectetur diam dui ut augue. Phasellus turpis diam, hendrerit faucibus convallis et, rhoncus ac mauris. Vivamus vel turpis id arcu mattis imperdiet a nec enim. Ut ultricies mauris at sapien sollicitudin pharetra. Donec dignissim, metus ut condimentum semper, sapien elit pulvinar nisi, id placerat est orci iaculis lectus. Suspendisse quis sem a libero placerat placerat. Etiam ligula nisi, mattis vitae faucibus nec, malesuada et leo.
+
+Fusce mollis venenatis vehicula. Maecenas sit amet tortor mi, et dapibus leo. In ullamcorper dignissim lorem nec interdum. Sed nisl arcu, aliquet vel facilisis sed, rhoncus at quam. Nunc et posuere arcu. Nam faucibus blandit mi ac lacinia. Nullam ultrices tellus a turpis tincidunt sit amet convallis lacus posuere. Proin vitae orci vel justo tempus consequat sed mollis elit. Integer pellentesque bibendum nunc, et gravida mi auctor et. In vitae arcu eros. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis vestibulum nulla bibendum justo fermentum suscipit. Cras lobortis lobortis vulputate. Donec risus nisl, sagittis vel congue vel, adipiscing ac augue. Curabitur at diam quis nisl fermentum luctus non ut nisi. Nulla sed justo urna, non viverra ante. Suspendisse congue, sem non convallis fringilla, est nisl varius nunc, id laoreet nisl neque in elit. Fusce posuere euismod mattis.
+
+Curabitur a massa vitae lectus laoreet eleifend. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc malesuada turpis vehicula velit placerat ut venenatis eros ultricies. Sed nulla ligula, pretium vestibulum sagittis sed, ornare at sem. Fusce ultrices, nibh non rhoncus semper, massa tortor eleifend ante, ac mollis odio arcu ac velit. Aenean quis augue eget lectus sollicitudin accumsan. Curabitur non tortor eros, eu condimentum nulla. Phasellus at sapien ac nibh pretium condimentum. Nulla rhoncus eros vel lorem ultricies dignissim. Donec tempor, risus in mollis pretium, justo urna fermentum mi, id varius ipsum ipsum quis felis. Mauris mollis diam quis lacus laoreet sit amet ultrices felis hendrerit. Nam ac dui nisi. Cras vel risus turpis.
+
+Vivamus eleifend sapien pulvinar libero blandit ullamcorper. Morbi vitae nisl eros, sit amet porttitor erat. Donec varius velit eu tellus feugiat a tempor nunc pellentesque. Morbi sed est libero. Nulla in turpis molestie orci posuere interdum vel vel erat. Curabitur tempus eros id sem scelerisque euismod. Pellentesque varius egestas metus, id cursus massa condimentum non. Donec sagittis ultricies lacus, sit amet iaculis magna bibendum vel. Nulla cursus velit vitae neque ultricies id bibendum dui eleifend. Pellentesque porttitor rutrum interdum. Fusce nulla mi, elementum vitae sagittis id, luctus id urna.
+
+Proin nec ornare magna. Morbi euismod sapien dolor, sed consectetur nisl. In erat dui, tristique ut fringilla sit amet, imperdiet eu sem. Quisque tristique augue sodales nunc malesuada nec varius lectus laoreet. In hac habitasse platea dictumst. Vestibulum a dolor leo, ut interdum lectus. Etiam eu tortor augue, nec tristique metus. Maecenas gravida mauris a ligula vulputate consequat. Suspendisse potenti. Proin id quam magna. Etiam at ipsum augue. Nam tincidunt bibendum mi, ac vehicula tellus pretium eu. Vivamus consectetur risus id enim aliquam et laoreet tortor lacinia. Phasellus interdum dapibus orci eu imperdiet. Nulla egestas, ipsum non rhoncus suscipit, tellus purus porttitor elit, et tempus arcu odio ut justo. Etiam id lorem sed velit sagittis consequat. Duis diam sem, scelerisque in mollis non, tempor eu elit. Nullam molestie blandit dapibus. Nullam interdum laoreet iaculis.
+
+Cras sagittis luctus risus vel placerat. In in justo eget nisi pellentesque varius ut quis mi. Cras eleifend leo ultricies metus auctor accumsan. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam vel velit orci, nec tristique metus. Aliquam viverra leo sit amet leo viverra tincidunt. Praesent pellentesque nisl vehicula leo fringilla blandit. Duis dignissim tincidunt placerat. Quisque ornare pellentesque nisi, a tempor odio laoreet sed. Ut dapibus dolor cursus arcu suscipit in facilisis nunc ornare. Suspendisse consectetur pulvinar tellus eget rutrum. Aenean sagittis egestas diam, sit amet posuere lorem euismod vitae.
+
+Morbi sit amet leo metus, non vehicula ligula. Mauris nec sem sit amet ipsum feugiat sodales id vitae risus. Nullam viverra nisi at erat vestibulum dictum. Morbi et nulla magna. Proin a augue neque, sit amet tristique orci. Suspendisse ornare lorem sodales augue vehicula nec varius turpis hendrerit. Praesent nulla augue, euismod ut pretium id, luctus vel mauris. Morbi eu elit eu augue scelerisque gravida. Sed porta tortor a magna mattis volutpat. Nullam vitae tellus quam, et rhoncus dolor. Nulla ultrices nunc nec mauris mattis in blandit nibh placerat. Nam velit arcu, ultrices a imperdiet eget, pulvinar vel augue. Sed at sapien magna. Nullam accumsan nulla in nulla bibendum molestie sollicitudin lorem faucibus. In nisl tortor, tincidunt ac molestie non, commodo ut dolor. Nullam non nunc enim. Mauris ultrices, dui nec scelerisque hendrerit, erat orci feugiat eros, sed elementum ligula ipsum at velit.
+
+Donec sit amet nisi at est aliquam euismod in eget justo. Ut justo turpis, lobortis quis accumsan sit amet, suscipit non lorem. Duis pulvinar lorem at magna porttitor tristique. Duis tortor mauris, auctor sit amet feugiat in, luctus et risus. Nunc lacinia, arcu id convallis lobortis, nibh sapien scelerisque dui, ac volutpat ante tellus nec odio. In euismod viverra nibh non fringilla. Nunc non nisl risus, at interdum nunc. Phasellus porta tempus aliquam. Cras massa tellus, aliquet a dignissim sed, posuere nec massa. Vestibulum et nisi nulla. Donec ut nisi ante. Sed ac justo eu ligula varius hendrerit a sed justo. Fusce ornare eleifend nisl, at condimentum arcu lobortis ut. Mauris neque felis, viverra ut dignissim dignissim, faucibus et lectus. Aenean laoreet tristique massa id congue.
+
+Mauris accumsan elit quis augue consectetur faucibus. Donec blandit, libero in tincidunt volutpat, purus est gravida eros, ut accumsan orci felis eu purus. Nam est nibh, tincidunt ut faucibus quis, consequat at est. Fusce nec diam ligula. Morbi eu ipsum purus, non semper neque. Maecenas in lacus arcu, vel imperdiet turpis. Curabitur eget nunc velit, in consequat nulla. Donec magna tortor, faucibus vitae hendrerit ac, pretium sed ipsum. Etiam pulvinar cursus enim facilisis consectetur. Maecenas pretium pellentesque nulla, nec viverra risus placerat sed. Nam rutrum justo id augue venenatis ut feugiat risus ultricies. Sed vitae risus nec velit rutrum faucibus at vel orci. Ut feugiat mi eu dui condimentum sit amet suscipit ligula imperdiet. Sed eu bibendum augue. Ut sit amet pulvinar libero. Duis luctus urna tincidunt purus porta euismod.
+
+Suspendisse ullamcorper mi congue lacus volutpat aliquam. Nam pharetra vestibulum enim. Aliquam erat volutpat. Ut convallis consequat neque. Donec commodo vulputate fermentum. Suspendisse potenti. Mauris at nibh ac felis blandit sagittis at sed velit. Morbi fringilla consequat eleifend. Duis lobortis, erat at vulputate posuere, odio diam sodales turpis, ut iaculis tortor leo ac risus. Proin blandit eleifend lacus ac imperdiet. Quisque consequat mollis elementum. Proin hendrerit odio ut orci tempor porta et non enim.
+
+Nullam luctus sagittis molestie. Proin sollicitudin rhoncus condimentum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque mattis, eros non ultrices sodales, libero sem iaculis dui, tempus porttitor libero nibh a eros. Donec et mauris imperdiet arcu semper luctus aliquet dictum turpis. Sed porttitor scelerisque vehicula. Sed eget metus elit, ac accumsan massa. Nam et diam quis purus rhoncus ultrices. Proin dapibus malesuada metus eu elementum. Aliquam luctus lorem non massa ornare non tincidunt quam ultricies. Vestibulum convallis diam id urna vestibulum aliquet. Nulla facilisi. Vestibulum nec egestas turpis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean purus elit, vestibulum quis vehicula vel, auctor vel odio. Mauris eu tellus nunc.
+
+Sed placerat, massa et adipiscing aliquam, massa neque gravida velit, vitae consectetur quam velit non augue. Vivamus et eros metus, et aliquet diam. Aliquam a dui sem. Aenean pretium lacus ut massa faucibus in iaculis sapien pellentesque. Integer odio nibh, condimentum et condimentum vitae, ullamcorper sit amet odio. Fusce vel velit ut diam imperdiet interdum eget at nibh. Suspendisse potenti. Proin vestibulum, ante nec scelerisque volutpat, sapien purus porta ante, at gravida arcu urna consequat dolor. Praesent lorem magna, fringilla quis faucibus id, ultrices sollicitudin risus. Etiam leo lectus, viverra eu laoreet in, sollicitudin eget felis.
+
+Vestibulum tincidunt enim ac diam commodo id placerat erat lacinia. Duis egestas ante venenatis est ullamcorper viverra. Fusce suscipit eleifend velit quis sollicitudin. Donec felis libero, ullamcorper tincidunt luctus eget, fermentum a risus. Phasellus placerat egestas dui, sit amet aliquet arcu tincidunt sit amet. Suspendisse pharetra pellentesque ante sed egestas. Ut sit amet nibh urna, quis tincidunt arcu. Fusce sed sapien in diam rutrum pretium. Duis eu congue diam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed placerat fermentum ligula eu pretium. Aliquam vitae orci nibh.
+
+Donec justo nisi, congue non ultrices eget, pharetra sit amet nunc. Etiam ullamcorper massa vel mauris semper posuere viverra nisi aliquet. Nullam mi tortor, feugiat sed viverra eu, congue id mi. Vestibulum nec enim sit amet libero dapibus hendrerit eget ac diam. Sed at massa nisl, a placerat tellus. Donec hendrerit tempus scelerisque. Nulla facilisi. Vivamus nec ipsum nisl, ut tempor mi. Integer ornare augue et orci scelerisque sed condimentum lectus scelerisque. Sed mauris lacus, egestas a laoreet facilisis, venenatis at ipsum. Aenean vel ante sed tortor sodales faucibus. Curabitur quis magna quis quam ultrices luctus vitae ac neque. Vivamus sed tortor et purus adipiscing consectetur hendrerit non eros. Vivamus et tristique erat. Maecenas eu quam nibh, sit amet fermentum ante. Fusce adipiscing congue nulla sodales condimentum. Nulla viverra dapibus enim vel rutrum. Mauris sodales varius metus sed gravida.
+
+Suspendisse potenti. Aliquam erat volutpat. Integer et diam purus, et semper erat. Proin ornare, lectus ac congue tincidunt, erat sapien ultrices erat, ac sagittis enim nulla faucibus ligula. In malesuada velit eu velit tincidunt et vestibulum nibh auctor. Integer in felis justo. Nullam in lorem lacus, eget sagittis odio. Quisque congue lorem vitae massa laoreet tempor. Quisque congue magna quis eros cursus vel luctus tellus gravida. Vivamus risus nibh, cursus pulvinar porttitor in, accumsan id orci. Donec hendrerit velit vel sem tristique porta. Vestibulum libero elit, aliquam et blandit nec, convallis id sem.
+
+Cras et odio urna. Sed ut semper metus. In hac habitasse platea dictumst. In hac habitasse platea dictumst. In nec augue eget sapien lacinia porta. Phasellus odio neque, tempus nec commodo at, vehicula ut lacus. Nullam accumsan ultricies placerat. Mauris tincidunt, erat ultrices placerat tincidunt, libero erat tempus nunc, eu consectetur risus est vel mauris. Duis in justo at augue lobortis molestie. Donec ut sem sed orci gravida tristique in at magna. Aliquam pellentesque, justo non mattis egestas, dolor purus aliquam elit, at blandit lectus neque non enim. Fusce sed turpis nisl, quis varius ligula. Proin id enim in neque scelerisque ultrices non id magna. Aenean tortor lectus, viverra eu elementum et, fringilla non arcu. Mauris eget odio eget enim aliquet fringilla. Proin pretium, libero eget dignissim rhoncus, ante sapien accumsan diam, a accumsan nibh neque id dolor. Sed est ante, euismod nec pulvinar sed, faucibus at turpis. Integer fringilla consequat sagittis. Sed elit ipsum, laoreet id viverra in, ornare sed massa.
+
+Praesent eget ligula quis orci condimentum congue ut id dolor. Ut eleifend, dui eu lacinia luctus, magna tellus consectetur nunc, gravida placerat elit risus sed nibh. Integer tristique ornare nibh, eu cursus ante ultrices et. Etiam vehicula pharetra purus quis aliquam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut at velit mauris. Nulla in ipsum ante, vel sollicitudin quam. Integer a justo ut mi tempor vulputate quis malesuada magna. Ut lacinia ligula nec massa tincidunt at vehicula tortor facilisis. Donec malesuada volutpat adipiscing. Donec iaculis mi at est venenatis consequat. Sed risus sem, accumsan ut dapibus sit amet, laoreet sed mauris.
+
+Phasellus quis euismod sem. Praesent sit amet odio libero. Proin ullamcorper lectus nec arcu pulvinar vitae commodo nunc porttitor. Sed accumsan tellus et nisl dictum vel ornare neque porttitor. Morbi id egestas massa. Nunc condimentum leo vitae nibh pulvinar facilisis. Nunc elit ligula, commodo sed mollis et, ullamcorper et risus. Curabitur risus justo, viverra vel malesuada quis, convallis vitae tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse potenti. Phasellus interdum nulla a est hendrerit quis scelerisque ante convallis. Duis suscipit dolor nec lectus rhoncus vestibulum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum pellentesque pulvinar tellus quis rhoncus.
+
+Integer et pellentesque lorem. Maecenas blandit laoreet justo, non interdum nulla pellentesque sed. Nullam rutrum justo et nibh varius convallis. Praesent rhoncus eleifend ante vitae venenatis. Suspendisse ullamcorper sem at tortor fermentum suscipit sit amet in libero. Aliquam erat volutpat. Suspendisse egestas accumsan tortor, quis egestas lacus vulputate et. Sed vitae turpis in purus volutpat consectetur. Duis imperdiet nisi non augue iaculis dictum. Cras ut ipsum enim, vitae convallis urna. Sed ornare, lorem ac pellentesque iaculis, urna augue egestas arcu, nec mollis dolor tortor non justo. Cras adipiscing, massa vel tristique dignissim, dolor arcu sollicitudin mauris, eget luctus tortor purus in velit. Aenean suscipit erat et dui sagittis elementum. Mauris elementum, lorem et placerat fringilla, ante enim luctus nisl, id posuere dolor urna vel metus. Proin ligula mi, elementum fermentum rhoncus eget, sagittis at eros. Integer fringilla porta varius. Nullam dignissim semper tempus.
+
+Curabitur leo nibh, cursus vitae ultrices id, vulputate sit amet arcu. Aenean vitae lectus turpis, et gravida odio. Praesent mattis sagittis diam, ut fermentum justo euismod et. Nam pharetra, nibh non gravida dignissim, ipsum leo malesuada augue, egestas semper ipsum est sed tortor. Sed quis malesuada elit. In hac habitasse platea dictumst. Mauris ornare aliquet purus, scelerisque gravida orci pretium sed. Nunc sed orci massa, vel molestie lectus. Quisque eget adipiscing odio. Donec vestibulum justo dui, quis malesuada urna. Donec pretium tellus eget erat condimentum ornare. Sed sem urna, rutrum nec elementum ac, ornare vel enim. Fusce pellentesque varius ultricies. Suspendisse vulputate consectetur erat, ut pellentesque felis congue sit amet. Maecenas nisi tellus, fringilla a aliquam sit amet, consectetur eget felis. Maecenas nec urna at lacus posuere hendrerit nec sit amet nisl. Proin quis ligula eu mauris volutpat hendrerit. In interdum bibendum ultricies. Cras sit amet neque at felis sodales scelerisque. Etiam et vulputate sem.
+
+Fusce neque nulla, pharetra sit amet varius eget, aliquam vel tortor. Curabitur a odio velit. Phasellus tempus luctus vulputate. Aenean et libero pulvinar velit aliquet vulputate. In hac habitasse platea dictumst. Etiam at massa urna, eu pulvinar elit. Vestibulum lectus risus, tempor eget cursus ac, fermentum a augue. Maecenas at neque at lacus mollis elementum quis id tellus. Pellentesque ultricies eleifend urna, at blandit augue commodo non. Praesent tincidunt mauris sit amet enim posuere ullamcorper. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In euismod dignissim ligula, et tincidunt justo hendrerit id.
+
+Donec mi eros, bibendum id suscipit vel, posuere non tortor. Praesent enim urna, posuere at mollis vel, commodo sit amet urna. Vestibulum quis arcu quam. Fusce risus tortor, tempor vel mollis sit amet, ornare et lacus. Quisque sit amet velit justo. Nam vitae erat et nisi ultrices vehicula in quis nunc. Curabitur tristique elit eu nibh fringilla eget adipiscing elit sollicitudin. Nulla nec leo vel enim luctus scelerisque a id augue. Quisque interdum rhoncus elit, consectetur viverra felis fringilla id. Mauris volutpat ultricies nibh quis euismod. Quisque mattis semper purus et aliquet. Praesent vestibulum pulvinar quam, a dictum enim mattis non. Aenean mollis vehicula lorem, vel cursus leo venenatis id. Vivamus dapibus bibendum diam, at ultricies massa interdum et. Nulla lobortis aliquet nisi, non vehicula elit commodo in. Donec commodo, elit vel malesuada suscipit, urna lacus feugiat mi, ac mollis metus enim nec mi. Quisque fermentum, quam quis commodo luctus, quam ligula rhoncus urna, vel molestie ipsum risus ut nulla. Donec mi ligula, pulvinar vel convallis sed, volutpat eu urna. Curabitur a gravida lorem. Quisque sagittis felis ac urna laoreet quis pretium dolor congue.
+
+Proin vehicula diam id odio laoreet in suscipit quam blandit. Nullam sed ante at augue iaculis dignissim et quis ligula. Integer cursus posuere egestas. Duis turpis lacus, bibendum sit amet hendrerit ut, tincidunt vestibulum ante. Maecenas faucibus velit sit amet erat hendrerit et sodales neque scelerisque. Proin sit amet risus pharetra justo tincidunt accumsan ut posuere urna. In massa odio, viverra et pretium at, lobortis non tellus. Aliquam facilisis eleifend facilisis. Maecenas a risus id ante semper ultricies nec nec quam. Curabitur elementum, arcu ut fermentum luctus, nulla lorem accumsan mauris, vitae elementum felis enim ullamcorper lectus. Nulla facilisi. Etiam at turpis sed turpis viverra posuere. Praesent porta mattis mi id feugiat.
+
+Fusce commodo sodales erat quis sodales. Vestibulum dolor felis, interdum semper consectetur eu, mollis eget turpis. Integer accumsan elit sit amet libero dapibus eu viverra tortor porttitor. Ut pulvinar mattis tellus, non pulvinar erat dignissim vitae. Donec sagittis tincidunt quam, in auctor est euismod eu. Aenean feugiat luctus dolor at tincidunt. Aenean a mi sed lacus porta dapibus. Pellentesque ligula est, ultricies vitae tincidunt nec, placerat quis ipsum. Morbi dignissim libero sed nunc mollis feugiat. Vivamus mauris ante, venenatis eget sodales pharetra, vestibulum a ipsum. Vestibulum suscipit tempor sem, sagittis bibendum diam vehicula tempor. Proin at imperdiet dui. Mauris et metus quis mauris tincidunt tempor.
+
+Nunc pulvinar scelerisque magna non lobortis. Pellentesque eget risus mauris, sed suscipit lectus. Nam pharetra magna non urna vehicula rutrum. Duis adipiscing elementum porta. Donec eleifend enim vitae justo ultrices sodales. Nunc facilisis dui nec justo pretium blandit eu in est. Sed turpis lectus, imperdiet ac convallis ut, adipiscing vel mauris. Nulla commodo sollicitudin ante, ut vestibulum leo sollicitudin vitae. Curabitur imperdiet tellus sed tellus tincidunt porta. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus convallis viverra vulputate. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;
+
+Sed at mi tortor. Morbi ac sapien nisl. Etiam at sollicitudin nisl. Vestibulum vulputate varius tortor. Donec id magna dolor, non molestie sem. Nullam dolor elit, vulputate ac convallis quis, adipiscing id neque. Aliquam at justo justo, non sagittis nunc. Nullam quis sem libero, non suscipit quam. Duis nec ipsum metus. Praesent nec turpis quam, non malesuada nisi. Praesent ultricies suscipit sollicitudin. Pellentesque at massa nec nisl aliquet dapibus eget et dolor. Vestibulum ullamcorper dui sit amet erat imperdiet varius. In porttitor ultrices purus in imperdiet. Maecenas at erat fringilla tellus ultricies placerat id ut nulla. Aliquam tempus condimentum nunc, in molestie erat laoreet et. Phasellus at erat in massa luctus facilisis quis id purus. Duis dui turpis, gravida in aliquet sit amet, condimentum sed magna. Praesent non tellus in nunc aliquam dictum quis a enim.
+
+Maecenas sed neque velit, ut iaculis neque. Morbi leo arcu, volutpat non sodales ut, volutpat in ligula. Curabitur blandit neque ac arcu lobortis egestas. Nunc id odio ante, in sodales quam. Suspendisse condimentum est et massa bibendum malesuada. Sed fermentum tellus vel lorem dignissim fermentum. Maecenas pretium est sit amet dui congue viverra. Nullam vestibulum accumsan sagittis. Phasellus sit amet justo leo. Pellentesque ut sem lectus, elementum convallis nisl. Pellentesque dictum porttitor nisi, vel feugiat dui interdum nec. Phasellus arcu risus, convallis sit amet sodales in, imperdiet sed lacus. Mauris sed quam sit amet est venenatis sodales. Ut eleifend quam in enim bibendum eu rutrum erat placerat. Nunc faucibus massa ac augue dignissim venenatis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed sed commodo urna. Nam eu urna tortor, eu pharetra magna. Pellentesque tortor elit, molestie sit amet rhoncus eget, aliquam a quam. Phasellus vel nunc et sem pellentesque hendrerit.
+
+Aliquam eu arcu ac felis volutpat scelerisque. Morbi ut dignissim nibh. Nullam convallis, odio a aliquet dignissim, purus leo elementum augue, vitae tristique neque dolor eu nulla. Vivamus sit amet massa a augue lacinia vehicula et vel dolor. Etiam sapien sem, consequat vel vehicula id, pellentesque at augue. Donec est neque, consequat ac convallis in, suscipit sed tortor. Maecenas imperdiet, dolor sit amet congue congue, metus urna suscipit libero, ut congue nisl sapien facilisis est. Nunc eget orci odio, ut aliquam dolor. Fusce nec leo eu enim sollicitudin pharetra in nec sapien. Cras id nisi vitae ipsum semper vehicula. Nunc eu magna ac felis vehicula eleifend vel non felis.
+
+Vestibulum mattis dapibus mauris varius pretium. Nulla facilisi. Morbi quis euismod turpis. Nunc dignissim molestie consectetur. Quisque a mattis ipsum. Ut viverra leo sed odio faucibus sodales. Sed placerat luctus mattis. Aenean auctor iaculis placerat. Pellentesque lorem dui, pharetra id faucibus eget, iaculis egestas diam. Sed a metus tellus, eu aliquam dolor. Pellentesque eget nunc urna. Ut placerat erat in velit ornare luctus.
+
+Proin pharetra enim non lectus fringilla eu varius diam fermentum. Etiam tellus quam, sagittis a pellentesque in, tincidunt non ipsum. Vivamus id faucibus metus. Aliquam sodales venenatis massa nec lacinia. Pellentesque a urna a quam accumsan sollicitudin. Donec feugiat ante a urna aliquam ut laoreet neque molestie. Sed metus erat, hendrerit ornare tempus ut, aliquet eget neque. Morbi rutrum, lectus sit amet dictum luctus, ante tellus molestie nunc, non interdum orci velit a lorem. Suspendisse scelerisque augue eu velit placerat ac iaculis est mattis. Mauris lorem quam, molestie vel tempus eget, tincidunt et est. Etiam sit amet risus ac tellus ultrices porta sit amet a nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae;
+
+Fusce orci leo, tempor sed fermentum ut, rhoncus et erat. Integer a vulputate diam. Pellentesque luctus ornare varius. Quisque ornare tempus lacus quis porta. Integer consequat vestibulum eleifend. Nulla id eros eget odio eleifend vehicula. Duis ultricies ante eget massa vestibulum suscipit. Nunc et dui mi. Aliquam sit amet nunc neque, ut iaculis lorem. Nunc ornare lacinia mauris sed semper. Donec venenatis mollis urna at posuere. Etiam vestibulum dignissim magna nec hendrerit. Nullam interdum suscipit eros, ac sollicitudin mi semper in. Etiam eget feugiat augue. Etiam id imperdiet enim. Proin sed libero id quam dapibus sollicitudin. Cras suscipit dapibus nisi, quis sagittis dui consectetur vitae. Aenean lobortis congue sapien a pulvinar.
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas a diam in nulla porta hendrerit. Suspendisse massa ligula, tristique eu molestie quis, congue ut neque. Nullam vitae libero eget justo feugiat gravida ullamcorper at quam. Aenean eget interdum risus. Aliquam erat volutpat. Morbi odio purus, pharetra at cursus eget, tristique sit amet est. Pellentesque et turpis nisi, vitae vulputate dolor. Quisque odio nunc, condimentum ut mollis eget, laoreet pretium metus. Morbi vel est a nulla ultricies laoreet. Morbi ac ultrices eros. Fusce et pharetra leo. Pellentesque volutpat urna orci, sit amet scelerisque urna. Etiam vel orci mauris. Etiam sit amet lectus id massa elementum accumsan. Ut tincidunt ultricies lorem lacinia tempor.
+
+Mauris placerat massa at arcu ultricies sit amet malesuada urna sollicitudin. Pellentesque eleifend rhoncus ullamcorper. Fusce malesuada tincidunt lorem vel ullamcorper. Fusce non quam sapien. In hac habitasse platea dictumst. Praesent facilisis feugiat tempus. Quisque dictum placerat odio, vitae tincidunt lorem tincidunt in. Nam molestie, nisl id tempor auctor, erat nunc gravida nisi, nec vulputate tellus turpis tincidunt mi. Maecenas pretium porttitor lectus, vitae volutpat massa rutrum quis. Mauris ac sapien a arcu interdum condimentum ut quis urna. Mauris ligula neque, malesuada non rutrum et, condimentum ac velit. Sed condimentum neque at eros placerat placerat. Sed porttitor nibh non ipsum vehicula auctor commodo velit lobortis. Aliquam auctor elementum elementum.
+
+Nam aliquam pretium purus vel auctor. Mauris et arcu vel libero adipiscing dictum fermentum sed metus. Mauris dictum elit sed neque pharetra ac facilisis ante volutpat. Ut ut aliquam ligula. Duis vitae tortor nibh. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi varius pulvinar purus id vehicula. Proin sit amet libero at leo varius egestas. Vestibulum posuere porttitor felis, nec lacinia orci rhoncus non. Suspendisse potenti. Maecenas ut tempor felis. Cras at ipsum vitae tellus luctus aliquet. Nulla mauris erat, feugiat et condimentum id, adipiscing sed tellus. Nunc condimentum luctus auctor. In hac habitasse platea dictumst. Cras libero ante, commodo at adipiscing ut, consectetur ut metus. Maecenas eros augue, cursus cursus porta vitae, ullamcorper egestas tortor. Nullam ante felis, viverra in convallis quis, gravida sit amet velit.
+
+Duis consectetur sagittis enim ut dignissim. Integer ut augue at odio vehicula tincidunt. Nam sapien tortor, euismod et suscipit eu, euismod in tellus. Nam ornare orci ac nulla consequat quis semper risus aliquam. Nunc tristique turpis et lacus venenatis a fermentum odio placerat. Morbi condimentum, enim ac tristique rutrum, sem nisi rhoncus orci, id mollis purus justo ut dui. Nulla facilisi. Suspendisse consectetur odio rhoncus ante porttitor ac eleifend metus suscipit. In porttitor tempus massa quis dictum. Integer in orci nibh. Duis nec risus eu nunc sagittis mattis at vitae nunc. Donec sed mi sed ante fermentum posuere nec a est. Quisque vel massa quam. Pellentesque feugiat massa venenatis risus bibendum sit amet dapibus lectus gravida. Mauris nunc lorem, interdum sit amet pulvinar vitae, euismod id mi. Suspendisse turpis elit, lobortis ac fringilla at, aliquet eget libero. Quisque eleifend ullamcorper pharetra. Fusce vitae eros tortor, sed pulvinar neque. Praesent pretium, felis quis adipiscing laoreet, sapien turpis molestie erat, malesuada pretium urna purus id ante. Aliquam ac massa sit amet sapien scelerisque convallis.
+
+Quisque eget libero leo. In nec diam vitae metus varius tempus vitae non purus. Phasellus porttitor, lectus vel aliquam tincidunt, nisl odio volutpat diam, nec ultrices elit quam eget lectus. Sed mollis purus at ipsum porta tempus. Sed rhoncus nisi vel magna rhoncus vitae tristique massa tempor. Etiam metus ligula, hendrerit eu accumsan vitae, euismod ac mi. Suspendisse dui turpis, congue ut fringilla et, laoreet eu enim. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc aliquet lorem quis dolor pharetra tempor. Integer molestie varius laoreet. Curabitur ultrices nibh sit amet elit condimentum sit amet sagittis elit venenatis. Aliquam magna nunc, suscipit sed aliquam in, fringilla vel libero. Nunc eget elit risus. Suspendisse imperdiet, magna vel pulvinar sodales, metus velit accumsan mi, sed venenatis erat dolor eget turpis. Proin lacinia tincidunt semper. Fusce vestibulum sodales massa, a dapibus libero lobortis a. Pellentesque augue mauris, posuere sed faucibus eget, molestie at ante. Proin orci nunc, auctor vel auctor vitae, ultricies sit amet lectus. Integer at nunc eget diam tincidunt suscipit vitae et libero. Donec ac quam tortor, in vestibulum leo.
+
+Praesent laoreet pharetra libero, quis cursus erat tincidunt ac. Vivamus euismod odio vel erat placerat sed vehicula eros rutrum. Sed fermentum, lectus feugiat feugiat dictum, quam sapien commodo tellus, vel ornare urna felis interdum est. Integer condimentum lectus eu nulla lacinia ut porta turpis tempor. Pellentesque quis semper justo. Duis malesuada faucibus condimentum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Proin condimentum est quis urna pulvinar vehicula. Nam convallis enim non nibh elementum blandit. Curabitur dui urna, aliquam sed posuere eget, porttitor et tortor. Nam vitae velit dignissim quam porttitor congue sed quis massa. Cras sed diam vestibulum nisl pretium rutrum vel at ipsum. In eget euismod sem. Pellentesque vitae sem et augue vehicula pretium sit amet et quam. Proin enim nunc, malesuada vel lobortis non, viverra non leo. Donec eu convallis nibh. Fusce sodales orci nec felis vulputate interdum at in sem. Nulla facilisi.
+
+Nunc posuere orci sed diam fringilla ullamcorper. Vivamus laoreet condimentum purus sit amet consequat. Donec at tristique ipsum. Donec tincidunt, nisi sit amet commodo sagittis, velit diam eleifend nulla, sed faucibus enim arcu eget nisi. Quisque condimentum laoreet ante vel posuere. Aliquam sit amet massa quis orci placerat posuere ut at velit. Ut eu commodo nisi. Pellentesque ornare quam et lorem facilisis nec venenatis ligula dictum. Aliquam vel arcu diam. Nullam ut elit nec lorem eleifend tincidunt vel sed orci. In vulputate semper felis, id tincidunt neque mollis a. Quisque eu nisi non justo vehicula pellentesque. Maecenas nec sem nibh, dictum sagittis nibh. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In diam purus, commodo eget hendrerit eget, aliquam a sapien. Sed et justo eros. Etiam eget massa urna, non gravida enim. Cras ac ornare ligula.
+
+Suspendisse potenti. Sed non suscipit arcu. Mauris augue elit, porttitor non hendrerit id, egestas a eros. Nunc id orci magna. Fusce massa urna, gravida et porttitor ac, posuere eget nisl. Proin sed.
+Here is the last line there is no return
diff --git a/testing/mozbase/mozdevice/sut_tests/test-files/smalltext.txt b/testing/mozbase/mozdevice/sut_tests/test-files/smalltext.txt
new file mode 100644
index 000000000..9ec831b83
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test-files/smalltext.txt
@@ -0,0 +1 @@
+this is a short text file
diff --git a/testing/mozbase/mozdevice/sut_tests/test-files/test_script.sh b/testing/mozbase/mozdevice/sut_tests/test-files/test_script.sh
new file mode 100644
index 000000000..4f56dae89
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test-files/test_script.sh
@@ -0,0 +1 @@
+echo $THE_ANSWER
diff --git a/testing/mozbase/mozdevice/sut_tests/test_datachannel.py b/testing/mozbase/mozdevice/sut_tests/test_datachannel.py
new file mode 100644
index 000000000..99b71a584
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_datachannel.py
@@ -0,0 +1,53 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import re
+import socket
+from time import strptime
+
+from dmunit import DeviceManagerTestCase, heartbeat_port
+
+
+class DataChannelTestCase(DeviceManagerTestCase):
+
+ runs_on_test_device = False
+
+ def runTest(self):
+ """This tests the heartbeat and the data channel.
+ """
+ ip = self.dm.host
+
+ # Let's connect
+ self._datasock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ # Assume 60 seconds between heartbeats
+ self._datasock.settimeout(float(60 * 2))
+ self._datasock.connect((ip, heartbeat_port))
+ self._connected = True
+
+ # Let's listen
+ numbeats = 0
+ capturedHeader = False
+ while numbeats < 3:
+ data = self._datasock.recv(1024)
+ print data
+ self.assertNotEqual(len(data), 0)
+
+ # Check for the header
+ if not capturedHeader:
+ m = re.match(r"(.*?) trace output", data)
+ self.assertNotEqual(m, None,
+ 'trace output line does not match. The line: ' + str(data))
+ capturedHeader = True
+
+ # Check for standard heartbeat messsage
+ m = re.match(r"(.*?) Thump thump - (.*)", data)
+ if m is None:
+ # This isn't an error, it usually means we've obtained some
+ # unexpected data from the device
+ continue
+
+ # Ensure it matches our format
+ mHeartbeatTime = m.group(1)
+ mHeartbeatTime = strptime(mHeartbeatTime, "%Y%m%d-%H:%M:%S")
+ numbeats = numbeats + 1
diff --git a/testing/mozbase/mozdevice/sut_tests/test_exec.py b/testing/mozbase/mozdevice/sut_tests/test_exec.py
new file mode 100644
index 000000000..e262bd1ad
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_exec.py
@@ -0,0 +1,24 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import posixpath
+from StringIO import StringIO
+
+from dmunit import DeviceManagerTestCase
+
+
+class ExecTestCase(DeviceManagerTestCase):
+
+ def runTest(self):
+ """Simple exec test, does not use env vars."""
+ out = StringIO()
+ filename = posixpath.join(self.dm.deviceRoot, 'test_exec_file')
+ # Make sure the file was not already there
+ self.dm.removeFile(filename)
+ self.dm.shell(['dd', 'if=/dev/zero', 'of=%s' % filename, 'bs=1024',
+ 'count=1'], out)
+ # Check that the file has been created
+ self.assertTrue(self.dm.fileExists(filename))
+ # Clean up
+ self.dm.removeFile(filename)
diff --git a/testing/mozbase/mozdevice/sut_tests/test_exec_env.py b/testing/mozbase/mozdevice/sut_tests/test_exec_env.py
new file mode 100644
index 000000000..bf7029e7d
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_exec_env.py
@@ -0,0 +1,32 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import posixpath
+from StringIO import StringIO
+
+from dmunit import DeviceManagerTestCase
+
+
+class ExecEnvTestCase(DeviceManagerTestCase):
+
+ def runTest(self):
+ """Exec test with env vars."""
+ # Push the file
+ localfile = os.path.join('test-files', 'test_script.sh')
+ remotefile = posixpath.join(self.dm.deviceRoot, 'test_script.sh')
+ self.dm.pushFile(localfile, remotefile)
+
+ # Run the cmd
+ out = StringIO()
+ self.dm.shell(['sh', remotefile], out, env={'THE_ANSWER': 42})
+
+ # Rewind the output file
+ out.seek(0)
+ # Make sure first line is 42
+ line = out.readline()
+ self.assertTrue(int(line) == 42)
+
+ # Clean up
+ self.dm.removeFile(remotefile)
diff --git a/testing/mozbase/mozdevice/sut_tests/test_fileExists.py b/testing/mozbase/mozdevice/sut_tests/test_fileExists.py
new file mode 100644
index 000000000..27822bc95
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_fileExists.py
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import tempfile
+import posixpath
+
+from dmunit import DeviceManagerTestCase
+
+
+class FileExistsTestCase(DeviceManagerTestCase):
+ """This tests the "fileExists" command.
+ """
+
+ def testOnRoot(self):
+ self.assertTrue(self.dm.fileExists('/'))
+
+ def testOnNonexistent(self):
+ self.assertFalse(self.dm.fileExists('/doesNotExist'))
+
+ def testOnRegularFile(self):
+ remote_path = posixpath.join(self.dm.deviceRoot, 'testFile')
+ self.assertFalse(self.dm.fileExists(remote_path))
+ with tempfile.NamedTemporaryFile() as f:
+ self.dm.pushFile(f.name, remote_path)
+ self.assertTrue(self.dm.fileExists(remote_path))
+ self.dm.removeFile(remote_path)
+
+ def testOnDirectory(self):
+ remote_path = posixpath.join(self.dm.deviceRoot, 'testDir')
+ remote_path_file = posixpath.join(remote_path, 'testFile')
+ self.assertFalse(self.dm.fileExists(remote_path))
+ with tempfile.NamedTemporaryFile() as f:
+ self.dm.pushFile(f.name, remote_path_file)
+ self.assertTrue(self.dm.fileExists(remote_path))
+ self.dm.removeFile(remote_path_file)
+ self.dm.removeDir(remote_path)
diff --git a/testing/mozbase/mozdevice/sut_tests/test_getdir.py b/testing/mozbase/mozdevice/sut_tests/test_getdir.py
new file mode 100644
index 000000000..00ea8c9ae
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_getdir.py
@@ -0,0 +1,51 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import posixpath
+import shutil
+import tempfile
+
+from mozdevice.devicemanager import DMError
+from dmunit import DeviceManagerTestCase
+
+
+class GetDirectoryTestCase(DeviceManagerTestCase):
+
+ def _setUp(self):
+ self.localsrcdir = tempfile.mkdtemp()
+ os.makedirs(os.path.join(self.localsrcdir, 'push1', 'sub.1', 'sub.2'))
+ path = os.path.join(self.localsrcdir,
+ 'push1', 'sub.1', 'sub.2', 'testfile')
+ file(path, 'w').close()
+ os.makedirs(os.path.join(self.localsrcdir, 'push1', 'emptysub'))
+ self.localdestdir = tempfile.mkdtemp()
+ self.expected_filelist = ['emptysub', 'sub.1']
+
+ def tearDown(self):
+ shutil.rmtree(self.localsrcdir)
+ shutil.rmtree(self.localdestdir)
+
+ def runTest(self):
+ """This tests the getDirectory() function.
+ """
+ testroot = posixpath.join(self.dm.deviceRoot, 'infratest')
+ self.dm.removeDir(testroot)
+ self.dm.mkDir(testroot)
+ self.dm.pushDir(
+ os.path.join(self.localsrcdir, 'push1'),
+ posixpath.join(testroot, 'push1'))
+ # pushDir doesn't copy over empty directories, but we want to make sure
+ # that they are retrieved correctly.
+ self.dm.mkDir(posixpath.join(testroot, 'push1', 'emptysub'))
+ self.dm.getDirectory(posixpath.join(testroot, 'push1'),
+ os.path.join(self.localdestdir, 'push1'))
+ self.assertTrue(os.path.exists(
+ os.path.join(self.localdestdir,
+ 'push1', 'sub.1', 'sub.2', 'testfile')))
+ self.assertTrue(os.path.exists(
+ os.path.join(self.localdestdir, 'push1', 'emptysub')))
+ self.assertRaises(DMError, self.dm.getDirectory,
+ '/dummy', os.path.join(self.localdestdir, '/none'))
+ self.assertFalse(os.path.exists(self.localdestdir + '/none'))
diff --git a/testing/mozbase/mozdevice/sut_tests/test_info.py b/testing/mozbase/mozdevice/sut_tests/test_info.py
new file mode 100644
index 000000000..57bb4fce0
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_info.py
@@ -0,0 +1,20 @@
+# 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 dmunit import DeviceManagerTestCase
+
+
+class InfoTestCase(DeviceManagerTestCase):
+
+ runs_on_test_device = False
+
+ def runTest(self):
+ """This tests the "info" command.
+ """
+ cmds = ('os', 'id', 'systime', 'uptime', 'screen', 'memory', 'power')
+ for c in cmds:
+ data = self.dm.getInfo(c)
+ print c + str(data)
+
+ # No real good way to verify this. If it doesn't throw, we're ok.
diff --git a/testing/mozbase/mozdevice/sut_tests/test_prompt.py b/testing/mozbase/mozdevice/sut_tests/test_prompt.py
new file mode 100644
index 000000000..9980ee2ab
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_prompt.py
@@ -0,0 +1,30 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import re
+import socket
+
+from dmunit import DeviceManagerTestCase
+
+
+class PromptTestCase(DeviceManagerTestCase):
+
+ def tearDown(self):
+ if self.sock:
+ self.sock.close()
+
+ def runTest(self):
+ """This tests getting a prompt from the device.
+ """
+ self.sock = None
+ ip = self.dm.host
+ port = self.dm.port
+
+ promptre = re.compile('.*\$\>\x00')
+ data = ""
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.sock.connect((ip, int(port)))
+ data = self.sock.recv(1024)
+ print data
+ self.assertTrue(promptre.match(data))
diff --git a/testing/mozbase/mozdevice/sut_tests/test_ps.py b/testing/mozbase/mozdevice/sut_tests/test_ps.py
new file mode 100644
index 000000000..b36e61179
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_ps.py
@@ -0,0 +1,27 @@
+# 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 dmunit import DeviceManagerTestCase
+
+
+class ProcessListTestCase(DeviceManagerTestCase):
+
+ def runTest(self):
+ """This tests getting a process list from the device.
+ """
+ proclist = self.dm.getProcessList()
+
+ # This returns a process list of the form:
+ # [[<procid>, <procname>], [<procid>, <procname>], ...]
+ # on android the userID is affixed to the process array:
+ # [[<procid>, <procname>, <userid>], ...]
+
+ self.assertNotEqual(len(proclist), 0)
+
+ for item in proclist:
+ self.assertIsInstance(item[0], int)
+ self.assertIsInstance(item[1], str)
+ self.assertGreater(len(item[1]), 0)
+ if len(item) > 2:
+ self.assertIsInstance(item[2], int)
diff --git a/testing/mozbase/mozdevice/sut_tests/test_pull.py b/testing/mozbase/mozdevice/sut_tests/test_pull.py
new file mode 100644
index 000000000..753d8ddd5
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_pull.py
@@ -0,0 +1,34 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import hashlib
+import os
+import posixpath
+
+from dmunit import DeviceManagerTestCase
+from mozdevice.devicemanager import DMError
+
+
+class PullTestCase(DeviceManagerTestCase):
+
+ def runTest(self):
+ """Tests the "pull" command with a binary file.
+ """
+ orig = hashlib.md5()
+ new = hashlib.md5()
+ local_test_file = os.path.join('test-files', 'mybinary.zip')
+ orig.update(file(local_test_file, 'r').read())
+
+ testroot = self.dm.deviceRoot
+ remote_test_file = posixpath.join(testroot, 'mybinary.zip')
+ self.dm.removeFile(remote_test_file)
+ self.dm.pushFile(local_test_file, remote_test_file)
+ new.update(self.dm.pullFile(remote_test_file))
+ # Use hexdigest() instead of digest() since values are printed
+ # if assert fails
+ self.assertEqual(orig.hexdigest(), new.hexdigest())
+
+ remote_missing_file = posixpath.join(testroot, 'doesnotexist')
+ self.dm.removeFile(remote_missing_file) # Just to be sure
+ self.assertRaises(DMError, self.dm.pullFile, remote_missing_file)
diff --git a/testing/mozbase/mozdevice/sut_tests/test_push1.py b/testing/mozbase/mozdevice/sut_tests/test_push1.py
new file mode 100644
index 000000000..f457d6cc5
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_push1.py
@@ -0,0 +1,38 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import posixpath
+
+from dmunit import DeviceManagerTestCase
+
+
+class Push1TestCase(DeviceManagerTestCase):
+
+ def runTest(self):
+ """This tests copying a directory structure to the device.
+ """
+ dvroot = self.dm.deviceRoot
+ dvpath = posixpath.join(dvroot, 'infratest')
+ self.dm.removeDir(dvpath)
+ self.dm.mkDir(dvpath)
+
+ p1 = os.path.join('test-files', 'push1')
+ # Set up local stuff
+ try:
+ os.rmdir(p1)
+ except:
+ pass
+
+ if not os.path.exists(p1):
+ os.makedirs(os.path.join(p1, 'sub.1', 'sub.2'))
+ if not os.path.exists(os.path.join(p1, 'sub.1', 'sub.2', 'testfile')):
+ file(os.path.join(p1, 'sub.1', 'sub.2', 'testfile'), 'w').close()
+
+ self.dm.pushDir(p1, posixpath.join(dvpath, 'push1'))
+
+ self.assertTrue(
+ self.dm.dirExists(posixpath.join(dvpath, 'push1', 'sub.1')))
+ self.assertTrue(self.dm.dirExists(
+ posixpath.join(dvpath, 'push1', 'sub.1', 'sub.2')))
diff --git a/testing/mozbase/mozdevice/sut_tests/test_push2.py b/testing/mozbase/mozdevice/sut_tests/test_push2.py
new file mode 100644
index 000000000..5ccea509e
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_push2.py
@@ -0,0 +1,39 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import posixpath
+
+from dmunit import DeviceManagerTestCase
+
+
+class Push2TestCase(DeviceManagerTestCase):
+
+ def runTest(self):
+ """This tests copying a directory structure with files to the device.
+ """
+ testroot = posixpath.join(self.dm.deviceRoot, 'infratest')
+ self.dm.removeDir(testroot)
+ self.dm.mkDir(testroot)
+ path = posixpath.join(testroot, 'push2')
+ self.dm.pushDir(os.path.join('test-files', 'push2'), path)
+
+ # Let's walk the tree and make sure everything is there
+ # though it's kind of cheesy, we'll use the validate file to compare
+ # hashes - we use the client side hashing when testing the cat command
+ # specifically, so that makes this a little less cheesy, I guess.
+ self.assertTrue(
+ self.dm.dirExists(posixpath.join(testroot, 'push2', 'sub1')))
+ self.assertTrue(self.dm.validateFile(
+ posixpath.join(testroot, 'push2', 'sub1', 'file1.txt'),
+ os.path.join('test-files', 'push2', 'sub1', 'file1.txt')))
+ self.assertTrue(self.dm.validateFile(
+ posixpath.join(testroot, 'push2', 'sub1', 'sub1.1', 'file2.txt'),
+ os.path.join('test-files', 'push2', 'sub1', 'sub1.1', 'file2.txt')))
+ self.assertTrue(self.dm.validateFile(
+ posixpath.join(testroot, 'push2', 'sub2', 'file3.txt'),
+ os.path.join('test-files', 'push2', 'sub2', 'file3.txt')))
+ self.assertTrue(self.dm.validateFile(
+ posixpath.join(testroot, 'push2', 'file4.bin'),
+ os.path.join('test-files', 'push2', 'file4.bin')))
diff --git a/testing/mozbase/mozdevice/sut_tests/test_pushbinary.py b/testing/mozbase/mozdevice/sut_tests/test_pushbinary.py
new file mode 100644
index 000000000..86809dc1f
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_pushbinary.py
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import posixpath
+
+from dmunit import DeviceManagerTestCase
+
+
+class PushBinaryTestCase(DeviceManagerTestCase):
+
+ def runTest(self):
+ """This tests copying a binary file.
+ """
+ testroot = self.dm.deviceRoot
+ self.dm.removeFile(posixpath.join(testroot, 'mybinary.zip'))
+ self.dm.pushFile(os.path.join('test-files', 'mybinary.zip'),
+ posixpath.join(testroot, 'mybinary.zip'))
diff --git a/testing/mozbase/mozdevice/sut_tests/test_pushsmalltext.py b/testing/mozbase/mozdevice/sut_tests/test_pushsmalltext.py
new file mode 100644
index 000000000..174b3b117
--- /dev/null
+++ b/testing/mozbase/mozdevice/sut_tests/test_pushsmalltext.py
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import posixpath
+
+from dmunit import DeviceManagerTestCase
+
+
+class PushSmallTextTestCase(DeviceManagerTestCase):
+
+ def runTest(self):
+ """This tests copying a small text file.
+ """
+ testroot = self.dm.deviceRoot
+ self.dm.removeFile(posixpath.join(testroot, 'smalltext.txt'))
+ self.dm.pushFile(os.path.join('test-files', 'smalltext.txt'),
+ posixpath.join(testroot, 'smalltext.txt'))
diff --git a/testing/mozbase/mozdevice/tests/droidsut_launch.py b/testing/mozbase/mozdevice/tests/droidsut_launch.py
new file mode 100644
index 000000000..b9872e096
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/droidsut_launch.py
@@ -0,0 +1,36 @@
+from sut import MockAgent
+import mozdevice
+import logging
+import unittest
+
+
+class LaunchTest(unittest.TestCase):
+
+ def test_nouserserial(self):
+ a = MockAgent(self, commands=[("ps",
+ "10029 549 com.android.launcher\n"
+ "10066 1198 com.twitter.android"),
+ ("info sutuserinfo", ""),
+ ("exec am start -W -n "
+ "org.mozilla.fennec/org.mozilla.gecko.BrowserApp -a "
+ "android.intent.action.VIEW",
+ "OK\nreturn code [0]")])
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port, logLevel=logging.DEBUG)
+ d.launchFennec("org.mozilla.fennec")
+ a.wait()
+
+ def test_userserial(self):
+ a = MockAgent(self, commands=[("ps",
+ "10029 549 com.android.launcher\n"
+ "10066 1198 com.twitter.android"),
+ ("info sutuserinfo", "User Serial:0"),
+ ("exec am start --user 0 -W -n "
+ "org.mozilla.fennec/org.mozilla.gecko.BrowserApp -a "
+ "android.intent.action.VIEW",
+ "OK\nreturn code [0]")])
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port, logLevel=logging.DEBUG)
+ d.launchFennec("org.mozilla.fennec")
+ a.wait()
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/manifest.ini b/testing/mozbase/mozdevice/tests/manifest.ini
new file mode 100644
index 000000000..63825c85b
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/manifest.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+skip-if = os == 'win'
+
+[sut_app.py]
+[sut_basic.py]
+[sut_chmod.py]
+[sut_copytree.py]
+[sut_fileExists.py]
+[sut_fileMethods.py]
+[sut_info.py]
+[sut_ip.py]
+[sut_kill.py]
+[sut_list.py]
+[sut_logcat.py]
+[sut_mkdir.py]
+[sut_movetree.py]
+[sut_ps.py]
+[sut_push.py]
+[sut_pull.py]
+[sut_remove.py]
+[sut_time.py]
+[sut_unpackfile.py]
+[droidsut_launch.py]
diff --git a/testing/mozbase/mozdevice/tests/sut.py b/testing/mozbase/mozdevice/tests/sut.py
new file mode 100644
index 000000000..76a5ed313
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+import datetime
+import socket
+import time
+
+from threading import Thread
+
+
+class MockAgent(object):
+
+ MAX_WAIT_TIME_SECONDS = 10
+ SOCKET_TIMEOUT_SECONDS = 5
+
+ def __init__(self, tester, start_commands=None, commands=[]):
+ if start_commands:
+ self.commands = start_commands
+ else:
+ self.commands = [("ver", "SUTAgentAndroid Version 1.14")]
+ self.commands = self.commands + commands
+
+ self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._sock.bind(("127.0.0.1", 0))
+ self._sock.listen(1)
+
+ self.tester = tester
+
+ self.thread = Thread(target=self._serve_thread)
+ self.thread.start()
+
+ self.should_stop = False
+
+ @property
+ def port(self):
+ return self._sock.getsockname()[1]
+
+ def _serve_thread(self):
+ conn = None
+ while self.commands:
+ if not conn:
+ conn, addr = self._sock.accept()
+ conn.settimeout(self.SOCKET_TIMEOUT_SECONDS)
+ conn.send("$>\x00")
+ (command, response) = self.commands.pop(0)
+ data = ''
+ timeout = datetime.datetime.now() + datetime.timedelta(
+ seconds=self.MAX_WAIT_TIME_SECONDS)
+ # The data might come in chunks, particularly if we are expecting
+ # multiple lines, as with push commands.
+ while (len(data) < len(command) and
+ datetime.datetime.now() < timeout):
+ try:
+ data += conn.recv(1024)
+ except socket.timeout:
+ # We handle timeouts in the main loop.
+ pass
+ self.tester.assertEqual(data.strip(), command)
+ # send response and prompt separately to test for bug 789496
+ # FIXME: Improve the mock agent, since overloading the meaning
+ # of 'response' is getting confusing.
+ if response is None: # code for "shut down"
+ conn.shutdown(socket.SHUT_RDWR)
+ conn.close()
+ conn = None
+ elif type(response) is int: # code for "time out"
+ max_timeout = 15.0
+ timeout = 0.0
+ interval = 0.1
+ while not self.should_stop and timeout < max_timeout:
+ time.sleep(interval)
+ timeout += interval
+ if timeout >= max_timeout:
+ raise Exception("Maximum timeout reached! This should not "
+ "happen")
+ return
+ else:
+ # pull is handled specially, as we just pass back the full
+ # command line
+ if "pull" in command:
+ conn.send(response)
+ else:
+ conn.send("%s\n" % response)
+ conn.send("$>\x00")
+
+ def wait(self):
+ self.thread.join()
diff --git a/testing/mozbase/mozdevice/tests/sut_app.py b/testing/mozbase/mozdevice/tests/sut_app.py
new file mode 100644
index 000000000..0a5d996ae
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_app.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class TestApp(unittest.TestCase):
+
+ def test_getAppRoot(self):
+ command = [("getapproot org.mozilla.firefox",
+ "/data/data/org.mozilla.firefox")]
+
+ m = MockAgent(self, commands=command)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+
+ self.assertEqual(command[0][1], d.getAppRoot('org.mozilla.firefox'))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_basic.py b/testing/mozbase/mozdevice/tests/sut_basic.py
new file mode 100644
index 000000000..666d4915c
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_basic.py
@@ -0,0 +1,73 @@
+from sut import MockAgent
+import mozdevice
+import logging
+import unittest
+
+
+class BasicTest(unittest.TestCase):
+
+ def test_init(self):
+ """Tests DeviceManager initialization."""
+ a = MockAgent(self)
+
+ mozdevice.DroidSUT("127.0.0.1", port=a.port, logLevel=logging.DEBUG)
+ # all testing done in device's constructor
+ a.wait()
+
+ def test_init_err(self):
+ """Tests error handling during initialization."""
+ a = MockAgent(self, start_commands=[("ver", "##AGENT-WARNING## No version")])
+ self.assertRaises(mozdevice.DMError,
+ lambda: mozdevice.DroidSUT("127.0.0.1",
+ port=a.port,
+ logLevel=logging.DEBUG))
+ a.wait()
+
+ def test_timeout_normal(self):
+ """Tests DeviceManager timeout, normal case."""
+ a = MockAgent(self, commands=[("isdir /mnt/sdcard/tests", "TRUE"),
+ ("cd /mnt/sdcard/tests", ""),
+ ("ls", "test.txt"),
+ ("rm /mnt/sdcard/tests/test.txt",
+ "Removed the file")])
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port, logLevel=logging.DEBUG)
+ ret = d.removeFile('/mnt/sdcard/tests/test.txt')
+ self.assertEqual(ret, None) # if we didn't throw an exception, we're ok
+ a.wait()
+
+ def test_timeout_timeout(self):
+ """Tests DeviceManager timeout, timeout case."""
+ a = MockAgent(self, commands=[("isdir /mnt/sdcard/tests", "TRUE"),
+ ("cd /mnt/sdcard/tests", ""),
+ ("ls", "test.txt"),
+ ("rm /mnt/sdcard/tests/test.txt", 0)])
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port, logLevel=logging.DEBUG)
+ d.default_timeout = 1
+ exceptionThrown = False
+ try:
+ d.removeFile('/mnt/sdcard/tests/test.txt')
+ except mozdevice.DMError:
+ exceptionThrown = True
+ self.assertEqual(exceptionThrown, True)
+ a.should_stop = True
+ a.wait()
+
+ def test_shell(self):
+ """Tests shell command"""
+ for cmd in [("exec foobar", False), ("execsu foobar", True)]:
+ for retcode in [1, 2]:
+ a = MockAgent(self, commands=[(cmd[0],
+ "\nreturn code [%s]" % retcode)])
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+ exceptionThrown = False
+ try:
+ d.shellCheckOutput(["foobar"], root=cmd[1])
+ except mozdevice.DMError:
+ exceptionThrown = True
+ expectedException = (retcode != 0)
+ self.assertEqual(exceptionThrown, expectedException)
+
+ a.wait()
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_chmod.py b/testing/mozbase/mozdevice/tests/sut_chmod.py
new file mode 100644
index 000000000..404330c03
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_chmod.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class TestChmod(unittest.TestCase):
+
+ def test_chmod(self):
+
+ command = [('chmod /mnt/sdcard/test',
+ 'Changing permissions for /storage/emulated/legacy/Test\n'
+ ' <empty>\n'
+ 'chmod /storage/emulated/legacy/Test ok\n')]
+ m = MockAgent(self, commands=command)
+ d = mozdevice.DroidSUT('127.0.0.1', port=m.port, logLevel=logging.DEBUG)
+
+ self.assertEqual(None, d.chmodDir('/mnt/sdcard/test'))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_copytree.py b/testing/mozbase/mozdevice/tests/sut_copytree.py
new file mode 100644
index 000000000..ec22828d0
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_copytree.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class CopyTreeTest(unittest.TestCase):
+
+ def test_copyFile(self):
+ commands = [('dd if=/mnt/sdcard/tests/test.txt of=/mnt/sdcard/tests/test2.txt', ''),
+ ('isdir /mnt/sdcard/tests', 'TRUE'),
+ ('cd /mnt/sdcard/tests', ''),
+ ('ls', 'test.txt\ntest2.txt')]
+
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+
+ self.assertEqual(None, d.copyTree('/mnt/sdcard/tests/test.txt',
+ '/mnt/sdcard/tests/test2.txt'))
+ expected = (commands[3][1].strip()).split('\n')
+ self.assertEqual(expected, d.listFiles('/mnt/sdcard/tests'))
+
+ def test_copyDir(self):
+ commands = [('dd if=/mnt/sdcard/tests/foo of=/mnt/sdcard/tests/bar', ''),
+ ('isdir /mnt/sdcard/tests', 'TRUE'),
+ ('cd /mnt/sdcard/tests', ''),
+ ('ls', 'foo\nbar')]
+
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port,
+ logLevel=logging.DEBUG)
+
+ self.assertEqual(None, d.copyTree('/mnt/sdcard/tests/foo',
+ '/mnt/sdcard/tests/bar'))
+ expected = (commands[3][1].strip()).split('\n')
+ self.assertEqual(expected, d.listFiles('/mnt/sdcard/tests'))
+
+ def test_copyNonEmptyDir(self):
+ commands = [('isdir /mnt/sdcard/tests/foo/bar', 'TRUE'),
+ ('dd if=/mnt/sdcard/tests/foo of=/mnt/sdcard/tests/foo2', ''),
+ ('isdir /mnt/sdcard/tests', 'TRUE'),
+ ('cd /mnt/sdcard/tests', ''),
+ ('ls', 'foo\nfoo2'),
+ ('isdir /mnt/sdcard/tests/foo2', 'TRUE'),
+ ('cd /mnt/sdcard/tests/foo2', ''),
+ ('ls', 'bar')]
+
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port,
+ logLevel=logging.DEBUG)
+
+ self.assertTrue(d.dirExists('/mnt/sdcard/tests/foo/bar'))
+ self.assertEqual(None, d.copyTree('/mnt/sdcard/tests/foo',
+ '/mnt/sdcard/tests/foo2'))
+ expected = (commands[4][1].strip()).split('\n')
+ self.assertEqual(expected, d.listFiles('/mnt/sdcard/tests'))
+ self.assertTrue(d.fileExists('/mnt/sdcard/tests/foo2/bar'))
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_fileExists.py b/testing/mozbase/mozdevice/tests/sut_fileExists.py
new file mode 100644
index 000000000..702fd2de3
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_fileExists.py
@@ -0,0 +1,29 @@
+from sut import MockAgent
+import mozdevice
+import unittest
+
+
+class FileExistsTest(unittest.TestCase):
+
+ commands = [('isdir /', 'TRUE'),
+ ('cd /', ''),
+ ('ls', 'init')]
+
+ def test_onRoot(self):
+ root_commands = [('isdir /', 'TRUE')]
+ a = MockAgent(self, commands=root_commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+ self.assertTrue(d.fileExists('/'))
+
+ def test_onNonexistent(self):
+ a = MockAgent(self, commands=self.commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+ self.assertFalse(d.fileExists('/doesNotExist'))
+
+ def test_onRegularFile(self):
+ a = MockAgent(self, commands=self.commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+ self.assertTrue(d.fileExists('/init'))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_fileMethods.py b/testing/mozbase/mozdevice/tests/sut_fileMethods.py
new file mode 100644
index 000000000..142950a81
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_fileMethods.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+
+import hashlib
+import mozdevice
+import logging
+import shutil
+import tempfile
+import unittest
+from sut import MockAgent
+
+
+class TestFileMethods(unittest.TestCase):
+ """ Class to test misc file methods """
+
+ content = "What is the answer to the life, universe and everything? 42"
+ h = hashlib.md5()
+ h.update(content)
+ temp_hash = h.hexdigest()
+
+ def test_validateFile(self):
+
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(self.content)
+ f.flush()
+
+ # Test Valid Hashes
+ commands_valid = [("hash /sdcard/test/file", self.temp_hash)]
+
+ m = MockAgent(self, commands=commands_valid)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ self.assertTrue(d.validateFile('/sdcard/test/file', f.name))
+
+ # Test invalid hashes
+ commands_invalid = [("hash /sdcard/test/file", "0this0hash0is0completely0invalid")]
+
+ m = MockAgent(self, commands=commands_invalid)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ self.assertFalse(d.validateFile('/sdcard/test/file', f.name))
+
+ def test_getFile(self):
+
+ fname = "/mnt/sdcard/file"
+ commands = [("pull %s" % fname, "%s,%s\n%s" % (fname, len(self.content), self.content)),
+ ("hash %s" % fname, self.temp_hash)]
+
+ with tempfile.NamedTemporaryFile() as f:
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ # No error means success
+ self.assertEqual(None, d.getFile(fname, f.name))
+
+ def test_getDirectory(self):
+
+ fname = "/mnt/sdcard/file"
+ commands = [("isdir /mnt/sdcard", "TRUE"),
+ ("isdir /mnt/sdcard", "TRUE"),
+ ("cd /mnt/sdcard", ""),
+ ("ls", "file"),
+ ("isdir %s" % fname, "FALSE"),
+ ("pull %s" % fname, "%s,%s\n%s" % (fname, len(self.content), self.content)),
+ ("hash %s" % fname, self.temp_hash)]
+
+ tmpdir = tempfile.mkdtemp()
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ self.assertEqual(None, d.getDirectory("/mnt/sdcard", tmpdir))
+
+ # Cleanup
+ shutil.rmtree(tmpdir)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_info.py b/testing/mozbase/mozdevice/tests/sut_info.py
new file mode 100644
index 000000000..93f3d4258
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_info.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+import mozdevice
+import logging
+import re
+import unittest
+from sut import MockAgent
+
+
+class TestGetInfo(unittest.TestCase):
+
+ commands = {'os': ('info os', 'JDQ39'),
+ 'id': ('info id', '11:22:33:44:55:66'),
+ 'uptime': ('info uptime', '0 days 0 hours 7 minutes 0 seconds 0 ms'),
+ 'uptimemillis': ('info uptimemillis', '666'),
+ 'systime': ('info systime', '2013/04/2 12:42:00:007'),
+ 'screen': ('info screen', 'X:768 Y:1184'),
+ 'rotation': ('info rotation', 'ROTATION:0'),
+ 'memory': ('info memory', 'PA:1351032832, FREE: 878645248'),
+ 'process': ('info process', '1000 527 system\n'
+ '10091 3443 org.mozilla.firefox\n'
+ '10112 3137 com.mozilla.SUTAgentAndroid\n'
+ '10035 807 com.android.launcher'),
+ 'disk': ('info disk', '/data: 6084923392 total, 980922368 available\n'
+ '/system: 867999744 total, 332333056 available\n'
+ '/mnt/sdcard: 6084923392 total, 980922368 available'),
+ 'power': ('info power', 'Power status:\n'
+ ' AC power OFFLINE\n'
+ ' Battery charge LOW DISCHARGING\n'
+ ' Remaining charge: 20%\n'
+ ' Battery Temperature: 25.2 (c)'),
+ 'sutuserinfo': ('info sutuserinfo', 'User Serial:0'),
+ 'temperature': ('info temperature', 'Temperature: unknown')
+ }
+
+ def test_getInfo(self):
+
+ for directive in self.commands.keys():
+ m = MockAgent(self, commands=[self.commands[directive]])
+ d = mozdevice.DroidSUT('127.0.0.1', port=m.port, logLevel=logging.DEBUG)
+
+ expected = re.sub(r'\ +', ' ', self.commands[directive][1]).split('\n')
+ # Account for slightly different return format for 'process'
+ if directive is 'process':
+ expected = [[x] for x in expected]
+
+ self.assertEqual(d.getInfo(directive=directive)[directive], expected)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_ip.py b/testing/mozbase/mozdevice/tests/sut_ip.py
new file mode 100644
index 000000000..31428a624
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_ip.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class TestGetIP(unittest.TestCase):
+ """ class to test IP methods """
+
+ commands = [('exec ifconfig eth0', 'eth0: ip 192.168.0.1 '
+ 'mask 255.255.255.0 flags [up broadcast running multicast]\n'
+ 'return code [0]'),
+ ('exec ifconfig wlan0', 'wlan0: ip 10.1.39.126\n'
+ 'mask 255.255.0.0 flags [up broadcast running multicast]\n'
+ 'return code [0]'),
+ ('exec ifconfig fake0', '##AGENT-WARNING## [ifconfig] '
+ 'command with arg(s) = [fake0] is currently not implemented.')
+ ]
+
+ def test_getIP_eth0(self):
+ m = MockAgent(self, commands=[self.commands[0]])
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ self.assertEqual('192.168.0.1', d.getIP(interfaces=['eth0']))
+
+ def test_getIP_wlan0(self):
+ m = MockAgent(self, commands=[self.commands[1]])
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ self.assertEqual('10.1.39.126', d.getIP(interfaces=['wlan0']))
+
+ def test_getIP_error(self):
+ m = MockAgent(self, commands=[self.commands[2]])
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ self.assertRaises(mozdevice.DMError, d.getIP, interfaces=['fake0'])
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_kill.py b/testing/mozbase/mozdevice/tests/sut_kill.py
new file mode 100644
index 000000000..fea2f57e0
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_kill.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class TestKill(unittest.TestCase):
+
+ def test_killprocess(self):
+ commands = [("ps", "1000 1486 com.android.settings\n"
+ "10016 420 com.android.location.fused\n"
+ "10023 335 com.android.systemui\n"),
+ ("kill com.android.settings",
+ "Successfully killed com.android.settings\n")]
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ # No error raised means success
+ self.assertEqual(None, d.killProcess("com.android.settings"))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_list.py b/testing/mozbase/mozdevice/tests/sut_list.py
new file mode 100644
index 000000000..a319fd725
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_list.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class TestListFiles(unittest.TestCase):
+ commands = [("isdir /mnt/sdcard", "TRUE"),
+ ("cd /mnt/sdcard", ""),
+ ("ls", "Android\nMusic\nPodcasts\nRingtones\nAlarms\n"
+ "Notifications\nPictures\nMovies\nDownload\nDCIM\n")]
+
+ def test_listFiles(self):
+ m = MockAgent(self, commands=self.commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+
+ expected = (self.commands[2][1].strip()).split("\n")
+ self.assertEqual(expected, d.listFiles("/mnt/sdcard"))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_logcat.py b/testing/mozbase/mozdevice/tests/sut_logcat.py
new file mode 100644
index 000000000..b4c1a742d
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_logcat.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class TestLogCat(unittest.TestCase):
+ """ Class to test methods associated with logcat """
+
+ def test_getLogcat(self):
+
+ logcat_output = (
+ "07-17 00:51:10.377 I/SUTAgentAndroid( 2933): onCreate\r\n"
+ "07-17 00:51:10.457 D/dalvikvm( 2933): GC_CONCURRENT freed 351K, 17% free 2523K/3008K, paused 5ms+2ms, total 38ms\r\n" # noqa
+ "07-17 00:51:10.497 I/SUTAgentAndroid( 2933): Caught exception creating file in /data/local/tmp: open failed: EACCES (Permission denied)\r\n" # noqa
+ "07-17 00:51:10.507 E/SUTAgentAndroid( 2933): ERROR: Cannot access world writeable test root\r\n" # noqa
+ "07-17 00:51:10.547 D/GeckoHealthRec( 3253): Initializing profile cache.\r\n"
+ "07-17 00:51:10.607 D/GeckoHealthRec( 3253): Looking for /data/data/org.mozilla.fennec/files/mozilla/c09kfhne.default/times.json\r\n" # noqa
+ "07-17 00:51:10.637 D/GeckoHealthRec( 3253): Using times.json for profile creation time.\r\n" # noqa
+ "07-17 00:51:10.707 D/GeckoHealthRec( 3253): Incorporating environment: times.json profile creation = 1374026758604\r\n" # noqa
+ "07-17 00:51:10.507 D/GeckoHealthRec( 3253): Requested prefs.\r\n"
+ "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): \r\n"
+ "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): Total Private Dirty Memory 3176 kb\r\n" # noqa
+ "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): Total Proportional Set Size Memory 5679 kb\r\n" # noqa
+ "07-17 06:50:54.907 I/SUTAgentAndroid( 3876): Total Shared Dirty Memory 9216 kb\r\n" # noqa
+ "07-17 06:55:21.627 I/SUTAgentAndroid( 3876): 127.0.0.1 : execsu /system/bin/logcat -v time -d dalvikvm:I " # noqa
+ "ConnectivityService:S WifiMonitor:S WifiStateTracker:S wpa_supplicant:S NetworkStateTracker:S\r\n" # noqa
+ "07-17 06:55:21.827 I/dalvikvm-heap( 3876): Grow heap (frag case) to 3.019MB for 102496-byte allocation\r\n" # noqa
+ "return code [0]")
+
+ inp = ("execsu /system/bin/logcat -v time -d "
+ "dalvikvm:I ConnectivityService:S WifiMonitor:S "
+ "WifiStateTracker:S wpa_supplicant:S NetworkStateTracker:S")
+
+ commands = [(inp, logcat_output)]
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ self.assertEqual(logcat_output[:-17].replace('\r\n', '\n').splitlines(True), d.getLogcat())
+
+ def test_recordLogcat(self):
+
+ commands = [("execsu /system/bin/logcat -c", "return code [0]")]
+
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ # No error raised means success
+ self.assertEqual(None, d.recordLogcat())
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_mkdir.py b/testing/mozbase/mozdevice/tests/sut_mkdir.py
new file mode 100644
index 000000000..bacaae324
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_mkdir.py
@@ -0,0 +1,78 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class MkDirsTest(unittest.TestCase):
+
+ def test_mkdirs(self):
+ subTests = [{'cmds': [('isdir /mnt/sdcard/baz/boop', 'FALSE'),
+ ('info os', 'android'),
+ ('isdir /mnt', 'TRUE'),
+ ('isdir /mnt/sdcard', 'TRUE'),
+ ('isdir /mnt/sdcard/baz', 'FALSE'),
+ ('mkdr /mnt/sdcard/baz',
+ '/mnt/sdcard/baz successfully created'),
+ ('isdir /mnt/sdcard/baz/boop', 'FALSE'),
+ ('mkdr /mnt/sdcard/baz/boop',
+ '/mnt/sdcard/baz/boop successfully created')],
+ 'expectException': False},
+ {'cmds': [('isdir /mnt/sdcard/baz/boop', 'FALSE'),
+ ('info os', 'android'),
+ ('isdir /mnt', 'TRUE'),
+ ('isdir /mnt/sdcard', 'TRUE'),
+ ('isdir /mnt/sdcard/baz', 'FALSE'),
+ ('mkdr /mnt/sdcard/baz',
+ "##AGENT-WARNING## "
+ "Could not create the directory /mnt/sdcard/baz")],
+ 'expectException': True},
+ ]
+ for subTest in subTests:
+ a = MockAgent(self, commands=subTest['cmds'])
+
+ exceptionThrown = False
+ try:
+ d = mozdevice.DroidSUT('127.0.0.1', port=a.port,
+ logLevel=logging.DEBUG)
+ d.mkDirs('/mnt/sdcard/baz/boop/bip')
+ except mozdevice.DMError:
+ exceptionThrown = True
+ self.assertEqual(exceptionThrown, subTest['expectException'])
+
+ a.wait()
+
+ def test_repeated_path_part(self):
+ """
+ Ensure that all dirs are created when last path part also found
+ earlier in the path (bug 826492).
+ """
+
+ cmds = [('isdir /mnt/sdcard/foo', 'FALSE'),
+ ('info os', 'android'),
+ ('isdir /mnt', 'TRUE'),
+ ('isdir /mnt/sdcard', 'TRUE'),
+ ('isdir /mnt/sdcard/foo', 'FALSE'),
+ ('mkdr /mnt/sdcard/foo',
+ '/mnt/sdcard/foo successfully created')]
+ a = MockAgent(self, commands=cmds)
+ d = mozdevice.DroidSUT('127.0.0.1', port=a.port,
+ logLevel=logging.DEBUG)
+ d.mkDirs('/mnt/sdcard/foo/foo')
+ a.wait()
+
+ def test_mkdirs_on_root(self):
+ cmds = [('isdir /', 'TRUE')]
+ a = MockAgent(self, commands=cmds)
+ d = mozdevice.DroidSUT('127.0.0.1', port=a.port,
+ logLevel=logging.DEBUG)
+ d.mkDirs('/foo')
+
+ a.wait()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_movetree.py b/testing/mozbase/mozdevice/tests/sut_movetree.py
new file mode 100644
index 000000000..0e106577c
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_movetree.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class MoveTreeTest(unittest.TestCase):
+
+ def test_moveFile(self):
+ commands = [('mv /mnt/sdcard/tests/test.txt /mnt/sdcard/tests/test1.txt', ''),
+ ('isdir /mnt/sdcard/tests', 'TRUE'),
+ ('cd /mnt/sdcard/tests', ''),
+ ('ls', 'test1.txt'),
+ ('isdir /mnt/sdcard/tests', 'TRUE'),
+ ('cd /mnt/sdcard/tests', ''),
+ ('ls', 'test1.txt')]
+
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ self.assertEqual(None, d.moveTree('/mnt/sdcard/tests/test.txt',
+ '/mnt/sdcard/tests/test1.txt'))
+ self.assertFalse(d.fileExists('/mnt/sdcard/tests/test.txt'))
+ self.assertTrue(d.fileExists('/mnt/sdcard/tests/test1.txt'))
+
+ def test_moveDir(self):
+ commands = [("mv /mnt/sdcard/tests/foo /mnt/sdcard/tests/bar", ""),
+ ('isdir /mnt/sdcard/tests', 'TRUE'),
+ ('cd /mnt/sdcard/tests', ''),
+ ('ls', 'bar')]
+
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ self.assertEqual(None, d.moveTree('/mnt/sdcard/tests/foo',
+ '/mnt/sdcard/tests/bar'))
+ self.assertTrue(d.fileExists('/mnt/sdcard/tests/bar'))
+
+ def test_moveNonEmptyDir(self):
+ commands = [('isdir /mnt/sdcard/tests/foo/bar', 'TRUE'),
+ ('mv /mnt/sdcard/tests/foo /mnt/sdcard/tests/foo2', ''),
+ ('isdir /mnt/sdcard/tests', 'TRUE'),
+ ('cd /mnt/sdcard/tests', ''),
+ ('ls', 'foo2'),
+ ('isdir /mnt/sdcard/tests/foo2', 'TRUE'),
+ ('cd /mnt/sdcard/tests/foo2', ''),
+ ('ls', 'bar')]
+
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port,
+ logLevel=logging.DEBUG)
+
+ self.assertTrue(d.dirExists('/mnt/sdcard/tests/foo/bar'))
+ self.assertEqual(None, d.moveTree('/mnt/sdcard/tests/foo',
+ '/mnt/sdcard/tests/foo2'))
+ self.assertTrue(d.fileExists('/mnt/sdcard/tests/foo2'))
+ self.assertTrue(d.fileExists('/mnt/sdcard/tests/foo2/bar'))
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_ps.py b/testing/mozbase/mozdevice/tests/sut_ps.py
new file mode 100644
index 000000000..03f431c1d
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_ps.py
@@ -0,0 +1,50 @@
+from sut import MockAgent
+import mozdevice
+import unittest
+
+
+class PsTest(unittest.TestCase):
+
+ pscommands = [('ps',
+ "10029 549 com.android.launcher\n"
+ "10066 1198 com.twitter.android")]
+
+ bad_pscommands = [('ps',
+ "abcdef 549 com.android.launcher\n"
+ "10066 1198 com.twitter.android")]
+
+ def test_processList(self):
+ a = MockAgent(self,
+ commands=self.pscommands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+ pslist = d.getProcessList()
+ self.assertEqual(len(pslist), 2)
+ self.assertEqual(pslist[0], [549, 'com.android.launcher', 10029])
+ self.assertEqual(pslist[1], [1198, 'com.twitter.android', 10066])
+
+ a.wait()
+
+ def test_badProcessList(self):
+ a = MockAgent(self,
+ commands=self.bad_pscommands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+ exceptionTriggered = False
+ try:
+ d.getProcessList()
+ except mozdevice.DMError:
+ exceptionTriggered = True
+
+ self.assertTrue(exceptionTriggered)
+
+ a.wait()
+
+ def test_processExist(self):
+ for i in [('com.android.launcher', 549),
+ ('com.fennec.android', None)]:
+ a = MockAgent(self, commands=self.pscommands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+ self.assertEqual(d.processExist(i[0]), i[1])
+ a.wait()
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_pull.py b/testing/mozbase/mozdevice/tests/sut_pull.py
new file mode 100644
index 000000000..c9fcae42a
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_pull.py
@@ -0,0 +1,47 @@
+from sut import MockAgent
+import mozdevice
+import logging
+import unittest
+
+
+class PullTest(unittest.TestCase):
+
+ def test_pull_success(self):
+ for count in [1, 4, 1024, 2048]:
+ cheeseburgers = ""
+ for i in range(count):
+ cheeseburgers += "cheeseburgers"
+
+ # pull file is kind of gross, make sure we can still execute commands after it's done
+ remoteName = "/mnt/sdcard/cheeseburgers"
+ a = MockAgent(self, commands=[("pull %s" % remoteName,
+ "%s,%s\n%s" % (remoteName,
+ len(cheeseburgers),
+ cheeseburgers)),
+ ("isdir /mnt/sdcard", "TRUE")])
+
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port,
+ logLevel=logging.DEBUG)
+ pulledData = d.pullFile("/mnt/sdcard/cheeseburgers")
+ self.assertEqual(pulledData, cheeseburgers)
+ d.dirExists('/mnt/sdcard')
+
+ def test_pull_failure(self):
+
+ # this test simulates only receiving a few bytes of what we expect
+ # to be larger file
+ remoteName = "/mnt/sdcard/cheeseburgers"
+ a = MockAgent(self, commands=[("pull %s" % remoteName,
+ "%s,15\n%s" % (remoteName,
+ "cheeseburgh"))])
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port,
+ logLevel=logging.DEBUG)
+ exceptionThrown = False
+ try:
+ d.pullFile("/mnt/sdcard/cheeseburgers")
+ except mozdevice.DMError:
+ exceptionThrown = True
+ self.assertTrue(exceptionThrown)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_push.py b/testing/mozbase/mozdevice/tests/sut_push.py
new file mode 100644
index 000000000..023d5315c
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_push.py
@@ -0,0 +1,88 @@
+from sut import MockAgent
+import mozfile
+import mozdevice
+import logging
+import unittest
+import hashlib
+import tempfile
+import os
+
+
+class PushTest(unittest.TestCase):
+
+ def test_push(self):
+ pushfile = "1234ABCD"
+ mdsum = hashlib.md5()
+ mdsum.update(pushfile)
+ expectedResponse = mdsum.hexdigest()
+
+ # (good response, no exception), (bad response, exception)
+ for response in [(expectedResponse, False), ("BADHASH", True)]:
+ cmd = "push /mnt/sdcard/foobar %s\r\n%s" % (len(pushfile), pushfile)
+ a = MockAgent(self, commands=[("isdir /mnt/sdcard", "TRUE"),
+ (cmd, response[0])])
+ exceptionThrown = False
+ with tempfile.NamedTemporaryFile() as f:
+ try:
+ f.write(pushfile)
+ f.flush()
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
+ d.pushFile(f.name, '/mnt/sdcard/foobar')
+ except mozdevice.DMError:
+ exceptionThrown = True
+ self.assertEqual(exceptionThrown, response[1])
+ a.wait()
+
+ def test_push_dir(self):
+ pushfile = "1234ABCD"
+ mdsum = hashlib.md5()
+ mdsum.update(pushfile)
+ expectedFileResponse = mdsum.hexdigest()
+
+ tempdir = tempfile.mkdtemp()
+ self.addCleanup(mozfile.remove, tempdir)
+ complex_path = os.path.join(tempdir, "baz")
+ os.mkdir(complex_path)
+ f = tempfile.NamedTemporaryFile(dir=complex_path)
+ f.write(pushfile)
+ f.flush()
+
+ subTests = [{'cmds': [("isdir /mnt/sdcard/baz", "TRUE"),
+ ("push /mnt/sdcard/baz/%s %s\r\n%s" %
+ (os.path.basename(f.name), len(pushfile),
+ pushfile),
+ expectedFileResponse)],
+ 'expectException': False},
+ {'cmds': [("isdir /mnt/sdcard/baz", "TRUE"),
+ ("push /mnt/sdcard/baz/%s %s\r\n%s" %
+ (os.path.basename(f.name), len(pushfile),
+ pushfile),
+ "BADHASH")],
+ 'expectException': True},
+ {'cmds': [("isdir /mnt/sdcard/baz", "FALSE"),
+ ('info os', 'android'),
+ ("isdir /mnt", "FALSE"),
+ ("mkdr /mnt",
+ "##AGENT-WARNING## Could not create the directory /mnt")],
+ 'expectException': True},
+
+ ]
+
+ for subTest in subTests:
+ a = MockAgent(self, commands=subTest['cmds'])
+
+ exceptionThrown = False
+ try:
+ d = mozdevice.DroidSUT("127.0.0.1", port=a.port,
+ logLevel=logging.DEBUG)
+ d.pushDir(tempdir, "/mnt/sdcard")
+ except mozdevice.DMError:
+ exceptionThrown = True
+ self.assertEqual(exceptionThrown, subTest['expectException'])
+
+ a.wait()
+
+ # FIXME: delete directory when done
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_remove.py b/testing/mozbase/mozdevice/tests/sut_remove.py
new file mode 100644
index 000000000..636190186
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_remove.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class TestRemove(unittest.TestCase):
+
+ def test_removeDir(self):
+ commands = [("isdir /mnt/sdcard/test", "TRUE"),
+ ("rmdr /mnt/sdcard/test", "Deleting file(s) from "
+ "/storage/emulated/legacy/Moztest\n"
+ " <empty>\n"
+ "Deleting directory "
+ "/storage/emulated/legacy/Moztest\n")]
+
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ # No error implies we're all good
+ self.assertEqual(None, d.removeDir("/mnt/sdcard/test"))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_time.py b/testing/mozbase/mozdevice/tests/sut_time.py
new file mode 100644
index 000000000..11dc421cb
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_time.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class TestGetCurrentTime(unittest.TestCase):
+
+ def test_getCurrentTime(self):
+ command = [('clok', '1349980200')]
+
+ m = MockAgent(self, commands=command)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ self.assertEqual(d.getCurrentTime(), int(command[0][1]))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/testing/mozbase/mozdevice/tests/sut_unpackfile.py b/testing/mozbase/mozdevice/tests/sut_unpackfile.py
new file mode 100644
index 000000000..1a531fe17
--- /dev/null
+++ b/testing/mozbase/mozdevice/tests/sut_unpackfile.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+
+import mozdevice
+import logging
+import unittest
+from sut import MockAgent
+
+
+class TestUnpack(unittest.TestCase):
+
+ def test_unpackFile(self):
+
+ commands = [("unzp /data/test/sample.zip /data/test/",
+ "Checksum: 653400271\n"
+ "1 of 1 successfully extracted\n")]
+ m = MockAgent(self, commands=commands)
+ d = mozdevice.DroidSUT("127.0.0.1", port=m.port, logLevel=logging.DEBUG)
+ # No error being thrown imples all is well
+ self.assertEqual(None, d.unpackFile("/data/test/sample.zip",
+ "/data/test/"))
+
+if __name__ == '__main__':
+ unittest.main()