зеркало из https://github.com/mozilla/gecko-dev.git
Back out 27fb990d7fc7 (bug 838374) for Android bustage
CLOSED TREE
This commit is contained in:
Родитель
370ddb03b5
Коммит
91489a9327
|
@ -4,19 +4,44 @@
|
|||
|
||||
__all__ = ['check_for_crashes']
|
||||
|
||||
import glob
|
||||
import os, sys, glob, urllib2, tempfile, re, subprocess, shutil, urlparse, zipfile
|
||||
import mozlog
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib2
|
||||
import zipfile
|
||||
|
||||
from mozfile import extract_zip
|
||||
from mozfile import is_url
|
||||
def is_url(thing):
|
||||
"""
|
||||
Return True if thing looks like a URL.
|
||||
"""
|
||||
# We want to download URLs like http://... but not Windows paths like c:\...
|
||||
parsed = urlparse.urlparse(thing)
|
||||
if 'scheme' in parsed:
|
||||
return len(parsed.scheme) >= 2
|
||||
else:
|
||||
return len(parsed[0]) >= 2
|
||||
|
||||
def extractall(zip, path = None):
|
||||
"""
|
||||
Compatibility shim for Python 2.6's ZipFile.extractall.
|
||||
"""
|
||||
if hasattr(zip, "extractall"):
|
||||
return zip.extractall(path)
|
||||
|
||||
if path is None:
|
||||
path = os.curdir
|
||||
|
||||
for name in self._zipfile.namelist():
|
||||
filename = os.path.normpath(os.path.join(path, name))
|
||||
if name.endswith("/"):
|
||||
os.makedirs(filename)
|
||||
else:
|
||||
path = os.path.split(filename)[0]
|
||||
if not os.path.isdir(path):
|
||||
os.makedirs(path)
|
||||
|
||||
try:
|
||||
f = open(filename, "wb")
|
||||
f.write(zip.read(name))
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def check_for_crashes(dump_directory, symbols_path,
|
||||
stackwalk_binary=None,
|
||||
|
@ -36,20 +61,17 @@ def check_for_crashes(dump_directory, symbols_path,
|
|||
`symbols_path` should be a path to a directory containing symbols to use for
|
||||
dump processing. This can either be a path to a directory containing Breakpad-format
|
||||
symbols, or a URL to a zip file containing a set of symbols.
|
||||
|
||||
|
||||
If `dump_save_path` is set, it should be a path to a directory in which to copy minidump
|
||||
files for safekeeping after a stack trace has been printed. If not set, the environment
|
||||
variable MINIDUMP_SAVE_PATH will be checked and its value used if it is not empty.
|
||||
|
||||
|
||||
If `test_name` is set it will be used as the test name in log output. If not set the
|
||||
filename of the calling function will be used.
|
||||
|
||||
Returns True if any minidumps were found, False otherwise.
|
||||
"""
|
||||
dumps = glob.glob(os.path.join(dump_directory, '*.dmp'))
|
||||
if not dumps:
|
||||
return False
|
||||
|
||||
log = mozlog.getLogger('mozcrash')
|
||||
if stackwalk_binary is None:
|
||||
stackwalk_binary = os.environ.get('MINIDUMP_STACKWALK', None)
|
||||
|
||||
|
@ -60,25 +82,28 @@ def check_for_crashes(dump_directory, symbols_path,
|
|||
except:
|
||||
test_name = "unknown"
|
||||
|
||||
try:
|
||||
log = mozlog.getLogger('mozcrash')
|
||||
remove_symbols = False
|
||||
# If our symbols are at a remote URL, download them now
|
||||
# We want to download URLs like http://... but not Windows paths like c:\...
|
||||
if symbols_path and is_url(symbols_path):
|
||||
log.info("Downloading symbols from: %s", symbols_path)
|
||||
remove_symbols = True
|
||||
# Get the symbols and write them to a temporary zipfile
|
||||
data = urllib2.urlopen(symbols_path)
|
||||
symbols_file = tempfile.TemporaryFile()
|
||||
symbols_file.write(data.read())
|
||||
# extract symbols to a temporary directory (which we'll delete after
|
||||
# processing all crashes)
|
||||
symbols_path = tempfile.mkdtemp()
|
||||
zfile = zipfile.ZipFile(symbols_file, 'r')
|
||||
extract_zip(zfile, symbols_path)
|
||||
zfile.close()
|
||||
# Check preconditions
|
||||
dumps = glob.glob(os.path.join(dump_directory, '*.dmp'))
|
||||
if len(dumps) == 0:
|
||||
return False
|
||||
|
||||
remove_symbols = False
|
||||
# If our symbols are at a remote URL, download them now
|
||||
if symbols_path and is_url(symbols_path):
|
||||
log.info("Downloading symbols from: %s", symbols_path)
|
||||
remove_symbols = True
|
||||
# Get the symbols and write them to a temporary zipfile
|
||||
data = urllib2.urlopen(symbols_path)
|
||||
symbols_file = tempfile.TemporaryFile()
|
||||
symbols_file.write(data.read())
|
||||
# extract symbols to a temporary directory (which we'll delete after
|
||||
# processing all crashes)
|
||||
symbols_path = tempfile.mkdtemp()
|
||||
zfile = zipfile.ZipFile(symbols_file, 'r')
|
||||
extractall(zfile, symbols_path)
|
||||
zfile.close()
|
||||
|
||||
try:
|
||||
for d in dumps:
|
||||
stackwalk_output = []
|
||||
stackwalk_output.append("Crash dump filename: " + d)
|
||||
|
@ -120,7 +145,7 @@ def check_for_crashes(dump_directory, symbols_path,
|
|||
stackwalk_output.append("MINIDUMP_STACKWALK binary not found: %s" % stackwalk_binary)
|
||||
if not top_frame:
|
||||
top_frame = "Unknown top frame"
|
||||
print "PROCESS-CRASH | %s | application crashed [%s]" % (test_name, top_frame)
|
||||
log.error("PROCESS-CRASH | %s | application crashed [%s]", test_name, top_frame)
|
||||
print '\n'.join(stackwalk_output)
|
||||
if dump_save_path is None:
|
||||
dump_save_path = os.environ.get('MINIDUMP_SAVE_PATH', None)
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
|
||||
from setuptools import setup
|
||||
|
||||
PACKAGE_VERSION = '0.5'
|
||||
PACKAGE_VERSION = '0.3'
|
||||
|
||||
# dependencies
|
||||
deps = ['mozfile >= 0.3',
|
||||
'mozlog']
|
||||
deps = []
|
||||
|
||||
setup(name='mozcrash',
|
||||
version=PACKAGE_VERSION,
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
[mozdevice](https://github.com/mozilla/mozbase/tree/master/mozdevice) provides
|
||||
an interface to interact with a remote device such as an Android phone connected
|
||||
to a workstation. Currently there are two implementations of the interface: one
|
||||
uses a TCP-based protocol to communicate with a server running on the device,
|
||||
another uses Android's adb utility.
|
|
@ -2,7 +2,9 @@
|
|||
# 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 devicemanager import DeviceManager, DMError
|
||||
from devicemanager import DMError
|
||||
from devicemanagerADB import DeviceManagerADB
|
||||
from devicemanagerSUT import DeviceManagerSUT
|
||||
from droid import DroidADB, DroidSUT, DroidConnectByHWID
|
||||
from emulator import Emulator
|
||||
from b2gemulator import B2GEmulator
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
# 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 platform
|
||||
|
||||
from emulator import Emulator
|
||||
|
||||
|
||||
class B2GEmulator(Emulator):
|
||||
|
||||
def __init__(self, homedir=None, noWindow=False, logcat_dir=None, arch="x86",
|
||||
emulatorBinary=None, res='480x800', userdata=None,
|
||||
memory='512', partition_size='512'):
|
||||
super(B2GEmulator, self).__init__(noWindow=noWindow, logcat_dir=logcat_dir,
|
||||
arch=arch, emulatorBinary=emulatorBinary,
|
||||
res=res, userdata=userdata,
|
||||
memory=memory, partition_size=partition_size)
|
||||
self.homedir = homedir
|
||||
if self.homedir is not None:
|
||||
self.homedir = os.path.expanduser(homedir)
|
||||
|
||||
def _check_file(self, filePath):
|
||||
if not os.path.exists(filePath):
|
||||
raise Exception(('File not found: %s; did you pass the B2G home '
|
||||
'directory as the homedir parameter, or set '
|
||||
'B2G_HOME correctly?') % filePath)
|
||||
|
||||
def _check_for_adb(self, host_dir):
|
||||
if self._default_adb() == 0:
|
||||
return
|
||||
adb_paths = [os.path.join(self.homedir,'glue','gonk','out','host',
|
||||
host_dir ,'bin','adb'),os.path.join(self.homedir, 'out',
|
||||
'host', host_dir,'bin','adb'),os.path.join(self.homedir,
|
||||
'bin','adb')]
|
||||
for option in adb_paths:
|
||||
if os.path.exists(option):
|
||||
self.adb = option
|
||||
return
|
||||
raise Exception('adb not found!')
|
||||
|
||||
def _locate_files(self):
|
||||
if self.homedir is None:
|
||||
self.homedir = os.getenv('B2G_HOME')
|
||||
if self.homedir is None:
|
||||
raise Exception('Must define B2G_HOME or pass the homedir parameter')
|
||||
self._check_file(self.homedir)
|
||||
|
||||
if self.arch not in ("x86", "arm"):
|
||||
raise Exception("Emulator architecture must be one of x86, arm, got: %s" %
|
||||
self.arch)
|
||||
|
||||
host_dir = "linux-x86"
|
||||
if platform.system() == "Darwin":
|
||||
host_dir = "darwin-x86"
|
||||
|
||||
host_bin_dir = os.path.join("out", "host", host_dir, "bin")
|
||||
|
||||
if self.arch == "x86":
|
||||
binary = os.path.join(host_bin_dir, "emulator-x86")
|
||||
kernel = "prebuilts/qemu-kernel/x86/kernel-qemu"
|
||||
sysdir = "out/target/product/generic_x86"
|
||||
self.tail_args = []
|
||||
else:
|
||||
binary = os.path.join(host_bin_dir, "emulator")
|
||||
kernel = "prebuilts/qemu-kernel/arm/kernel-qemu-armv7"
|
||||
sysdir = "out/target/product/generic"
|
||||
self.tail_args = ["-cpu", "cortex-a8"]
|
||||
|
||||
self._check_for_adb(host_dir)
|
||||
|
||||
if not self.binary:
|
||||
self.binary = os.path.join(self.homedir, binary)
|
||||
|
||||
self._check_file(self.binary)
|
||||
|
||||
self.kernelImg = os.path.join(self.homedir, kernel)
|
||||
self._check_file(self.kernelImg)
|
||||
|
||||
self.sysDir = os.path.join(self.homedir, sysdir)
|
||||
self._check_file(self.sysDir)
|
||||
|
||||
if not self.dataImg:
|
||||
self.dataImg = os.path.join(self.sysDir, 'userdata.img')
|
||||
self._check_file(self.dataImg)
|
|
@ -11,7 +11,6 @@ import StringIO
|
|||
import zlib
|
||||
|
||||
from Zeroconf import Zeroconf, ServiceBrowser
|
||||
from functools import wraps
|
||||
|
||||
class DMError(Exception):
|
||||
"generic devicemanager exception."
|
||||
|
@ -26,57 +25,348 @@ class DMError(Exception):
|
|||
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.
|
||||
"""
|
||||
class DeviceManager:
|
||||
|
||||
_logcatNeedsRoot = True
|
||||
|
||||
@abstractmethod
|
||||
def getInfo(self, directive=None):
|
||||
def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
|
||||
"""
|
||||
Returns a dictionary of information strings about the device.
|
||||
Executes shell command on device and returns exit code
|
||||
|
||||
:param directive: information you want to get. Options are:
|
||||
cmd - Command string to execute
|
||||
outputfile - File to store output
|
||||
env - Environment to pass to exec command
|
||||
cwd - Directory to execute command from
|
||||
timeout - specified in seconds, defaults to 'default_timeout'
|
||||
root - Specifies whether command requires root privileges
|
||||
"""
|
||||
|
||||
- `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
|
||||
- `process` - list of running processes (same as ps)
|
||||
- `disk` - total, free, available bytes on disk
|
||||
- `power` - power status (charge, battery temp)
|
||||
- `temperature` - device temperature
|
||||
def shellCheckOutput(self, cmd, env=None, cwd=None, timeout=None, root=False):
|
||||
"""
|
||||
executes shell command on device and returns the the output
|
||||
|
||||
If `directive` is `None`, will return all available information
|
||||
env - Environment to pass to exec command
|
||||
cwd - Directory to execute command from
|
||||
timeout - specified in seconds, defaults to 'default_timeout'
|
||||
root - Specifies whether command requires root privileges
|
||||
"""
|
||||
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 pushFile(self, localname, destname, retryLimit=1):
|
||||
"""
|
||||
Copies localname from the host to destname on the device
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def getCurrentTime(self):
|
||||
def mkDir(self, name):
|
||||
"""
|
||||
Returns device time in milliseconds since the epoch.
|
||||
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
|
||||
"""
|
||||
dirParts = filename.rsplit('/', 1)
|
||||
if not self.dirExists(dirParts[0]):
|
||||
parts = filename.split('/')
|
||||
name = ""
|
||||
for part in parts:
|
||||
if part == parts[-1]:
|
||||
break
|
||||
if part != "":
|
||||
name += '/' + part
|
||||
self.mkDir(name) # mkDir will check previous existence
|
||||
|
||||
@abstractmethod
|
||||
def pushDir(self, localDir, remoteDir, retryLimit=1):
|
||||
"""
|
||||
Push localDir from host to remoteDir on the device
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def fileExists(self, filepath):
|
||||
"""
|
||||
Checks if filepath exists and is a file on the device file system
|
||||
|
||||
returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def listFiles(self, rootdir):
|
||||
"""
|
||||
Lists files on the device rootdir
|
||||
|
||||
returns:
|
||||
success: array of filenames, ['file1', 'file2', ...]
|
||||
failure: None
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def removeFile(self, filename):
|
||||
"""
|
||||
Removes filename from the device
|
||||
|
||||
returns:
|
||||
success: output of telnet
|
||||
failure: None
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def removeDir(self, remoteDir):
|
||||
"""
|
||||
Does a recursive delete of directory on the device: rm -Rf remoteDir
|
||||
|
||||
returns:
|
||||
success: output of telnet
|
||||
failure: None
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def getProcessList(self):
|
||||
"""
|
||||
Lists the running processes on the device
|
||||
|
||||
returns:
|
||||
success: array of process tuples
|
||||
failure: []
|
||||
"""
|
||||
|
||||
def processExist(self, appname):
|
||||
"""
|
||||
Iterates process list and checks if pid exists
|
||||
|
||||
returns:
|
||||
success: pid
|
||||
failure: None
|
||||
"""
|
||||
if not isinstance(appname, basestring):
|
||||
raise TypeError("appname %s is not a string" % appname)
|
||||
|
||||
pid = None
|
||||
|
||||
#filter out extra spaces
|
||||
parts = filter(lambda x: x != '', appname.split(' '))
|
||||
appname = ' '.join(parts)
|
||||
|
||||
#filter out the quoted env string if it exists
|
||||
#ex: '"name=value;name2=value2;etc=..." process args' -> 'process args'
|
||||
parts = appname.split('"')
|
||||
if (len(parts) > 2):
|
||||
appname = ' '.join(parts[2:]).strip()
|
||||
|
||||
pieces = appname.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):
|
||||
pid = proc[0]
|
||||
break
|
||||
return pid
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def killProcess(self, appname, forceKill=False):
|
||||
"""
|
||||
Kills the process named appname.
|
||||
If forceKill is True, process is killed regardless of state
|
||||
|
||||
returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def catFile(self, remoteFile):
|
||||
"""
|
||||
Returns the contents of remoteFile
|
||||
|
||||
returns:
|
||||
success: filecontents, string
|
||||
failure: None
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def pullFile(self, remoteFile):
|
||||
"""
|
||||
Returns contents of remoteFile using the "pull" command.
|
||||
|
||||
returns:
|
||||
success: output of pullfile, string
|
||||
failure: None
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def getFile(self, remoteFile, localFile):
|
||||
"""
|
||||
Copy file from device (remoteFile) to host (localFile)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def getDirectory(self, remoteDir, localDir, checkDir=True):
|
||||
"""
|
||||
Copy directory structure from device (remoteDir) to host (localDir)
|
||||
|
||||
returns:
|
||||
success: list of files, string
|
||||
failure: None
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def validateFile(self, remoteFile, localFile):
|
||||
"""
|
||||
Checks if the remoteFile has the same md5 hash as the localFile
|
||||
|
||||
returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def _getRemoteHash(self, filename):
|
||||
"""
|
||||
Return the md5 sum of a file on the device
|
||||
|
||||
returns:
|
||||
success: MD5 hash for given filename
|
||||
failure: None
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _getLocalHash(filename):
|
||||
"""
|
||||
Return the MD5 sum of a file on the host
|
||||
|
||||
returns:
|
||||
success: MD5 hash for given filename
|
||||
failure: None
|
||||
"""
|
||||
|
||||
f = open(filename, 'rb')
|
||||
if (f == 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
|
||||
|
||||
@abstractmethod
|
||||
def getDeviceRoot(self):
|
||||
"""
|
||||
Gets the device root for the testing area on the device
|
||||
For all devices we will use / type slashes and depend on the device-agent
|
||||
to sort those out. The agent will return us the device location where we
|
||||
should store things, we will then create our /tests structure relative to
|
||||
that returned path.
|
||||
Structure on the device is as follows:
|
||||
/tests
|
||||
/<fennec>|<firefox> --> approot
|
||||
/profile
|
||||
/xpcshell
|
||||
/reftest
|
||||
/mochitest
|
||||
|
||||
returns:
|
||||
success: path for device root
|
||||
failure: None
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def getAppRoot(self, packageName=None):
|
||||
"""
|
||||
Returns the app root directory
|
||||
E.g /tests/fennec or /tests/firefox
|
||||
|
||||
returns:
|
||||
success: path for app root
|
||||
failure: None
|
||||
"""
|
||||
# TODO Support org.mozilla.firefox and B2G
|
||||
|
||||
def getTestRoot(self, harness):
|
||||
"""
|
||||
Gets the directory location on the device for a specific test type
|
||||
Harness is one of: xpcshell|reftest|mochitest
|
||||
|
||||
returns:
|
||||
success: path for test root
|
||||
failure: None
|
||||
"""
|
||||
|
||||
devroot = self.getDeviceRoot()
|
||||
if (devroot == None):
|
||||
return None
|
||||
|
||||
if (re.search('xpcshell', harness, re.I)):
|
||||
self.testRoot = devroot + '/xpcshell'
|
||||
elif (re.search('?(i)reftest', harness)):
|
||||
self.testRoot = devroot + '/reftest'
|
||||
elif (re.search('?(i)mochitest', harness)):
|
||||
self.testRoot = devroot + '/mochitest'
|
||||
return self.testRoot
|
||||
|
||||
@abstractmethod
|
||||
def getTempDir(self):
|
||||
"""
|
||||
Gets the temporary directory we are using on this device
|
||||
base on our device root, ensuring also that it exists.
|
||||
|
||||
returns:
|
||||
success: path for temporary directory
|
||||
failure: None
|
||||
"""
|
||||
|
||||
def signal(self, processID, signalType, signalAction):
|
||||
"""
|
||||
Sends a specific process ID a signal code and action.
|
||||
For Example: SIGINT and SIGDFL to process x
|
||||
"""
|
||||
#currently not implemented in device agent - todo
|
||||
pass
|
||||
|
||||
def getReturnCode(self, processID):
|
||||
"""Get a return code from process ending -- needs support on device-agent"""
|
||||
# TODO: make this real
|
||||
|
||||
return 0
|
||||
|
||||
def getIP(self, interfaces=['eth0', 'wlan0']):
|
||||
"""
|
||||
Returns the IP of the device, or None if no connection exists.
|
||||
Gets the IP of the device, or None if no connection exists.
|
||||
"""
|
||||
for interface in interfaces:
|
||||
match = re.match(r"%s: ip (\S+)" % interface,
|
||||
|
@ -84,16 +374,128 @@ class DeviceManager(object):
|
|||
if match:
|
||||
return match.group(1)
|
||||
|
||||
@abstractmethod
|
||||
def unpackFile(self, file_path, dest_dir=None):
|
||||
"""
|
||||
Unzips a remote bundle to a remote location
|
||||
If dest_dir is not specified, the bundle is extracted
|
||||
in the same directory
|
||||
|
||||
returns:
|
||||
success: output of unzip command
|
||||
failure: None
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def reboot(self, ipAddr=None, port=30000):
|
||||
"""
|
||||
Reboots the device
|
||||
|
||||
returns:
|
||||
success: status from test agent
|
||||
failure: None
|
||||
"""
|
||||
|
||||
def validateDir(self, localDir, remoteDir):
|
||||
"""
|
||||
Validate localDir from host to remoteDir on the device
|
||||
|
||||
returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
|
||||
if (self.debug >= 2):
|
||||
print "validating directory: " + localDir + " to " + remoteDir
|
||||
for root, dirs, files in os.walk(localDir):
|
||||
parts = root.split(localDir)
|
||||
for f in files:
|
||||
remoteRoot = remoteDir + '/' + parts[1]
|
||||
remoteRoot = remoteRoot.replace('/', '/')
|
||||
if (parts[1] == ""):
|
||||
remoteRoot = remoteDir
|
||||
remoteName = remoteRoot + '/' + f
|
||||
if (self.validateFile(remoteName, os.path.join(root, f)) <> True):
|
||||
return False
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def getInfo(self, directive=None):
|
||||
"""
|
||||
Returns information about the device:
|
||||
Directive indicates the information you want to get, your choices 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
|
||||
process - list of running processes (same as ps)
|
||||
disk - total, free, available bytes on disk
|
||||
power - power status (charge, battery temp)
|
||||
all - all of them - or call it with no parameters to get all the information
|
||||
|
||||
returns: dict of info strings by directive name
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def installApp(self, appBundlePath, destPath=None):
|
||||
"""
|
||||
Installs an application onto the device
|
||||
appBundlePath - path to the application bundle on the device
|
||||
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
|
||||
appName - the name of the application (e.g org.mozilla.fennec)
|
||||
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
|
||||
appName - the name of the application (e.g org.mozilla.fennec)
|
||||
installPath - the path to where the application was installed (optional)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000):
|
||||
"""
|
||||
Updates the application on the device.
|
||||
appBundlePath - path to the application bundle on the device
|
||||
processName - used to end the process if the applicaiton is currently running (optional)
|
||||
destPath - Destination directory to where the application should be installed (optional)
|
||||
ipAddr - IP address to await a callback ping to let us know that the device has updated
|
||||
properly - defaults to current IP.
|
||||
port - port to await a callback ping to let us know that the device has updated properly
|
||||
defaults to 30000, and counts up from there if it finds a conflict
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def getCurrentTime(self):
|
||||
"""
|
||||
Returns device time in milliseconds since the epoch
|
||||
|
||||
returns:
|
||||
success: time in ms
|
||||
failure: None
|
||||
"""
|
||||
|
||||
def recordLogcat(self):
|
||||
"""
|
||||
Clears the logcat file making it easier to view specific events.
|
||||
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)
|
||||
|
||||
def getLogcat(self, filterSpecs=["dalvikvm:I", "ConnectivityService:S",
|
||||
def getLogcat(self, filterSpecs=["dalvikvm:S", "ConnectivityService:S",
|
||||
"WifiMonitor:S", "WifiStateTracker:S",
|
||||
"wpa_supplicant:S", "NetworkStateTracker:S"],
|
||||
format="time",
|
||||
|
@ -110,6 +512,23 @@ class DeviceManager(object):
|
|||
|
||||
return lines
|
||||
|
||||
@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'')])
|
||||
|
||||
def saveScreenshot(self, filename):
|
||||
"""
|
||||
Takes a screenshot of what's being display on the device. Uses
|
||||
|
@ -138,366 +557,18 @@ class DeviceManager(object):
|
|||
self.removeFile(tempScreenshotFile)
|
||||
|
||||
@abstractmethod
|
||||
def pushFile(self, localFilename, remoteFilename, retryLimit=1):
|
||||
"""
|
||||
Copies localname from the host to destname on the device.
|
||||
def chmodDir(self, remoteDir, mask="777"):
|
||||
"""
|
||||
Recursively changes file permissions in a directory
|
||||
|
||||
@abstractmethod
|
||||
def pushDir(self, localDirname, remoteDirname, retryLimit=1):
|
||||
returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
Push local directory from host to remote directory on the device,
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def pullFile(self, remoteFilename):
|
||||
"""
|
||||
Returns contents of remoteFile using the "pull" command.
|
||||
"""
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
||||
if (self.debug >= 2):
|
||||
print "validating directory: " + localDirname + " to " + 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)) <> 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`
|
||||
"""
|
||||
dirParts = filename.rsplit('/', 1)
|
||||
if not self.dirExists(dirParts[0]):
|
||||
parts = filename.split('/')
|
||||
name = ""
|
||||
for part in parts:
|
||||
if part is parts[-1]:
|
||||
break
|
||||
if part != "":
|
||||
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 and is a file on the device file system.
|
||||
"""
|
||||
|
||||
@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 chmodDir(self, remoteDirname, mask="777"):
|
||||
"""
|
||||
Recursively changes file permissions in a directory.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def getDeviceRoot(self):
|
||||
"""
|
||||
Gets the device root for the testing area on the device.
|
||||
|
||||
For all devices we will use / type slashes and depend on the device-agent
|
||||
to sort those out. The agent will return us the device location where we
|
||||
should store things, we will then create our /tests structure relative to
|
||||
that returned path.
|
||||
|
||||
Structure on the device is as follows:
|
||||
|
||||
::
|
||||
|
||||
/tests
|
||||
/<fennec>|<firefox> --> approot
|
||||
/profile
|
||||
/xpcshell
|
||||
/reftest
|
||||
/mochitest
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def getAppRoot(self, packageName=None):
|
||||
"""
|
||||
Returns the app root directory.
|
||||
|
||||
E.g /tests/fennec or /tests/firefox
|
||||
"""
|
||||
# TODO Support org.mozilla.firefox and B2G
|
||||
|
||||
def getTestRoot(self, harnessName):
|
||||
"""
|
||||
Gets the directory location on the device for a specific test type.
|
||||
|
||||
:param harnessName: one of: "xpcshell", "reftest", "mochitest"
|
||||
"""
|
||||
|
||||
devroot = self.getDeviceRoot()
|
||||
if (devroot == None):
|
||||
return None
|
||||
|
||||
if (re.search('xpcshell', harnessName, re.I)):
|
||||
self.testRoot = devroot + '/xpcshell'
|
||||
elif (re.search('?(i)reftest', harnessName)):
|
||||
self.testRoot = devroot + '/reftest'
|
||||
elif (re.search('?(i)mochitest', harnessName)):
|
||||
self.testRoot = devroot + '/mochitest'
|
||||
return self.testRoot
|
||||
|
||||
@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: Command string 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.
|
||||
|
||||
: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
|
||||
"""
|
||||
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 processExist(self, processName):
|
||||
"""
|
||||
Returns True if process with name processName is running on device.
|
||||
"""
|
||||
if not isinstance(processName, basestring):
|
||||
raise TypeError("Process name %s is not a string" % processName)
|
||||
|
||||
pid = 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):
|
||||
pid = proc[0]
|
||||
break
|
||||
return pid
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def killProcess(self, processName, forceKill=False):
|
||||
"""
|
||||
Kills the process named processName. If forceKill is True, process is
|
||||
killed regardless of state.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def reboot(self, ipAddr=None, port=30000):
|
||||
"""
|
||||
Reboots the device.
|
||||
|
||||
Some implementations may optionally support waiting for a TCP callback from
|
||||
the device once it has restarted before returning, but this is not
|
||||
guaranteed.
|
||||
"""
|
||||
|
||||
@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, ipAddr=None, port=30000):
|
||||
"""
|
||||
Updates the application on the device.
|
||||
|
||||
: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 ipAddr: IP address to await a callback ping to let us know that
|
||||
the device has updated properly (defaults to current
|
||||
IP)
|
||||
:param port: port to await a callback ping to let us know that the
|
||||
device has updated properly defaults to 30000, and counts
|
||||
up from there if it finds a conflict
|
||||
"""
|
||||
|
||||
@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 == 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.
|
||||
"""
|
||||
""" Utility function to return escaped and quoted version of command line """
|
||||
quotedCmd = []
|
||||
|
||||
for arg in cmd:
|
||||
|
@ -567,7 +638,7 @@ class NetworkTools:
|
|||
except:
|
||||
if seed > maxportnum:
|
||||
print "Automation Error: Could not find open port after checking 5000 ports"
|
||||
raise
|
||||
raise
|
||||
seed += 1
|
||||
except:
|
||||
print "Automation Error: Socket error trying to find open port"
|
||||
|
|
|
@ -11,16 +11,11 @@ import tempfile
|
|||
import time
|
||||
|
||||
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 = False
|
||||
_haveSu = False
|
||||
_useRunAs = False
|
||||
_useDDCopy = False
|
||||
_useZip = False
|
||||
_logcatNeedsRoot = False
|
||||
_pollingInterval = 0.01
|
||||
|
@ -28,7 +23,7 @@ class DeviceManagerADB(DeviceManager):
|
|||
_tempDir = None
|
||||
default_timeout = 300
|
||||
|
||||
def __init__(self, host=None, port=5555, retryLimit=5, packageName='fennec',
|
||||
def __init__(self, host=None, port=20701, retryLimit=5, packageName='fennec',
|
||||
adbPath='adb', deviceSerial=None, deviceRoot=None, **kwargs):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
@ -87,6 +82,16 @@ class DeviceManagerADB(DeviceManager):
|
|||
self._disconnectRemoteADB()
|
||||
|
||||
def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
|
||||
"""
|
||||
Executes shell command on device. Returns exit code.
|
||||
|
||||
cmd - Command string to execute
|
||||
outputfile - File to store output
|
||||
env - Environment to pass to exec command
|
||||
cwd - Directory to execute command from
|
||||
timeout - specified in seconds, defaults to 'default_timeout'
|
||||
root - Specifies whether command requires root privileges
|
||||
"""
|
||||
# FIXME: this function buffers all output of the command into memory,
|
||||
# always. :(
|
||||
|
||||
|
@ -160,6 +165,9 @@ class DeviceManagerADB(DeviceManager):
|
|||
self._checkCmd(["disconnect", self.host + ":" + str(self.port)])
|
||||
|
||||
def pushFile(self, localname, destname, retryLimit=None):
|
||||
"""
|
||||
Copies localname from the host to destname on the device
|
||||
"""
|
||||
# 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
|
||||
|
@ -173,18 +181,27 @@ class DeviceManagerADB(DeviceManager):
|
|||
remoteTmpFile = self.getTempDir() + "/" + os.path.basename(localname)
|
||||
self._checkCmd(["push", os.path.realpath(localname), remoteTmpFile],
|
||||
retryLimit=retryLimit)
|
||||
self.shellCheckOutput(["dd", "if=" + remoteTmpFile, "of=" + destname])
|
||||
if self._useDDCopy:
|
||||
self.shellCheckOutput(["dd", "if=" + remoteTmpFile, "of=" + destname])
|
||||
else:
|
||||
self.shellCheckOutput(["cp", remoteTmpFile, destname])
|
||||
self.shellCheckOutput(["rm", remoteTmpFile])
|
||||
else:
|
||||
self._checkCmd(["push", os.path.realpath(localname), destname],
|
||||
retryLimit=retryLimit)
|
||||
|
||||
def mkDir(self, name):
|
||||
"""
|
||||
Creates a single directory on the device file system
|
||||
"""
|
||||
result = self._runCmdAs(["shell", "mkdir", name]).stdout.read()
|
||||
if 'read-only file system' in result.lower():
|
||||
raise DMError("Error creating directory: read only file system")
|
||||
|
||||
def pushDir(self, localDir, remoteDir, retryLimit=None):
|
||||
"""
|
||||
Push localDir from host to remoteDir on the device
|
||||
"""
|
||||
# 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
|
||||
|
@ -218,6 +235,9 @@ class DeviceManagerADB(DeviceManager):
|
|||
shutil.rmtree(tmpDir)
|
||||
|
||||
def dirExists(self, remotePath):
|
||||
"""
|
||||
Return True if remotePath is an existing directory on the device.
|
||||
"""
|
||||
p = self._runCmd(["shell", "ls", "-a", remotePath + '/'])
|
||||
|
||||
data = p.stdout.readlines()
|
||||
|
@ -228,6 +248,9 @@ class DeviceManagerADB(DeviceManager):
|
|||
return True
|
||||
|
||||
def fileExists(self, filepath):
|
||||
"""
|
||||
Return True if filepath exists and is a file on the device file system
|
||||
"""
|
||||
p = self._runCmd(["shell", "ls", "-a", filepath])
|
||||
data = p.stdout.readlines()
|
||||
if (len(data) == 1):
|
||||
|
@ -236,16 +259,27 @@ class DeviceManagerADB(DeviceManager):
|
|||
return False
|
||||
|
||||
def removeFile(self, filename):
|
||||
"""
|
||||
Removes filename from the device
|
||||
"""
|
||||
if self.fileExists(filename):
|
||||
self._runCmd(["shell", "rm", filename])
|
||||
|
||||
def removeDir(self, remoteDir):
|
||||
"""
|
||||
Does a recursive delete of directory on the device: rm -Rf remoteDir
|
||||
"""
|
||||
if (self.dirExists(remoteDir)):
|
||||
self._runCmd(["shell", "rm", "-r", remoteDir]).wait()
|
||||
else:
|
||||
self.removeFile(remoteDir.strip())
|
||||
|
||||
def listFiles(self, rootdir):
|
||||
"""
|
||||
Lists files on the device rootdir
|
||||
|
||||
returns array of filenames, ['file1', 'file2', ...]
|
||||
"""
|
||||
p = self._runCmd(["shell", "ls", "-a", rootdir])
|
||||
data = p.stdout.readlines()
|
||||
data[:] = [item.rstrip('\r\n') for item in data]
|
||||
|
@ -263,6 +297,13 @@ class DeviceManagerADB(DeviceManager):
|
|||
return data
|
||||
|
||||
def getProcessList(self):
|
||||
"""
|
||||
Lists the running processes on the device
|
||||
|
||||
returns:
|
||||
success: array of process tuples
|
||||
failure: []
|
||||
"""
|
||||
p = self._runCmd(["shell", "ps"])
|
||||
# first line is the headers
|
||||
p.stdout.readline()
|
||||
|
@ -270,11 +311,7 @@ class DeviceManagerADB(DeviceManager):
|
|||
ret = []
|
||||
while (proc):
|
||||
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]]))
|
||||
ret.append(list([int(els[1]), els[len(els) - 1], els[0]]))
|
||||
proc = p.stdout.readline()
|
||||
return ret
|
||||
|
||||
|
@ -340,6 +377,11 @@ class DeviceManagerADB(DeviceManager):
|
|||
return outputFile
|
||||
|
||||
def killProcess(self, appname, forceKill=False):
|
||||
"""
|
||||
Kills the process named appname.
|
||||
|
||||
If forceKill is True, process is killed regardless of state
|
||||
"""
|
||||
procs = self.getProcessList()
|
||||
for (pid, name, user) in procs:
|
||||
if name == appname:
|
||||
|
@ -353,6 +395,12 @@ class DeviceManagerADB(DeviceManager):
|
|||
raise DMError("Error killing process "
|
||||
"'%s': %s" % (appname, p.stdout.read()))
|
||||
|
||||
def catFile(self, remoteFile):
|
||||
"""
|
||||
Returns the contents of remoteFile
|
||||
"""
|
||||
return self.pullFile(remoteFile)
|
||||
|
||||
def _runPull(self, remoteFile, localFile):
|
||||
"""
|
||||
Pulls remoteFile from device to host
|
||||
|
@ -381,6 +429,9 @@ class DeviceManagerADB(DeviceManager):
|
|||
raise DMError("Error pulling remote file '%s' to '%s'" % (remoteFile, localFile))
|
||||
|
||||
def pullFile(self, remoteFile):
|
||||
"""
|
||||
Returns contents of remoteFile using the "pull" command.
|
||||
"""
|
||||
# TODO: add debug flags and allow for printing stdout
|
||||
localFile = tempfile.mkstemp()[1]
|
||||
self._runPull(remoteFile, localFile)
|
||||
|
@ -392,12 +443,21 @@ class DeviceManagerADB(DeviceManager):
|
|||
return ret
|
||||
|
||||
def getFile(self, remoteFile, localFile):
|
||||
"""
|
||||
Copy file from device (remoteFile) to host (localFile).
|
||||
"""
|
||||
self._runPull(remoteFile, localFile)
|
||||
|
||||
def getDirectory(self, remoteDir, localDir, checkDir=True):
|
||||
self._runCmd(["pull", remoteDir, localDir]).wait()
|
||||
"""
|
||||
Copy directory structure from device (remoteDir) to host (localDir)
|
||||
"""
|
||||
self._runCmd(["pull", remoteDir, localDir])
|
||||
|
||||
def validateFile(self, remoteFile, localFile):
|
||||
"""
|
||||
Returns True if remoteFile has the same md5 hash as the localFile
|
||||
"""
|
||||
md5Remote = self._getRemoteHash(remoteFile)
|
||||
md5Local = self._getLocalHash(localFile)
|
||||
if md5Remote is None or md5Local is None:
|
||||
|
@ -433,6 +493,13 @@ class DeviceManagerADB(DeviceManager):
|
|||
raise
|
||||
return
|
||||
|
||||
# /mnt/sdcard/tests is preferred to /data/local/tests, but this can be
|
||||
# over-ridden by creating /data/local/tests
|
||||
testRoot = "/data/local/tests"
|
||||
if (self.dirExists(testRoot)):
|
||||
self.deviceRoot = testRoot
|
||||
return
|
||||
|
||||
paths = [('/mnt/sdcard', 'tests'),
|
||||
('/data/local', 'tests')]
|
||||
for (basePath, subPath) in paths:
|
||||
|
@ -449,9 +516,29 @@ class DeviceManagerADB(DeviceManager):
|
|||
% ", ".join(["'%s'" % os.path.join(b, s) for b, s in paths]))
|
||||
|
||||
def getDeviceRoot(self):
|
||||
"""
|
||||
Gets the device root for the testing area on the device
|
||||
|
||||
For all devices we will use / type slashes and depend on the device-agent
|
||||
to sort those out. The agent will return us the device location where we
|
||||
should store things, we will then create our /tests structure relative to
|
||||
that returned path.
|
||||
Structure on the device is as follows:
|
||||
/tests
|
||||
/<fennec>|<firefox> --> approot
|
||||
/profile
|
||||
/xpcshell
|
||||
/reftest
|
||||
/mochitest
|
||||
"""
|
||||
return self.deviceRoot
|
||||
|
||||
def getTempDir(self):
|
||||
"""
|
||||
Return a temporary directory on the device
|
||||
|
||||
Will also ensure that directory exists
|
||||
"""
|
||||
# Cache result to speed up operations depending
|
||||
# on the temporary directory.
|
||||
if not self._tempDir:
|
||||
|
@ -461,6 +548,11 @@ class DeviceManagerADB(DeviceManager):
|
|||
return self._tempDir
|
||||
|
||||
def getAppRoot(self, packageName):
|
||||
"""
|
||||
Returns the app root directory
|
||||
|
||||
E.g /tests/fennec or /tests/firefox
|
||||
"""
|
||||
devroot = self.getDeviceRoot()
|
||||
if (devroot == None):
|
||||
return None
|
||||
|
@ -475,6 +567,9 @@ class DeviceManagerADB(DeviceManager):
|
|||
raise DMError("Failed to get application root for: %s" % packageName)
|
||||
|
||||
def reboot(self, wait = False, **kwargs):
|
||||
"""
|
||||
Reboots the device
|
||||
"""
|
||||
self._runCmd(["reboot"])
|
||||
if (not wait):
|
||||
return
|
||||
|
@ -483,15 +578,47 @@ class DeviceManagerADB(DeviceManager):
|
|||
self._checkCmd(["wait-for-device", "shell", "ls", "/sbin"])
|
||||
|
||||
def updateApp(self, appBundlePath, **kwargs):
|
||||
"""
|
||||
Updates the application on the device.
|
||||
|
||||
appBundlePath - path to the application bundle on the device
|
||||
processName - used to end the process if the applicaiton is currently running (optional)
|
||||
destPath - Destination directory to where the application should be installed (optional)
|
||||
ipAddr - IP address to await a callback ping to let us know that the device has updated
|
||||
properly - defaults to current IP.
|
||||
port - port to await a callback ping to let us know that the device has updated properly
|
||||
defaults to 30000, and counts up from there if it finds a conflict
|
||||
"""
|
||||
return self._runCmd(["install", "-r", appBundlePath]).stdout.read()
|
||||
|
||||
def getCurrentTime(self):
|
||||
"""
|
||||
Returns device time in milliseconds since the epoch
|
||||
"""
|
||||
timestr = self._runCmd(["shell", "date", "+%s"]).stdout.read().strip()
|
||||
if (not timestr or not timestr.isdigit()):
|
||||
raise DMError("Unable to get current time using date (got: '%s')" % timestr)
|
||||
return str(int(timestr)*1000)
|
||||
|
||||
def getInfo(self, directive=None):
|
||||
"""
|
||||
Returns information about the device
|
||||
|
||||
Directive indicates the information you want to get, your choices 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
|
||||
process - list of running processes (same as ps)
|
||||
disk - total, free, available bytes on disk
|
||||
power - power status (charge, battery temp)
|
||||
all - all of them - or call it with no parameters to get all the information
|
||||
|
||||
returns: dictionary of info strings by directive name
|
||||
"""
|
||||
ret = {}
|
||||
if (directive == "id" or directive == "all"):
|
||||
ret["id"] = self._runCmd(["get-serialno"]).stdout.read()
|
||||
|
@ -516,12 +643,24 @@ class DeviceManagerADB(DeviceManager):
|
|||
return ret
|
||||
|
||||
def uninstallApp(self, appName, installPath=None):
|
||||
"""
|
||||
Uninstalls the named application from device and DOES NOT cause a reboot
|
||||
|
||||
appName - the name of the application (e.g org.mozilla.fennec)
|
||||
installPath - the path to where the application was installed (optional)
|
||||
"""
|
||||
data = self._runCmd(["uninstall", appName]).stdout.read().strip()
|
||||
status = data.split('\n')[0].strip()
|
||||
if status != 'Success':
|
||||
raise DMError("uninstall failed for %s. Got: %s" % (appName, status))
|
||||
|
||||
def uninstallAppAndReboot(self, appName, installPath=None):
|
||||
"""
|
||||
Uninstalls the named application from device and causes a reboot
|
||||
|
||||
appName - the name of the application (e.g org.mozilla.fennec)
|
||||
installPath - the path to where the application was installed (optional)
|
||||
"""
|
||||
self.uninstallApp(appName)
|
||||
self.reboot()
|
||||
return
|
||||
|
@ -583,7 +722,7 @@ class DeviceManagerADB(DeviceManager):
|
|||
timeout = int(timeout)
|
||||
retries = 0
|
||||
while retries < retryLimit:
|
||||
proc = subprocess.Popen(finalArgs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
proc = subprocess.Popen(finalArgs)
|
||||
start_time = time.time()
|
||||
ret_code = proc.poll()
|
||||
while ((time.time() - start_time) <= timeout) and ret_code == None:
|
||||
|
@ -611,6 +750,9 @@ class DeviceManagerADB(DeviceManager):
|
|||
return self._checkCmd(args, timeout, retryLimit=retryLimit)
|
||||
|
||||
def chmodDir(self, remoteDir, mask="777"):
|
||||
"""
|
||||
Recursively changes file permissions in a directory
|
||||
"""
|
||||
if (self.dirExists(remoteDir)):
|
||||
files = self.listFiles(remoteDir.strip())
|
||||
for f in files:
|
||||
|
@ -632,7 +774,7 @@ class DeviceManagerADB(DeviceManager):
|
|||
"""
|
||||
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)
|
||||
raise DMError("invalid adb path, or adb not executable: %s", self._adbPath)
|
||||
|
||||
try:
|
||||
self._checkCmd(["version"])
|
||||
|
@ -659,13 +801,28 @@ class DeviceManagerADB(DeviceManager):
|
|||
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
|
||||
ret = None
|
||||
try:
|
||||
ret = self._checkCmd(["shell", "echo"])
|
||||
self._checkCmd(["shell", "echo"])
|
||||
except subprocess.CalledProcessError:
|
||||
raise DMError("unable to connect to device: is it plugged in?")
|
||||
if ret:
|
||||
raise DMError("unable to connect to device")
|
||||
|
||||
def _isCpAvailable(self):
|
||||
"""
|
||||
Checks to see if cp command is installed
|
||||
"""
|
||||
# Some Android systems may not have a cp command installed,
|
||||
# or it may not be executable by the user.
|
||||
data = self._runCmd(["shell", "cp"]).stdout.read()
|
||||
if (re.search('Usage', data)):
|
||||
return True
|
||||
else:
|
||||
data = self._runCmd(["shell", "dd", "-"]).stdout.read()
|
||||
if (re.search('unknown operand', data)):
|
||||
print "'cp' not found, but 'dd' was found as a replacement"
|
||||
self._useDDCopy = True
|
||||
return True
|
||||
print "unable to execute 'cp' on device; consider installing busybox from Android Market"
|
||||
return False
|
||||
|
||||
def _verifyRunAs(self):
|
||||
# If a valid package name is available, and certain other
|
||||
|
@ -677,7 +834,7 @@ class DeviceManagerADB(DeviceManager):
|
|||
# file copy via run-as.
|
||||
self._useRunAs = False
|
||||
devroot = self.getDeviceRoot()
|
||||
if self._packageName and devroot:
|
||||
if (self._packageName and self._isCpAvailable() and devroot):
|
||||
tmpDir = self.getTempDir()
|
||||
|
||||
# The problem here is that run-as doesn't cause a non-zero exit code
|
||||
|
@ -688,7 +845,10 @@ class DeviceManagerADB(DeviceManager):
|
|||
|
||||
tmpfile = tempfile.NamedTemporaryFile()
|
||||
self._checkCmd(["push", tmpfile.name, tmpDir + "/tmpfile"])
|
||||
self._checkCmd(["shell", "run-as", self._packageName, "dd", "if=" + tmpDir + "/tmpfile", "of=" + devroot + "/sanity/tmpfile"])
|
||||
if self._useDDCopy:
|
||||
self._checkCmd(["shell", "run-as", self._packageName, "dd", "if=" + tmpDir + "/tmpfile", "of=" + devroot + "/sanity/tmpfile"])
|
||||
else:
|
||||
self._checkCmd(["shell", "run-as", self._packageName, "cp", tmpDir + "/tmpfile", devroot + "/sanity"])
|
||||
if (self.fileExists(devroot + "/sanity/tmpfile")):
|
||||
print "will execute commands via run-as " + self._packageName
|
||||
self._useRunAs = True
|
||||
|
|
|
@ -4,24 +4,19 @@
|
|||
|
||||
import select
|
||||
import socket
|
||||
import SocketServer
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
import posixpath
|
||||
import subprocess
|
||||
from threading import Thread
|
||||
import StringIO
|
||||
from devicemanager import DeviceManager, DMError, NetworkTools, _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.
|
||||
"""
|
||||
|
||||
debug = 2
|
||||
_base_prompt = '$>'
|
||||
_base_prompt_re = '\$\>'
|
||||
|
@ -30,9 +25,6 @@ class DeviceManagerSUT(DeviceManager):
|
|||
_agentErrorRE = re.compile('^##AGENT-WARNING##\ ?(.*)')
|
||||
default_timeout = 300
|
||||
|
||||
reboot_timeout = 600
|
||||
reboot_settling_time = 60
|
||||
|
||||
def __init__(self, host, port = 20701, retryLimit = 5, deviceRoot = None, **kwargs):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
@ -46,9 +38,7 @@ class DeviceManagerSUT(DeviceManager):
|
|||
|
||||
# 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)
|
||||
self.agentVersion = re.sub('SUTAgentAndroid Version ', '', verstring)
|
||||
|
||||
def _cmdNeedsResponse(self, cmd):
|
||||
""" Not all commands need a response from the agent:
|
||||
|
@ -170,23 +160,17 @@ class DeviceManagerSUT(DeviceManager):
|
|||
raise DMError("Automation Error: unable to create socket: "+str(msg))
|
||||
|
||||
try:
|
||||
self._sock.settimeout(float(timeout))
|
||||
self._sock.connect((self.host, int(self.port)))
|
||||
if select.select([self._sock], [], [], timeout)[0]:
|
||||
self._sock.recv(1024)
|
||||
else:
|
||||
raise DMError("Remote Device Error: Timeout in connecting", fatal=True)
|
||||
return False
|
||||
self._everConnected = True
|
||||
except socket.error, 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, 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)
|
||||
raise DMError("Remote Device Error: Unable to connect socket: "+str(msg))
|
||||
|
||||
for cmd in cmdlist:
|
||||
cmdline = '%s\r\n' % cmd['cmd']
|
||||
|
@ -294,16 +278,21 @@ class DeviceManagerSUT(DeviceManager):
|
|||
raise DMError("Automation Error: Error closing socket")
|
||||
|
||||
def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
|
||||
"""
|
||||
Executes shell command on device. Returns exit code.
|
||||
|
||||
cmd - Command string to execute
|
||||
outputfile - File to store output
|
||||
env - Environment to pass to exec command
|
||||
cwd - Directory to execute command from
|
||||
timeout - specified in seconds, defaults to 'default_timeout'
|
||||
root - Specifies whether command requires root privileges
|
||||
"""
|
||||
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'))
|
||||
haveExecSu = (StrictVersion(self.agentVersion) >= StrictVersion('1.13'))
|
||||
|
||||
# Depending on agent version we send one of the following commands here:
|
||||
# * exec (run as normal user)
|
||||
|
@ -341,6 +330,9 @@ class DeviceManagerSUT(DeviceManager):
|
|||
raise DMError("Automation Error: Error finding end of line/return value when running '%s'" % cmdline)
|
||||
|
||||
def pushFile(self, localname, destname, retryLimit = None):
|
||||
"""
|
||||
Copies localname from the host to destname on the device
|
||||
"""
|
||||
retryLimit = retryLimit or self.retryLimit
|
||||
self.mkDirs(destname)
|
||||
|
||||
|
@ -353,7 +345,7 @@ class DeviceManagerSUT(DeviceManager):
|
|||
raise DMError("DeviceManager: Error reading file to push")
|
||||
|
||||
if (self.debug >= 3):
|
||||
print "push returned: %s" % remoteHash
|
||||
print "push returned: %s" % hash
|
||||
|
||||
localHash = self._getLocalHash(localname)
|
||||
|
||||
|
@ -362,10 +354,16 @@ class DeviceManagerSUT(DeviceManager):
|
|||
"remotehash: %s)" % (localHash, remoteHash))
|
||||
|
||||
def mkDir(self, name):
|
||||
"""
|
||||
Creates a single directory on the device file system
|
||||
"""
|
||||
if not self.dirExists(name):
|
||||
self._runCmds([{ 'cmd': 'mkdr ' + name }])
|
||||
|
||||
def pushDir(self, localDir, remoteDir, retryLimit = None):
|
||||
"""
|
||||
Push localDir from host to remoteDir on the device
|
||||
"""
|
||||
retryLimit = retryLimit or self.retryLimit
|
||||
if (self.debug >= 2):
|
||||
print "pushing directory: %s to %s" % (localDir, remoteDir)
|
||||
|
@ -392,6 +390,9 @@ class DeviceManagerSUT(DeviceManager):
|
|||
|
||||
|
||||
def dirExists(self, remotePath):
|
||||
"""
|
||||
Return True if remotePath is an existing directory on the device.
|
||||
"""
|
||||
ret = self._runCmds([{ 'cmd': 'isdir ' + remotePath }]).strip()
|
||||
if not ret:
|
||||
raise DMError('Automation Error: DeviceManager isdir returned null')
|
||||
|
@ -399,6 +400,9 @@ class DeviceManagerSUT(DeviceManager):
|
|||
return ret == 'TRUE'
|
||||
|
||||
def fileExists(self, filepath):
|
||||
"""
|
||||
Return True if filepath exists and is a file on the device file system
|
||||
"""
|
||||
# Because we always have / style paths we make this a lot easier with some
|
||||
# assumptions
|
||||
s = filepath.split('/')
|
||||
|
@ -406,6 +410,11 @@ class DeviceManagerSUT(DeviceManager):
|
|||
return s[-1] in self.listFiles(containingpath)
|
||||
|
||||
def listFiles(self, rootdir):
|
||||
"""
|
||||
Lists files on the device rootdir
|
||||
|
||||
returns array of filenames, ['file1', 'file2', ...]
|
||||
"""
|
||||
rootdir = rootdir.rstrip('/')
|
||||
if (self.dirExists(rootdir) == False):
|
||||
return []
|
||||
|
@ -418,16 +427,27 @@ class DeviceManagerSUT(DeviceManager):
|
|||
return files
|
||||
|
||||
def removeFile(self, filename):
|
||||
"""
|
||||
Removes filename from the device
|
||||
"""
|
||||
if (self.debug>= 2):
|
||||
print "removing file: " + filename
|
||||
if self.fileExists(filename):
|
||||
self._runCmds([{ 'cmd': 'rm ' + filename }])
|
||||
|
||||
def removeDir(self, remoteDir):
|
||||
"""
|
||||
Does a recursive delete of directory on the device: rm -Rf remoteDir
|
||||
"""
|
||||
if self.dirExists(remoteDir):
|
||||
self._runCmds([{ 'cmd': 'rmdr ' + remoteDir }])
|
||||
|
||||
def getProcessList(self):
|
||||
"""
|
||||
Lists the running processes on the device
|
||||
|
||||
returns: array of process tuples
|
||||
"""
|
||||
data = self._runCmds([{ 'cmd': 'ps' }])
|
||||
|
||||
processTuples = []
|
||||
|
@ -450,7 +470,7 @@ class DeviceManagerSUT(DeviceManager):
|
|||
|
||||
return processTuples
|
||||
|
||||
def fireProcess(self, appname, failIfRunning=False, maxWaitTime=30):
|
||||
def fireProcess(self, appname, failIfRunning=False):
|
||||
"""
|
||||
Starts a process
|
||||
|
||||
|
@ -468,22 +488,11 @@ class DeviceManagerSUT(DeviceManager):
|
|||
print "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
|
||||
|
||||
pid = self.processExist(appname)
|
||||
if (self.debug >= 4):
|
||||
print "got pid: %s for process: %s" % (pid, appname)
|
||||
return pid
|
||||
|
@ -520,27 +529,34 @@ class DeviceManagerSUT(DeviceManager):
|
|||
return outputFile
|
||||
|
||||
def killProcess(self, appname, forceKill=False):
|
||||
"""
|
||||
Kills the process named appname
|
||||
|
||||
If forceKill is True, process is killed regardless of state
|
||||
"""
|
||||
if forceKill:
|
||||
print "WARNING: killProcess(): forceKill parameter unsupported on SUT"
|
||||
retries = 0
|
||||
while retries < self.retryLimit:
|
||||
try:
|
||||
if self.processExist(appname):
|
||||
self._runCmds([{ 'cmd': 'kill ' + appname }])
|
||||
return
|
||||
except DMError, err:
|
||||
retries +=1
|
||||
print ("WARNING: try %d of %d failed to kill %s" %
|
||||
(retries, self.retryLimit, appname))
|
||||
if self.debug >= 4:
|
||||
print err
|
||||
if retries >= self.retryLimit:
|
||||
raise err
|
||||
if self.processExist(appname):
|
||||
self._runCmds([{ 'cmd': 'kill ' + appname }])
|
||||
|
||||
def getTempDir(self):
|
||||
"""
|
||||
Return a temporary directory on the device
|
||||
|
||||
Will also ensure that directory exists
|
||||
"""
|
||||
return self._runCmds([{ 'cmd': 'tmpd' }]).strip()
|
||||
|
||||
def catFile(self, remoteFile):
|
||||
"""
|
||||
Returns the contents of remoteFile
|
||||
"""
|
||||
return self._runCmds([{ 'cmd': 'cat ' + remoteFile }])
|
||||
|
||||
def pullFile(self, remoteFile):
|
||||
"""
|
||||
Returns contents of remoteFile using the "pull" command.
|
||||
"""
|
||||
# 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
|
||||
|
@ -638,6 +654,9 @@ class DeviceManagerSUT(DeviceManager):
|
|||
return buf[:-len(prompt)]
|
||||
|
||||
def getFile(self, remoteFile, localFile):
|
||||
"""
|
||||
Copy file from device (remoteFile) to host (localFile)
|
||||
"""
|
||||
data = self.pullFile(remoteFile)
|
||||
|
||||
fhandle = open(localFile, 'wb')
|
||||
|
@ -648,6 +667,9 @@ class DeviceManagerSUT(DeviceManager):
|
|||
remoteFile)
|
||||
|
||||
def getDirectory(self, remoteDir, localDir, checkDir=True):
|
||||
"""
|
||||
Copy directory structure from device (remoteDir) to host (localDir)
|
||||
"""
|
||||
if (self.debug >= 2):
|
||||
print "getting files in '" + remoteDir + "'"
|
||||
if checkDir and not self.dirExists(remoteDir):
|
||||
|
@ -671,6 +693,9 @@ class DeviceManagerSUT(DeviceManager):
|
|||
self.getFile(remotePath, localPath)
|
||||
|
||||
def validateFile(self, remoteFile, localFile):
|
||||
"""
|
||||
Returns True if remoteFile has the same md5 hash as the localFile
|
||||
"""
|
||||
remoteHash = self._getRemoteHash(remoteFile)
|
||||
localHash = self._getLocalHash(localFile)
|
||||
|
||||
|
@ -683,12 +708,30 @@ class DeviceManagerSUT(DeviceManager):
|
|||
return False
|
||||
|
||||
def _getRemoteHash(self, filename):
|
||||
"""
|
||||
Return the md5 sum of a file on the device
|
||||
"""
|
||||
data = self._runCmds([{ 'cmd': 'hash ' + filename }]).strip()
|
||||
if self.debug >= 3:
|
||||
print "remote hash returned: '%s'" % data
|
||||
return data
|
||||
|
||||
def getDeviceRoot(self):
|
||||
"""
|
||||
Gets the device root for the testing area on the device
|
||||
|
||||
For all devices we will use / type slashes and depend on the device-agent
|
||||
to sort those out. The agent will return us the device location where we
|
||||
should store things, we will then create our /tests structure relative to
|
||||
that returned path.
|
||||
Structure on the device is as follows:
|
||||
/tests
|
||||
/<fennec>|<firefox> --> approot
|
||||
/profile
|
||||
/xpcshell
|
||||
/reftest
|
||||
/mochitest
|
||||
"""
|
||||
if not self.deviceRoot:
|
||||
data = self._runCmds([{ 'cmd': 'testroot' }])
|
||||
self.deviceRoot = data.strip() + '/tests'
|
||||
|
@ -699,96 +742,88 @@ class DeviceManagerSUT(DeviceManager):
|
|||
return self.deviceRoot
|
||||
|
||||
def getAppRoot(self, packageName):
|
||||
"""
|
||||
Returns the app root directory
|
||||
|
||||
E.g /tests/fennec or /tests/firefox
|
||||
"""
|
||||
data = self._runCmds([{ 'cmd': 'getapproot ' + packageName }])
|
||||
|
||||
return data.strip()
|
||||
|
||||
def unpackFile(self, filePath, destDir=None):
|
||||
def unpackFile(self, file_path, dest_dir=None):
|
||||
"""
|
||||
Unzips a bundle to a location on the device
|
||||
Unzips a remote bundle to a remote location
|
||||
|
||||
If destDir is not specified, the bundle is extracted in the same directory
|
||||
If dest_dir is not specified, the bundle is extracted
|
||||
in the same directory
|
||||
"""
|
||||
devroot = self.getDeviceRoot()
|
||||
if (devroot == None):
|
||||
return None
|
||||
|
||||
# if no destDir is passed in just set it to filePath's folder
|
||||
if not destDir:
|
||||
destDir = posixpath.dirname(filePath)
|
||||
# if no dest_dir is passed in just set it to file_path's folder
|
||||
if not dest_dir:
|
||||
dest_dir = posixpath.dirname(file_path)
|
||||
|
||||
if destDir[-1] != '/':
|
||||
destDir += '/'
|
||||
if dest_dir[-1] != '/':
|
||||
dest_dir += '/'
|
||||
|
||||
self._runCmds([{ 'cmd': 'unzp %s %s' % (filePath, destDir)}])
|
||||
|
||||
def _wait_for_reboot(self, host, port):
|
||||
if self.debug >= 3:
|
||||
print 'Creating server with %s:%d' % (host, port)
|
||||
timeout_expires = time.time() + self.reboot_timeout
|
||||
conn = None
|
||||
data = ''
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.settimeout(60.0)
|
||||
s.bind((host, port))
|
||||
s.listen(1)
|
||||
while not data and time.time() < timeout_expires:
|
||||
try:
|
||||
if not conn:
|
||||
conn, _ = s.accept()
|
||||
# Receiving any data is good enough.
|
||||
data = conn.recv(1024)
|
||||
if data:
|
||||
conn.sendall('OK')
|
||||
conn.close()
|
||||
except socket.timeout:
|
||||
print '.'
|
||||
except socket.error, e:
|
||||
if e.errno != errno.EAGAIN and e.errno != errno.EWOULDBLOCK:
|
||||
raise
|
||||
if data:
|
||||
# Sleep to ensure not only we are online, but all our services are
|
||||
# also up.
|
||||
time.sleep(self.reboot_settling_time)
|
||||
else:
|
||||
print 'Automation Error: Timed out waiting for reboot callback.'
|
||||
s.close()
|
||||
return data
|
||||
self._runCmds([{ 'cmd': 'unzp %s %s' % (file_path, dest_dir)}])
|
||||
|
||||
def reboot(self, ipAddr=None, port=30000):
|
||||
"""
|
||||
Reboots the device
|
||||
"""
|
||||
cmd = 'rebt'
|
||||
|
||||
if self.debug > 3:
|
||||
if (self.debug > 3):
|
||||
print "INFO: sending rebt command"
|
||||
|
||||
if ipAddr is not None:
|
||||
# The update.info command tells the SUTAgent to send a TCP message
|
||||
# after restarting.
|
||||
if (ipAddr is not None):
|
||||
#create update.info file:
|
||||
destname = '/data/data/com.mozilla.SUTAgentAndroid/files/update.info'
|
||||
data = "%s,%s\rrebooting\r" % (ipAddr, port)
|
||||
self._runCmds([{'cmd': 'push %s %s' % (destname, len(data)),
|
||||
'data': data}])
|
||||
self._runCmds([{ 'cmd': 'push %s %s' % (destname, len(data)), 'data': data }])
|
||||
|
||||
ip, port = self._getCallbackIpAndPort(ipAddr, port)
|
||||
cmd += " %s %s" % (ip, port)
|
||||
# Set up our callback server
|
||||
callbacksvr = callbackServer(ip, port, self.debug)
|
||||
|
||||
status = self._runCmds([{'cmd': cmd}])
|
||||
status = self._runCmds([{ 'cmd': cmd }])
|
||||
|
||||
if ipAddr is not None:
|
||||
status = self._wait_for_reboot(ipAddr, port)
|
||||
if (ipAddr is not None):
|
||||
status = callbacksvr.disconnect()
|
||||
|
||||
if self.debug > 3:
|
||||
if (self.debug > 3):
|
||||
print "INFO: rebt- got status back: " + str(status)
|
||||
|
||||
def getInfo(self, directive=None):
|
||||
"""
|
||||
Returns information about the device
|
||||
|
||||
Directive indicates the information you want to get, your choices 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
|
||||
process - list of running processes (same as ps)
|
||||
disk - total, free, available bytes on disk
|
||||
power - power status (charge, battery temp)
|
||||
all - all of them - or call it with no parameters to get all the information
|
||||
|
||||
returns: dictionary of info strings by directive name
|
||||
"""
|
||||
data = None
|
||||
result = {}
|
||||
collapseSpaces = re.compile(' +')
|
||||
|
||||
directives = ['os','id','uptime','uptimemillis','systime','screen',
|
||||
'rotation','memory','process','disk','power','sutuserinfo',
|
||||
'temperature']
|
||||
'rotation','memory','process','disk','power']
|
||||
if (directive in directives):
|
||||
directives = [directive]
|
||||
|
||||
|
@ -815,6 +850,12 @@ class DeviceManagerSUT(DeviceManager):
|
|||
return result
|
||||
|
||||
def installApp(self, appBundlePath, destPath=None):
|
||||
"""
|
||||
Installs an application onto the device
|
||||
|
||||
appBundlePath - path to the application bundle on the device
|
||||
destPath - destination directory of where application should be installed to (optional)
|
||||
"""
|
||||
cmd = 'inst ' + appBundlePath
|
||||
if destPath:
|
||||
cmd += ' ' + destPath
|
||||
|
@ -827,6 +868,12 @@ class DeviceManagerSUT(DeviceManager):
|
|||
raise DMError("Remove Device Error: Error installing app. Error message: %s" % data)
|
||||
|
||||
def uninstallApp(self, appName, installPath=None):
|
||||
"""
|
||||
Uninstalls the named application from device and DOES NOT cause a reboot
|
||||
|
||||
appName - the name of the application (e.g org.mozilla.fennec)
|
||||
installPath - the path to where the application was installed (optional)
|
||||
"""
|
||||
cmd = 'uninstall ' + appName
|
||||
if installPath:
|
||||
cmd += ' ' + installPath
|
||||
|
@ -840,6 +887,12 @@ class DeviceManagerSUT(DeviceManager):
|
|||
raise DMError("Remote Device Error: uninstall failed for %s" % appName)
|
||||
|
||||
def uninstallAppAndReboot(self, appName, installPath=None):
|
||||
"""
|
||||
Uninstalls the named application from device and causes a reboot
|
||||
|
||||
appName - the name of the application (e.g org.mozilla.fennec)
|
||||
installPath - the path to where the application was installed (optional)
|
||||
"""
|
||||
cmd = 'uninst ' + appName
|
||||
if installPath:
|
||||
cmd += ' ' + installPath
|
||||
|
@ -850,33 +903,49 @@ class DeviceManagerSUT(DeviceManager):
|
|||
return
|
||||
|
||||
def updateApp(self, appBundlePath, processName=None, destPath=None, ipAddr=None, port=30000):
|
||||
"""
|
||||
Updates the application on the device.
|
||||
|
||||
appBundlePath - path to the application bundle on the device
|
||||
processName - used to end the process if the applicaiton is currently running (optional)
|
||||
destPath - Destination directory to where the application should be installed (optional)
|
||||
ipAddr - IP address to await a callback ping to let us know that the device has updated
|
||||
properly - defaults to current IP.
|
||||
port - port to await a callback ping to let us know that the device has updated properly
|
||||
defaults to 30000, and counts up from there if it finds a conflict
|
||||
"""
|
||||
status = None
|
||||
cmd = 'updt '
|
||||
if processName is None:
|
||||
if (processName == None):
|
||||
# Then we pass '' for processName
|
||||
cmd += "'' " + appBundlePath
|
||||
else:
|
||||
cmd += processName + ' ' + appBundlePath
|
||||
|
||||
if destPath:
|
||||
if (destPath):
|
||||
cmd += " " + destPath
|
||||
|
||||
if ipAddr is not None:
|
||||
if (ipAddr is not None):
|
||||
ip, port = self._getCallbackIpAndPort(ipAddr, port)
|
||||
cmd += " %s %s" % (ip, port)
|
||||
# Set up our callback server
|
||||
callbacksvr = callbackServer(ip, port, self.debug)
|
||||
|
||||
if self.debug >= 3:
|
||||
if (self.debug >= 3):
|
||||
print "INFO: updateApp using command: " + str(cmd)
|
||||
|
||||
status = self._runCmds([{'cmd': cmd}])
|
||||
status = self._runCmds([{ 'cmd': cmd }])
|
||||
|
||||
if ipAddr is not None:
|
||||
status = self._wait_for_reboot(ip, port)
|
||||
status = callbacksvr.disconnect()
|
||||
|
||||
if self.debug >= 3:
|
||||
print "INFO: updateApp: got status back: %s" + str(status)
|
||||
if (self.debug >= 3):
|
||||
print "INFO: updateApp: got status back: " + str(status)
|
||||
|
||||
def getCurrentTime(self):
|
||||
"""
|
||||
Returns device time in milliseconds since the epoch
|
||||
"""
|
||||
return self._runCmds([{ 'cmd': 'clok' }]).strip()
|
||||
|
||||
def _getCallbackIpAndPort(self, aIp, aPort):
|
||||
|
@ -915,7 +984,7 @@ class DeviceManagerSUT(DeviceManager):
|
|||
|
||||
def adjustResolution(self, width=1680, height=1050, type='hdmi'):
|
||||
"""
|
||||
Adjust the screen resolution on the device, REBOOT REQUIRED
|
||||
adjust the screen resolution on the device, REBOOT REQUIRED
|
||||
|
||||
NOTE: this only works on a tegra ATM
|
||||
|
||||
|
@ -957,4 +1026,64 @@ class DeviceManagerSUT(DeviceManager):
|
|||
self._runCmds([{ 'cmd': "exec setprop persist.tegra.dpy%s.mode.height %s" % (screentype, height) }])
|
||||
|
||||
def chmodDir(self, remoteDir, **kwargs):
|
||||
"""
|
||||
Recursively changes file permissions in a directory
|
||||
"""
|
||||
self._runCmds([{ 'cmd': "chmod "+remoteDir }])
|
||||
|
||||
gCallbackData = ''
|
||||
|
||||
class myServer(SocketServer.TCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
class callbackServer():
|
||||
def __init__(self, ip, port, debuglevel):
|
||||
global gCallbackData
|
||||
if (debuglevel >= 1):
|
||||
print "DEBUG: gCallbackData is: %s on port: %s" % (gCallbackData, port)
|
||||
gCallbackData = ''
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
self.connected = False
|
||||
self.debug = debuglevel
|
||||
if (self.debug >= 3):
|
||||
print "Creating server with " + str(ip) + ":" + str(port)
|
||||
self.server = myServer((ip, port), self.myhandler)
|
||||
self.server_thread = Thread(target=self.server.serve_forever)
|
||||
self.server_thread.setDaemon(True)
|
||||
self.server_thread.start()
|
||||
|
||||
def disconnect(self, step = 60, timeout = 600):
|
||||
t = 0
|
||||
if (self.debug >= 3):
|
||||
print "Calling disconnect on callback server"
|
||||
while t < timeout:
|
||||
if (gCallbackData):
|
||||
# Got the data back
|
||||
if (self.debug >= 3):
|
||||
print "Got data back from agent: " + str(gCallbackData)
|
||||
break
|
||||
else:
|
||||
if (self.debug >= 0):
|
||||
print '.',
|
||||
time.sleep(step)
|
||||
t += step
|
||||
|
||||
try:
|
||||
if (self.debug >= 3):
|
||||
print "Shutting down server now"
|
||||
self.server.shutdown()
|
||||
except:
|
||||
if (self.debug >= 1):
|
||||
print "Automation Error: Unable to shutdown callback server - check for a connection on port: " + str(self.port)
|
||||
|
||||
#sleep 1 additional step to ensure not only we are online, but all our services are online
|
||||
time.sleep(step)
|
||||
return gCallbackData
|
||||
|
||||
class myhandler(SocketServer.BaseRequestHandler):
|
||||
def handle(self):
|
||||
global gCallbackData
|
||||
gCallbackData = self.request.recv(1024)
|
||||
#print "Callback Handler got data: " + str(gCallbackData)
|
||||
self.request.send("OK")
|
||||
|
|
|
@ -25,11 +25,6 @@ class DMCli(object):
|
|||
'max_args': 1,
|
||||
'help_args': '<file>',
|
||||
'help': 'push this package file to the device and install it' },
|
||||
'uninstall': { 'function': lambda a: self.dm.uninstallApp(a),
|
||||
'min_args': 1,
|
||||
'max_args': 1,
|
||||
'help_args': '<packagename>',
|
||||
'help': 'uninstall the named app from the device' },
|
||||
'killapp': { 'function': self.killapp,
|
||||
'min_args': 1,
|
||||
'max_args': 1,
|
||||
|
@ -110,13 +105,7 @@ class DMCli(object):
|
|||
'max_args': 1,
|
||||
'help_args': '<png file>',
|
||||
'help': 'capture screenshot of device in action'
|
||||
},
|
||||
'sutver': { 'function': self.sutver,
|
||||
'min_args': 0,
|
||||
'max_args': 0,
|
||||
'help_args': '',
|
||||
'help': 'SUTAgent\'s product name and version (SUT only)'
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -292,13 +281,6 @@ class DMCli(object):
|
|||
print "FALSE"
|
||||
return errno.ENOTDIR
|
||||
|
||||
def sutver(self):
|
||||
if self.options.dmtype == 'sut':
|
||||
print '%s Version %s' % (self.dm.agentProductName,
|
||||
self.dm.agentVersion)
|
||||
else:
|
||||
print 'Must use SUT transport to get SUT version.'
|
||||
|
||||
def cli(args=sys.argv[1:]):
|
||||
# process the command line
|
||||
cli = DMCli()
|
||||
|
|
|
@ -3,39 +3,29 @@
|
|||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import StringIO
|
||||
import re
|
||||
import threading
|
||||
|
||||
from Zeroconf import Zeroconf, ServiceBrowser
|
||||
from devicemanager import ZeroconfListener, NetworkTools
|
||||
from devicemanagerADB import DeviceManagerADB
|
||||
from devicemanagerSUT import DeviceManagerSUT
|
||||
from devicemanager import DMError
|
||||
|
||||
class DroidMixin(object):
|
||||
"""Mixin to extend DeviceManager with Android-specific functionality"""
|
||||
|
||||
def _getExtraAmStartArgs(self):
|
||||
return []
|
||||
|
||||
def launchApplication(self, appName, activityName, intent, url=None,
|
||||
extras=None):
|
||||
"""
|
||||
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
|
||||
returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
# only one instance of an application may be running at once
|
||||
if self.processExist(appName):
|
||||
raise DMError("Only one instance of an application may be running "
|
||||
"at once")
|
||||
return False
|
||||
|
||||
acmd = [ "am", "start" ] + self._getExtraAmStartArgs() + \
|
||||
["-W", "-n", "%s/%s" % (appName, activityName)]
|
||||
acmd = [ "am", "start", "-W", "-n", "%s/%s" % (appName, activityName)]
|
||||
|
||||
if intent:
|
||||
acmd.extend(["-a", intent])
|
||||
|
@ -55,25 +45,24 @@ class DroidMixin(object):
|
|||
|
||||
# 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)
|
||||
# and ignore
|
||||
shellOutput = StringIO.StringIO()
|
||||
if self.shell(acmd, shellOutput) == 0:
|
||||
return
|
||||
return True
|
||||
|
||||
shellOutput.seek(0)
|
||||
raise DMError("Unable to launch application (shell output: '%s')" % shellOutput.read())
|
||||
return False
|
||||
|
||||
def launchFennec(self, appName, intent="android.intent.action.VIEW",
|
||||
mozEnv=None, extraArgs=None, url=None):
|
||||
mozEnv=None, extraArgs=None, url=None):
|
||||
"""
|
||||
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
|
||||
WARNING: FIXME: This would go better in mozrunner. Please do not
|
||||
use this method if you are not comfortable with it going away sometime
|
||||
in the near future
|
||||
returns:
|
||||
success: True
|
||||
failure: False
|
||||
"""
|
||||
extras = {}
|
||||
|
||||
|
@ -88,34 +77,14 @@ class DroidMixin(object):
|
|||
if extraArgs:
|
||||
extras['args'] = " ".join(extraArgs)
|
||||
|
||||
self.launchApplication(appName, ".App", intent, url=url, extras=extras)
|
||||
return self.launchApplication(appName, ".App", intent, url=url,
|
||||
extras=extras)
|
||||
|
||||
class DroidADB(DeviceManagerADB, DroidMixin):
|
||||
pass
|
||||
|
||||
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
|
||||
|
||||
if self.userSerial is not None:
|
||||
return [ "--user", self.userSerial ]
|
||||
|
||||
return []
|
||||
pass
|
||||
|
||||
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."""
|
||||
|
|
|
@ -0,0 +1,310 @@
|
|||
# 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 abc import abstractmethod
|
||||
import datetime
|
||||
from mozprocess import ProcessHandlerMixin
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
from telnetlib import Telnet
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from emulator_battery import EmulatorBattery
|
||||
|
||||
|
||||
class LogcatProc(ProcessHandlerMixin):
|
||||
"""Process handler for logcat which saves all output to a logfile.
|
||||
"""
|
||||
|
||||
def __init__(self, logfile, cmd, **kwargs):
|
||||
self.logfile = logfile
|
||||
kwargs.setdefault('processOutputLine', []).append(self.log_output)
|
||||
ProcessHandlerMixin.__init__(self, cmd, **kwargs)
|
||||
|
||||
def log_output(self, line):
|
||||
f = open(self.logfile, 'a')
|
||||
f.write(line + "\n")
|
||||
f.flush()
|
||||
|
||||
|
||||
class Emulator(object):
|
||||
|
||||
deviceRe = re.compile(r"^emulator-(\d+)(\s*)(.*)$")
|
||||
|
||||
def __init__(self, noWindow=False, logcat_dir=None, arch="x86",
|
||||
emulatorBinary=None, res='480x800', userdata=None,
|
||||
memory='512', partition_size='512'):
|
||||
self.port = None
|
||||
self._emulator_launched = False
|
||||
self.proc = None
|
||||
self.local_port = None
|
||||
self.telnet = None
|
||||
self._tmp_userdata = None
|
||||
self._adb_started = False
|
||||
self.logcat_dir = logcat_dir
|
||||
self.logcat_proc = None
|
||||
self.arch = arch
|
||||
self.binary = emulatorBinary
|
||||
self.memory = str(memory)
|
||||
self.partition_size = str(partition_size)
|
||||
self.res = res
|
||||
self.battery = EmulatorBattery(self)
|
||||
self.noWindow = noWindow
|
||||
self.dataImg = userdata
|
||||
self.copy_userdata = self.dataImg is None
|
||||
|
||||
def __del__(self):
|
||||
if self.telnet:
|
||||
self.telnet.write('exit\n')
|
||||
self.telnet.read_all()
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
qemuArgs = [self.binary,
|
||||
'-kernel', self.kernelImg,
|
||||
'-sysdir', self.sysDir,
|
||||
'-data', self.dataImg]
|
||||
if self.noWindow:
|
||||
qemuArgs.append('-no-window')
|
||||
qemuArgs.extend(['-memory', self.memory,
|
||||
'-partition-size', self.partition_size,
|
||||
'-verbose',
|
||||
'-skin', self.res,
|
||||
'-gpu', 'on',
|
||||
'-qemu'] + self.tail_args)
|
||||
return qemuArgs
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
if self._emulator_launched:
|
||||
return self.proc is not None and self.proc.poll() is None
|
||||
else:
|
||||
return self.port is not None
|
||||
|
||||
def check_for_crash(self):
|
||||
"""
|
||||
Checks if the emulator has crashed or not. Always returns False if
|
||||
we've connected to an already-running emulator, since we can't track
|
||||
the emulator's pid in that case. Otherwise, returns True iff
|
||||
self.proc is not None (meaning the emulator hasn't been explicitly
|
||||
closed), and self.proc.poll() is also not None (meaning the emulator
|
||||
process has terminated).
|
||||
"""
|
||||
if (self._emulator_launched and self.proc is not None
|
||||
and self.proc.poll() is not None):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _default_adb(self):
|
||||
adb = subprocess.Popen(['which', 'adb'],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
retcode = adb.wait()
|
||||
if retcode == 0:
|
||||
self.adb = adb.stdout.read().strip() # remove trailing newline
|
||||
return retcode
|
||||
|
||||
def _check_for_adb(self):
|
||||
if not os.path.exists(self.adb):
|
||||
if self._default_adb() != 0:
|
||||
raise Exception('adb not found!')
|
||||
|
||||
def _run_adb(self, args):
|
||||
args.insert(0, self.adb)
|
||||
adb = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
retcode = adb.wait()
|
||||
if retcode:
|
||||
raise Exception('adb terminated with exit code %d: %s'
|
||||
% (retcode, adb.stdout.read()))
|
||||
return adb.stdout.read()
|
||||
|
||||
def _get_telnet_response(self, command=None):
|
||||
output = []
|
||||
assert(self.telnet)
|
||||
if command is not None:
|
||||
self.telnet.write('%s\n' % command)
|
||||
while True:
|
||||
line = self.telnet.read_until('\n')
|
||||
output.append(line.rstrip())
|
||||
if line.startswith('OK'):
|
||||
return output
|
||||
elif line.startswith('KO:'):
|
||||
raise Exception('bad telnet response: %s' % line)
|
||||
|
||||
def _run_telnet(self, command):
|
||||
if not self.telnet:
|
||||
self.telnet = Telnet('localhost', self.port)
|
||||
self._get_telnet_response()
|
||||
return self._get_telnet_response(command)
|
||||
|
||||
def close(self):
|
||||
if self.is_running and self._emulator_launched:
|
||||
self.proc.terminate()
|
||||
self.proc.wait()
|
||||
if self._adb_started:
|
||||
self._run_adb(['kill-server'])
|
||||
self._adb_started = False
|
||||
if self.proc:
|
||||
retcode = self.proc.poll()
|
||||
self.proc = None
|
||||
if self._tmp_userdata:
|
||||
os.remove(self._tmp_userdata)
|
||||
self._tmp_userdata = None
|
||||
return retcode
|
||||
if self.logcat_proc:
|
||||
self.logcat_proc.kill()
|
||||
return 0
|
||||
|
||||
def _get_adb_devices(self):
|
||||
offline = set()
|
||||
online = set()
|
||||
output = self._run_adb(['devices'])
|
||||
for line in output.split('\n'):
|
||||
m = self.deviceRe.match(line)
|
||||
if m:
|
||||
if m.group(3) == 'offline':
|
||||
offline.add(m.group(1))
|
||||
else:
|
||||
online.add(m.group(1))
|
||||
return (online, offline)
|
||||
|
||||
def restart(self):
|
||||
if not self._emulator_launched:
|
||||
return
|
||||
self.close()
|
||||
self.start()
|
||||
|
||||
def start_adb(self):
|
||||
result = self._run_adb(['start-server'])
|
||||
# We keep track of whether we've started adb or not, so we know
|
||||
# if we need to kill it.
|
||||
if 'daemon started successfully' in result:
|
||||
self._adb_started = True
|
||||
else:
|
||||
self._adb_started = False
|
||||
|
||||
def connect(self):
|
||||
self._check_for_adb()
|
||||
self.start_adb()
|
||||
|
||||
online, offline = self._get_adb_devices()
|
||||
now = datetime.datetime.now()
|
||||
while online == set([]):
|
||||
time.sleep(1)
|
||||
if datetime.datetime.now() - now > datetime.timedelta(seconds=60):
|
||||
raise Exception('timed out waiting for emulator to be available')
|
||||
online, offline = self._get_adb_devices()
|
||||
self.port = int(list(online)[0])
|
||||
|
||||
@abstractmethod
|
||||
def _locate_files(self):
|
||||
pass
|
||||
|
||||
def start(self):
|
||||
self._locate_files()
|
||||
self.start_adb()
|
||||
|
||||
qemu_args = self.args[:]
|
||||
if self.copy_userdata:
|
||||
# Make a copy of the userdata.img for this instance of the emulator
|
||||
# to use.
|
||||
self._tmp_userdata = tempfile.mktemp(prefix='emulator')
|
||||
shutil.copyfile(self.dataImg, self._tmp_userdata)
|
||||
qemu_args[qemu_args.index('-data') + 1] = self._tmp_userdata
|
||||
|
||||
original_online, original_offline = self._get_adb_devices()
|
||||
|
||||
self.proc = subprocess.Popen(qemu_args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
online, offline = self._get_adb_devices()
|
||||
now = datetime.datetime.now()
|
||||
while online - original_online == set([]):
|
||||
time.sleep(1)
|
||||
if datetime.datetime.now() - now > datetime.timedelta(seconds=60):
|
||||
raise Exception('timed out waiting for emulator to start')
|
||||
online, offline = self._get_adb_devices()
|
||||
self.port = int(list(online - original_online)[0])
|
||||
self._emulator_launched = True
|
||||
|
||||
if self.logcat_dir:
|
||||
self.save_logcat()
|
||||
|
||||
# setup DNS fix for networking
|
||||
self._run_adb(['-s', 'emulator-%d' % self.port,
|
||||
'shell', 'setprop', 'net.dns1', '10.0.2.3'])
|
||||
|
||||
def _save_logcat_proc(self, filename, cmd):
|
||||
self.logcat_proc = LogcatProc(filename, cmd)
|
||||
self.logcat_proc.run()
|
||||
self.logcat_proc.waitForFinish()
|
||||
self.logcat_proc = None
|
||||
|
||||
def rotate_log(self, srclog, index=1):
|
||||
""" Rotate a logfile, by recursively rotating logs further in the sequence,
|
||||
deleting the last file if necessary.
|
||||
"""
|
||||
destlog = os.path.join(self.logcat_dir, 'emulator-%d.%d.log' % (self.port, index))
|
||||
if os.path.exists(destlog):
|
||||
if index == 3:
|
||||
os.remove(destlog)
|
||||
else:
|
||||
self.rotate_log(destlog, index + 1)
|
||||
shutil.move(srclog, destlog)
|
||||
|
||||
def save_logcat(self):
|
||||
""" Save the output of logcat to a file.
|
||||
"""
|
||||
filename = os.path.join(self.logcat_dir, "emulator-%d.log" % self.port)
|
||||
if os.path.exists(filename):
|
||||
self.rotate_log(filename)
|
||||
cmd = [self.adb, '-s', 'emulator-%d' % self.port, 'logcat']
|
||||
|
||||
# We do this in a separate process because we call mozprocess's
|
||||
# waitForFinish method to process logcat's output, and this method
|
||||
# blocks.
|
||||
proc = multiprocessing.Process(target=self._save_logcat_proc, args=(filename, cmd))
|
||||
proc.daemon = True
|
||||
proc.start()
|
||||
|
||||
def setup_port_forwarding(self, remote_port):
|
||||
""" Set up TCP port forwarding to the specified port on the device,
|
||||
using any availble local port, and return the local port.
|
||||
"""
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind(("", 0))
|
||||
local_port = s.getsockname()[1]
|
||||
s.close()
|
||||
|
||||
self._run_adb(['-s', 'emulator-%d' % self.port, 'forward',
|
||||
'tcp:%d' % local_port,
|
||||
'tcp:%d' % remote_port])
|
||||
|
||||
self.local_port = local_port
|
||||
|
||||
return local_port
|
||||
|
||||
def wait_for_port(self, timeout=300):
|
||||
assert(self.local_port)
|
||||
starttime = datetime.datetime.now()
|
||||
while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(('localhost', self.local_port))
|
||||
data = sock.recv(16)
|
||||
sock.close()
|
||||
if '"from"' in data:
|
||||
return True
|
||||
except:
|
||||
import traceback
|
||||
print traceback.format_exc()
|
||||
time.sleep(1)
|
||||
return False
|
|
@ -0,0 +1,52 @@
|
|||
# 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/.
|
||||
|
||||
class EmulatorBattery(object):
|
||||
|
||||
def __init__(self, emulator):
|
||||
self.emulator = emulator
|
||||
|
||||
def get_state(self):
|
||||
status = {}
|
||||
state = {}
|
||||
|
||||
response = self.emulator._run_telnet('power display')
|
||||
for line in response:
|
||||
if ':' in line:
|
||||
field, value = line.split(':')
|
||||
value = value.strip()
|
||||
if value == 'true':
|
||||
value = True
|
||||
elif value == 'false':
|
||||
value = False
|
||||
elif field == 'capacity':
|
||||
value = float(value)
|
||||
status[field] = value
|
||||
|
||||
state['level'] = status.get('capacity', 0.0) / 100
|
||||
if status.get('AC') == 'online':
|
||||
state['charging'] = True
|
||||
else:
|
||||
state['charging'] = False
|
||||
|
||||
return state
|
||||
|
||||
def get_charging(self):
|
||||
return self.get_state()['charging']
|
||||
|
||||
def get_level(self):
|
||||
return self.get_state()['level']
|
||||
|
||||
def set_level(self, level):
|
||||
self.emulator._run_telnet('power capacity %d' % (level * 100))
|
||||
|
||||
def set_charging(self, charging):
|
||||
if charging:
|
||||
cmd = 'power ac on'
|
||||
else:
|
||||
cmd = 'power ac off'
|
||||
self.emulator._run_telnet(cmd)
|
||||
|
||||
charging = property(get_charging, set_charging)
|
||||
level = property(get_level, set_level)
|
|
@ -1,125 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import 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()
|
|
@ -4,7 +4,9 @@
|
|||
|
||||
from setuptools import setup
|
||||
|
||||
PACKAGE_VERSION = '0.21'
|
||||
PACKAGE_VERSION = '0.18'
|
||||
|
||||
deps = ['mozprocess == 0.8']
|
||||
|
||||
setup(name='mozdevice',
|
||||
version=PACKAGE_VERSION,
|
||||
|
@ -14,16 +16,15 @@ setup(name='mozdevice',
|
|||
keywords='',
|
||||
author='Mozilla Automation and Testing Team',
|
||||
author_email='tools@lists.mozilla.org',
|
||||
url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
|
||||
url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
|
||||
license='MPL',
|
||||
packages=['mozdevice'],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=[],
|
||||
install_requires=deps,
|
||||
entry_points="""
|
||||
# -*- Entry points: -*-
|
||||
[console_scripts]
|
||||
dm = mozdevice.dmcli:cli
|
||||
sutini = mozdevice.sutini:main
|
||||
""",
|
||||
)
|
||||
|
|
|
@ -9,7 +9,6 @@ import unittest
|
|||
|
||||
ip = ''
|
||||
port = 0
|
||||
heartbeat_port = 0
|
||||
|
||||
|
||||
class DeviceManagerTestCase(unittest.TestCase):
|
||||
|
@ -27,6 +26,7 @@ class DeviceManagerTestCase(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
self.dm = devicemanagerSUT.DeviceManagerSUT(host=ip, port=port)
|
||||
self.dm.debug = 3
|
||||
self.dmerror = devicemanager.DMError
|
||||
self.nettools = devicemanager.NetworkTools
|
||||
self._setUp()
|
||||
|
|
|
@ -12,13 +12,9 @@ import dmunit
|
|||
import genfiles
|
||||
|
||||
|
||||
def main(ip, port, heartbeat_port, scripts, directory, isTestDevice, verbose):
|
||||
def main(ip, port, scripts, directory, isTestDevice):
|
||||
dmunit.ip = ip
|
||||
dmunit.port = port
|
||||
dmunit.heartbeat_port = heartbeat_port
|
||||
if verbose:
|
||||
from mozdevice.devicemanagerSUT import DeviceManagerSUT
|
||||
DeviceManagerSUT.debug = 4
|
||||
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
|
@ -71,10 +67,6 @@ if __name__ == "__main__":
|
|||
"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=[])
|
||||
|
@ -87,10 +79,7 @@ if __name__ == "__main__":
|
|||
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)
|
||||
main(options.ip, options.port, options.scripts,
|
||||
options.dir, options.isTestDevice)
|
||||
|
|
|
@ -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/.
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import posixpath
|
||||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
|
||||
class Cat2TestCase(DeviceManagerTestCase):
|
||||
|
||||
def runTest(self):
|
||||
"""This tests copying a binary file to and from the device the binary.
|
||||
File is > 64K.
|
||||
"""
|
||||
testroot = posixpath.join(self.dm.getDeviceRoot(), 'infratest')
|
||||
self.dm.removeDir(testroot)
|
||||
self.dm.mkDir(testroot)
|
||||
origFile = open(os.path.join('test-files', 'mybinary.zip'), 'rb').read()
|
||||
self.dm.pushFile(
|
||||
os.path.join('test-files', 'mybinary.zip'),
|
||||
posixpath.join(testroot, 'mybinary.zip'))
|
||||
resultFile = self.dm.catFile(posixpath.join(testroot, 'mybinary.zip'))
|
||||
self.assertEqual(hashlib.md5(origFile).hexdigest(),
|
||||
hashlib.md5(resultFile).hexdigest())
|
|
@ -2,32 +2,34 @@
|
|||
# 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
|
||||
import re
|
||||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
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.
|
||||
""" This tests the heartbeat and the data channel
|
||||
"""
|
||||
ip = self.dm.host
|
||||
port = 20700
|
||||
|
||||
# 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._datasock.connect((ip, port))
|
||||
self._connected = True
|
||||
|
||||
# Let's listen
|
||||
numbeats = 0
|
||||
capturedHeader = False
|
||||
while numbeats < 3:
|
||||
while(numbeats < 3):
|
||||
data = self._datasock.recv(1024)
|
||||
print data
|
||||
self.assertNotEqual(len(data), 0)
|
||||
|
@ -37,6 +39,7 @@ class DataChannelTestCase(DeviceManagerTestCase):
|
|||
m = re.match(r"(.*?) trace output", data)
|
||||
self.assertNotEqual(m, None,
|
||||
'trace output line does not match. The line: ' + str(data))
|
||||
lastHeartbeatTime = strptime(m.group(1), "%Y%m%d-%H:%M:%S")
|
||||
capturedHeader = True
|
||||
|
||||
# Check for standard heartbeat messsage
|
||||
|
@ -49,4 +52,5 @@ class DataChannelTestCase(DeviceManagerTestCase):
|
|||
# Ensure it matches our format
|
||||
mHeartbeatTime = m.group(1)
|
||||
mHeartbeatTime = strptime(mHeartbeatTime, "%Y%m%d-%H:%M:%S")
|
||||
mDeviceID = m.group(2)
|
||||
numbeats = numbeats + 1
|
||||
|
|
|
@ -2,22 +2,22 @@
|
|||
# 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
|
||||
import posixpath
|
||||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
class ExecTestCase(DeviceManagerTestCase):
|
||||
|
||||
class ProcessListTestCase(DeviceManagerTestCase):
|
||||
|
||||
def runTest(self):
|
||||
"""Simple exec test, does not use env vars."""
|
||||
""" simple exec test, does not use env vars """
|
||||
out = StringIO()
|
||||
filename = posixpath.join(self.dm.getDeviceRoot(), 'test_exec_file')
|
||||
# Make sure the file was not already there
|
||||
# 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.dm.shell(['touch', filename], out)
|
||||
# check that the file has been created
|
||||
self.assertTrue(self.dm.fileExists(filename))
|
||||
# Clean up
|
||||
# clean up
|
||||
self.dm.removeFile(filename)
|
||||
|
|
|
@ -2,30 +2,31 @@
|
|||
# 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 StringIO import StringIO
|
||||
import os
|
||||
import posixpath
|
||||
from StringIO import StringIO
|
||||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
class ExecEnvTestCase(DeviceManagerTestCase):
|
||||
|
||||
class ProcessListTestCase(DeviceManagerTestCase):
|
||||
|
||||
def runTest(self):
|
||||
"""Exec test with env vars."""
|
||||
# Push the file
|
||||
""" simple exec test, does not use env vars """
|
||||
# push the file
|
||||
localfile = os.path.join('test-files', 'test_script.sh')
|
||||
remotefile = posixpath.join(self.dm.getDeviceRoot(), 'test_script.sh')
|
||||
self.dm.pushFile(localfile, remotefile)
|
||||
|
||||
# Run the cmd
|
||||
# run the cmd
|
||||
out = StringIO()
|
||||
self.dm.shell(['sh', remotefile], out, env={'THE_ANSWER': 42})
|
||||
|
||||
# Rewind the output file
|
||||
# rewind the output file
|
||||
out.seek(0)
|
||||
# Make sure first line is 42
|
||||
# make sure first line is 42
|
||||
line = out.readline()
|
||||
self.assertTrue(int(line) == 42)
|
||||
|
||||
# Clean up
|
||||
# clean up
|
||||
self.dm.removeFile(remotefile)
|
||||
|
|
|
@ -7,9 +7,9 @@ import posixpath
|
|||
import shutil
|
||||
import tempfile
|
||||
|
||||
from mozdevice.devicemanager import DMError
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
|
||||
class GetDirectoryTestCase(DeviceManagerTestCase):
|
||||
|
||||
def _setUp(self):
|
||||
|
@ -38,13 +38,17 @@ class GetDirectoryTestCase(DeviceManagerTestCase):
|
|||
# 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'))
|
||||
filelist = self.dm.getDirectory(
|
||||
posixpath.join(testroot, 'push1'),
|
||||
os.path.join(self.localdestdir, 'push1'))
|
||||
filelist.sort()
|
||||
self.assertEqual(filelist, self.expected_filelist)
|
||||
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'))
|
||||
filelist = self.dm.getDirectory('/dummy',
|
||||
os.path.join(self.localdestdir, '/none'))
|
||||
self.assertEqual(filelist, None)
|
||||
self.assertFalse(os.path.exists(self.localdestdir + '/none'))
|
||||
|
|
|
@ -4,16 +4,22 @@
|
|||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
|
||||
class InfoTestCase(DeviceManagerTestCase):
|
||||
|
||||
runs_on_test_device = False
|
||||
|
||||
def runTest(self):
|
||||
"""This tests the "info" command.
|
||||
""" This tests the "info" command
|
||||
"""
|
||||
cmds = ('os', 'id', 'systime', 'uptime', 'screen', 'memory', 'power')
|
||||
cmds = ('os', 'id', 'systime', 'uptime', 'screen',
|
||||
'memory', 'power')
|
||||
for c in cmds:
|
||||
data = self.dm.getInfo(c)
|
||||
print c + str(data)
|
||||
|
||||
print " ==== Now we call them all ===="
|
||||
#data = self.dm.getInfo('all')
|
||||
#print str(data)
|
||||
|
||||
# No real good way to verify this. If it doesn't throw, we're ok.
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# 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 IsDirTestCase(DeviceManagerTestCase):
|
||||
|
||||
def runTest(self):
|
||||
"""This tests the isDir() function.
|
||||
"""
|
||||
testroot = posixpath.join(self.dm.getDeviceRoot(), 'infratest')
|
||||
self.dm.removeDir(testroot)
|
||||
self.dm.mkDir(testroot)
|
||||
self.assertTrue(self.dm.isDir(testroot))
|
||||
testdir = posixpath.join(testroot, 'testdir')
|
||||
self.assertFalse(self.dm.isDir(testdir))
|
||||
self.dm.mkDir(testdir)
|
||||
self.assertTrue(self.dm.isDir(testdir))
|
||||
self.dm.pushFile(os.path.join('test-files', 'mytext.txt'),
|
||||
posixpath.join(testdir, 'mytext.txt'))
|
||||
self.assertFalse(self.dm.isDir(posixpath.join(testdir, 'mytext.txt')))
|
||||
self.dm.removeDir(testroot)
|
||||
self.assertFalse(self.dm.isDir(testroot))
|
||||
self.assertFalse(self.dm.isDir(testdir))
|
||||
self.assertFalse(self.dm.isDir(posixpath.join(testdir, 'mytext.txt')))
|
||||
self.assertFalse(self.dm.isDir(posixpath.join('/', 'noroot', 'nosub')))
|
|
@ -7,6 +7,7 @@ import socket
|
|||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
|
||||
class PromptTestCase(DeviceManagerTestCase):
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -26,4 +27,4 @@ class PromptTestCase(DeviceManagerTestCase):
|
|||
self.sock.connect((ip, int(port)))
|
||||
data = self.sock.recv(1024)
|
||||
print data
|
||||
self.assertTrue(promptre.match(data))
|
||||
self.assert_(promptre.match(data))
|
||||
|
|
|
@ -2,26 +2,27 @@
|
|||
# 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
|
||||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
|
||||
class ProcessListTestCase(DeviceManagerTestCase):
|
||||
|
||||
def runTest(self):
|
||||
"""This tests getting a process list from the device.
|
||||
""" 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>], ...]
|
||||
# [[<procid>,<procname>],[<procid>,<procname>]...]
|
||||
# on android the userID is affixed to the process array:
|
||||
# [[<procid>, <procname>, <userid>], ...]
|
||||
# [[<procid>, <procname>, <userid>]...]
|
||||
procid = re.compile('^[a-f0-9]+')
|
||||
procname = re.compile('.+')
|
||||
|
||||
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)
|
||||
|
||||
self.assert_(procid.match(item[0]))
|
||||
self.assert_(procname.match(item[1]))
|
||||
|
|
|
@ -7,27 +7,27 @@ 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()
|
||||
m_orig = hashlib.md5()
|
||||
m_new = hashlib.md5()
|
||||
local_test_file = os.path.join('test-files', 'mybinary.zip')
|
||||
orig.update(file(local_test_file, 'r').read())
|
||||
m_orig.update(file(local_test_file, 'r').read())
|
||||
|
||||
testroot = self.dm.getDeviceRoot()
|
||||
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
|
||||
m_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())
|
||||
self.assertEqual(m_orig.hexdigest(), m_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)
|
||||
self.dm.removeFile(remote_missing_file) # just to be sure
|
||||
self.assertEqual(self.dm.pullFile(remote_missing_file), None)
|
||||
|
|
|
@ -7,10 +7,11 @@ import posixpath
|
|||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
|
||||
class Push1TestCase(DeviceManagerTestCase):
|
||||
|
||||
def runTest(self):
|
||||
"""This tests copying a directory structure to the device.
|
||||
""" This tests copying a directory structure to the device
|
||||
"""
|
||||
dvroot = self.dm.getDeviceRoot()
|
||||
dvpath = posixpath.join(dvroot, 'infratest')
|
||||
|
@ -29,9 +30,11 @@ class Push1TestCase(DeviceManagerTestCase):
|
|||
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()
|
||||
|
||||
# push the directory
|
||||
self.dm.pushDir(p1, posixpath.join(dvpath, 'push1'))
|
||||
|
||||
self.assertTrue(
|
||||
# verify
|
||||
self.assert_(
|
||||
self.dm.dirExists(posixpath.join(dvpath, 'push1', 'sub.1')))
|
||||
self.assertTrue(self.dm.dirExists(
|
||||
self.assert_(self.dm.dirExists(
|
||||
posixpath.join(dvpath, 'push1', 'sub.1', 'sub.2')))
|
||||
|
|
|
@ -7,10 +7,11 @@ import posixpath
|
|||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
|
||||
class Push2TestCase(DeviceManagerTestCase):
|
||||
|
||||
def runTest(self):
|
||||
"""This tests copying a directory structure with files to the device.
|
||||
""" This tests copying a directory structure with files to the device
|
||||
"""
|
||||
testroot = posixpath.join(self.dm.getDeviceRoot(), 'infratest')
|
||||
self.dm.removeDir(testroot)
|
||||
|
@ -22,17 +23,17 @@ class Push2TestCase(DeviceManagerTestCase):
|
|||
# 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.assert_(
|
||||
self.dm.dirExists(posixpath.join(testroot, 'push2', 'sub1')))
|
||||
self.assertTrue(self.dm.validateFile(
|
||||
self.assert_(self.dm.validateFile(
|
||||
posixpath.join(testroot, 'push2', 'sub1', 'file1.txt'),
|
||||
os.path.join('test-files', 'push2', 'sub1', 'file1.txt')))
|
||||
self.assertTrue(self.dm.validateFile(
|
||||
self.assert_(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(
|
||||
self.assert_(self.dm.validateFile(
|
||||
posixpath.join(testroot, 'push2', 'sub2', 'file3.txt'),
|
||||
os.path.join('test-files', 'push2', 'sub2', 'file3.txt')))
|
||||
self.assertTrue(self.dm.validateFile(
|
||||
self.assert_(self.dm.validateFile(
|
||||
posixpath.join(testroot, 'push2', 'file4.bin'),
|
||||
os.path.join('test-files', 'push2', 'file4.bin')))
|
||||
|
|
|
@ -7,6 +7,7 @@ import posixpath
|
|||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
|
||||
class PushBinaryTestCase(DeviceManagerTestCase):
|
||||
|
||||
def runTest(self):
|
||||
|
@ -14,5 +15,6 @@ class PushBinaryTestCase(DeviceManagerTestCase):
|
|||
"""
|
||||
testroot = self.dm.getDeviceRoot()
|
||||
self.dm.removeFile(posixpath.join(testroot, 'mybinary.zip'))
|
||||
self.dm.pushFile(os.path.join('test-files', 'mybinary.zip'),
|
||||
posixpath.join(testroot, 'mybinary.zip'))
|
||||
self.assert_(self.dm.pushFile(
|
||||
os.path.join('test-files', 'mybinary.zip'),
|
||||
posixpath.join(testroot, 'mybinary.zip')))
|
||||
|
|
|
@ -7,6 +7,7 @@ import posixpath
|
|||
|
||||
from dmunit import DeviceManagerTestCase
|
||||
|
||||
|
||||
class PushSmallTextTestCase(DeviceManagerTestCase):
|
||||
|
||||
def runTest(self):
|
||||
|
@ -14,5 +15,6 @@ class PushSmallTextTestCase(DeviceManagerTestCase):
|
|||
"""
|
||||
testroot = self.dm.getDeviceRoot()
|
||||
self.dm.removeFile(posixpath.join(testroot, 'smalltext.txt'))
|
||||
self.dm.pushFile(os.path.join('test-files', 'smalltext.txt'),
|
||||
posixpath.join(testroot, 'smalltext.txt'))
|
||||
self.assert_(self.dm.pushFile(
|
||||
os.path.join('test-files', 'smalltext.txt'),
|
||||
posixpath.join(testroot, 'smalltext.txt')))
|
||||
|
|
|
@ -1,63 +1,41 @@
|
|||
# Any copyright is dedicated to the Public Domain.
|
||||
# http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
from sut import MockAgent
|
||||
import mozdevice
|
||||
import unittest
|
||||
from sut import MockAgent
|
||||
|
||||
class MkDirsTest(unittest.TestCase):
|
||||
class PushTest(unittest.TestCase):
|
||||
|
||||
def test_mkdirs(self):
|
||||
subTests = [{'cmds': [('isdir /mnt/sdcard/baz/boop', 'FALSE'),
|
||||
('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'),
|
||||
('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},
|
||||
subTests = [ { 'cmds': [ ("isdir /mnt/sdcard/baz/boop", "FALSE"),
|
||||
("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"),
|
||||
("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'])
|
||||
a = MockAgent(self, commands = subTest['cmds'])
|
||||
|
||||
exceptionThrown = False
|
||||
try:
|
||||
mozdevice.DroidSUT.debug = 4
|
||||
d = mozdevice.DroidSUT('127.0.0.1', port=a.port)
|
||||
d.mkDirs('/mnt/sdcard/baz/boop/bip')
|
||||
except mozdevice.DMError:
|
||||
d = mozdevice.DroidSUT("127.0.0.1", port=a.port)
|
||||
d.mkDirs("/mnt/sdcard/baz/boop/bip")
|
||||
except mozdevice.DMError, e:
|
||||
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'),
|
||||
('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)
|
||||
mozdevice.DroidSUT.debug = 4
|
||||
d = mozdevice.DroidSUT('127.0.0.1', port=a.port)
|
||||
d.mkDirs('/mnt/sdcard/foo/foo')
|
||||
a.wait()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
mozfile is a convenience library for taking care of some common file-related
|
||||
tasks in automated testing, such as extracting files or recursively removing
|
||||
directories.
|
||||
|
|
@ -5,10 +5,9 @@
|
|||
import os
|
||||
import tarfile
|
||||
import tempfile
|
||||
import urlparse
|
||||
import zipfile
|
||||
|
||||
__all__ = ['extract_tarball', 'extract_zip', 'extract', 'is_url', 'rmtree', 'NamedTemporaryFile']
|
||||
__all__ = ['extract_tarball', 'extract_zip', 'extract', 'rmtree', 'NamedTemporaryFile']
|
||||
|
||||
|
||||
### utilities for extracting archives
|
||||
|
@ -28,15 +27,7 @@ def extract_tarball(src, dest):
|
|||
def extract_zip(src, dest):
|
||||
"""extract a zip file"""
|
||||
|
||||
if isinstance(src, zipfile.ZipFile):
|
||||
bundle = src
|
||||
else:
|
||||
try:
|
||||
bundle = zipfile.ZipFile(src)
|
||||
except Exception, e:
|
||||
print "src: %s" % src
|
||||
raise
|
||||
|
||||
bundle = zipfile.ZipFile(src)
|
||||
namelist = bundle.namelist()
|
||||
|
||||
for name in namelist:
|
||||
|
@ -187,15 +178,3 @@ class NamedTemporaryFile(object):
|
|||
|
||||
self.file.__exit__(None, None, None)
|
||||
os.unlink(self.__dict__['_path'])
|
||||
|
||||
|
||||
def is_url(thing):
|
||||
"""
|
||||
Return True if thing looks like a URL.
|
||||
"""
|
||||
|
||||
parsed = urlparse.urlparse(thing)
|
||||
if 'scheme' in parsed:
|
||||
return len(parsed.scheme) >= 2
|
||||
else:
|
||||
return len(parsed[0]) >= 2
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
from setuptools import setup
|
||||
|
||||
PACKAGE_VERSION = '0.3'
|
||||
PACKAGE_VERSION = '0.2'
|
||||
|
||||
setup(name='mozfile',
|
||||
version=PACKAGE_VERSION,
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
tests for is_url
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from mozfile import is_url
|
||||
|
||||
class TestIsUrl(unittest.TestCase):
|
||||
"""test the is_url function"""
|
||||
|
||||
def test_is_url(self):
|
||||
self.assertTrue(is_url('http://mozilla.org'))
|
||||
self.assertFalse(is_url('/usr/bin/mozilla.org'))
|
||||
self.assertTrue(is_url('file:///usr/bin/mozilla.org'))
|
||||
self.assertFalse(is_url('c:\foo\bar'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,2 +1 @@
|
|||
[test.py]
|
||||
[is_url.py]
|
|
@ -0,0 +1,168 @@
|
|||
[mozprocess](https://github.com/mozilla/mozbase/tree/master/mozprocess)
|
||||
provides python process management via an operating system
|
||||
and platform transparent interface to Mozilla platforms of interest.
|
||||
Mozprocess aims to provide the ability
|
||||
to robustly terminate a process (by timeout or otherwise), along with
|
||||
any child processes, on Windows, OS X, and Linux. Mozprocess utilizes
|
||||
and extends `subprocess.Popen` to these ends.
|
||||
|
||||
|
||||
# API
|
||||
|
||||
[mozprocess.processhandler:ProcessHandler](https://github.com/mozilla/mozbase/blob/master/mozprocess/mozprocess/processhandler.py)
|
||||
is the central exposed API for mozprocess. `ProcessHandler` utilizes
|
||||
a contained subclass of [subprocess.Popen](http://docs.python.org/library/subprocess.html),
|
||||
`Process`, which does the brunt of the process management.
|
||||
|
||||
## Basic usage
|
||||
|
||||
process = ProcessHandler(['command', '-line', 'arguments'],
|
||||
cwd=None, # working directory for cmd; defaults to None
|
||||
env={}, # environment to use for the process; defaults to os.environ
|
||||
)
|
||||
process.run(timeout=60) # seconds
|
||||
process.wait()
|
||||
|
||||
`ProcessHandler` offers several other properties and methods as part of its API:
|
||||
|
||||
def __init__(self,
|
||||
cmd,
|
||||
args=None,
|
||||
cwd=None,
|
||||
env=None,
|
||||
ignore_children = False,
|
||||
processOutputLine=(),
|
||||
onTimeout=(),
|
||||
onFinish=(),
|
||||
**kwargs):
|
||||
"""
|
||||
cmd = Command to run
|
||||
args = array of arguments (defaults to None)
|
||||
cwd = working directory for cmd (defaults to None)
|
||||
env = environment to use for the process (defaults to os.environ)
|
||||
ignore_children = when True, causes system to ignore child processes,
|
||||
defaults to False (which tracks child processes)
|
||||
processOutputLine = handlers to process the output line
|
||||
onTimeout = handlers for timeout event
|
||||
kwargs = keyword args to pass directly into Popen
|
||||
|
||||
NOTE: Child processes will be tracked by default. If for any reason
|
||||
we are unable to track child processes and ignore_children is set to False,
|
||||
then we will fall back to only tracking the root process. The fallback
|
||||
will be logged.
|
||||
"""
|
||||
|
||||
@property
|
||||
def timedOut(self):
|
||||
"""True if the process has timed out."""
|
||||
|
||||
|
||||
def run(self, timeout=None, outputTimeout=None):
|
||||
"""
|
||||
Starts the process.
|
||||
|
||||
If timeout is not None, the process will be allowed to continue for
|
||||
that number of seconds before being killed.
|
||||
|
||||
If outputTimeout is not None, the process will be allowed to continue
|
||||
for that number of seconds without producing any output before
|
||||
being killed.
|
||||
"""
|
||||
|
||||
def kill(self):
|
||||
"""
|
||||
Kills the managed process and if you created the process with
|
||||
'ignore_children=False' (the default) then it will also
|
||||
also kill all child processes spawned by it.
|
||||
If you specified 'ignore_children=True' when creating the process,
|
||||
only the root process will be killed.
|
||||
|
||||
Note that this does not manage any state, save any output etc,
|
||||
it immediately kills the process.
|
||||
"""
|
||||
|
||||
def readWithTimeout(self, f, timeout):
|
||||
"""
|
||||
Try to read a line of output from the file object |f|.
|
||||
|f| must be a pipe, like the |stdout| member of a subprocess.Popen
|
||||
object created with stdout=PIPE. If no output
|
||||
is received within |timeout| seconds, return a blank line.
|
||||
Returns a tuple (line, did_timeout), where |did_timeout| is True
|
||||
if the read timed out, and False otherwise.
|
||||
|
||||
Calls a private member because this is a different function based on
|
||||
the OS
|
||||
"""
|
||||
|
||||
def processOutputLine(self, line):
|
||||
"""Called for each line of output that a process sends to stdout/stderr."""
|
||||
for handler in self.processOutputLineHandlers:
|
||||
handler(line)
|
||||
|
||||
def onTimeout(self):
|
||||
"""Called when a process times out."""
|
||||
for handler in self.onTimeoutHandlers:
|
||||
handler()
|
||||
|
||||
def onFinish(self):
|
||||
"""Called when a process finishes without a timeout."""
|
||||
for handler in self.onFinishHandlers:
|
||||
handler()
|
||||
|
||||
def wait(self, timeout=None):
|
||||
"""
|
||||
Waits until all output has been read and the process is
|
||||
terminated.
|
||||
|
||||
If timeout is not None, will return after timeout seconds.
|
||||
This timeout only causes the wait function to return and
|
||||
does not kill the process.
|
||||
"""
|
||||
|
||||
See https://github.com/mozilla/mozbase/blob/master/mozprocess/mozprocess/processhandler.py
|
||||
for the python implementation.
|
||||
|
||||
`ProcessHandler` extends `ProcessHandlerMixin` which by default prints the
|
||||
output, logs to a file (if specified), and stores the output (if specified, by
|
||||
default `True`). `ProcessHandlerMixin`, by default, does none of these things
|
||||
and has no handlers for `onTimeout`, `processOutput`, or `onFinish`.
|
||||
|
||||
`ProcessHandler` may be subclassed to handle process timeouts (by overriding
|
||||
the `onTimeout()` method), process completion (by overriding
|
||||
`onFinish()`), and to process the command output (by overriding
|
||||
`processOutputLine()`).
|
||||
|
||||
## Examples
|
||||
|
||||
In the most common case, a process_handler is created, then run followed by wait are called:
|
||||
|
||||
proc_handler = ProcessHandler([cmd, args])
|
||||
proc_handler.run(outputTimeout=60) # will time out after 60 seconds without output
|
||||
proc_handler.wait()
|
||||
|
||||
Often, the main thread will do other things:
|
||||
|
||||
proc_handler = ProcessHandler([cmd, args])
|
||||
proc_handler.run(timeout=60) # will time out after 60 seconds regardless of output
|
||||
do_other_work()
|
||||
|
||||
if proc_handler.proc.poll() is None:
|
||||
proc_handler.wait()
|
||||
|
||||
By default output is printed to stdout, but anything is possible:
|
||||
|
||||
# this example writes output to both stderr and a file called 'output.log'
|
||||
def some_func(line):
|
||||
print >> sys.stderr, line
|
||||
|
||||
with open('output.log', 'a') as log:
|
||||
log.write('%s\n' % line)
|
||||
|
||||
proc_handler = ProcessHandler([cmd, args], processOutputLine=some_func)
|
||||
proc_handler.run()
|
||||
proc_handler.wait()
|
||||
|
||||
# TODO
|
||||
|
||||
- Document improvements over `subprocess.Popen.kill`
|
||||
- Introduce test the show improvements over `subprocess.Popen.kill`
|
|
@ -2,14 +2,22 @@
|
|||
# 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
|
||||
from setuptools import setup
|
||||
|
||||
PACKAGE_VERSION = '0.9'
|
||||
PACKAGE_VERSION = '0.8'
|
||||
|
||||
# take description from README
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
try:
|
||||
description = file(os.path.join(here, 'README.md')).read()
|
||||
except (OSError, IOError):
|
||||
description = ''
|
||||
|
||||
setup(name='mozprocess',
|
||||
version=PACKAGE_VERSION,
|
||||
description="Mozilla-authored process handling",
|
||||
long_description='see http://mozbase.readthedocs.org/',
|
||||
long_description=description,
|
||||
classifiers=['Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
[test_mozprocess.py]
|
||||
[mozprocess1.py]
|
||||
[mozprocess2.py]
|
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from time import sleep
|
||||
|
||||
from mozprocess import processhandler
|
||||
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def make_proclaunch(aDir):
|
||||
"""
|
||||
Makes the proclaunch executable.
|
||||
Params:
|
||||
aDir - the directory in which to issue the make commands
|
||||
Returns:
|
||||
the path to the proclaunch executable that is generated
|
||||
"""
|
||||
# Ideally make should take care of this, but since it doesn't - on windows,
|
||||
# anyway, let's just call out both targets explicitly.
|
||||
p = subprocess.call(["make", "-C", "iniparser"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=aDir)
|
||||
p = subprocess.call(["make"],stdout=subprocess.PIPE, stderr=subprocess.PIPE ,cwd=aDir)
|
||||
if sys.platform == "win32":
|
||||
exepath = os.path.join(aDir, "proclaunch.exe")
|
||||
else:
|
||||
exepath = os.path.join(aDir, "proclaunch")
|
||||
return exepath
|
||||
|
||||
def check_for_process(processName):
|
||||
"""
|
||||
Use to determine if process of the given name is still running.
|
||||
|
||||
Returns:
|
||||
detected -- True if process is detected to exist, False otherwise
|
||||
output -- if process exists, stdout of the process, '' otherwise
|
||||
"""
|
||||
output = ''
|
||||
if sys.platform == "win32":
|
||||
# On windows we use tasklist
|
||||
p1 = subprocess.Popen(["tasklist"], stdout=subprocess.PIPE)
|
||||
output = p1.communicate()[0]
|
||||
detected = False
|
||||
for line in output.splitlines():
|
||||
if processName in line:
|
||||
detected = True
|
||||
break
|
||||
else:
|
||||
p1 = subprocess.Popen(["ps", "-ef"], stdout=subprocess.PIPE)
|
||||
p2 = subprocess.Popen(["grep", processName], stdin=p1.stdout, stdout=subprocess.PIPE)
|
||||
p1.stdout.close()
|
||||
output = p2.communicate()[0]
|
||||
detected = False
|
||||
for line in output.splitlines():
|
||||
if "grep %s" % processName in line:
|
||||
continue
|
||||
elif processName in line and not 'defunct' in line:
|
||||
detected = True
|
||||
break
|
||||
|
||||
return detected, output
|
||||
|
||||
|
||||
class ProcTest1(unittest.TestCase):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# Ideally, I'd use setUpClass but that only exists in 2.7.
|
||||
# So, we'll do this make step now.
|
||||
self.proclaunch = make_proclaunch(here)
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
def test_process_normal_finish(self):
|
||||
"""Process is started, runs to completion while we wait for it"""
|
||||
|
||||
p = processhandler.ProcessHandler([self.proclaunch, "process_normal_finish.ini"],
|
||||
cwd=here)
|
||||
p.run()
|
||||
p.wait()
|
||||
|
||||
detected, output = check_for_process(self.proclaunch)
|
||||
self.determine_status(detected,
|
||||
output,
|
||||
p.proc.returncode,
|
||||
p.didTimeout)
|
||||
|
||||
def test_process_waittimeout(self):
|
||||
""" Process is started, runs but we time out waiting on it
|
||||
to complete
|
||||
"""
|
||||
p = processhandler.ProcessHandler([self.proclaunch, "process_waittimeout.ini"],
|
||||
cwd=here)
|
||||
p.run(timeout=10)
|
||||
p.wait()
|
||||
|
||||
detected, output = check_for_process(self.proclaunch)
|
||||
self.determine_status(detected,
|
||||
output,
|
||||
p.proc.returncode,
|
||||
p.didTimeout,
|
||||
False,
|
||||
['returncode', 'didtimeout'])
|
||||
|
||||
def test_process_kill(self):
|
||||
""" Process is started, we kill it
|
||||
"""
|
||||
p = processhandler.ProcessHandler([self.proclaunch, "process_normal_finish.ini"],
|
||||
cwd=here)
|
||||
p.run()
|
||||
p.kill()
|
||||
|
||||
detected, output = check_for_process(self.proclaunch)
|
||||
self.determine_status(detected,
|
||||
output,
|
||||
p.proc.returncode,
|
||||
p.didTimeout)
|
||||
|
||||
def determine_status(self,
|
||||
detected=False,
|
||||
output='',
|
||||
returncode=0,
|
||||
didtimeout=False,
|
||||
isalive=False,
|
||||
expectedfail=[]):
|
||||
"""
|
||||
Use to determine if the situation has failed.
|
||||
Parameters:
|
||||
detected -- value from check_for_process to determine if the process is detected
|
||||
output -- string of data from detected process, can be ''
|
||||
returncode -- return code from process, defaults to 0
|
||||
didtimeout -- True if process timed out, defaults to False
|
||||
isalive -- Use True to indicate we pass if the process exists; however, by default
|
||||
the test will pass if the process does not exist (isalive == False)
|
||||
expectedfail -- Defaults to [], used to indicate a list of fields that are expected to fail
|
||||
"""
|
||||
if 'returncode' in expectedfail:
|
||||
self.assertTrue(returncode, "Detected an unexpected return code of: %s" % returncode)
|
||||
elif not isalive:
|
||||
self.assertTrue(returncode == 0, "Detected non-zero return code of: %d" % returncode)
|
||||
|
||||
if 'didtimeout' in expectedfail:
|
||||
self.assertTrue(didtimeout, "Detected that process didn't time out")
|
||||
else:
|
||||
self.assertTrue(not didtimeout, "Detected that process timed out")
|
||||
|
||||
if isalive:
|
||||
self.assertTrue(detected, "Detected process is not running, process output: %s" % output)
|
||||
else:
|
||||
self.assertTrue(not detected, "Detected process is still running, process output: %s" % output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -14,6 +14,11 @@ from mozprocess import processhandler
|
|||
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# This tests specifically the case reported in bug 671316
|
||||
# TODO: Because of the way mutt works we can't just load a utils.py in here.
|
||||
# so, for all process handler tests, copy these two
|
||||
# utility functions to to the top of your source.
|
||||
|
||||
def make_proclaunch(aDir):
|
||||
"""
|
||||
Makes the proclaunch executable.
|
||||
|
@ -22,10 +27,10 @@ def make_proclaunch(aDir):
|
|||
Returns:
|
||||
the path to the proclaunch executable that is generated
|
||||
"""
|
||||
# Ideally make should take care of this, but since it doesn't,
|
||||
# on windows anyway, let's just call out both targets explicitly.
|
||||
p = subprocess.call(["make", "-C", "iniparser"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=aDir)
|
||||
p = subprocess.call(["make"],stdout=subprocess.PIPE, stderr=subprocess.PIPE ,cwd=aDir)
|
||||
# Ideally make should take care of this, but since it doesn't - on windows,
|
||||
# anyway, let's just call out both targets explicitly.
|
||||
p = subprocess.call(["make", "-C", "iniparser"],stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=aDir)
|
||||
p = subprocess.call(["make"],stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=aDir)
|
||||
if sys.platform == "win32":
|
||||
exepath = os.path.join(aDir, "proclaunch.exe")
|
||||
else:
|
||||
|
@ -34,16 +39,12 @@ def make_proclaunch(aDir):
|
|||
|
||||
def check_for_process(processName):
|
||||
"""
|
||||
Use to determine if process of the given name is still running.
|
||||
Use to determine if process is still running.
|
||||
|
||||
Returns:
|
||||
detected -- True if process is detected to exist, False otherwise
|
||||
output -- if process exists, stdout of the process, '' otherwise
|
||||
"""
|
||||
# TODO: replace with
|
||||
# https://github.com/mozilla/mozbase/blob/master/mozprocess/mozprocess/pid.py
|
||||
# which should be augmented from talos
|
||||
# see https://bugzilla.mozilla.org/show_bug.cgi?id=705864
|
||||
output = ''
|
||||
if sys.platform == "win32":
|
||||
# On windows we use tasklist
|
||||
|
@ -69,41 +70,22 @@ def check_for_process(processName):
|
|||
|
||||
return detected, output
|
||||
|
||||
class ProcTest2(unittest.TestCase):
|
||||
|
||||
class ProcTest(unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.proclaunch = make_proclaunch(here)
|
||||
# Ideally, I'd use setUpClass but that only exists in 2.7.
|
||||
# So, we'll do this make step now.
|
||||
self.proclaunch = make_proclaunch(here)
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
files = [('proclaunch',),
|
||||
('proclaunch.exe',),
|
||||
('iniparser', 'dictionary.o'),
|
||||
('iniparser', 'iniparser.lib'),
|
||||
('iniparser', 'iniparser.o'),
|
||||
('iniparser', 'libiniparser.a'),
|
||||
('iniparser', 'libiniparser.so.0'),
|
||||
]
|
||||
files = [os.path.join(here, *path) for path in files]
|
||||
errors = []
|
||||
for path in files:
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError as e:
|
||||
errors.append(str(e))
|
||||
if errors:
|
||||
raise OSError("Error(s) encountered tearing down %s.%s:\n%s" % (cls.__module__, cls.__name__, '\n'.join(errors)))
|
||||
del cls.proclaunch
|
||||
|
||||
def test_process_normal_finish(self):
|
||||
"""Process is started, runs to completion while we wait for it"""
|
||||
|
||||
p = processhandler.ProcessHandler([self.proclaunch, "process_normal_finish.ini"],
|
||||
def test_process_waitnotimeout(self):
|
||||
""" Process is started, runs to completion before our wait times out
|
||||
"""
|
||||
p = processhandler.ProcessHandler([self.proclaunch,
|
||||
"process_waittimeout_10s.ini"],
|
||||
cwd=here)
|
||||
p.run()
|
||||
p.run(timeout=30)
|
||||
p.wait()
|
||||
|
||||
detected, output = check_for_process(self.proclaunch)
|
||||
|
@ -113,7 +95,8 @@ class ProcTest(unittest.TestCase):
|
|||
p.didTimeout)
|
||||
|
||||
def test_process_wait(self):
|
||||
"""Process is started runs to completion while we wait indefinitely"""
|
||||
""" Process is started runs to completion while we wait indefinitely
|
||||
"""
|
||||
|
||||
p = processhandler.ProcessHandler([self.proclaunch,
|
||||
"process_waittimeout_10s.ini"],
|
||||
|
@ -127,23 +110,6 @@ class ProcTest(unittest.TestCase):
|
|||
p.proc.returncode,
|
||||
p.didTimeout)
|
||||
|
||||
def test_process_timeout(self):
|
||||
""" Process is started, runs but we time out waiting on it
|
||||
to complete
|
||||
"""
|
||||
p = processhandler.ProcessHandler([self.proclaunch, "process_waittimeout.ini"],
|
||||
cwd=here)
|
||||
p.run(timeout=10)
|
||||
p.wait()
|
||||
|
||||
detected, output = check_for_process(self.proclaunch)
|
||||
self.determine_status(detected,
|
||||
output,
|
||||
p.proc.returncode,
|
||||
p.didTimeout,
|
||||
False,
|
||||
['returncode', 'didtimeout'])
|
||||
|
||||
def test_process_waittimeout(self):
|
||||
"""
|
||||
Process is started, then wait is called and times out.
|
||||
|
@ -162,36 +128,7 @@ class ProcTest(unittest.TestCase):
|
|||
p.proc.returncode,
|
||||
p.didTimeout,
|
||||
True,
|
||||
())
|
||||
|
||||
def test_process_waitnotimeout(self):
|
||||
""" Process is started, runs to completion before our wait times out
|
||||
"""
|
||||
p = processhandler.ProcessHandler([self.proclaunch,
|
||||
"process_waittimeout_10s.ini"],
|
||||
cwd=here)
|
||||
p.run(timeout=30)
|
||||
p.wait()
|
||||
|
||||
detected, output = check_for_process(self.proclaunch)
|
||||
self.determine_status(detected,
|
||||
output,
|
||||
p.proc.returncode,
|
||||
p.didTimeout)
|
||||
|
||||
def test_process_kill(self):
|
||||
"""Process is started, we kill it"""
|
||||
|
||||
p = processhandler.ProcessHandler([self.proclaunch, "process_normal_finish.ini"],
|
||||
cwd=here)
|
||||
p.run()
|
||||
p.kill()
|
||||
|
||||
detected, output = check_for_process(self.proclaunch)
|
||||
self.determine_status(detected,
|
||||
output,
|
||||
p.proc.returncode,
|
||||
p.didTimeout)
|
||||
[])
|
||||
|
||||
def test_process_output_twice(self):
|
||||
"""
|
||||
|
@ -211,15 +148,16 @@ class ProcTest(unittest.TestCase):
|
|||
p.proc.returncode,
|
||||
p.didTimeout,
|
||||
False,
|
||||
())
|
||||
[])
|
||||
|
||||
|
||||
def determine_status(self,
|
||||
detected=False,
|
||||
output='',
|
||||
returncode=0,
|
||||
didtimeout=False,
|
||||
output = '',
|
||||
returncode = 0,
|
||||
didtimeout = False,
|
||||
isalive=False,
|
||||
expectedfail=()):
|
||||
expectedfail=[]):
|
||||
"""
|
||||
Use to determine if the situation has failed.
|
||||
Parameters:
|
|
@ -0,0 +1,141 @@
|
|||
[Mozprofile](https://github.com/mozilla/mozbase/tree/master/mozprofile)
|
||||
is a python tool for creating and managing profiles for Mozilla's
|
||||
applications (Firefox, Thunderbird, etc.). In addition to creating profiles,
|
||||
mozprofile can install [addons](https://developer.mozilla.org/en/addons)
|
||||
and set
|
||||
[preferences](https://developer.mozilla.org/En/A_Brief_Guide_to_Mozilla_Preferences).
|
||||
Mozprofile can be utilized from the command line or as an API.
|
||||
|
||||
|
||||
# Command Line Usage
|
||||
|
||||
mozprofile may be used to create profiles, set preferences in
|
||||
profiles, or install addons into profiles.
|
||||
|
||||
The profile to be operated on may be specified with the `--profile`
|
||||
switch. If a profile is not specified, one will be created in a
|
||||
temporary directory which will be echoed to the terminal:
|
||||
|
||||
(mozmill)> mozprofile
|
||||
/tmp/tmp4q1iEU.mozrunner
|
||||
(mozmill)> ls /tmp/tmp4q1iEU.mozrunner
|
||||
user.js
|
||||
|
||||
To run mozprofile from the command line enter:
|
||||
`mozprofile --help` for a list of options.
|
||||
|
||||
|
||||
# API Usage
|
||||
|
||||
To use mozprofile as an API you can import
|
||||
[mozprofile.profile](https://github.com/mozilla/mozbase/tree/master/mozprofile/mozprofile/profile.py)
|
||||
and/or the
|
||||
[AddonManager](https://github.com/mozilla/mozbase/tree/master/mozprofile/mozprofile/addons.py).
|
||||
|
||||
`mozprofile.profile` features a generic `Profile` class. In addition,
|
||||
subclasses `FirefoxProfile` and `ThundebirdProfile` are available
|
||||
with preset preferences for those applications.
|
||||
|
||||
`mozprofile.profile:Profile`:
|
||||
|
||||
def __init__(self,
|
||||
profile=None, # Path to the profile
|
||||
addons=None, # String of one or list of addons to install
|
||||
addon_manifests=None, # Manifest for addons, see http://ahal.ca/blog/2011/bulk-installing-fx-addons/
|
||||
preferences=None, # Dictionary or class of preferences
|
||||
locations=None, # locations to proxy
|
||||
proxy=False, # setup a proxy
|
||||
restore=True # If true remove all installed addons preferences when cleaning up
|
||||
):
|
||||
|
||||
def reset(self):
|
||||
"""reset the profile to the beginning state"""
|
||||
|
||||
def set_preferences(self, preferences, filename='user.js'):
|
||||
"""Adds preferences dict to profile preferences"""
|
||||
|
||||
def clean_preferences(self):
|
||||
"""Removed preferences added by mozrunner."""
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup operations for the profile."""
|
||||
|
||||
|
||||
`mozprofile.addons:AddonManager`:
|
||||
|
||||
def __init__(self, profile):
|
||||
"""profile - the path to the profile for which we install addons"""
|
||||
|
||||
def install_addons(self, addons=None, manifests=None):
|
||||
"""
|
||||
Installs all types of addons
|
||||
addons - a list of addon paths to install
|
||||
manifest - a list of addon manifests to install
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_amo_install_path(self, query):
|
||||
"""
|
||||
Return the addon xpi install path for the specified AMO query.
|
||||
See: https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
|
||||
for query documentation.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def addon_details(cls, addon_path):
|
||||
"""
|
||||
returns a dictionary of details about the addon
|
||||
- addon_path : path to the addon directory
|
||||
Returns:
|
||||
{'id': u'rainbow@colors.org', # id of the addon
|
||||
'version': u'1.4', # version of the addon
|
||||
'name': u'Rainbow', # name of the addon
|
||||
'unpack': False } # whether to unpack the addon
|
||||
"""
|
||||
|
||||
def clean_addons(self):
|
||||
"""Cleans up addons in the profile."""
|
||||
|
||||
|
||||
# Installing Addons
|
||||
|
||||
Addons may be installed individually or from a manifest.
|
||||
|
||||
Example:
|
||||
|
||||
from mozprofile import FirefoxProfile
|
||||
|
||||
# create new profile to pass to mozmill/mozrunner
|
||||
profile = FirefoxProfile(addons=["adblock.xpi"])
|
||||
|
||||
|
||||
# Setting Preferences
|
||||
|
||||
Preferences can be set in several ways:
|
||||
|
||||
- using the API: You can pass preferences in to the Profile class's
|
||||
constructor: `obj = FirefoxProfile(preferences=[("accessibility.typeaheadfind.flashBar", 0)])`
|
||||
- using a JSON blob file: `mozprofile --preferences myprefs.json`
|
||||
- using a `.ini` file: `mozprofile --preferences myprefs.ini`
|
||||
- via the command line: `mozprofile --pref key:value --pref key:value [...]`
|
||||
|
||||
When setting preferences from an `.ini` file or the `--pref` switch,
|
||||
the value will be interpolated as an integer or a boolean
|
||||
(`true`/`false`) if possible.
|
||||
|
||||
# Setting Permissions
|
||||
|
||||
mozprofile also takes care of adding permissions to the profile.
|
||||
See https://github.com/mozilla/mozbase/blob/master/mozprofile/mozprofile/permissions.py
|
||||
|
||||
|
||||
# Resources
|
||||
|
||||
Other Mozilla programs offer additional and overlapping functionality
|
||||
for profiles. There is also substantive documentation on profiles and
|
||||
their management.
|
||||
|
||||
- [ProfileManager](https://developer.mozilla.org/en/Profile_Manager) :
|
||||
XULRunner application for managing profiles. Has a GUI and CLI.
|
||||
- [python-profilemanager](http://k0s.org/mozilla/hg/profilemanager/) : python CLI interface similar to ProfileManager
|
||||
- profile documentation : http://support.mozilla.com/en-US/kb/Profiles
|
|
@ -2,16 +2,6 @@
|
|||
# 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/.
|
||||
|
||||
"""
|
||||
To use mozprofile as an API you can import mozprofile.profile_ and/or the AddonManager_.
|
||||
|
||||
``mozprofile.profile`` features a generic ``Profile`` class. In addition,
|
||||
subclasses ``FirefoxProfile`` and ``ThundebirdProfile`` are available
|
||||
with preset preferences for those applications.
|
||||
"""
|
||||
|
||||
from profile import *
|
||||
from addons import *
|
||||
from cli import *
|
||||
from prefs import *
|
||||
from webapps import *
|
||||
|
|
|
@ -16,12 +16,12 @@ AMO_API_VERSION = "1.5"
|
|||
|
||||
class AddonManager(object):
|
||||
"""
|
||||
Handles all operations regarding addons in a profile including: installing and cleaning addons
|
||||
Handles all operations regarding addons including: installing and cleaning addons
|
||||
"""
|
||||
|
||||
def __init__(self, profile):
|
||||
"""
|
||||
:param profile: the path to the profile for which we install addons
|
||||
profile - the path to the profile for which we install addons
|
||||
"""
|
||||
self.profile = profile
|
||||
|
||||
|
@ -33,15 +33,11 @@ class AddonManager(object):
|
|||
# addons that we've installed; needed for cleanup
|
||||
self._addon_dirs = []
|
||||
|
||||
# backup dir for already existing addons
|
||||
self.backup_dir = None
|
||||
|
||||
def install_addons(self, addons=None, manifests=None):
|
||||
"""
|
||||
Installs all types of addons
|
||||
|
||||
:param addons: a list of addon paths to install
|
||||
:param manifest: a list of addon manifests to install
|
||||
addons - a list of addon paths to install
|
||||
manifest - a list of addon manifests to install
|
||||
"""
|
||||
# install addon paths
|
||||
if addons:
|
||||
|
@ -56,12 +52,12 @@ class AddonManager(object):
|
|||
manifests = [manifests]
|
||||
for manifest in manifests:
|
||||
self.install_from_manifest(manifest)
|
||||
self.installed_manifests.extend(manifests)
|
||||
self.installed_manifests.extended(manifests)
|
||||
|
||||
def install_from_manifest(self, filepath):
|
||||
"""
|
||||
Installs addons from a manifest
|
||||
:param filepath: path to the manifest of addons to install
|
||||
filepath - path to the manifest of addons to install
|
||||
"""
|
||||
manifest = ManifestParser()
|
||||
manifest.read(filepath)
|
||||
|
@ -86,11 +82,9 @@ class AddonManager(object):
|
|||
@classmethod
|
||||
def get_amo_install_path(self, query):
|
||||
"""
|
||||
Get the addon xpi install path for the specified AMO query.
|
||||
|
||||
:param query: query-documentation_
|
||||
|
||||
.. _query-documentation: https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
|
||||
Return the addon xpi install path for the specified AMO query.
|
||||
See: https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
|
||||
for query documentation.
|
||||
"""
|
||||
response = urllib2.urlopen(query)
|
||||
dom = minidom.parseString(response.read())
|
||||
|
@ -101,16 +95,13 @@ class AddonManager(object):
|
|||
@classmethod
|
||||
def addon_details(cls, addon_path):
|
||||
"""
|
||||
Returns a dictionary of details about the addon.
|
||||
|
||||
:param addon_path: path to the addon directory
|
||||
|
||||
Returns::
|
||||
|
||||
{'id': u'rainbow@colors.org', # id of the addon
|
||||
'version': u'1.4', # version of the addon
|
||||
'name': u'Rainbow', # name of the addon
|
||||
'unpack': False } # whether to unpack the addon
|
||||
returns a dictionary of details about the addon
|
||||
- addon_path : path to the addon directory
|
||||
Returns:
|
||||
{'id': u'rainbow@colors.org', # id of the addon
|
||||
'version': u'1.4', # version of the addon
|
||||
'name': u'Rainbow', # name of the addon
|
||||
'unpack': False } # whether to unpack the addon
|
||||
"""
|
||||
|
||||
# TODO: We don't use the unpack variable yet, but we should: bug 662683
|
||||
|
@ -161,10 +152,10 @@ class AddonManager(object):
|
|||
|
||||
def install_from_path(self, path, unpack=False):
|
||||
"""
|
||||
Installs addon from a filepath, url or directory of addons in the profile.
|
||||
|
||||
:param path: url, path to .xpi, or directory of addons
|
||||
:param unpack: whether to unpack unless specified otherwise in the install.rdf
|
||||
Installs addon from a filepath, url
|
||||
or directory of addons in the profile.
|
||||
- path: url, path to .xpi, or directory of addons
|
||||
- unpack: whether to unpack unless specified otherwise in the install.rdf
|
||||
"""
|
||||
|
||||
# if the addon is a url, download it
|
||||
|
@ -218,16 +209,8 @@ class AddonManager(object):
|
|||
if not unpack and not addon_details['unpack'] and xpifile:
|
||||
if not os.path.exists(extensions_path):
|
||||
os.makedirs(extensions_path)
|
||||
# save existing xpi file to restore later
|
||||
if os.path.exists(addon_path + '.xpi'):
|
||||
self.backup_dir = self.backup_dir or tempfile.mkdtemp()
|
||||
shutil.copy(addon_path + '.xpi', self.backup_dir)
|
||||
shutil.copy(xpifile, addon_path + '.xpi')
|
||||
else:
|
||||
# save existing dir to restore later
|
||||
if os.path.exists(addon_path):
|
||||
self.backup_dir = self.backup_dir or tempfile.mkdtemp()
|
||||
dir_util.copy_tree(addon_path, self.backup_dir, preserve_symlinks=1)
|
||||
dir_util.copy_tree(addon, addon_path, preserve_symlinks=1)
|
||||
self._addon_dirs.append(addon_path)
|
||||
|
||||
|
@ -244,14 +227,3 @@ class AddonManager(object):
|
|||
for addon in self._addon_dirs:
|
||||
if os.path.isdir(addon):
|
||||
dir_util.remove_tree(addon)
|
||||
# restore backups
|
||||
if self.backup_dir and os.path.isdir(self.backup_dir):
|
||||
extensions_path = os.path.join(self.profile, 'extensions', 'staged')
|
||||
for backup in os.listdir(self.backup_dir):
|
||||
backup_path = os.path.join(self.backup_dir, backup)
|
||||
addon_path = os.path.join(extensions_path, addon)
|
||||
shutil.move(backup_path, addon_path)
|
||||
if not os.listdir(self.backup_dir):
|
||||
shutil.rmtree(self.backup_dir, ignore_errors=True)
|
||||
|
||||
__del__ = clean_addons
|
||||
|
|
|
@ -19,7 +19,6 @@ from profile import Profile
|
|||
__all__ = ['MozProfileCLI', 'cli']
|
||||
|
||||
class MozProfileCLI(object):
|
||||
"""The Command Line Interface for ``mozprofile``."""
|
||||
|
||||
module = 'mozprofile'
|
||||
|
||||
|
@ -76,22 +75,16 @@ class MozProfileCLI(object):
|
|||
|
||||
return prefs()
|
||||
|
||||
def profile(self, restore=False):
|
||||
"""create the profile"""
|
||||
|
||||
kwargs = self.profile_args()
|
||||
kwargs['restore'] = restore
|
||||
return Profile(**kwargs)
|
||||
|
||||
|
||||
def cli(args=sys.argv[1:]):
|
||||
""" Handles the command line arguments for ``mozprofile`` via ``sys.argv``"""
|
||||
|
||||
# process the command line
|
||||
cli = MozProfileCLI(args)
|
||||
|
||||
# create the profile
|
||||
profile = cli.profile()
|
||||
kwargs = cli.profile_args()
|
||||
kwargs['restore'] = False
|
||||
profile = Profile(**kwargs)
|
||||
|
||||
# if no profile was passed in print the newly created profile
|
||||
if not cli.options.profile:
|
||||
|
|
|
@ -23,7 +23,7 @@ import urlparse
|
|||
|
||||
|
||||
class LocationError(Exception):
|
||||
"""Signifies an improperly formed location."""
|
||||
"Signifies an improperly formed location."
|
||||
|
||||
def __str__(self):
|
||||
s = "Bad location"
|
||||
|
@ -33,35 +33,35 @@ class LocationError(Exception):
|
|||
|
||||
|
||||
class MissingPrimaryLocationError(LocationError):
|
||||
"""No primary location defined in locations file."""
|
||||
"No primary location defined in locations file."
|
||||
|
||||
def __init__(self):
|
||||
LocationError.__init__(self, "missing primary location")
|
||||
|
||||
|
||||
class MultiplePrimaryLocationsError(LocationError):
|
||||
"""More than one primary location defined."""
|
||||
"More than one primary location defined."
|
||||
|
||||
def __init__(self):
|
||||
LocationError.__init__(self, "multiple primary locations")
|
||||
|
||||
|
||||
class DuplicateLocationError(LocationError):
|
||||
"""Same location defined twice."""
|
||||
"Same location defined twice."
|
||||
|
||||
def __init__(self, url):
|
||||
LocationError.__init__(self, "duplicate location: %s" % url)
|
||||
|
||||
|
||||
class BadPortLocationError(LocationError):
|
||||
"""Location has invalid port value."""
|
||||
"Location has invalid port value."
|
||||
|
||||
def __init__(self, given_port):
|
||||
LocationError.__init__(self, "bad value for port: %s" % given_port)
|
||||
|
||||
|
||||
class LocationsSyntaxError(Exception):
|
||||
"""Signifies a syntax error on a particular line in server-locations.txt."""
|
||||
"Signifies a syntax error on a particular line in server-locations.txt."
|
||||
|
||||
def __init__(self, lineno, err=None):
|
||||
self.err = err
|
||||
|
@ -77,7 +77,7 @@ class LocationsSyntaxError(Exception):
|
|||
|
||||
|
||||
class Location(object):
|
||||
"""Represents a location line in server-locations.txt."""
|
||||
"Represents a location line in server-locations.txt."
|
||||
|
||||
attrs = ('scheme', 'host', 'port')
|
||||
|
||||
|
@ -91,7 +91,7 @@ class Location(object):
|
|||
raise BadPortLocationError(self.port)
|
||||
|
||||
def isEqual(self, location):
|
||||
"""compare scheme://host:port, but ignore options"""
|
||||
"compare scheme://host:port, but ignore options"
|
||||
return len([i for i in self.attrs if getattr(self, i) == getattr(location, i)]) == len(self.attrs)
|
||||
|
||||
__eq__ = isEqual
|
||||
|
@ -140,13 +140,14 @@ class ServerLocations(object):
|
|||
|
||||
def read(self, filename, check_for_primary=True):
|
||||
"""
|
||||
Reads the file and adds all valid locations to the ``self._locations`` array.
|
||||
Reads the file (in the format of server-locations.txt) and add all
|
||||
valid locations to the self._locations array.
|
||||
|
||||
:param filename: in the format of server-locations.txt_
|
||||
:param check_for_primary: if True, a ``MissingPrimaryLocationError`` exception is raised if no primary is found
|
||||
|
||||
.. _server-locations.txt: http://mxr.mozilla.org/mozilla-central/source/build/pgo/server-locations.txt
|
||||
If check_for_primary is True, a MissingPrimaryLocationError
|
||||
exception is raised if no primary is found.
|
||||
|
||||
This format:
|
||||
http://mxr.mozilla.org/mozilla-central/source/build/pgo/server-locations.txt
|
||||
The only exception is that the port, if not defined, defaults to 80 or 443.
|
||||
|
||||
FIXME: Shouldn't this default to the protocol-appropriate port? Is
|
||||
|
@ -206,8 +207,6 @@ class ServerLocations(object):
|
|||
|
||||
|
||||
class Permissions(object):
|
||||
"""Allows handling of permissions for ``mozprofile``"""
|
||||
|
||||
_num_permissions = 0
|
||||
|
||||
def __init__(self, profileDir, locations=None):
|
||||
|
@ -232,7 +231,6 @@ class Permissions(object):
|
|||
# Open database and create table
|
||||
permDB = sqlite3.connect(os.path.join(self._profileDir, "permissions.sqlite"))
|
||||
cursor = permDB.cursor();
|
||||
cursor.execute("PRAGMA schema_version = 3;")
|
||||
# SQL copied from
|
||||
# http://mxr.mozilla.org/mozilla-central/source/extensions/cookie/nsPermissionManager.cpp
|
||||
cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
|
||||
|
|
|
@ -6,13 +6,9 @@
|
|||
user preferences
|
||||
"""
|
||||
|
||||
__all__ = ('PreferencesReadError', 'Preferences')
|
||||
|
||||
import os
|
||||
import re
|
||||
import tokenize
|
||||
from ConfigParser import SafeConfigParser as ConfigParser
|
||||
from StringIO import StringIO
|
||||
|
||||
try:
|
||||
import json
|
||||
|
@ -33,8 +29,7 @@ class Preferences(object):
|
|||
|
||||
def add(self, prefs, cast=False):
|
||||
"""
|
||||
:param prefs:
|
||||
:param cast: whether to cast strings to value, e.g. '1' -> 1
|
||||
- cast: whether to cast strings to value, e.g. '1' -> 1
|
||||
"""
|
||||
# wants a list of 2-tuples
|
||||
if isinstance(prefs, dict):
|
||||
|
@ -44,10 +39,7 @@ class Preferences(object):
|
|||
self._prefs += prefs
|
||||
|
||||
def add_file(self, path):
|
||||
"""a preferences from a file
|
||||
|
||||
:param path:
|
||||
"""
|
||||
"""a preferences from a file"""
|
||||
self.add(self.read(path))
|
||||
|
||||
def __call__(self):
|
||||
|
@ -59,7 +51,6 @@ class Preferences(object):
|
|||
interpolate a preference from a string
|
||||
from the command line or from e.g. an .ini file, there is no good way to denote
|
||||
what type the preference value is, as natively it is a string
|
||||
|
||||
- integers will get cast to integers
|
||||
- true/false will get cast to True/False
|
||||
- anything enclosed in single quotes will be treated as a string with the ''s removed from both sides
|
||||
|
@ -160,27 +151,18 @@ class Preferences(object):
|
|||
|
||||
comment = re.compile('/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/', re.MULTILINE)
|
||||
|
||||
marker = '##//' # magical marker
|
||||
token = '##//' # magical token
|
||||
lines = [i.strip() for i in file(path).readlines() if i.strip()]
|
||||
_lines = []
|
||||
for line in lines:
|
||||
if line.startswith(('#', '//')):
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
if '//' in line:
|
||||
line = line.replace('//', marker)
|
||||
line = line.replace('//', token)
|
||||
_lines.append(line)
|
||||
string = '\n'.join(_lines)
|
||||
string = re.sub(comment, '', string)
|
||||
|
||||
# skip trailing comments
|
||||
processed_tokens = []
|
||||
f_obj = StringIO(string)
|
||||
for token in tokenize.generate_tokens(f_obj.readline):
|
||||
if token[0] == tokenize.COMMENT:
|
||||
continue
|
||||
processed_tokens.append(token[:2]) # [:2] gets around http://bugs.python.org/issue9974
|
||||
string = tokenize.untokenize(processed_tokens)
|
||||
|
||||
retval = []
|
||||
def pref(a, b):
|
||||
retval.append((a, b))
|
||||
|
@ -195,15 +177,15 @@ class Preferences(object):
|
|||
print line
|
||||
raise
|
||||
|
||||
# de-magic the marker
|
||||
# de-magic the token
|
||||
for index, (key, value) in enumerate(retval):
|
||||
if isinstance(value, basestring) and marker in value:
|
||||
retval[index] = (key, value.replace(marker, '//'))
|
||||
if isinstance(value, basestring) and token in value:
|
||||
retval[index] = (key, value.replace(token, '//'))
|
||||
|
||||
return retval
|
||||
|
||||
@classmethod
|
||||
def write(cls, _file, prefs, pref_string='user_pref("%s", %s);'):
|
||||
def write(_file, prefs, pref_string='user_pref("%s", %s);'):
|
||||
"""write preferences to a file"""
|
||||
|
||||
if isinstance(_file, basestring):
|
||||
|
@ -220,7 +202,7 @@ class Preferences(object):
|
|||
elif value is False:
|
||||
print >> f, pref_string % (key, 'false')
|
||||
elif isinstance(value, basestring):
|
||||
print >> f, pref_string % (key, repr(str(value)))
|
||||
print >> f, pref_string % (key, repr(string(value)))
|
||||
else:
|
||||
print >> f, pref_string % (key, value) # should be numeric!
|
||||
|
||||
|
|
|
@ -7,34 +7,29 @@ __all__ = ['Profile', 'FirefoxProfile', 'ThunderbirdProfile']
|
|||
import os
|
||||
import time
|
||||
import tempfile
|
||||
import types
|
||||
import uuid
|
||||
from addons import AddonManager
|
||||
from permissions import Permissions
|
||||
from shutil import copytree, rmtree
|
||||
from webapps import WebappCollection
|
||||
from shutil import rmtree
|
||||
|
||||
try:
|
||||
import json
|
||||
import simplejson
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
import json as simplejson
|
||||
|
||||
class Profile(object):
|
||||
"""Handles all operations regarding profile. Created new profiles, installs extensions,
|
||||
sets preferences and handles cleanup."""
|
||||
|
||||
def __init__(self, profile=None, addons=None, addon_manifests=None, apps=None,
|
||||
preferences=None, locations=None, proxy=None, restore=True):
|
||||
"""
|
||||
:param profile: Path to the profile
|
||||
:param addons: String of one or list of addons to install
|
||||
:param addon_manifests: Manifest for addons, see http://ahal.ca/blog/2011/bulk-installing-fx-addons/
|
||||
:param apps: Dictionary or class of webapps to install
|
||||
:param preferences: Dictionary or class of preferences
|
||||
:param locations: locations to proxy
|
||||
:param proxy: setup a proxy - dict of server-loc,server-port,ssl-port
|
||||
:param restore: If true remove all installed addons preferences when cleaning up
|
||||
"""
|
||||
def __init__(self,
|
||||
profile=None, # Path to the profile
|
||||
addons=None, # String of one or list of addons to install
|
||||
addon_manifests=None, # Manifest for addons, see http://ahal.ca/blog/2011/bulk-installing-fx-addons/
|
||||
preferences=None, # Dictionary or class of preferences
|
||||
locations=None, # locations to proxy
|
||||
proxy=None, # setup a proxy - dict of server-loc,server-port,ssl-port
|
||||
restore=True # If true remove all installed addons preferences when cleaning up
|
||||
):
|
||||
|
||||
# if true, remove installed addons/prefs afterwards
|
||||
self.restore = restore
|
||||
|
@ -85,10 +80,6 @@ class Profile(object):
|
|||
self.addon_manager = AddonManager(self.profile)
|
||||
self.addon_manager.install_addons(addons, addon_manifests)
|
||||
|
||||
# handle webapps
|
||||
self.webapps = WebappCollection(profile=self.profile, apps=apps)
|
||||
self.webapps.update_manifests()
|
||||
|
||||
def exists(self):
|
||||
"""returns whether the profile exists or not"""
|
||||
return os.path.exists(self.profile)
|
||||
|
@ -109,30 +100,6 @@ class Profile(object):
|
|||
locations=self._locations,
|
||||
proxy = self._proxy)
|
||||
|
||||
@classmethod
|
||||
def clone(cls, path_from, path_to=None, **kwargs):
|
||||
"""Instantiate a temporary profile via cloning
|
||||
- path: path of the basis to clone
|
||||
- kwargs: arguments to the profile constructor
|
||||
"""
|
||||
if not path_to:
|
||||
tempdir = tempfile.mkdtemp() # need an unused temp dir name
|
||||
rmtree(tempdir) # copytree requires that dest does not exist
|
||||
path_to = tempdir
|
||||
copytree(path_from, path_to)
|
||||
|
||||
def cleanup_clone(fn):
|
||||
"""Deletes a cloned profile when restore is True"""
|
||||
def wrapped(self):
|
||||
fn(self)
|
||||
if self.restore and os.path.exists(self.profile):
|
||||
rmtree(self.profile, onerror=self._cleanup_error)
|
||||
return wrapped
|
||||
|
||||
c = cls(path_to, **kwargs)
|
||||
c.__del__ = c.cleanup = types.MethodType(cleanup_clone(cls.cleanup), c)
|
||||
return c
|
||||
|
||||
def create_new_profile(self):
|
||||
"""Create a new clean profile in tmp which is a simple empty folder"""
|
||||
profile = tempfile.mkdtemp(suffix='.mozrunner')
|
||||
|
@ -144,6 +111,7 @@ class Profile(object):
|
|||
def set_preferences(self, preferences, filename='user.js'):
|
||||
"""Adds preferences dict to profile preferences"""
|
||||
|
||||
|
||||
# append to the file
|
||||
prefs_file = os.path.join(self.profile, filename)
|
||||
f = open(prefs_file, 'a')
|
||||
|
@ -160,7 +128,7 @@ class Profile(object):
|
|||
|
||||
# write the preferences
|
||||
f.write('\n%s\n' % self.delimeters[0])
|
||||
_prefs = [(json.dumps(k), json.dumps(v) )
|
||||
_prefs = [(simplejson.dumps(k), simplejson.dumps(v) )
|
||||
for k, v in preferences]
|
||||
for _pref in _prefs:
|
||||
f.write('user_pref(%s, %s);\n' % _pref)
|
||||
|
@ -251,7 +219,6 @@ class Profile(object):
|
|||
self.clean_preferences()
|
||||
self.addon_manager.clean_addons()
|
||||
self.permissions.clean_db()
|
||||
self.webapps.clean()
|
||||
|
||||
__del__ = cleanup
|
||||
|
||||
|
@ -267,8 +234,6 @@ class FirefoxProfile(Profile):
|
|||
'browser.tabs.warnOnClose' : False,
|
||||
# Don't warn when exiting the browser
|
||||
'browser.warnOnQuit': False,
|
||||
# Don't send Firefox health reports to the production server
|
||||
'datareporting.healthreport.documentServerURI' : 'http://%(server)s/healthreport/',
|
||||
# Only install add-ons from the profile and the application scope
|
||||
# Also ensure that those are not getting disabled.
|
||||
# see: https://developer.mozilla.org/en/Installing_extensions
|
||||
|
@ -282,19 +247,13 @@ class FirefoxProfile(Profile):
|
|||
'extensions.update.enabled' : False,
|
||||
# Don't open a dialog to show available add-on updates
|
||||
'extensions.update.notifyUser' : False,
|
||||
# Enable test mode to run multiple tests in parallel
|
||||
'focusmanager.testmode' : True,
|
||||
# Suppress delay for main action in popup notifications
|
||||
'security.notification_enable_delay' : 0,
|
||||
# Suppress automatic safe mode after crashes
|
||||
'toolkit.startup.max_resumed_crashes' : -1,
|
||||
# Don't report telemetry information
|
||||
'toolkit.telemetry.enabled' : False,
|
||||
'toolkit.telemetry.enabledPreRelease' : False,
|
||||
# Enable test mode to run multiple tests in parallel
|
||||
'focusmanager.testmode' : True,
|
||||
}
|
||||
|
||||
class ThunderbirdProfile(Profile):
|
||||
"""Specialized Profile subclass for Thunderbird"""
|
||||
preferences = {'extensions.update.enabled' : False,
|
||||
'extensions.update.notifyUser' : False,
|
||||
'browser.shell.checkDefaultBrowser' : False,
|
||||
|
|
|
@ -1,279 +0,0 @@
|
|||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
"""
|
||||
Handles installing open webapps (https://developer.mozilla.org/en-US/docs/Apps)
|
||||
to a profile. A webapp object is a dict that contains some metadata about
|
||||
the webapp and must at least include a name, description and manifestURL.
|
||||
|
||||
Each webapp has a manifest (https://developer.mozilla.org/en-US/docs/Apps/Manifest).
|
||||
Additionally there is a separate json manifest that keeps track of the installed
|
||||
webapps, their manifestURLs and their permissions.
|
||||
"""
|
||||
|
||||
__all__ = ["Webapp", "WebappCollection", "WebappFormatException", "APP_STATUS_NOT_INSTALLED",
|
||||
"APP_STATUS_INSTALLED", "APP_STATUS_PRIVILEGED", "APP_STATUS_CERTIFIED"]
|
||||
|
||||
from string import Template
|
||||
import os
|
||||
import shutil
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
# from http://hg.mozilla.org/mozilla-central/file/add0b94c2c0b/caps/idl/nsIPrincipal.idl#l163
|
||||
APP_STATUS_NOT_INSTALLED = 0
|
||||
APP_STATUS_INSTALLED = 1
|
||||
APP_STATUS_PRIVILEGED = 2
|
||||
APP_STATUS_CERTIFIED = 3
|
||||
|
||||
class WebappFormatException(Exception):
|
||||
"""thrown for invalid webapp objects"""
|
||||
|
||||
class Webapp(dict):
|
||||
"""A webapp definition"""
|
||||
|
||||
required_keys = ('name', 'description', 'manifestURL')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
dict.__init__(self, *args, **kwargs)
|
||||
except (TypeError, ValueError):
|
||||
raise WebappFormatException("Webapp object should be an instance of type 'dict'")
|
||||
self.validate()
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Webapps are considered equal if they have the same name"""
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self['name'] == other['name']
|
||||
|
||||
def __ne__(self, other):
|
||||
"""Webapps are considered not equal if they have different names"""
|
||||
return not self.__eq__(other)
|
||||
|
||||
def validate(self):
|
||||
# TODO some keys are required if another key has a certain value
|
||||
for key in self.required_keys:
|
||||
if key not in self:
|
||||
raise WebappFormatException("Webapp object missing required key '%s'" % key)
|
||||
|
||||
|
||||
class WebappCollection(object):
|
||||
"""A list-like object that collects webapps and updates the webapp manifests"""
|
||||
|
||||
json_template = Template(""""$name": {
|
||||
"origin": "$origin",
|
||||
"installOrigin": "$origin",
|
||||
"receipt": null,
|
||||
"installTime": 132333986000,
|
||||
"manifestURL": "$manifestURL",
|
||||
"localId": $localId,
|
||||
"id": "$name",
|
||||
"appStatus": $appStatus,
|
||||
"csp": "$csp"
|
||||
}""")
|
||||
|
||||
manifest_template = Template("""{
|
||||
"name": "$name",
|
||||
"csp": "$csp",
|
||||
"description": "$description",
|
||||
"launch_path": "/",
|
||||
"developer": {
|
||||
"name": "Mozilla",
|
||||
"url": "https://mozilla.org/"
|
||||
},
|
||||
"permissions": [
|
||||
],
|
||||
"locales": {
|
||||
"en-US": {
|
||||
"name": "$name",
|
||||
"description": "$description"
|
||||
}
|
||||
},
|
||||
"default_locale": "en-US",
|
||||
"icons": {
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
def __init__(self, profile, apps=None, json_template=None, manifest_template=None):
|
||||
"""
|
||||
:param profile: the file path to a profile
|
||||
:param apps: [optional] a list of webapp objects or file paths to json files describing webapps
|
||||
:param json_template: [optional] string template describing the webapp json format
|
||||
:param manifest_template: [optional] string template describing the webapp manifest format
|
||||
"""
|
||||
if not isinstance(profile, basestring):
|
||||
raise TypeError("Must provide path to a profile, received '%s'" % type(profile))
|
||||
self.profile = profile
|
||||
self.webapps_dir = os.path.join(self.profile, 'webapps')
|
||||
self.backup_dir = os.path.join(self.profile, '.mozprofile_backup', 'webapps')
|
||||
|
||||
self._apps = []
|
||||
self._installed_apps = []
|
||||
if apps:
|
||||
if not isinstance(apps, (list, set, tuple)):
|
||||
apps = [apps]
|
||||
|
||||
for app in apps:
|
||||
if isinstance(app, basestring) and os.path.isfile(app):
|
||||
self.extend(self.read_json(app))
|
||||
else:
|
||||
self.append(app)
|
||||
|
||||
self.json_template = json_template or self.json_template
|
||||
self.manifest_template = manifest_template or self.manifest_template
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._apps.__getitem__(index)
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
return self._apps.__setitem__(index, Webapp(value))
|
||||
|
||||
def __delitem__(self, index):
|
||||
return self._apps.__delitem__(index)
|
||||
|
||||
def __len__(self):
|
||||
return self._apps.__len__()
|
||||
|
||||
def __contains__(self, value):
|
||||
return self._apps.__contains__(Webapp(value))
|
||||
|
||||
def append(self, value):
|
||||
return self._apps.append(Webapp(value))
|
||||
|
||||
def insert(self, index, value):
|
||||
return self._apps.insert(index, Webapp(value))
|
||||
|
||||
def extend(self, values):
|
||||
return self._apps.extend([Webapp(v) for v in values])
|
||||
|
||||
def remove(self, value):
|
||||
return self._apps.remove(Webapp(value))
|
||||
|
||||
def _write_webapps_json(self, apps):
|
||||
contents = []
|
||||
for app in apps:
|
||||
contents.append(self.json_template.substitute(app))
|
||||
contents = '{\n' + ',\n'.join(contents) + '\n}\n'
|
||||
webapps_json_path = os.path.join(self.webapps_dir, 'webapps.json')
|
||||
webapps_json_file = open(webapps_json_path, "w")
|
||||
webapps_json_file.write(contents)
|
||||
webapps_json_file.close()
|
||||
|
||||
def _write_webapp_manifests(self, write_apps=[], remove_apps=[]):
|
||||
# Write manifests for installed apps
|
||||
for app in write_apps:
|
||||
manifest_dir = os.path.join(self.webapps_dir, app['name'])
|
||||
manifest_path = os.path.join(manifest_dir, 'manifest.webapp')
|
||||
if not os.path.isfile(manifest_path):
|
||||
if not os.path.isdir(manifest_dir):
|
||||
os.mkdir(manifest_dir)
|
||||
manifest = self.manifest_template.substitute(app)
|
||||
manifest_file = open(manifest_path, "a")
|
||||
manifest_file.write(manifest)
|
||||
manifest_file.close()
|
||||
# Remove manifests for removed apps
|
||||
for app in remove_apps:
|
||||
self._installed_apps.remove(app)
|
||||
manifest_dir = os.path.join(self.webapps_dir, app['name'])
|
||||
if os.path.isdir(manifest_dir):
|
||||
shutil.rmtree(manifest_dir)
|
||||
|
||||
def update_manifests(self):
|
||||
"""Updates the webapp manifests with the webapps represented in this collection
|
||||
|
||||
If update_manifests is called a subsequent time, there could have been apps added or
|
||||
removed to the collection in the interim. The manifests will be adjusted accordingly
|
||||
"""
|
||||
apps_to_install = [app for app in self._apps if app not in self._installed_apps]
|
||||
apps_to_remove = [app for app in self._installed_apps if app not in self._apps]
|
||||
if apps_to_install == apps_to_remove == []:
|
||||
# nothing to do
|
||||
return
|
||||
|
||||
if not os.path.isdir(self.webapps_dir):
|
||||
os.makedirs(self.webapps_dir)
|
||||
elif not self._installed_apps:
|
||||
shutil.copytree(self.webapps_dir, self.backup_dir)
|
||||
|
||||
webapps_json_path = os.path.join(self.webapps_dir, 'webapps.json')
|
||||
webapps_json = []
|
||||
if os.path.isfile(webapps_json_path):
|
||||
webapps_json = self.read_json(webapps_json_path, description="description")
|
||||
webapps_json = [a for a in webapps_json if a not in apps_to_remove]
|
||||
|
||||
# Iterate over apps already in webapps.json to determine the starting local
|
||||
# id and to ensure apps are properly formatted
|
||||
start_id = 1
|
||||
for local_id, app in enumerate(webapps_json):
|
||||
app['localId'] = local_id + 1
|
||||
start_id += 1
|
||||
if not app.get('csp'):
|
||||
app['csp'] = ''
|
||||
if not app.get('appStatus'):
|
||||
app['appStatus'] = 3
|
||||
|
||||
# Append apps_to_install to the pre-existent apps
|
||||
for local_id, app in enumerate(apps_to_install):
|
||||
app['localId'] = local_id + start_id
|
||||
# ignore if it's already installed
|
||||
if app in webapps_json:
|
||||
start_id -= 1
|
||||
continue
|
||||
webapps_json.append(app)
|
||||
self._installed_apps.append(app)
|
||||
|
||||
# Write the full contents to webapps.json
|
||||
self._write_webapps_json(webapps_json)
|
||||
|
||||
# Create/remove manifest file for each app.
|
||||
self._write_webapp_manifests(apps_to_install, apps_to_remove)
|
||||
|
||||
def clean(self):
|
||||
"""Remove all webapps that were installed and restore profile to previous state"""
|
||||
if self._installed_apps and os.path.isdir(self.webapps_dir):
|
||||
shutil.rmtree(self.webapps_dir)
|
||||
|
||||
if os.path.isdir(self.backup_dir):
|
||||
shutil.copytree(self.backup_dir, self.webapps_dir)
|
||||
shutil.rmtree(self.backup_dir)
|
||||
|
||||
self._apps = []
|
||||
self._installed_apps = []
|
||||
|
||||
@classmethod
|
||||
def read_json(cls, path, **defaults):
|
||||
"""Reads a json file which describes a set of webapps. The json format is either a
|
||||
dictionary where each key represents the name of a webapp (e.g B2G format) or a list
|
||||
of webapp objects.
|
||||
|
||||
:param path: Path to a json file defining webapps
|
||||
:param defaults: Default key value pairs added to each webapp object if key doesn't exist
|
||||
|
||||
Returns a list of Webapp objects
|
||||
"""
|
||||
f = open(path, 'r')
|
||||
app_json = json.load(f)
|
||||
f.close()
|
||||
|
||||
apps = []
|
||||
if isinstance(app_json, dict):
|
||||
for k, v in app_json.iteritems():
|
||||
v['name'] = k
|
||||
apps.append(v)
|
||||
else:
|
||||
apps = app_json
|
||||
if not isinstance(apps, list):
|
||||
apps = [apps]
|
||||
|
||||
ret = []
|
||||
for app in apps:
|
||||
d = defaults.copy()
|
||||
d.update(app)
|
||||
ret.append(Webapp(**d))
|
||||
return ret
|
|
@ -2,10 +2,11 @@
|
|||
# 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 sys
|
||||
from setuptools import setup
|
||||
|
||||
PACKAGE_VERSION = '0.5'
|
||||
PACKAGE_VERSION = '0.4'
|
||||
|
||||
# we only support python 2 right now
|
||||
assert sys.version_info[0] == 2
|
||||
|
@ -22,10 +23,17 @@ except ImportError:
|
|||
deps.append('pysqlite')
|
||||
|
||||
|
||||
# take description from README
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
try:
|
||||
description = file(os.path.join(here, 'README.md')).read()
|
||||
except (OSError, IOError):
|
||||
description = ''
|
||||
|
||||
setup(name='mozprofile',
|
||||
version=PACKAGE_VERSION,
|
||||
description="Library to create and modify Mozilla application profiles",
|
||||
long_description="see http://mozbase.readthedocs.org/",
|
||||
description="Handling of Mozilla Gecko based application profiles",
|
||||
long_description=description,
|
||||
classifiers=['Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import os
|
||||
import shutil
|
||||
try:
|
||||
import sqlite3
|
||||
except ImportError:
|
||||
from pysqlite2 import dbapi2 as sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
from mozprofile.permissions import Permissions
|
||||
|
||||
class PermissionsTest(unittest.TestCase):
|
||||
|
||||
locations = """http://mochi.test:8888 primary,privileged
|
||||
http://127.0.0.1:80 noxul
|
||||
http://127.0.0.1:8888 privileged
|
||||
"""
|
||||
|
||||
profile_dir = None
|
||||
locations_file = None
|
||||
|
||||
def setUp(self):
|
||||
self.profile_dir = tempfile.mkdtemp()
|
||||
self.locations_file = tempfile.NamedTemporaryFile()
|
||||
self.locations_file.write(self.locations)
|
||||
self.locations_file.flush()
|
||||
|
||||
def tearDown(self):
|
||||
if self.profile_dir:
|
||||
shutil.rmtree(self.profile_dir)
|
||||
if self.locations_file:
|
||||
self.locations_file.close()
|
||||
|
||||
def test_schema_version(self):
|
||||
perms = Permissions(self.profile_dir, self.locations_file.name)
|
||||
perms_db_filename = os.path.join(self.profile_dir, 'permissions.sqlite')
|
||||
perms.write_db(self.locations_file)
|
||||
|
||||
stmt = 'PRAGMA schema_version;'
|
||||
|
||||
con = sqlite3.connect(perms_db_filename)
|
||||
cur = con.cursor()
|
||||
cur.execute(stmt)
|
||||
entries = cur.fetchall()
|
||||
|
||||
schema_version = entries[0][0]
|
||||
self.assertEqual(schema_version, 3)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,6 +0,0 @@
|
|||
# A leading comment
|
||||
user_pref("browser.startup.homepage", "http://planet.mozilla.org"); # A trailing comment
|
||||
user_pref("zoom.minPercent", 30);
|
||||
// Another leading comment
|
||||
user_pref("zoom.maxPercent", 300); // Another trailing comment
|
||||
user_pref("webgl.verbose", "false");
|
|
@ -1,50 +0,0 @@
|
|||
[{ "name": "http_example_org",
|
||||
"csp": "",
|
||||
"origin": "http://example.org",
|
||||
"manifestURL": "http://example.org/manifest.webapp",
|
||||
"description": "http://example.org App",
|
||||
"appStatus": 1
|
||||
},
|
||||
{ "name": "https_example_com",
|
||||
"csp": "",
|
||||
"origin": "https://example.com",
|
||||
"manifestURL": "https://example.com/manifest.webapp",
|
||||
"description": "https://example.com App",
|
||||
"appStatus": 1
|
||||
},
|
||||
{ "name": "http_test1_example_org",
|
||||
"csp": "",
|
||||
"origin": "http://test1.example.org",
|
||||
"manifestURL": "http://test1.example.org/manifest.webapp",
|
||||
"description": "http://test1.example.org App",
|
||||
"appStatus": 1
|
||||
},
|
||||
{ "name": "http_test1_example_org_8000",
|
||||
"csp": "",
|
||||
"origin": "http://test1.example.org:8000",
|
||||
"manifestURL": "http://test1.example.org:8000/manifest.webapp",
|
||||
"description": "http://test1.example.org:8000 App",
|
||||
"appStatus": 1
|
||||
},
|
||||
{ "name": "http_sub1_test1_example_org",
|
||||
"csp": "",
|
||||
"origin": "http://sub1.test1.example.org",
|
||||
"manifestURL": "http://sub1.test1.example.org/manifest.webapp",
|
||||
"description": "http://sub1.test1.example.org App",
|
||||
"appStatus": 1
|
||||
},
|
||||
{ "name": "https_example_com_privileged",
|
||||
"csp": "",
|
||||
"origin": "https://example.com",
|
||||
"manifestURL": "https://example.com/manifest_priv.webapp",
|
||||
"description": "https://example.com Privileged App",
|
||||
"appStatus": 2
|
||||
},
|
||||
{ "name": "https_example_com_certified",
|
||||
"csp": "",
|
||||
"origin": "https://example.com",
|
||||
"manifestURL": "https://example.com/manifest_cert.webapp",
|
||||
"description": "https://example.com Certified App",
|
||||
"appStatus": 3
|
||||
}
|
||||
]
|
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
"https_example_csp_certified": {
|
||||
"csp": "default-src *; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'",
|
||||
"origin": "https://example.com",
|
||||
"manifestURL": "https://example.com/manifest_csp_cert.webapp",
|
||||
"description": "https://example.com certified app with manifest policy",
|
||||
"appStatus": 3
|
||||
},
|
||||
"https_example_csp_installed": {
|
||||
"csp": "default-src *; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'",
|
||||
"origin": "https://example.com",
|
||||
"manifestURL": "https://example.com/manifest_csp_inst.webapp",
|
||||
"description": "https://example.com installed app with manifest policy",
|
||||
"appStatus": 1
|
||||
},
|
||||
"https_example_csp_privileged": {
|
||||
"csp": "default-src *; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'",
|
||||
"origin": "https://example.com",
|
||||
"manifestURL": "https://example.com/manifest_csp_priv.webapp",
|
||||
"description": "https://example.com privileged app with manifest policy",
|
||||
"appStatus": 2
|
||||
},
|
||||
"https_a_domain_certified": {
|
||||
"csp": "",
|
||||
"origin": "https://acertified.com",
|
||||
"manifestURL": "https://acertified.com/manifest.webapp",
|
||||
"description": "https://acertified.com certified app",
|
||||
"appStatus": 3
|
||||
},
|
||||
"https_a_domain_privileged": {
|
||||
"csp": "",
|
||||
"origin": "https://aprivileged.com",
|
||||
"manifestURL": "https://aprivileged.com/manifest.webapp",
|
||||
"description": "https://aprivileged.com privileged app ",
|
||||
"appStatus": 2
|
||||
}
|
||||
}
|
|
@ -4,6 +4,3 @@
|
|||
[permissions.py]
|
||||
[bug758250.py]
|
||||
[test_nonce.py]
|
||||
[bug785146.py]
|
||||
[test_clone_cleanup.py]
|
||||
[test_webapps.py]
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from mozprofile.profile import Profile
|
||||
|
||||
class CloneCleanupTest(unittest.TestCase):
|
||||
"""
|
||||
test cleanup logic for the clone functionality
|
||||
see https://bugzilla.mozilla.org/show_bug.cgi?id=642843
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# make a profile with one preference
|
||||
path = tempfile.mktemp()
|
||||
self.profile = Profile(path,
|
||||
preferences={'foo': 'bar'},
|
||||
restore=False)
|
||||
user_js = os.path.join(self.profile.profile, 'user.js')
|
||||
self.assertTrue(os.path.exists(user_js))
|
||||
|
||||
def test_restore_true(self):
|
||||
# make a clone of this profile with restore=True
|
||||
clone = Profile.clone(self.profile.profile, restore=True)
|
||||
|
||||
clone.cleanup()
|
||||
|
||||
# clone should be deleted
|
||||
self.assertFalse(os.path.exists(clone.profile))
|
||||
|
||||
def test_restore_false(self):
|
||||
# make a clone of this profile with restore=False
|
||||
clone = Profile.clone(self.profile.profile, restore=False)
|
||||
|
||||
clone.cleanup()
|
||||
|
||||
# clone should still be around on the filesystem
|
||||
self.assertTrue(os.path.exists(clone.profile))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -1,36 +1,28 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from mozprofile.cli import MozProfileCLI
|
||||
from mozprofile.prefs import Preferences
|
||||
from mozprofile.profile import Profile
|
||||
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
class PreferencesTest(unittest.TestCase):
|
||||
"""test mozprofile preference handling"""
|
||||
"""test mozprofile"""
|
||||
|
||||
def run_command(self, *args):
|
||||
"""
|
||||
invokes mozprofile command line via the CLI factory
|
||||
- args : command line arguments (equivalent of sys.argv[1:])
|
||||
runs mozprofile;
|
||||
returns (stdout, stderr, code)
|
||||
"""
|
||||
|
||||
# instantiate the factory
|
||||
cli = MozProfileCLI(list(args))
|
||||
|
||||
# create the profile
|
||||
profile = cli.profile()
|
||||
|
||||
# return path to profile
|
||||
return profile.profile
|
||||
process = subprocess.Popen(args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = process.communicate()
|
||||
stdout = stdout.strip()
|
||||
stderr = stderr.strip()
|
||||
return stdout, stderr, process.returncode
|
||||
|
||||
def compare_generated(self, _prefs, commandline):
|
||||
"""
|
||||
|
@ -39,7 +31,7 @@ class PreferencesTest(unittest.TestCase):
|
|||
compares the results
|
||||
cleans up
|
||||
"""
|
||||
profile = self.run_command(*commandline)
|
||||
profile, stderr, code = self.run_command(*commandline)
|
||||
prefs_file = os.path.join(profile, 'user.js')
|
||||
self.assertTrue(os.path.exists(prefs_file))
|
||||
read = Preferences.read_prefs(prefs_file)
|
||||
|
@ -49,10 +41,8 @@ class PreferencesTest(unittest.TestCase):
|
|||
shutil.rmtree(profile)
|
||||
|
||||
def test_basic_prefs(self):
|
||||
"""test setting a pref from the command line entry point"""
|
||||
|
||||
_prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"}
|
||||
commandline = []
|
||||
commandline = ["mozprofile"]
|
||||
_prefs = _prefs.items()
|
||||
for pref, value in _prefs:
|
||||
commandline += ["--pref", "%s:%s" % (pref, value)]
|
||||
|
@ -64,7 +54,7 @@ class PreferencesTest(unittest.TestCase):
|
|||
("zoom.minPercent", 30),
|
||||
("zoom.maxPercent", 300),
|
||||
("webgl.verbose", 'false')]
|
||||
commandline = []
|
||||
commandline = ["mozprofile"]
|
||||
for pref, value in _prefs:
|
||||
commandline += ["--pref", "%s:%s" % (pref, value)]
|
||||
_prefs = [(i, Preferences.cast(j)) for i, j in _prefs]
|
||||
|
@ -79,24 +69,22 @@ browser.startup.homepage = http://planet.mozilla.org/
|
|||
[foo]
|
||||
browser.startup.homepage = http://github.com/
|
||||
"""
|
||||
try:
|
||||
fd, name = tempfile.mkstemp(suffix='.ini')
|
||||
os.write(fd, _ini)
|
||||
os.close(fd)
|
||||
commandline = ["--preferences", name]
|
||||
fd, name = tempfile.mkstemp(suffix='.ini')
|
||||
os.write(fd, _ini)
|
||||
os.close(fd)
|
||||
commandline = ["mozprofile", "--preferences", name]
|
||||
|
||||
# test the [DEFAULT] section
|
||||
_prefs = {'browser.startup.homepage': 'http://planet.mozilla.org/'}
|
||||
self.compare_generated(_prefs, commandline)
|
||||
# test the [DEFAULT] section
|
||||
_prefs = {'browser.startup.homepage': 'http://planet.mozilla.org/'}
|
||||
self.compare_generated(_prefs, commandline)
|
||||
|
||||
# test a specific section
|
||||
_prefs = {'browser.startup.homepage': 'http://github.com/'}
|
||||
commandline[-1] = commandline[-1] + ':foo'
|
||||
self.compare_generated(_prefs, commandline)
|
||||
# test a specific section
|
||||
_prefs = {'browser.startup.homepage': 'http://github.com/'}
|
||||
commandline[-1] = commandline[-1] + ':foo'
|
||||
self.compare_generated(_prefs, commandline)
|
||||
|
||||
finally:
|
||||
# cleanup
|
||||
os.remove(name)
|
||||
# cleanup
|
||||
os.remove(name)
|
||||
|
||||
def test_reset_should_remove_added_prefs(self):
|
||||
"""Check that when we call reset the items we expect are updated"""
|
||||
|
@ -121,8 +109,8 @@ browser.startup.homepage = http://github.com/
|
|||
if line.startswith('#MozRunner Prefs End')]))
|
||||
|
||||
profile.reset()
|
||||
self.assertNotEqual(prefs1,
|
||||
Preferences.read_prefs(os.path.join(profile.profile, 'user.js')),
|
||||
self.assertNotEqual(prefs1, \
|
||||
Preferences.read_prefs(os.path.join(profile.profile, 'user.js')),\
|
||||
"I pity the fool who left my pref")
|
||||
|
||||
def test_magic_markers(self):
|
||||
|
@ -204,8 +192,9 @@ user_pref("webgl.force-enabled", true);
|
|||
# make sure you have the original preferences
|
||||
prefs = Preferences.read_prefs(user_js)
|
||||
self.assertTrue(prefs == original_prefs)
|
||||
finally:
|
||||
except:
|
||||
shutil.rmtree(tempdir)
|
||||
raise
|
||||
|
||||
def test_json(self):
|
||||
_prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"}
|
||||
|
@ -216,43 +205,9 @@ user_pref("webgl.force-enabled", true);
|
|||
os.write(fd, json)
|
||||
os.close(fd)
|
||||
|
||||
commandline = ["--preferences", name]
|
||||
commandline = ["mozprofile", "--preferences", name]
|
||||
self.compare_generated(_prefs, commandline)
|
||||
|
||||
def test_prefs_write(self):
|
||||
"""test that the Preferences.write() method correctly serializes preferences"""
|
||||
|
||||
_prefs = {'browser.startup.homepage': "http://planet.mozilla.org",
|
||||
'zoom.minPercent': 30,
|
||||
'zoom.maxPercent': 300}
|
||||
|
||||
# make a Preferences manager with the testing preferences
|
||||
preferences = Preferences(_prefs)
|
||||
|
||||
# write them to a temporary location
|
||||
path = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(suffix='.js', delete=False) as f:
|
||||
path = f.name
|
||||
preferences.write(f, _prefs)
|
||||
|
||||
# read them back and ensure we get what we put in
|
||||
self.assertEqual(dict(Preferences.read_prefs(path)), _prefs)
|
||||
|
||||
finally:
|
||||
# cleanup
|
||||
os.remove(path)
|
||||
|
||||
def test_read_prefs_with_comments(self):
|
||||
"""test reading preferences from a prefs.js file that contains comments"""
|
||||
|
||||
_prefs = {'browser.startup.homepage': 'http://planet.mozilla.org',
|
||||
'zoom.minPercent': 30,
|
||||
'zoom.maxPercent': 300,
|
||||
'webgl.verbose': 'false'}
|
||||
path = os.path.join(here, 'files', 'prefs_with_comments.js')
|
||||
self.assertEqual(dict(Preferences.read_prefs(path)), _prefs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -1,197 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
test installing and managing webapps in a profile
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import unittest
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from mozprofile.webapps import WebappCollection, Webapp, WebappFormatException
|
||||
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
class WebappTest(unittest.TestCase):
|
||||
"""Tests reading, installing and cleaning webapps
|
||||
from a profile.
|
||||
"""
|
||||
manifest_path_1 = os.path.join(here, 'files', 'webapps1.json')
|
||||
manifest_path_2 = os.path.join(here, 'files', 'webapps2.json')
|
||||
|
||||
def setUp(self):
|
||||
self.profile = mkdtemp(prefix='test_webapp')
|
||||
self.webapps_dir = os.path.join(self.profile, 'webapps')
|
||||
self.webapps_json_path = os.path.join(self.webapps_dir, 'webapps.json')
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.profile)
|
||||
|
||||
def test_read_json_manifest(self):
|
||||
"""Tests WebappCollection.read_json"""
|
||||
# Parse a list of webapp objects and verify it worked
|
||||
manifest_json_1 = WebappCollection.read_json(self.manifest_path_1)
|
||||
self.assertEqual(len(manifest_json_1), 7)
|
||||
for app in manifest_json_1:
|
||||
self.assertIsInstance(app, Webapp)
|
||||
for key in Webapp.required_keys:
|
||||
self.assertIn(key, app)
|
||||
|
||||
# Parse a dictionary of webapp objects and verify it worked
|
||||
manifest_json_2 = WebappCollection.read_json(self.manifest_path_2)
|
||||
self.assertEqual(len(manifest_json_2), 5)
|
||||
for app in manifest_json_2:
|
||||
self.assertIsInstance(app, Webapp)
|
||||
for key in Webapp.required_keys:
|
||||
self.assertIn(key, app)
|
||||
|
||||
def test_invalid_webapp(self):
|
||||
"""Tests a webapp with a missing required key"""
|
||||
webapps = WebappCollection(self.profile)
|
||||
# Missing the required key "description", exception should be raised
|
||||
self.assertRaises(WebappFormatException, webapps.append, { 'name': 'foo' })
|
||||
|
||||
def test_webapp_collection(self):
|
||||
"""Tests the methods of the WebappCollection object"""
|
||||
webapp_1 = { 'name': 'test_app_1',
|
||||
'description': 'a description',
|
||||
'manifestURL': 'http://example.com/1/manifest.webapp',
|
||||
'appStatus': 1 }
|
||||
|
||||
webapp_2 = { 'name': 'test_app_2',
|
||||
'description': 'another description',
|
||||
'manifestURL': 'http://example.com/2/manifest.webapp',
|
||||
'appStatus': 2 }
|
||||
|
||||
webapp_3 = { 'name': 'test_app_2',
|
||||
'description': 'a third description',
|
||||
'manifestURL': 'http://example.com/3/manifest.webapp',
|
||||
'appStatus': 3 }
|
||||
|
||||
webapps = WebappCollection(self.profile)
|
||||
self.assertEqual(len(webapps), 0)
|
||||
|
||||
# WebappCollection should behave like a list
|
||||
def invalid_index():
|
||||
webapps[0]
|
||||
self.assertRaises(IndexError, invalid_index)
|
||||
|
||||
# Append a webapp object
|
||||
webapps.append(webapp_1)
|
||||
self.assertTrue(len(webapps), 1)
|
||||
self.assertIsInstance(webapps[0], Webapp)
|
||||
self.assertEqual(len(webapps[0]), len(webapp_1))
|
||||
self.assertEqual(len(set(webapps[0].items()) & set(webapp_1.items())), len(webapp_1))
|
||||
|
||||
# Remove a webapp object
|
||||
webapps.remove(webapp_1)
|
||||
self.assertEqual(len(webapps), 0)
|
||||
|
||||
# Extend a list of webapp objects
|
||||
webapps.extend([webapp_1, webapp_2])
|
||||
self.assertEqual(len(webapps), 2)
|
||||
self.assertTrue(webapp_1 in webapps)
|
||||
self.assertTrue(webapp_2 in webapps)
|
||||
self.assertNotEquals(webapps[0], webapps[1])
|
||||
|
||||
# Insert a webapp object
|
||||
webapps.insert(1, webapp_3)
|
||||
self.assertEqual(len(webapps), 3)
|
||||
self.assertEqual(webapps[1], webapps[2])
|
||||
for app in webapps:
|
||||
self.assertIsInstance(app, Webapp)
|
||||
|
||||
# Assigning an invalid type (must be accepted by the dict() constructor) should throw
|
||||
def invalid_type():
|
||||
webapps[2] = 1
|
||||
self.assertRaises(WebappFormatException, invalid_type)
|
||||
|
||||
def test_install_webapps(self):
|
||||
"""Test installing webapps into a profile that has no prior webapps"""
|
||||
webapps = WebappCollection(self.profile, apps=self.manifest_path_1)
|
||||
self.assertFalse(os.path.exists(self.webapps_dir))
|
||||
|
||||
# update the webapp manifests for the first time
|
||||
webapps.update_manifests()
|
||||
self.assertFalse(os.path.isdir(os.path.join(self.profile, webapps.backup_dir)))
|
||||
self.assertTrue(os.path.isfile(self.webapps_json_path))
|
||||
|
||||
webapps_json = webapps.read_json(self.webapps_json_path, description="fake description")
|
||||
self.assertEqual(len(webapps_json), 7)
|
||||
for app in webapps_json:
|
||||
self.assertIsInstance(app, Webapp)
|
||||
|
||||
manifest_json_1 = webapps.read_json(self.manifest_path_1)
|
||||
manifest_json_2 = webapps.read_json(self.manifest_path_2)
|
||||
self.assertEqual(len(webapps_json), len(manifest_json_1))
|
||||
for app in webapps_json:
|
||||
self.assertTrue(app in manifest_json_1)
|
||||
|
||||
# Remove one of the webapps from WebappCollection after it got installed
|
||||
removed_app = manifest_json_1[2]
|
||||
webapps.remove(removed_app)
|
||||
# Add new webapps to the collection
|
||||
webapps.extend(manifest_json_2)
|
||||
|
||||
# update the webapp manifests a second time
|
||||
webapps.update_manifests()
|
||||
self.assertFalse(os.path.isdir(os.path.join(self.profile, webapps.backup_dir)))
|
||||
self.assertTrue(os.path.isfile(self.webapps_json_path))
|
||||
|
||||
webapps_json = webapps.read_json(self.webapps_json_path, description="a description")
|
||||
self.assertEqual(len(webapps_json), 11)
|
||||
|
||||
# The new apps should be added
|
||||
for app in webapps_json:
|
||||
self.assertIsInstance(app, Webapp)
|
||||
self.assertTrue(os.path.isfile(os.path.join(self.webapps_dir, app['name'], 'manifest.webapp')))
|
||||
# The removed app should not exist in the manifest
|
||||
self.assertNotIn(removed_app, webapps_json)
|
||||
self.assertFalse(os.path.exists(os.path.join(self.webapps_dir, removed_app['name'])))
|
||||
|
||||
# Cleaning should delete the webapps directory entirely since there was nothing there before
|
||||
webapps.clean()
|
||||
self.assertFalse(os.path.isdir(self.webapps_dir))
|
||||
|
||||
def test_install_webapps_preexisting(self):
|
||||
"""Tests installing webapps when the webapps directory already exists"""
|
||||
manifest_json_2 = WebappCollection.read_json(self.manifest_path_2)
|
||||
|
||||
# Synthesize a pre-existing webapps directory
|
||||
os.mkdir(self.webapps_dir)
|
||||
shutil.copyfile(self.manifest_path_2, self.webapps_json_path)
|
||||
for app in manifest_json_2:
|
||||
app_path = os.path.join(self.webapps_dir, app['name'])
|
||||
os.mkdir(app_path)
|
||||
f = open(os.path.join(app_path, 'manifest.webapp'), 'w')
|
||||
f.close()
|
||||
|
||||
webapps = WebappCollection(self.profile, apps=self.manifest_path_1)
|
||||
self.assertTrue(os.path.exists(self.webapps_dir))
|
||||
|
||||
# update webapp manifests for the first time
|
||||
webapps.update_manifests()
|
||||
# A backup should be created
|
||||
self.assertTrue(os.path.isdir(os.path.join(self.profile, webapps.backup_dir)))
|
||||
|
||||
# Both manifests should remain installed
|
||||
webapps_json = webapps.read_json(self.webapps_json_path, description='a fake description')
|
||||
self.assertEqual(len(webapps_json), 12)
|
||||
for app in webapps_json:
|
||||
self.assertIsInstance(app, Webapp)
|
||||
self.assertTrue(os.path.isfile(os.path.join(self.webapps_dir, app['name'], 'manifest.webapp')))
|
||||
|
||||
# Upon cleaning the backup should be restored
|
||||
webapps.clean()
|
||||
self.assertFalse(os.path.isdir(os.path.join(self.profile, webapps.backup_dir)))
|
||||
|
||||
# The original webapps should still be installed
|
||||
webapps_json = webapps.read_json(self.webapps_json_path)
|
||||
for app in webapps_json:
|
||||
self.assertIsInstance(app, Webapp)
|
||||
self.assertTrue(os.path.isfile(os.path.join(self.webapps_dir, app['name'], 'manifest.webapp')))
|
||||
self.assertEqual(webapps_json, manifest_json_2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,43 @@
|
|||
[mozrunner](https://github.com/mozilla/mozbase/tree/master/mozrunner)
|
||||
is a [python package](http://pypi.python.org/pypi/mozrunner)
|
||||
which handles running of Mozilla applications.
|
||||
mozrunner utilizes [mozprofile](https://github.com/mozilla/mozbase/tree/master/mozprofile)
|
||||
for managing application profiles
|
||||
and [mozprocess](https://github.com/mozilla/mozbase/tree/master/mozprocess) for robust process control.
|
||||
|
||||
mozrunner may be used from the command line or programmatically as an API.
|
||||
|
||||
|
||||
# Command Line Usage
|
||||
|
||||
The `mozrunner` command will launch the application (specified by
|
||||
`--app`) from a binary specified with `-b` or as located on the `PATH`.
|
||||
|
||||
mozrunner takes the command line options from
|
||||
[mozprofile](https://github.com/mozilla/mozbase/tree/master/mozprofile) for constructing the profile to be used by
|
||||
the application.
|
||||
|
||||
Run `mozrunner --help` for detailed information on the command line
|
||||
program.
|
||||
|
||||
|
||||
# API Usage
|
||||
|
||||
mozrunner features a base class,
|
||||
[mozrunner.runner.Runner](https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/runner.py)
|
||||
which is an integration layer API for interfacing with Mozilla applications.
|
||||
|
||||
mozrunner also exposes two application specific classes,
|
||||
`FirefoxRunner` and `ThunderbirdRunner` which record the binary names
|
||||
necessary for the `Runner` class to find them on the system.
|
||||
|
||||
Example API usage:
|
||||
|
||||
from mozrunner import FirefoxRunner
|
||||
|
||||
# start Firefox on a new profile
|
||||
runner = FirefoxRunner()
|
||||
runner.start()
|
||||
|
||||
See also a comparable implementation for [selenium](http://seleniumhq.org/):
|
||||
http://code.google.com/p/selenium/source/browse/trunk/py/selenium/webdriver/firefox/firefox_binary.py
|
|
@ -2,17 +2,24 @@
|
|||
# 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 sys
|
||||
from setuptools import setup
|
||||
|
||||
PACKAGE_NAME = "mozrunner"
|
||||
PACKAGE_VERSION = '5.15'
|
||||
PACKAGE_VERSION = '5.14'
|
||||
|
||||
desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
|
||||
# take description from README
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
try:
|
||||
description = file(os.path.join(here, 'README.md')).read()
|
||||
except (OSError, IOError):
|
||||
description = ''
|
||||
|
||||
deps = ['mozinfo >= 0.4',
|
||||
'mozprocess >= 0.8',
|
||||
'mozprofile >= 0.4',
|
||||
deps = ['mozinfo == 0.4',
|
||||
'mozprocess == 0.8',
|
||||
'mozprofile == 0.4',
|
||||
]
|
||||
|
||||
# we only support python 2 right now
|
||||
|
@ -21,7 +28,7 @@ assert sys.version_info[0] == 2
|
|||
setup(name=PACKAGE_NAME,
|
||||
version=PACKAGE_VERSION,
|
||||
description=desc,
|
||||
long_description="see http://mozbase.readthedocs.org/",
|
||||
long_description=description,
|
||||
classifiers=['Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
|
||||
|
@ -33,7 +40,7 @@ setup(name=PACKAGE_NAME,
|
|||
keywords='mozilla',
|
||||
author='Mozilla Automation and Tools team',
|
||||
author_email='tools@lists.mozilla.org',
|
||||
url='https://wiki.mozilla.org/Auto-tools/Projects/Mozbase',
|
||||
url='https://wiki.mozilla.org/Auto-tools/Projects/MozBase',
|
||||
license='MPL 2.0',
|
||||
packages=['mozrunner'],
|
||||
zip_safe=False,
|
||||
|
|
Загрузка…
Ссылка в новой задаче