Bug 838374 - release mozprofile, mozprocess, mozdevice (and other?) and mirror to m-c;r=ahal

This commit is contained in:
Jeff Hammel 2013-03-21 21:06:28 -07:00
parent b66ec13931
commit 5d433229b3
62 changed files with 1963 additions and 2284 deletions

View File

@ -34,6 +34,7 @@ try:
import mozcrash
except:
deps = ['mozcrash',
'mozfile',
'mozlog']
for dep in deps:
module = os.path.join(mozbase, dep)

View File

@ -4,44 +4,19 @@
__all__ = ['check_for_crashes']
import os, sys, glob, urllib2, tempfile, re, subprocess, shutil, urlparse, zipfile
import glob
import mozlog
import os
import re
import shutil
import subprocess
import sys
import tempfile
import urllib2
import zipfile
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()
from mozfile import extract_zip
from mozfile import is_url
def check_for_crashes(dump_directory, symbols_path,
stackwalk_binary=None,
@ -61,17 +36,20 @@ 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.
"""
log = mozlog.getLogger('mozcrash')
dumps = glob.glob(os.path.join(dump_directory, '*.dmp'))
if not dumps:
return False
if stackwalk_binary is None:
stackwalk_binary = os.environ.get('MINIDUMP_STACKWALK', None)
@ -82,28 +60,25 @@ def check_for_crashes(dump_directory, symbols_path,
except:
test_name = "unknown"
# 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:
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()
for d in dumps:
stackwalk_output = []
stackwalk_output.append("Crash dump filename: " + d)
@ -145,7 +120,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"
log.error("PROCESS-CRASH | %s | application crashed [%s]", test_name, top_frame)
print "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)

View File

@ -4,10 +4,11 @@
from setuptools import setup
PACKAGE_VERSION = '0.3'
PACKAGE_VERSION = '0.5'
# dependencies
deps = []
deps = ['mozfile >= 0.3',
'mozlog']
setup(name='mozcrash',
version=PACKAGE_VERSION,

View File

@ -1,5 +0,0 @@
[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.

View File

@ -2,9 +2,7 @@
# 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 DMError
from devicemanager import DeviceManager, DMError
from devicemanagerADB import DeviceManagerADB
from devicemanagerSUT import DeviceManagerSUT
from droid import DroidADB, DroidSUT, DroidConnectByHWID
from emulator import Emulator
from b2gemulator import B2GEmulator

View File

@ -1,87 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import 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)

View File

@ -11,6 +11,7 @@ import StringIO
import zlib
from Zeroconf import Zeroconf, ServiceBrowser
from functools import wraps
class DMError(Exception):
"generic devicemanager exception."
@ -25,348 +26,57 @@ 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:
class DeviceManager(object):
"""
Represents a connection to a device. Once an implementation of this class
is successfully instantiated, you may do things like list/copy files to
the device, launch processes on the device, and install or remove
applications from the device.
Never instantiate this class directly! Instead, instantiate an
implementation of it like DeviceManagerADB or DeviceManagerSUT.
"""
_logcatNeedsRoot = True
@abstractmethod
def shell(self, cmd, outputfile, env=None, cwd=None, timeout=None, root=False):
def getInfo(self, directive=None):
"""
Executes shell command on device and returns exit code
Returns a dictionary of information strings about the device.
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
"""
:param directive: information you want to get. Options are:
def shellCheckOutput(self, cmd, env=None, cwd=None, timeout=None, root=False):
"""
executes shell command on device and returns the the output
- `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
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
If `directive` is `None`, will return all available information
"""
@abstractmethod
def mkDir(self, name):
def getCurrentTime(self):
"""
Creates a single directory on the device file system
Returns device time in milliseconds since the epoch.
"""
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']):
"""
Gets the IP of the device, or None if no connection exists.
Returns the IP of the device, or None if no connection exists.
"""
for interface in interfaces:
match = re.match(r"%s: ip (\S+)" % interface,
@ -374,128 +84,16 @@ class DeviceManager:
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:S", "ConnectivityService:S",
def getLogcat(self, filterSpecs=["dalvikvm:I", "ConnectivityService:S",
"WifiMonitor:S", "WifiStateTracker:S",
"wpa_supplicant:S", "NetworkStateTracker:S"],
format="time",
@ -512,23 +110,6 @@ class DeviceManager:
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
@ -557,18 +138,366 @@ class DeviceManager:
self.removeFile(tempScreenshotFile)
@abstractmethod
def chmodDir(self, remoteDir, mask="777"):
def pushFile(self, localFilename, remoteFilename, retryLimit=1):
"""
Copies localname from the host to destname on the device.
"""
Recursively changes file permissions in a directory
returns:
success: True
failure: False
@abstractmethod
def pushDir(self, localDirname, remoteDirname, retryLimit=1):
"""
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:
@ -638,7 +567,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"

View File

@ -11,11 +11,16 @@ 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
@ -23,7 +28,7 @@ class DeviceManagerADB(DeviceManager):
_tempDir = None
default_timeout = 300
def __init__(self, host=None, port=20701, retryLimit=5, packageName='fennec',
def __init__(self, host=None, port=5555, retryLimit=5, packageName='fennec',
adbPath='adb', deviceSerial=None, deviceRoot=None, **kwargs):
self.host = host
self.port = port
@ -82,16 +87,6 @@ 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. :(
@ -165,9 +160,6 @@ 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
@ -181,27 +173,18 @@ class DeviceManagerADB(DeviceManager):
remoteTmpFile = self.getTempDir() + "/" + os.path.basename(localname)
self._checkCmd(["push", os.path.realpath(localname), remoteTmpFile],
retryLimit=retryLimit)
if self._useDDCopy:
self.shellCheckOutput(["dd", "if=" + remoteTmpFile, "of=" + destname])
else:
self.shellCheckOutput(["cp", remoteTmpFile, destname])
self.shellCheckOutput(["dd", "if=" + remoteTmpFile, "of=" + 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
@ -235,9 +218,6 @@ 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()
@ -248,9 +228,6 @@ 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):
@ -259,27 +236,16 @@ 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]
@ -297,13 +263,6 @@ 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()
@ -311,7 +270,11 @@ class DeviceManagerADB(DeviceManager):
ret = []
while (proc):
els = proc.split()
ret.append(list([int(els[1]), els[len(els) - 1], els[0]]))
# 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]]))
proc = p.stdout.readline()
return ret
@ -377,11 +340,6 @@ 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:
@ -395,12 +353,6 @@ 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
@ -429,9 +381,6 @@ 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)
@ -443,21 +392,12 @@ 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):
"""
Copy directory structure from device (remoteDir) to host (localDir)
"""
self._runCmd(["pull", remoteDir, localDir])
self._runCmd(["pull", remoteDir, localDir]).wait()
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:
@ -493,13 +433,6 @@ 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:
@ -516,29 +449,9 @@ 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:
@ -548,11 +461,6 @@ 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
@ -567,9 +475,6 @@ 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
@ -578,47 +483,15 @@ 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()
@ -643,24 +516,12 @@ 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
@ -722,7 +583,7 @@ class DeviceManagerADB(DeviceManager):
timeout = int(timeout)
retries = 0
while retries < retryLimit:
proc = subprocess.Popen(finalArgs)
proc = subprocess.Popen(finalArgs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
start_time = time.time()
ret_code = proc.poll()
while ((time.time() - start_time) <= timeout) and ret_code == None:
@ -750,9 +611,6 @@ 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:
@ -774,7 +632,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"])
@ -801,28 +659,13 @@ 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:
self._checkCmd(["shell", "echo"])
ret = self._checkCmd(["shell", "echo"])
except subprocess.CalledProcessError:
raise DMError("unable to connect to device: is it plugged in?")
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
if ret:
raise DMError("unable to connect to device")
def _verifyRunAs(self):
# If a valid package name is available, and certain other
@ -834,7 +677,7 @@ class DeviceManagerADB(DeviceManager):
# file copy via run-as.
self._useRunAs = False
devroot = self.getDeviceRoot()
if (self._packageName and self._isCpAvailable() and devroot):
if self._packageName and devroot:
tmpDir = self.getTempDir()
# The problem here is that run-as doesn't cause a non-zero exit code
@ -845,10 +688,7 @@ class DeviceManagerADB(DeviceManager):
tmpfile = tempfile.NamedTemporaryFile()
self._checkCmd(["push", tmpfile.name, tmpDir + "/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"])
self._checkCmd(["shell", "run-as", self._packageName, "dd", "if=" + tmpDir + "/tmpfile", "of=" + devroot + "/sanity/tmpfile"])
if (self.fileExists(devroot + "/sanity/tmpfile")):
print "will execute commands via run-as " + self._packageName
self._useRunAs = True

View File

@ -4,19 +4,24 @@
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 = '\$\>'
@ -25,6 +30,9 @@ 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
@ -38,7 +46,9 @@ class DeviceManagerSUT(DeviceManager):
# Get version
verstring = self._runCmds([{ 'cmd': 'ver' }])
self.agentVersion = re.sub('SUTAgentAndroid Version ', '', verstring)
ver_re = re.match('(\S+) Version (\S+)', verstring)
self.agentProductName = ver_re.group(1)
self.agentVersion = ver_re.group(2)
def _cmdNeedsResponse(self, cmd):
""" Not all commands need a response from the agent:
@ -160,17 +170,23 @@ 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: Unable to connect socket: "+str(msg))
raise DMError("Remote Device Error: Did not get prompt after connecting: " + str(msg), fatal=True)
# future recv() timeouts are handled by select() calls
self._sock.settimeout(None)
for cmd in cmdlist:
cmdline = '%s\r\n' % cmd['cmd']
@ -278,21 +294,16 @@ 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)
haveExecSu = (StrictVersion(self.agentVersion) >= StrictVersion('1.13'))
# execcwd/execcwdsu currently unsupported in Negatus; see bug 824127.
if cwd and self.agentProductName == 'SUTAgentNegatus':
raise DMError("Negatus does not support execcwd/execcwdsu")
haveExecSu = (self.agentProductName == 'SUTAgentNegatus' or
StrictVersion(self.agentVersion) >= StrictVersion('1.13'))
# Depending on agent version we send one of the following commands here:
# * exec (run as normal user)
@ -330,9 +341,6 @@ 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)
@ -345,7 +353,7 @@ class DeviceManagerSUT(DeviceManager):
raise DMError("DeviceManager: Error reading file to push")
if (self.debug >= 3):
print "push returned: %s" % hash
print "push returned: %s" % remoteHash
localHash = self._getLocalHash(localname)
@ -354,16 +362,10 @@ 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)
@ -390,9 +392,6 @@ 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')
@ -400,9 +399,6 @@ 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('/')
@ -410,11 +406,6 @@ 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 []
@ -427,27 +418,16 @@ 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 = []
@ -470,7 +450,7 @@ class DeviceManagerSUT(DeviceManager):
return processTuples
def fireProcess(self, appname, failIfRunning=False):
def fireProcess(self, appname, failIfRunning=False, maxWaitTime=30):
"""
Starts a process
@ -488,11 +468,22 @@ 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.
pid = self.processExist(appname)
# 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
if (self.debug >= 4):
print "got pid: %s for process: %s" % (pid, appname)
return pid
@ -529,34 +520,27 @@ 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"
if self.processExist(appname):
self._runCmds([{ 'cmd': 'kill ' + appname }])
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
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
@ -654,9 +638,6 @@ 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')
@ -667,9 +648,6 @@ 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):
@ -693,9 +671,6 @@ 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)
@ -708,30 +683,12 @@ 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'
@ -742,88 +699,96 @@ 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, file_path, dest_dir=None):
def unpackFile(self, filePath, destDir=None):
"""
Unzips a remote bundle to a remote location
Unzips a bundle to a location on the device
If dest_dir is not specified, the bundle is extracted
in the same directory
If destDir is not specified, the bundle is extracted in the same directory
"""
devroot = self.getDeviceRoot()
if (devroot == None):
return None
# 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 no destDir is passed in just set it to filePath's folder
if not destDir:
destDir = posixpath.dirname(filePath)
if dest_dir[-1] != '/':
dest_dir += '/'
if destDir[-1] != '/':
destDir += '/'
self._runCmds([{ 'cmd': 'unzp %s %s' % (file_path, 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
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):
#create update.info file:
if ipAddr is not None:
# The update.info command tells the SUTAgent to send a TCP message
# after restarting.
destname = '/data/data/com.mozilla.SUTAgentAndroid/files/update.info'
data = "%s,%s\rrebooting\r" % (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 = callbacksvr.disconnect()
if ipAddr is not None:
status = self._wait_for_reboot(ipAddr, port)
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']
'rotation','memory','process','disk','power','sutuserinfo',
'temperature']
if (directive in directives):
directives = [directive]
@ -850,12 +815,6 @@ 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
@ -868,12 +827,6 @@ 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
@ -887,12 +840,6 @@ 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
@ -903,49 +850,33 @@ 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 == None):
if processName is 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 = callbacksvr.disconnect()
status = self._wait_for_reboot(ip, port)
if (self.debug >= 3):
print "INFO: updateApp: got status back: " + str(status)
if self.debug >= 3:
print "INFO: updateApp: got status back: %s" + str(status)
def getCurrentTime(self):
"""
Returns device time in milliseconds since the epoch
"""
return self._runCmds([{ 'cmd': 'clok' }]).strip()
def _getCallbackIpAndPort(self, aIp, aPort):
@ -984,7 +915,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
@ -1026,64 +957,4 @@ 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")

View File

@ -25,6 +25,11 @@ 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,
@ -105,7 +110,13 @@ 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)'
},
}
@ -281,6 +292,13 @@ 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()

View File

@ -3,29 +3,39 @@
# 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
returns:
success: True
failure: False
: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
"""
# only one instance of an application may be running at once
if self.processExist(appName):
return False
raise DMError("Only one instance of an application may be running "
"at once")
acmd = [ "am", "start", "-W", "-n", "%s/%s" % (appName, activityName)]
acmd = [ "am", "start" ] + self._getExtraAmStartArgs() + \
["-W", "-n", "%s/%s" % (appName, activityName)]
if intent:
acmd.extend(["-a", intent])
@ -45,24 +55,25 @@ 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
# and ignore (except on error)
shellOutput = StringIO.StringIO()
if self.shell(acmd, shellOutput) == 0:
return True
return
return False
shellOutput.seek(0)
raise DMError("Unable to launch application (shell output: '%s')" % shellOutput.read())
def launchFennec(self, appName, intent="android.intent.action.VIEW",
mozEnv=None, extraArgs=None, url=None):
mozEnv=None, extraArgs=None, url=None):
"""
Convenience method to launch Fennec on Android with various debugging
arguments
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
: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
"""
extras = {}
@ -77,14 +88,34 @@ class DroidMixin(object):
if extraArgs:
extras['args'] = " ".join(extraArgs)
return self.launchApplication(appName, ".App", intent, url=url,
extras=extras)
self.launchApplication(appName, ".App", intent, url=url, extras=extras)
class DroidADB(DeviceManagerADB, DroidMixin):
pass
class DroidSUT(DeviceManagerSUT, DroidMixin):
pass
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 []
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."""

View File

@ -1,310 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from 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

View File

@ -1,52 +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/.
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)

View File

@ -0,0 +1,125 @@
# 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()

View File

@ -4,9 +4,7 @@
from setuptools import setup
PACKAGE_VERSION = '0.18'
deps = ['mozprocess == 0.8']
PACKAGE_VERSION = '0.21'
setup(name='mozdevice',
version=PACKAGE_VERSION,
@ -16,15 +14,16 @@ 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=deps,
install_requires=[],
entry_points="""
# -*- Entry points: -*-
[console_scripts]
dm = mozdevice.dmcli:cli
sutini = mozdevice.sutini:main
""",
)

View File

@ -9,6 +9,7 @@ import unittest
ip = ''
port = 0
heartbeat_port = 0
class DeviceManagerTestCase(unittest.TestCase):
@ -26,7 +27,6 @@ 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()

View File

@ -12,9 +12,13 @@ import dmunit
import genfiles
def main(ip, port, scripts, directory, isTestDevice):
def main(ip, port, heartbeat_port, scripts, directory, isTestDevice, verbose):
dmunit.ip = ip
dmunit.port = port
dmunit.heartbeat_port = heartbeat_port
if verbose:
from mozdevice.devicemanagerSUT import DeviceManagerSUT
DeviceManagerSUT.debug = 4
suite = unittest.TestSuite()
@ -67,6 +71,10 @@ 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=[])
@ -79,7 +87,10 @@ 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.scripts,
options.dir, options.isTestDevice)
main(options.ip, options.port, options.heartbeat_port, options.scripts,
options.dir, options.isTestDevice, options.verbose)

View File

@ -1,27 +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 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())

View File

@ -2,34 +2,32 @@
# 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, port))
self._datasock.connect((ip, heartbeat_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)
@ -39,7 +37,6 @@ 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
@ -52,5 +49,4 @@ 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

View File

@ -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/.
from StringIO import StringIO
import posixpath
from StringIO import StringIO
from dmunit import DeviceManagerTestCase
class ProcessListTestCase(DeviceManagerTestCase):
class ExecTestCase(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(['touch', filename], out)
# check that the file has been created
self.dm.shell(['dd', 'if=/dev/zero', 'of=%s' % filename, 'bs=1024',
'count=1'], out)
# Check that the file has been created
self.assertTrue(self.dm.fileExists(filename))
# clean up
# Clean up
self.dm.removeFile(filename)

View File

@ -2,31 +2,30 @@
# 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 ProcessListTestCase(DeviceManagerTestCase):
class ExecEnvTestCase(DeviceManagerTestCase):
def runTest(self):
""" simple exec test, does not use env vars """
# push the file
"""Exec test with 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)

View File

@ -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,17 +38,13 @@ 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'))
filelist = self.dm.getDirectory(
posixpath.join(testroot, 'push1'),
os.path.join(self.localdestdir, 'push1'))
filelist.sort()
self.assertEqual(filelist, self.expected_filelist)
self.dm.getDirectory(posixpath.join(testroot, 'push1'),
os.path.join(self.localdestdir, 'push1'))
self.assertTrue(os.path.exists(
os.path.join(self.localdestdir,
'push1', 'sub.1', 'sub.2', 'testfile')))
self.assertTrue(os.path.exists(
os.path.join(self.localdestdir, 'push1', 'emptysub')))
filelist = self.dm.getDirectory('/dummy',
os.path.join(self.localdestdir, '/none'))
self.assertEqual(filelist, None)
self.assertRaises(DMError, self.dm.getDirectory,
'/dummy', os.path.join(self.localdestdir, '/none'))
self.assertFalse(os.path.exists(self.localdestdir + '/none'))

View File

@ -4,22 +4,16 @@
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.

View File

@ -1,31 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import posixpath
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')))

View File

@ -7,7 +7,6 @@ import socket
from dmunit import DeviceManagerTestCase
class PromptTestCase(DeviceManagerTestCase):
def tearDown(self):
@ -27,4 +26,4 @@ class PromptTestCase(DeviceManagerTestCase):
self.sock.connect((ip, int(port)))
data = self.sock.recv(1024)
print data
self.assert_(promptre.match(data))
self.assertTrue(promptre.match(data))

View File

@ -2,27 +2,26 @@
# 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 = re.compile('^[a-f0-9]+')
procname = re.compile('.+')
# [[<procid>, <procname>, <userid>], ...]
self.assertNotEqual(len(proclist), 0)
for item in proclist:
self.assert_(procid.match(item[0]))
self.assert_(procname.match(item[1]))
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)

View File

@ -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.
"""
m_orig = hashlib.md5()
m_new = hashlib.md5()
orig = hashlib.md5()
new = hashlib.md5()
local_test_file = os.path.join('test-files', 'mybinary.zip')
m_orig.update(file(local_test_file, 'r').read())
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)
m_new.update(self.dm.pullFile(remote_test_file))
# use hexdigest() instead of digest() since values are printed
new.update(self.dm.pullFile(remote_test_file))
# Use hexdigest() instead of digest() since values are printed
# if assert fails
self.assertEqual(m_orig.hexdigest(), m_new.hexdigest())
self.assertEqual(orig.hexdigest(), new.hexdigest())
remote_missing_file = posixpath.join(testroot, 'doesnotexist')
self.dm.removeFile(remote_missing_file) # just to be sure
self.assertEqual(self.dm.pullFile(remote_missing_file), None)
self.dm.removeFile(remote_missing_file) # Just to be sure
self.assertRaises(DMError, self.dm.pullFile, remote_missing_file)

View File

@ -7,11 +7,10 @@ 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')
@ -30,11 +29,9 @@ 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'))
# verify
self.assert_(
self.assertTrue(
self.dm.dirExists(posixpath.join(dvpath, 'push1', 'sub.1')))
self.assert_(self.dm.dirExists(
self.assertTrue(self.dm.dirExists(
posixpath.join(dvpath, 'push1', 'sub.1', 'sub.2')))

View File

@ -7,11 +7,10 @@ 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)
@ -23,17 +22,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.assert_(
self.assertTrue(
self.dm.dirExists(posixpath.join(testroot, 'push2', 'sub1')))
self.assert_(self.dm.validateFile(
self.assertTrue(self.dm.validateFile(
posixpath.join(testroot, 'push2', 'sub1', 'file1.txt'),
os.path.join('test-files', 'push2', 'sub1', 'file1.txt')))
self.assert_(self.dm.validateFile(
self.assertTrue(self.dm.validateFile(
posixpath.join(testroot, 'push2', 'sub1', 'sub1.1', 'file2.txt'),
os.path.join('test-files', 'push2', 'sub1', 'sub1.1', 'file2.txt')))
self.assert_(self.dm.validateFile(
self.assertTrue(self.dm.validateFile(
posixpath.join(testroot, 'push2', 'sub2', 'file3.txt'),
os.path.join('test-files', 'push2', 'sub2', 'file3.txt')))
self.assert_(self.dm.validateFile(
self.assertTrue(self.dm.validateFile(
posixpath.join(testroot, 'push2', 'file4.bin'),
os.path.join('test-files', 'push2', 'file4.bin')))

View File

@ -7,7 +7,6 @@ import posixpath
from dmunit import DeviceManagerTestCase
class PushBinaryTestCase(DeviceManagerTestCase):
def runTest(self):
@ -15,6 +14,5 @@ class PushBinaryTestCase(DeviceManagerTestCase):
"""
testroot = self.dm.getDeviceRoot()
self.dm.removeFile(posixpath.join(testroot, 'mybinary.zip'))
self.assert_(self.dm.pushFile(
os.path.join('test-files', 'mybinary.zip'),
posixpath.join(testroot, 'mybinary.zip')))
self.dm.pushFile(os.path.join('test-files', 'mybinary.zip'),
posixpath.join(testroot, 'mybinary.zip'))

View File

@ -7,7 +7,6 @@ import posixpath
from dmunit import DeviceManagerTestCase
class PushSmallTextTestCase(DeviceManagerTestCase):
def runTest(self):
@ -15,6 +14,5 @@ class PushSmallTextTestCase(DeviceManagerTestCase):
"""
testroot = self.dm.getDeviceRoot()
self.dm.removeFile(posixpath.join(testroot, 'smalltext.txt'))
self.assert_(self.dm.pushFile(
os.path.join('test-files', 'smalltext.txt'),
posixpath.join(testroot, 'smalltext.txt')))
self.dm.pushFile(os.path.join('test-files', 'smalltext.txt'),
posixpath.join(testroot, 'smalltext.txt'))

View File

@ -1,41 +1,63 @@
from sut import MockAgent
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/
import mozdevice
import unittest
from sut import MockAgent
class PushTest(unittest.TestCase):
class MkDirsTest(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, e:
d = mozdevice.DroidSUT('127.0.0.1', port=a.port)
d.mkDirs('/mnt/sdcard/baz/boop/bip')
except mozdevice.DMError:
exceptionThrown = True
self.assertEqual(exceptionThrown, subTest['expectException'])
a.wait()
def test_repeated_path_part(self):
"""
Ensure that all dirs are created when last path part also found
earlier in the path (bug 826492).
"""
cmds = [('isdir /mnt/sdcard/foo', 'FALSE'),
('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()

View File

@ -1,4 +0,0 @@
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.

View File

@ -5,9 +5,10 @@
import os
import tarfile
import tempfile
import urlparse
import zipfile
__all__ = ['extract_tarball', 'extract_zip', 'extract', 'rmtree', 'NamedTemporaryFile']
__all__ = ['extract_tarball', 'extract_zip', 'extract', 'is_url', 'rmtree', 'NamedTemporaryFile']
### utilities for extracting archives
@ -27,7 +28,15 @@ def extract_tarball(src, dest):
def extract_zip(src, dest):
"""extract a zip file"""
bundle = zipfile.ZipFile(src)
if isinstance(src, zipfile.ZipFile):
bundle = src
else:
try:
bundle = zipfile.ZipFile(src)
except Exception, e:
print "src: %s" % src
raise
namelist = bundle.namelist()
for name in namelist:
@ -178,3 +187,15 @@ 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

View File

@ -4,7 +4,7 @@
from setuptools import setup
PACKAGE_VERSION = '0.2'
PACKAGE_VERSION = '0.3'
setup(name='mozfile',
version=PACKAGE_VERSION,

View File

@ -0,0 +1,20 @@
#!/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()

View File

@ -1 +1,2 @@
[test.py]
[is_url.py]

View File

@ -1,168 +0,0 @@
[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`

View File

@ -2,22 +2,14 @@
# 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.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 = ''
PACKAGE_VERSION = '0.9'
setup(name='mozprocess',
version=PACKAGE_VERSION,
description="Mozilla-authored process handling",
long_description=description,
long_description='see http://mozbase.readthedocs.org/',
classifiers=['Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',

View File

@ -1,2 +1 @@
[mozprocess1.py]
[mozprocess2.py]
[test_mozprocess.py]

View File

@ -1,157 +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 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()

View File

@ -14,11 +14,6 @@ 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.
@ -27,10 +22,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:
@ -39,12 +34,16 @@ def make_proclaunch(aDir):
def check_for_process(processName):
"""
Use to determine if process is still running.
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
"""
# 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
@ -70,22 +69,41 @@ def check_for_process(processName):
return detected, output
class ProcTest2(unittest.TestCase):
def __init__(self, *args, **kwargs):
class ProcTest(unittest.TestCase):
# 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 setUpClass(cls):
cls.proclaunch = make_proclaunch(here)
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"],
@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"],
cwd=here)
p.run(timeout=30)
p.run()
p.wait()
detected, output = check_for_process(self.proclaunch)
@ -95,8 +113,7 @@ class ProcTest2(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"],
@ -110,6 +127,23 @@ class ProcTest2(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.
@ -128,7 +162,36 @@ class ProcTest2(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):
"""
@ -148,16 +211,15 @@ class ProcTest2(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:

View File

@ -1,141 +0,0 @@
[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

View File

@ -2,6 +2,16 @@
# 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 *

View File

@ -16,12 +16,12 @@ AMO_API_VERSION = "1.5"
class AddonManager(object):
"""
Handles all operations regarding addons including: installing and cleaning addons
Handles all operations regarding addons in a profile including: installing and cleaning addons
"""
def __init__(self, profile):
"""
profile - the path to the profile for which we install addons
:param profile: the path to the profile for which we install addons
"""
self.profile = profile
@ -33,11 +33,15 @@ 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
addons - a list of addon paths to install
manifest - a list of addon manifests to install
:param addons: a list of addon paths to install
:param manifest: a list of addon manifests to install
"""
# install addon paths
if addons:
@ -52,12 +56,12 @@ class AddonManager(object):
manifests = [manifests]
for manifest in manifests:
self.install_from_manifest(manifest)
self.installed_manifests.extended(manifests)
self.installed_manifests.extend(manifests)
def install_from_manifest(self, filepath):
"""
Installs addons from a manifest
filepath - path to the manifest of addons to install
:param filepath: path to the manifest of addons to install
"""
manifest = ManifestParser()
manifest.read(filepath)
@ -82,9 +86,11 @@ class AddonManager(object):
@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.
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
"""
response = urllib2.urlopen(query)
dom = minidom.parseString(response.read())
@ -95,13 +101,16 @@ class AddonManager(object):
@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
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
"""
# TODO: We don't use the unpack variable yet, but we should: bug 662683
@ -152,10 +161,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.
- path: url, path to .xpi, or directory of addons
- unpack: whether to unpack unless specified otherwise in the install.rdf
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
"""
# if the addon is a url, download it
@ -209,8 +218,16 @@ 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)
@ -227,3 +244,14 @@ 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

View File

@ -19,6 +19,7 @@ from profile import Profile
__all__ = ['MozProfileCLI', 'cli']
class MozProfileCLI(object):
"""The Command Line Interface for ``mozprofile``."""
module = 'mozprofile'
@ -75,16 +76,22 @@ 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
kwargs = cli.profile_args()
kwargs['restore'] = False
profile = Profile(**kwargs)
profile = cli.profile()
# if no profile was passed in print the newly created profile
if not cli.options.profile:

View File

@ -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,14 +140,13 @@ class ServerLocations(object):
def read(self, filename, check_for_primary=True):
"""
Reads the file (in the format of server-locations.txt) and add all
valid locations to the self._locations array.
Reads the file and adds all valid locations to the ``self._locations`` array.
If check_for_primary is True, a MissingPrimaryLocationError
exception is raised if no primary is found.
: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
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
@ -207,6 +206,8 @@ class ServerLocations(object):
class Permissions(object):
"""Allows handling of permissions for ``mozprofile``"""
_num_permissions = 0
def __init__(self, profileDir, locations=None):
@ -231,6 +232,7 @@ 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 (

View File

@ -6,9 +6,13 @@
user preferences
"""
__all__ = ('PreferencesReadError', 'Preferences')
import os
import re
import tokenize
from ConfigParser import SafeConfigParser as ConfigParser
from StringIO import StringIO
try:
import json
@ -29,7 +33,8 @@ class Preferences(object):
def add(self, prefs, cast=False):
"""
- cast: whether to cast strings to value, e.g. '1' -> 1
:param prefs:
:param cast: whether to cast strings to value, e.g. '1' -> 1
"""
# wants a list of 2-tuples
if isinstance(prefs, dict):
@ -39,7 +44,10 @@ class Preferences(object):
self._prefs += prefs
def add_file(self, path):
"""a preferences from a file"""
"""a preferences from a file
:param path:
"""
self.add(self.read(path))
def __call__(self):
@ -51,6 +59,7 @@ 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
@ -151,18 +160,27 @@ class Preferences(object):
comment = re.compile('/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/', re.MULTILINE)
token = '##//' # magical token
marker = '##//' # magical marker
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('//', token)
line = line.replace('//', marker)
_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))
@ -177,15 +195,15 @@ class Preferences(object):
print line
raise
# de-magic the token
# de-magic the marker
for index, (key, value) in enumerate(retval):
if isinstance(value, basestring) and token in value:
retval[index] = (key, value.replace(token, '//'))
if isinstance(value, basestring) and marker in value:
retval[index] = (key, value.replace(marker, '//'))
return retval
@classmethod
def write(_file, prefs, pref_string='user_pref("%s", %s);'):
def write(cls, _file, prefs, pref_string='user_pref("%s", %s);'):
"""write preferences to a file"""
if isinstance(_file, basestring):
@ -202,7 +220,7 @@ class Preferences(object):
elif value is False:
print >> f, pref_string % (key, 'false')
elif isinstance(value, basestring):
print >> f, pref_string % (key, repr(string(value)))
print >> f, pref_string % (key, repr(str(value)))
else:
print >> f, pref_string % (key, value) # should be numeric!

View File

@ -7,29 +7,34 @@ __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 rmtree
from shutil import copytree, rmtree
from webapps import WebappCollection
try:
import simplejson
import json
except ImportError:
import json as simplejson
import simplejson as json
class Profile(object):
"""Handles all operations regarding profile. Created new profiles, installs extensions,
sets preferences and handles cleanup."""
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
):
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
"""
# if true, remove installed addons/prefs afterwards
self.restore = restore
@ -80,6 +85,10 @@ 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)
@ -100,6 +109,30 @@ 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')
@ -111,7 +144,6 @@ 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')
@ -128,7 +160,7 @@ class Profile(object):
# write the preferences
f.write('\n%s\n' % self.delimeters[0])
_prefs = [(simplejson.dumps(k), simplejson.dumps(v) )
_prefs = [(json.dumps(k), json.dumps(v) )
for k, v in preferences]
for _pref in _prefs:
f.write('user_pref(%s, %s);\n' % _pref)
@ -219,6 +251,7 @@ class Profile(object):
self.clean_preferences()
self.addon_manager.clean_addons()
self.permissions.clean_db()
self.webapps.clean()
__del__ = cleanup
@ -234,6 +267,8 @@ 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
@ -247,13 +282,19 @@ class FirefoxProfile(Profile):
'extensions.update.enabled' : False,
# Don't open a dialog to show available add-on updates
'extensions.update.notifyUser' : False,
# Suppress automatic safe mode after crashes
'toolkit.startup.max_resumed_crashes' : -1,
# 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,
}
class ThunderbirdProfile(Profile):
"""Specialized Profile subclass for Thunderbird"""
preferences = {'extensions.update.enabled' : False,
'extensions.update.notifyUser' : False,
'browser.shell.checkDefaultBrowser' : False,

View File

@ -0,0 +1,279 @@
# 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

View File

@ -2,11 +2,10 @@
# 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.4'
PACKAGE_VERSION = '0.5'
# we only support python 2 right now
assert sys.version_info[0] == 2
@ -23,17 +22,10 @@ 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="Handling of Mozilla Gecko based application profiles",
long_description=description,
description="Library to create and modify Mozilla application profiles",
long_description="see http://mozbase.readthedocs.org/",
classifiers=['Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',

View File

@ -0,0 +1,55 @@
#!/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()

View File

@ -0,0 +1,6 @@
# 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");

View File

@ -0,0 +1,50 @@
[{ "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
}
]

View File

@ -0,0 +1,37 @@
{
"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
}
}

View File

@ -4,3 +4,6 @@
[permissions.py]
[bug758250.py]
[test_nonce.py]
[bug785146.py]
[test_clone_cleanup.py]
[test_webapps.py]

View File

@ -0,0 +1,49 @@
#!/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()

View File

@ -1,28 +1,36 @@
#!/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"""
"""test mozprofile preference handling"""
def run_command(self, *args):
"""
runs mozprofile;
returns (stdout, stderr, code)
invokes mozprofile command line via the CLI factory
- args : command line arguments (equivalent of sys.argv[1:])
"""
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
# instantiate the factory
cli = MozProfileCLI(list(args))
# create the profile
profile = cli.profile()
# return path to profile
return profile.profile
def compare_generated(self, _prefs, commandline):
"""
@ -31,7 +39,7 @@ class PreferencesTest(unittest.TestCase):
compares the results
cleans up
"""
profile, stderr, code = self.run_command(*commandline)
profile = 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)
@ -41,8 +49,10 @@ 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 = ["mozprofile"]
commandline = []
_prefs = _prefs.items()
for pref, value in _prefs:
commandline += ["--pref", "%s:%s" % (pref, value)]
@ -54,7 +64,7 @@ class PreferencesTest(unittest.TestCase):
("zoom.minPercent", 30),
("zoom.maxPercent", 300),
("webgl.verbose", 'false')]
commandline = ["mozprofile"]
commandline = []
for pref, value in _prefs:
commandline += ["--pref", "%s:%s" % (pref, value)]
_prefs = [(i, Preferences.cast(j)) for i, j in _prefs]
@ -69,22 +79,24 @@ browser.startup.homepage = http://planet.mozilla.org/
[foo]
browser.startup.homepage = http://github.com/
"""
fd, name = tempfile.mkstemp(suffix='.ini')
os.write(fd, _ini)
os.close(fd)
commandline = ["mozprofile", "--preferences", name]
try:
fd, name = tempfile.mkstemp(suffix='.ini')
os.write(fd, _ini)
os.close(fd)
commandline = ["--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)
# cleanup
os.remove(name)
finally:
# cleanup
os.remove(name)
def test_reset_should_remove_added_prefs(self):
"""Check that when we call reset the items we expect are updated"""
@ -109,8 +121,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):
@ -192,9 +204,8 @@ user_pref("webgl.force-enabled", true);
# make sure you have the original preferences
prefs = Preferences.read_prefs(user_js)
self.assertTrue(prefs == original_prefs)
except:
finally:
shutil.rmtree(tempdir)
raise
def test_json(self):
_prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"}
@ -205,9 +216,43 @@ user_pref("webgl.force-enabled", true);
os.write(fd, json)
os.close(fd)
commandline = ["mozprofile", "--preferences", name]
commandline = ["--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()

View File

@ -0,0 +1,197 @@
#!/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()

View File

@ -1,43 +0,0 @@
[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

View File

@ -2,24 +2,17 @@
# 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.14'
PACKAGE_VERSION = '5.15'
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
@ -28,7 +21,7 @@ assert sys.version_info[0] == 2
setup(name=PACKAGE_NAME,
version=PACKAGE_VERSION,
description=desc,
long_description=description,
long_description="see http://mozbase.readthedocs.org/",
classifiers=['Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
@ -40,7 +33,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,

View File

@ -28,6 +28,7 @@ try:
import mozcrash
except:
deps = ['mozcrash',
'mozfile',
'mozlog']
for dep in deps:
module = os.path.join(mozbase, dep)