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