Revert "Revert "Upgrade to 5.0.71.48""
This reverts commit f2e3994fa5148cc3d9946666f0b0596290192b0e,
and updates the x64 makefile properly so it doesn't break that
build.
Change-Id: Ib83e35bfbae6af627451c926a9650ec57c045605
diff --git a/build/android/pylib/utils/__init__.py b/build/android/pylib/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/build/android/pylib/utils/__init__.py
diff --git a/build/android/pylib/utils/apk_helper.py b/build/android/pylib/utils/apk_helper.py
new file mode 100644
index 0000000..dd45807
--- /dev/null
+++ b/build/android/pylib/utils/apk_helper.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.android.apk_helper import *
diff --git a/build/android/pylib/utils/argparse_utils.py b/build/android/pylib/utils/argparse_utils.py
new file mode 100644
index 0000000..e456d9d
--- /dev/null
+++ b/build/android/pylib/utils/argparse_utils.py
@@ -0,0 +1,50 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import argparse
+
+
+class CustomHelpAction(argparse.Action):
+ '''Allows defining custom help actions.
+
+ Help actions can run even when the parser would otherwise fail on missing
+ arguments. The first help or custom help command mentioned on the command
+ line will have its help text displayed.
+
+ Usage:
+ parser = argparse.ArgumentParser(...)
+ CustomHelpAction.EnableFor(parser)
+ parser.add_argument('--foo-help',
+ action='custom_help',
+ custom_help_text='this is the help message',
+ help='What this helps with')
+ '''
+ # Derived from argparse._HelpAction from
+ # https://github.com/python/cpython/blob/master/Lib/argparse.py
+
+ # pylint: disable=redefined-builtin
+ # (complains about 'help' being redefined)
+ def __init__(self,
+ option_strings,
+ dest=argparse.SUPPRESS,
+ default=argparse.SUPPRESS,
+ custom_help_text=None,
+ help=None):
+ super(CustomHelpAction, self).__init__(option_strings=option_strings,
+ dest=dest,
+ default=default,
+ nargs=0,
+ help=help)
+
+ if not custom_help_text:
+ raise ValueError('custom_help_text is required')
+ self._help_text = custom_help_text
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ print self._help_text
+ parser.exit()
+
+ @staticmethod
+ def EnableFor(parser):
+ parser.register('action', 'custom_help', CustomHelpAction)
diff --git a/build/android/pylib/utils/base_error.py b/build/android/pylib/utils/base_error.py
new file mode 100644
index 0000000..263479a
--- /dev/null
+++ b/build/android/pylib/utils/base_error.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.base_error import *
diff --git a/build/android/pylib/utils/command_option_parser.py b/build/android/pylib/utils/command_option_parser.py
new file mode 100644
index 0000000..cf501d0
--- /dev/null
+++ b/build/android/pylib/utils/command_option_parser.py
@@ -0,0 +1,75 @@
+# Copyright 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""An option parser which handles the first arg as a command.
+
+Add other nice functionality such as printing a list of commands
+and an example in usage.
+"""
+
+import optparse
+import sys
+
+
+class CommandOptionParser(optparse.OptionParser):
+ """Wrapper class for OptionParser to help with listing commands."""
+
+ def __init__(self, *args, **kwargs):
+ """Creates a CommandOptionParser.
+
+ Args:
+ commands_dict: A dictionary mapping command strings to an object defining
+ - add_options_func: Adds options to the option parser
+ - run_command_func: Runs the command itself.
+ example: An example command.
+ everything else: Passed to optparse.OptionParser contructor.
+ """
+ self.commands_dict = kwargs.pop('commands_dict', {})
+ self.example = kwargs.pop('example', '')
+ if not 'usage' in kwargs:
+ kwargs['usage'] = 'Usage: %prog <command> [options]'
+ optparse.OptionParser.__init__(self, *args, **kwargs)
+
+ #override
+ def get_usage(self):
+ normal_usage = optparse.OptionParser.get_usage(self)
+ command_list = self.get_command_list()
+ example = self.get_example()
+ return self.expand_prog_name(normal_usage + example + command_list)
+
+ #override
+ def get_command_list(self):
+ if self.commands_dict.keys():
+ return '\nCommands:\n %s\n' % '\n '.join(
+ sorted(self.commands_dict.keys()))
+ return ''
+
+ def get_example(self):
+ if self.example:
+ return '\nExample:\n %s\n' % self.example
+ return ''
+
+
+def ParseAndExecute(option_parser, argv=None):
+ """Parses options/args from argv and runs the specified command.
+
+ Args:
+ option_parser: A CommandOptionParser object.
+ argv: Command line arguments. If None, automatically draw from sys.argv.
+
+ Returns:
+ An exit code.
+ """
+ if not argv:
+ argv = sys.argv
+
+ if len(argv) < 2 or argv[1] not in option_parser.commands_dict:
+ # Parse args first, if this is '--help', optparse will print help and exit
+ option_parser.parse_args(argv)
+ option_parser.error('Invalid command.')
+
+ cmd = option_parser.commands_dict[argv[1]]
+ cmd.add_options_func(option_parser)
+ options, args = option_parser.parse_args(argv)
+ return cmd.run_command_func(argv[1], options, args, option_parser)
diff --git a/build/android/pylib/utils/device_temp_file.py b/build/android/pylib/utils/device_temp_file.py
new file mode 100644
index 0000000..ae1edc8
--- /dev/null
+++ b/build/android/pylib/utils/device_temp_file.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.android.device_temp_file import *
diff --git a/build/android/pylib/utils/emulator.py b/build/android/pylib/utils/emulator.py
new file mode 100644
index 0000000..e2a5fea
--- /dev/null
+++ b/build/android/pylib/utils/emulator.py
@@ -0,0 +1,520 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Provides an interface to start and stop Android emulator.
+
+ Emulator: The class provides the methods to launch/shutdown the emulator with
+ the android virtual device named 'avd_armeabi' .
+"""
+
+import logging
+import os
+import signal
+import subprocess
+import time
+
+from devil.android import device_errors
+from devil.android import device_utils
+from devil.android.sdk import adb_wrapper
+from devil.utils import cmd_helper
+from pylib import constants
+from pylib import pexpect
+from pylib.utils import time_profile
+
+# Default sdcard size in the format of [amount][unit]
+DEFAULT_SDCARD_SIZE = '512M'
+# Default internal storage (MB) of emulator image
+DEFAULT_STORAGE_SIZE = '1024M'
+
+# Each emulator has 60 secs of wait time for launching
+_BOOT_WAIT_INTERVALS = 6
+_BOOT_WAIT_INTERVAL_TIME = 10
+
+# Path for avd files and avd dir
+_BASE_AVD_DIR = os.path.expanduser(os.path.join('~', '.android', 'avd'))
+_TOOLS_ANDROID_PATH = os.path.join(constants.ANDROID_SDK_ROOT,
+ 'tools', 'android')
+
+# Template used to generate config.ini files for the emulator
+CONFIG_TEMPLATE = """avd.ini.encoding=ISO-8859-1
+hw.dPad=no
+hw.lcd.density=320
+sdcard.size={sdcard.size}
+hw.cpu.arch={hw.cpu.arch}
+hw.device.hash=-708107041
+hw.camera.back=none
+disk.dataPartition.size=800M
+hw.gpu.enabled={gpu}
+skin.path=720x1280
+skin.dynamic=yes
+hw.keyboard=yes
+hw.ramSize=1024
+hw.device.manufacturer=Google
+hw.sdCard=yes
+hw.mainKeys=no
+hw.accelerometer=yes
+skin.name=720x1280
+abi.type={abi.type}
+hw.trackBall=no
+hw.device.name=Galaxy Nexus
+hw.battery=yes
+hw.sensors.proximity=yes
+image.sysdir.1=system-images/android-{api.level}/default/{abi.type}/
+hw.sensors.orientation=yes
+hw.audioInput=yes
+hw.camera.front=none
+hw.gps=yes
+vm.heapSize=128
+{extras}"""
+
+CONFIG_REPLACEMENTS = {
+ 'x86': {
+ '{hw.cpu.arch}': 'x86',
+ '{abi.type}': 'x86',
+ '{extras}': ''
+ },
+ 'arm': {
+ '{hw.cpu.arch}': 'arm',
+ '{abi.type}': 'armeabi-v7a',
+ '{extras}': 'hw.cpu.model=cortex-a8\n'
+ },
+ 'mips': {
+ '{hw.cpu.arch}': 'mips',
+ '{abi.type}': 'mips',
+ '{extras}': ''
+ }
+}
+
+class EmulatorLaunchException(Exception):
+ """Emulator failed to launch."""
+ pass
+
+def WaitForEmulatorLaunch(num):
+ """Wait for emulators to finish booting
+
+ Emulators on bots are launch with a separate background process, to avoid
+ running tests before the emulators are fully booted, this function waits for
+ a number of emulators to finish booting
+
+ Arg:
+ num: the amount of emulators to wait.
+ """
+ for _ in range(num*_BOOT_WAIT_INTERVALS):
+ emulators = [device_utils.DeviceUtils(a)
+ for a in adb_wrapper.AdbWrapper.Devices()
+ if a.is_emulator]
+ if len(emulators) >= num:
+ logging.info('All %d emulators launched', num)
+ return
+ logging.info(
+ 'Waiting for %d emulators, %d of them already launched', num,
+ len(emulators))
+ time.sleep(_BOOT_WAIT_INTERVAL_TIME)
+ raise Exception("Expected %d emulators, %d launched within time limit" %
+ (num, len(emulators)))
+
+def KillAllEmulators():
+ """Kill all running emulators that look like ones we started.
+
+ There are odd 'sticky' cases where there can be no emulator process
+ running but a device slot is taken. A little bot trouble and we're out of
+ room forever.
+ """
+ logging.info('Killing all existing emulators and existing the program')
+ emulators = [device_utils.DeviceUtils(a)
+ for a in adb_wrapper.AdbWrapper.Devices()
+ if a.is_emulator]
+ if not emulators:
+ return
+ for e in emulators:
+ e.adb.Emu(['kill'])
+ logging.info('Emulator killing is async; give a few seconds for all to die.')
+ for _ in range(10):
+ if not any(a.is_emulator for a in adb_wrapper.AdbWrapper.Devices()):
+ return
+ time.sleep(1)
+
+
+def DeleteAllTempAVDs():
+ """Delete all temporary AVDs which are created for tests.
+
+ If the test exits abnormally and some temporary AVDs created when testing may
+ be left in the system. Clean these AVDs.
+ """
+ logging.info('Deleting all the avd files')
+ avds = device_utils.GetAVDs()
+ if not avds:
+ return
+ for avd_name in avds:
+ if 'run_tests_avd' in avd_name:
+ cmd = [_TOOLS_ANDROID_PATH, '-s', 'delete', 'avd', '--name', avd_name]
+ cmd_helper.RunCmd(cmd)
+ logging.info('Delete AVD %s', avd_name)
+
+
+class PortPool(object):
+ """Pool for emulator port starting position that changes over time."""
+ _port_min = 5554
+ _port_max = 5585
+ _port_current_index = 0
+
+ @classmethod
+ def port_range(cls):
+ """Return a range of valid ports for emulator use.
+
+ The port must be an even number between 5554 and 5584. Sometimes
+ a killed emulator "hangs on" to a port long enough to prevent
+ relaunch. This is especially true on slow machines (like a bot).
+ Cycling through a port start position helps make us resilient."""
+ ports = range(cls._port_min, cls._port_max, 2)
+ n = cls._port_current_index
+ cls._port_current_index = (n + 1) % len(ports)
+ return ports[n:] + ports[:n]
+
+
+def _GetAvailablePort():
+ """Returns an available TCP port for the console."""
+ used_ports = []
+ emulators = [device_utils.DeviceUtils(a)
+ for a in adb_wrapper.AdbWrapper.Devices()
+ if a.is_emulator]
+ for emulator in emulators:
+ used_ports.append(emulator.adb.GetDeviceSerial().split('-')[1])
+ for port in PortPool.port_range():
+ if str(port) not in used_ports:
+ return port
+
+
+def LaunchTempEmulators(emulator_count, abi, api_level, enable_kvm=False,
+ kill_and_launch=True, sdcard_size=DEFAULT_SDCARD_SIZE,
+ storage_size=DEFAULT_STORAGE_SIZE, wait_for_boot=True,
+ headless=False):
+ """Create and launch temporary emulators and wait for them to boot.
+
+ Args:
+ emulator_count: number of emulators to launch.
+ abi: the emulator target platform
+ api_level: the api level (e.g., 19 for Android v4.4 - KitKat release)
+ wait_for_boot: whether or not to wait for emulators to boot up
+ headless: running emulator with no ui
+
+ Returns:
+ List of emulators.
+ """
+ emulators = []
+ for n in xrange(emulator_count):
+ t = time_profile.TimeProfile('Emulator launch %d' % n)
+ # Creates a temporary AVD.
+ avd_name = 'run_tests_avd_%d' % n
+ logging.info('Emulator launch %d with avd_name=%s and api=%d',
+ n, avd_name, api_level)
+ emulator = Emulator(avd_name, abi, enable_kvm=enable_kvm,
+ sdcard_size=sdcard_size, storage_size=storage_size,
+ headless=headless)
+ emulator.CreateAVD(api_level)
+ emulator.Launch(kill_all_emulators=(n == 0 and kill_and_launch))
+ t.Stop()
+ emulators.append(emulator)
+ # Wait for all emulators to boot completed.
+ if wait_for_boot:
+ for emulator in emulators:
+ emulator.ConfirmLaunch(True)
+ logging.info('All emulators are fully booted')
+ return emulators
+
+
+def LaunchEmulator(avd_name, abi, kill_and_launch=True, enable_kvm=False,
+ sdcard_size=DEFAULT_SDCARD_SIZE,
+ storage_size=DEFAULT_STORAGE_SIZE, headless=False):
+ """Launch an existing emulator with name avd_name.
+
+ Args:
+ avd_name: name of existing emulator
+ abi: the emulator target platform
+ headless: running emulator with no ui
+
+ Returns:
+ emulator object.
+ """
+ logging.info('Specified emulator named avd_name=%s launched', avd_name)
+ emulator = Emulator(avd_name, abi, enable_kvm=enable_kvm,
+ sdcard_size=sdcard_size, storage_size=storage_size,
+ headless=headless)
+ emulator.Launch(kill_all_emulators=kill_and_launch)
+ emulator.ConfirmLaunch(True)
+ return emulator
+
+
+class Emulator(object):
+ """Provides the methods to launch/shutdown the emulator.
+
+ The emulator has the android virtual device named 'avd_armeabi'.
+
+ The emulator could use any even TCP port between 5554 and 5584 for the
+ console communication, and this port will be part of the device name like
+ 'emulator-5554'. Assume it is always True, as the device name is the id of
+ emulator managed in this class.
+
+ Attributes:
+ emulator: Path of Android's emulator tool.
+ popen: Popen object of the running emulator process.
+ device: Device name of this emulator.
+ """
+
+ # Signals we listen for to kill the emulator on
+ _SIGNALS = (signal.SIGINT, signal.SIGHUP)
+
+ # Time to wait for an emulator launch, in seconds. This includes
+ # the time to launch the emulator and a wait-for-device command.
+ _LAUNCH_TIMEOUT = 120
+
+ # Timeout interval of wait-for-device command before bouncing to a a
+ # process life check.
+ _WAITFORDEVICE_TIMEOUT = 5
+
+ # Time to wait for a 'wait for boot complete' (property set on device).
+ _WAITFORBOOT_TIMEOUT = 300
+
+ def __init__(self, avd_name, abi, enable_kvm=False,
+ sdcard_size=DEFAULT_SDCARD_SIZE,
+ storage_size=DEFAULT_STORAGE_SIZE, headless=False):
+ """Init an Emulator.
+
+ Args:
+ avd_name: name of the AVD to create
+ abi: target platform for emulator being created, defaults to x86
+ """
+ android_sdk_root = constants.ANDROID_SDK_ROOT
+ self.emulator = os.path.join(android_sdk_root, 'tools', 'emulator')
+ self.android = _TOOLS_ANDROID_PATH
+ self.popen = None
+ self.device_serial = None
+ self.abi = abi
+ self.avd_name = avd_name
+ self.sdcard_size = sdcard_size
+ self.storage_size = storage_size
+ self.enable_kvm = enable_kvm
+ self.headless = headless
+
+ @staticmethod
+ def _DeviceName():
+ """Return our device name."""
+ port = _GetAvailablePort()
+ return ('emulator-%d' % port, port)
+
+ def CreateAVD(self, api_level):
+ """Creates an AVD with the given name.
+
+ Args:
+ api_level: the api level of the image
+
+ Return avd_name.
+ """
+
+ if self.abi == 'arm':
+ abi_option = 'armeabi-v7a'
+ elif self.abi == 'mips':
+ abi_option = 'mips'
+ else:
+ abi_option = 'x86'
+
+ api_target = 'android-%s' % api_level
+
+ avd_command = [
+ self.android,
+ '--silent',
+ 'create', 'avd',
+ '--name', self.avd_name,
+ '--abi', abi_option,
+ '--target', api_target,
+ '--sdcard', self.sdcard_size,
+ '--force',
+ ]
+ avd_cmd_str = ' '.join(avd_command)
+ logging.info('Create AVD command: %s', avd_cmd_str)
+ avd_process = pexpect.spawn(avd_cmd_str)
+
+ # Instead of creating a custom profile, we overwrite config files.
+ avd_process.expect('Do you wish to create a custom hardware profile')
+ avd_process.sendline('no\n')
+ avd_process.expect('Created AVD \'%s\'' % self.avd_name)
+
+ # Replace current configuration with default Galaxy Nexus config.
+ ini_file = os.path.join(_BASE_AVD_DIR, '%s.ini' % self.avd_name)
+ new_config_ini = os.path.join(_BASE_AVD_DIR, '%s.avd' % self.avd_name,
+ 'config.ini')
+
+ # Remove config files with defaults to replace with Google's GN settings.
+ os.unlink(ini_file)
+ os.unlink(new_config_ini)
+
+ # Create new configuration files with Galaxy Nexus by Google settings.
+ with open(ini_file, 'w') as new_ini:
+ new_ini.write('avd.ini.encoding=ISO-8859-1\n')
+ new_ini.write('target=%s\n' % api_target)
+ new_ini.write('path=%s/%s.avd\n' % (_BASE_AVD_DIR, self.avd_name))
+ new_ini.write('path.rel=avd/%s.avd\n' % self.avd_name)
+
+ custom_config = CONFIG_TEMPLATE
+ replacements = CONFIG_REPLACEMENTS[self.abi]
+ for key in replacements:
+ custom_config = custom_config.replace(key, replacements[key])
+ custom_config = custom_config.replace('{api.level}', str(api_level))
+ custom_config = custom_config.replace('{sdcard.size}', self.sdcard_size)
+ custom_config.replace('{gpu}', 'no' if self.headless else 'yes')
+
+ with open(new_config_ini, 'w') as new_config_ini:
+ new_config_ini.write(custom_config)
+
+ return self.avd_name
+
+
+ def _DeleteAVD(self):
+ """Delete the AVD of this emulator."""
+ avd_command = [
+ self.android,
+ '--silent',
+ 'delete',
+ 'avd',
+ '--name', self.avd_name,
+ ]
+ logging.info('Delete AVD command: %s', ' '.join(avd_command))
+ cmd_helper.RunCmd(avd_command)
+
+ def ResizeAndWipeAvd(self, storage_size):
+ """Wipes old AVD and creates new AVD of size |storage_size|.
+
+ This serves as a work around for '-partition-size' and '-wipe-data'
+ """
+ userdata_img = os.path.join(_BASE_AVD_DIR, '%s.avd' % self.avd_name,
+ 'userdata.img')
+ userdata_qemu_img = os.path.join(_BASE_AVD_DIR, '%s.avd' % self.avd_name,
+ 'userdata-qemu.img')
+ resize_cmd = ['resize2fs', userdata_img, '%s' % storage_size]
+ logging.info('Resizing userdata.img to ideal size')
+ cmd_helper.RunCmd(resize_cmd)
+ wipe_cmd = ['cp', userdata_img, userdata_qemu_img]
+ logging.info('Replacing userdata-qemu.img with the new userdata.img')
+ cmd_helper.RunCmd(wipe_cmd)
+
+ def Launch(self, kill_all_emulators):
+ """Launches the emulator asynchronously. Call ConfirmLaunch() to ensure the
+ emulator is ready for use.
+
+ If fails, an exception will be raised.
+ """
+ if kill_all_emulators:
+ KillAllEmulators() # just to be sure
+ self._AggressiveImageCleanup()
+ (self.device_serial, port) = self._DeviceName()
+ self.ResizeAndWipeAvd(storage_size=self.storage_size)
+ emulator_command = [
+ self.emulator,
+ # Speed up emulator launch by 40%. Really.
+ '-no-boot-anim',
+ ]
+ if self.headless:
+ emulator_command.extend([
+ '-no-skin',
+ '-no-audio',
+ '-no-window'
+ ])
+ else:
+ emulator_command.extend([
+ '-gpu', 'on'
+ ])
+ emulator_command.extend([
+ # Use a familiar name and port.
+ '-avd', self.avd_name,
+ '-port', str(port),
+ # all the argument after qemu are sub arguments for qemu
+ '-qemu', '-m', '1024',
+ ])
+ if self.abi == 'x86' and self.enable_kvm:
+ emulator_command.extend([
+ # For x86 emulator --enable-kvm will fail early, avoiding accidental
+ # runs in a slow mode (i.e. without hardware virtualization support).
+ '--enable-kvm',
+ ])
+
+ logging.info('Emulator launch command: %s', ' '.join(emulator_command))
+ self.popen = subprocess.Popen(args=emulator_command,
+ stderr=subprocess.STDOUT)
+ self._InstallKillHandler()
+
+ @staticmethod
+ def _AggressiveImageCleanup():
+ """Aggressive cleanup of emulator images.
+
+ Experimentally it looks like our current emulator use on the bot
+ leaves image files around in /tmp/android-$USER. If a "random"
+ name gets reused, we choke with a 'File exists' error.
+ TODO(jrg): is there a less hacky way to accomplish the same goal?
+ """
+ logging.info('Aggressive Image Cleanup')
+ emulator_imagedir = '/tmp/android-%s' % os.environ['USER']
+ if not os.path.exists(emulator_imagedir):
+ return
+ for image in os.listdir(emulator_imagedir):
+ full_name = os.path.join(emulator_imagedir, image)
+ if 'emulator' in full_name:
+ logging.info('Deleting emulator image %s', full_name)
+ os.unlink(full_name)
+
+ def ConfirmLaunch(self, wait_for_boot=False):
+ """Confirm the emulator launched properly.
+
+ Loop on a wait-for-device with a very small timeout. On each
+ timeout, check the emulator process is still alive.
+ After confirming a wait-for-device can be successful, make sure
+ it returns the right answer.
+ """
+ seconds_waited = 0
+ number_of_waits = 2 # Make sure we can wfd twice
+
+ device = device_utils.DeviceUtils(self.device_serial)
+ while seconds_waited < self._LAUNCH_TIMEOUT:
+ try:
+ device.adb.WaitForDevice(
+ timeout=self._WAITFORDEVICE_TIMEOUT, retries=1)
+ number_of_waits -= 1
+ if not number_of_waits:
+ break
+ except device_errors.CommandTimeoutError:
+ seconds_waited += self._WAITFORDEVICE_TIMEOUT
+ device.adb.KillServer()
+ self.popen.poll()
+ if self.popen.returncode != None:
+ raise EmulatorLaunchException('EMULATOR DIED')
+
+ if seconds_waited >= self._LAUNCH_TIMEOUT:
+ raise EmulatorLaunchException('TIMEOUT with wait-for-device')
+
+ logging.info('Seconds waited on wait-for-device: %d', seconds_waited)
+ if wait_for_boot:
+ # Now that we checked for obvious problems, wait for a boot complete.
+ # Waiting for the package manager is sometimes problematic.
+ device.WaitUntilFullyBooted(timeout=self._WAITFORBOOT_TIMEOUT)
+ logging.info('%s is now fully booted', self.avd_name)
+
+ def Shutdown(self):
+ """Shuts down the process started by launch."""
+ self._DeleteAVD()
+ if self.popen:
+ self.popen.poll()
+ if self.popen.returncode == None:
+ self.popen.kill()
+ self.popen = None
+
+ def _ShutdownOnSignal(self, _signum, _frame):
+ logging.critical('emulator _ShutdownOnSignal')
+ for sig in self._SIGNALS:
+ signal.signal(sig, signal.SIG_DFL)
+ self.Shutdown()
+ raise KeyboardInterrupt # print a stack
+
+ def _InstallKillHandler(self):
+ """Install a handler to kill the emulator when we exit unexpectedly."""
+ for sig in self._SIGNALS:
+ signal.signal(sig, self._ShutdownOnSignal)
diff --git a/build/android/pylib/utils/findbugs.py b/build/android/pylib/utils/findbugs.py
new file mode 100644
index 0000000..0456893
--- /dev/null
+++ b/build/android/pylib/utils/findbugs.py
@@ -0,0 +1,155 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import os
+import xml.dom.minidom
+
+from devil.utils import cmd_helper
+from pylib import constants
+from pylib.constants import host_paths
+
+
+_FINDBUGS_HOME = os.path.join(host_paths.DIR_SOURCE_ROOT, 'third_party',
+ 'findbugs')
+_FINDBUGS_JAR = os.path.join(_FINDBUGS_HOME, 'lib', 'findbugs.jar')
+_FINDBUGS_MAX_HEAP = 768
+_FINDBUGS_PLUGIN_PATH = os.path.join(
+ host_paths.DIR_SOURCE_ROOT, 'tools', 'android', 'findbugs_plugin', 'lib',
+ 'chromiumPlugin.jar')
+
+
+def _ParseXmlResults(results_doc):
+ warnings = set()
+ for en in (n for n in results_doc.documentElement.childNodes
+ if n.nodeType == xml.dom.Node.ELEMENT_NODE):
+ if en.tagName == 'BugInstance':
+ warnings.add(_ParseBugInstance(en))
+ return warnings
+
+
+def _GetMessage(node):
+ for c in (n for n in node.childNodes
+ if n.nodeType == xml.dom.Node.ELEMENT_NODE):
+ if c.tagName == 'Message':
+ if (len(c.childNodes) == 1
+ and c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE):
+ return c.childNodes[0].data
+ return None
+
+
+def _ParseBugInstance(node):
+ bug = FindBugsWarning(node.getAttribute('type'))
+ msg_parts = []
+ for c in (n for n in node.childNodes
+ if n.nodeType == xml.dom.Node.ELEMENT_NODE):
+ if c.tagName == 'Class':
+ msg_parts.append(_GetMessage(c))
+ elif c.tagName == 'Method':
+ msg_parts.append(_GetMessage(c))
+ elif c.tagName == 'Field':
+ msg_parts.append(_GetMessage(c))
+ elif c.tagName == 'SourceLine':
+ bug.file_name = c.getAttribute('sourcefile')
+ if c.hasAttribute('start'):
+ bug.start_line = int(c.getAttribute('start'))
+ if c.hasAttribute('end'):
+ bug.end_line = int(c.getAttribute('end'))
+ msg_parts.append(_GetMessage(c))
+ elif (c.tagName == 'ShortMessage' and len(c.childNodes) == 1
+ and c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE):
+ msg_parts.append(c.childNodes[0].data)
+ bug.message = tuple(m for m in msg_parts if m)
+ return bug
+
+
+class FindBugsWarning(object):
+
+ def __init__(self, bug_type='', end_line=0, file_name='', message=None,
+ start_line=0):
+ self.bug_type = bug_type
+ self.end_line = end_line
+ self.file_name = file_name
+ if message is None:
+ self.message = tuple()
+ else:
+ self.message = message
+ self.start_line = start_line
+
+ def __cmp__(self, other):
+ return (cmp(self.file_name, other.file_name)
+ or cmp(self.start_line, other.start_line)
+ or cmp(self.end_line, other.end_line)
+ or cmp(self.bug_type, other.bug_type)
+ or cmp(self.message, other.message))
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ def __hash__(self):
+ return hash((self.bug_type, self.end_line, self.file_name, self.message,
+ self.start_line))
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __str__(self):
+ return '%s: %s' % (self.bug_type, '\n '.join(self.message))
+
+
+def Run(exclude, classes_to_analyze, auxiliary_classes, output_file,
+ findbug_args, jars):
+ """Run FindBugs.
+
+ Args:
+ exclude: the exclude xml file, refer to FindBugs's -exclude command option.
+ classes_to_analyze: the list of classes need to analyze, refer to FindBug's
+ -onlyAnalyze command line option.
+ auxiliary_classes: the classes help to analyze, refer to FindBug's
+ -auxclasspath command line option.
+ output_file: An optional path to dump XML results to.
+ findbug_args: A list of addtional command line options to pass to Findbugs.
+ """
+ # TODO(jbudorick): Get this from the build system.
+ system_classes = [
+ os.path.join(constants.ANDROID_SDK_ROOT, 'platforms',
+ 'android-%s' % constants.ANDROID_SDK_VERSION, 'android.jar')
+ ]
+ system_classes.extend(os.path.abspath(classes)
+ for classes in auxiliary_classes or [])
+
+ cmd = ['java',
+ '-classpath', '%s:' % _FINDBUGS_JAR,
+ '-Xmx%dm' % _FINDBUGS_MAX_HEAP,
+ '-Dfindbugs.home="%s"' % _FINDBUGS_HOME,
+ '-jar', _FINDBUGS_JAR,
+ '-textui', '-sortByClass',
+ '-pluginList', _FINDBUGS_PLUGIN_PATH, '-xml:withMessages']
+ if system_classes:
+ cmd.extend(['-auxclasspath', ':'.join(system_classes)])
+ if classes_to_analyze:
+ cmd.extend(['-onlyAnalyze', classes_to_analyze])
+ if exclude:
+ cmd.extend(['-exclude', os.path.abspath(exclude)])
+ if output_file:
+ cmd.extend(['-output', output_file])
+ if findbug_args:
+ cmd.extend(findbug_args)
+ cmd.extend(os.path.abspath(j) for j in jars or [])
+
+ if output_file:
+ _, _, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd)
+
+ results_doc = xml.dom.minidom.parse(output_file)
+ else:
+ _, raw_out, stderr = cmd_helper.GetCmdStatusOutputAndError(cmd)
+ results_doc = xml.dom.minidom.parseString(raw_out)
+
+ for line in stderr.splitlines():
+ logging.debug(' %s', line)
+
+ current_warnings_set = _ParseXmlResults(results_doc)
+
+ return (' '.join(cmd), current_warnings_set)
+
diff --git a/build/android/pylib/utils/host_utils.py b/build/android/pylib/utils/host_utils.py
new file mode 100644
index 0000000..ba8c9d2
--- /dev/null
+++ b/build/android/pylib/utils/host_utils.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.utils.host_utils import *
diff --git a/build/android/pylib/utils/isolator.py b/build/android/pylib/utils/isolator.py
new file mode 100644
index 0000000..f8177e0
--- /dev/null
+++ b/build/android/pylib/utils/isolator.py
@@ -0,0 +1,192 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import fnmatch
+import glob
+import os
+import shutil
+import sys
+import tempfile
+
+from devil.utils import cmd_helper
+from pylib import constants
+from pylib.constants import host_paths
+
+
+_ISOLATE_SCRIPT = os.path.join(
+ host_paths.DIR_SOURCE_ROOT, 'tools', 'swarming_client', 'isolate.py')
+
+
+def DefaultPathVariables():
+ return {
+ 'DEPTH': host_paths.DIR_SOURCE_ROOT,
+ 'PRODUCT_DIR': constants.GetOutDirectory(),
+ }
+
+
+def DefaultConfigVariables():
+ # Note: This list must match the --config-vars in build/isolate.gypi
+ return {
+ 'CONFIGURATION_NAME': constants.GetBuildType(),
+ 'OS': 'android',
+ 'asan': '0',
+ 'branding': 'Chromium',
+ 'chromeos': '0',
+ 'component': 'static_library',
+ 'enable_pepper_cdms': '0',
+ 'enable_plugins': '0',
+ 'fastbuild': '0',
+ 'icu_use_data_file_flag': '1',
+ 'kasko': '0',
+ 'lsan': '0',
+ 'msan': '0',
+ # TODO(maruel): This may not always be true.
+ 'target_arch': 'arm',
+ 'tsan': '0',
+ 'use_custom_libcxx': '0',
+ 'use_instrumented_libraries': '0',
+ 'use_prebuilt_instrumented_libraries': '0',
+ 'use_ozone': '0',
+ 'use_x11': '0',
+ 'v8_use_external_startup_data': '1',
+ 'msvs_version': '0',
+ }
+
+
+def IsIsolateEmpty(isolate_path):
+ """Returns whether there are no files in the .isolate."""
+ with open(isolate_path) as f:
+ return "'files': []" in f.read()
+
+
+class Isolator(object):
+ """Manages calls to isolate.py for the android test runner scripts."""
+
+ def __init__(self):
+ self._isolate_deps_dir = tempfile.mkdtemp()
+
+ @property
+ def isolate_deps_dir(self):
+ return self._isolate_deps_dir
+
+ def Clear(self):
+ """Deletes the isolate dependency directory."""
+ if os.path.exists(self._isolate_deps_dir):
+ shutil.rmtree(self._isolate_deps_dir)
+
+ def Remap(self, isolate_abs_path, isolated_abs_path,
+ path_variables=None, config_variables=None):
+ """Remaps data dependencies into |self._isolate_deps_dir|.
+
+ Args:
+ isolate_abs_path: The absolute path to the .isolate file, which specifies
+ data dependencies in the source tree.
+ isolated_abs_path: The absolute path to the .isolated file, which is
+ generated by isolate.py and specifies data dependencies in
+ |self._isolate_deps_dir| and their digests.
+ path_variables: A dict containing everything that should be passed
+ as a |--path-variable| to the isolate script. Defaults to the return
+ value of |DefaultPathVariables()|.
+ config_variables: A dict containing everything that should be passed
+ as a |--config-variable| to the isolate script. Defaults to the return
+ value of |DefaultConfigVariables()|.
+ Raises:
+ Exception if the isolate command fails for some reason.
+ """
+ if not path_variables:
+ path_variables = DefaultPathVariables()
+ if not config_variables:
+ config_variables = DefaultConfigVariables()
+
+ isolate_cmd = [
+ sys.executable, _ISOLATE_SCRIPT, 'remap',
+ '--isolate', isolate_abs_path,
+ '--isolated', isolated_abs_path,
+ '--outdir', self._isolate_deps_dir,
+ ]
+ for k, v in path_variables.iteritems():
+ isolate_cmd.extend(['--path-variable', k, v])
+ for k, v in config_variables.iteritems():
+ isolate_cmd.extend(['--config-variable', k, v])
+
+ exit_code, _ = cmd_helper.GetCmdStatusAndOutput(isolate_cmd)
+ if exit_code:
+ raise Exception('isolate command failed: %s' % ' '.join(isolate_cmd))
+
+ def VerifyHardlinks(self):
+ """Checks |isolate_deps_dir| for a hardlink.
+
+ Returns:
+ True if a hardlink is found.
+ False if nothing is found.
+ Raises:
+ Exception if a non-hardlink is found.
+ """
+ for root, _, filenames in os.walk(self._isolate_deps_dir):
+ if filenames:
+ linked_file = os.path.join(root, filenames[0])
+ orig_file = os.path.join(
+ self._isolate_deps_dir,
+ os.path.relpath(linked_file, self._isolate_deps_dir))
+ if os.stat(linked_file).st_ino == os.stat(orig_file).st_ino:
+ return True
+ else:
+ raise Exception('isolate remap command did not use hardlinks.')
+ return False
+
+ def PurgeExcluded(self, deps_exclusion_list):
+ """Deletes anything on |deps_exclusion_list| from |self._isolate_deps_dir|.
+
+ Args:
+ deps_exclusion_list: A list of globs to exclude from the isolate
+ dependency directory.
+ """
+ excluded_paths = (
+ x for y in deps_exclusion_list
+ for x in glob.glob(
+ os.path.abspath(os.path.join(self._isolate_deps_dir, y))))
+ for p in excluded_paths:
+ if os.path.isdir(p):
+ shutil.rmtree(p)
+ else:
+ os.remove(p)
+
+ @classmethod
+ def _DestructiveMerge(cls, src, dest):
+ if os.path.exists(dest) and os.path.isdir(dest):
+ for p in os.listdir(src):
+ cls._DestructiveMerge(os.path.join(src, p), os.path.join(dest, p))
+ os.rmdir(src)
+ else:
+ shutil.move(src, dest)
+
+
+ def MoveOutputDeps(self):
+ """Moves files from the output directory to the top level of
+ |self._isolate_deps_dir|.
+
+ Moves pak files from the output directory to to <isolate_deps_dir>/paks
+ Moves files from the product directory to <isolate_deps_dir>
+ """
+ # On Android, all pak files need to be in the top-level 'paks' directory.
+ paks_dir = os.path.join(self._isolate_deps_dir, 'paks')
+ os.mkdir(paks_dir)
+
+ deps_out_dir = os.path.join(
+ self._isolate_deps_dir,
+ os.path.relpath(os.path.join(constants.GetOutDirectory(), os.pardir),
+ host_paths.DIR_SOURCE_ROOT))
+ for root, _, filenames in os.walk(deps_out_dir):
+ for filename in fnmatch.filter(filenames, '*.pak'):
+ shutil.move(os.path.join(root, filename), paks_dir)
+
+ # Move everything in PRODUCT_DIR to top level.
+ deps_product_dir = os.path.join(
+ deps_out_dir, os.path.basename(constants.GetOutDirectory()))
+ if os.path.isdir(deps_product_dir):
+ for p in os.listdir(deps_product_dir):
+ Isolator._DestructiveMerge(os.path.join(deps_product_dir, p),
+ os.path.join(self._isolate_deps_dir, p))
+ os.rmdir(deps_product_dir)
+ os.rmdir(deps_out_dir)
diff --git a/build/android/pylib/utils/logging_utils.py b/build/android/pylib/utils/logging_utils.py
new file mode 100644
index 0000000..2c2eabf
--- /dev/null
+++ b/build/android/pylib/utils/logging_utils.py
@@ -0,0 +1,98 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import contextlib
+import logging
+import os
+
+from pylib.constants import host_paths
+
+_COLORAMA_PATH = os.path.join(
+ host_paths.DIR_SOURCE_ROOT, 'third_party', 'colorama', 'src')
+
+with host_paths.SysPath(_COLORAMA_PATH):
+ import colorama
+
+class ColorStreamHandler(logging.StreamHandler):
+ """Handler that can be used to colorize logging output.
+
+ Example using a specific logger:
+
+ logger = logging.getLogger('my_logger')
+ logger.addHandler(ColorStreamHandler())
+ logger.info('message')
+
+ Example using the root logger:
+
+ ColorStreamHandler.MakeDefault()
+ logging.info('message')
+
+ """
+ # pylint does not see members added dynamically in the constructor.
+ # pylint: disable=no-member
+ color_map = {
+ logging.DEBUG: colorama.Fore.CYAN,
+ logging.WARNING: colorama.Fore.YELLOW,
+ logging.ERROR: colorama.Fore.RED,
+ logging.CRITICAL: colorama.Back.RED + colorama.Style.BRIGHT,
+ }
+
+ def __init__(self, force_color=False):
+ super(ColorStreamHandler, self).__init__()
+ self.force_color = force_color
+
+ @property
+ def is_tty(self):
+ isatty = getattr(self.stream, 'isatty', None)
+ return isatty and isatty()
+
+ #override
+ def format(self, record):
+ message = logging.StreamHandler.format(self, record)
+ if self.force_color or self.is_tty:
+ return self.Colorize(message, record.levelno)
+ return message
+
+ def Colorize(self, message, log_level):
+ try:
+ return self.color_map[log_level] + message + colorama.Style.RESET_ALL
+ except KeyError:
+ return message
+
+ @staticmethod
+ def MakeDefault(force_color=False):
+ """
+ Replaces the default logging handlers with a coloring handler. To use
+ a colorizing handler at the same time as others, either register them
+ after this call, or add the ColorStreamHandler on the logger using
+ Logger.addHandler()
+
+ Args:
+ force_color: Set to True to bypass the tty check and always colorize.
+ """
+ # If the existing handlers aren't removed, messages are duplicated
+ logging.getLogger().handlers = []
+ logging.getLogger().addHandler(ColorStreamHandler(force_color))
+
+
+@contextlib.contextmanager
+def SuppressLogging(level=logging.ERROR):
+ """Momentarilly suppress logging events from all loggers.
+
+ TODO(jbudorick): This is not thread safe. Log events from other threads might
+ also inadvertently dissapear.
+
+ Example:
+
+ with logging_utils.SuppressLogging():
+ # all but CRITICAL logging messages are suppressed
+ logging.info('just doing some thing') # not shown
+ logging.critical('something really bad happened') # still shown
+
+ Args:
+ level: logging events with this or lower levels are suppressed.
+ """
+ logging.disable(level)
+ yield
+ logging.disable(logging.NOTSET)
diff --git a/build/android/pylib/utils/md5sum.py b/build/android/pylib/utils/md5sum.py
new file mode 100644
index 0000000..a8fedcc
--- /dev/null
+++ b/build/android/pylib/utils/md5sum.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.android.md5sum import *
diff --git a/build/android/pylib/utils/mock_calls.py b/build/android/pylib/utils/mock_calls.py
new file mode 100644
index 0000000..c65109d
--- /dev/null
+++ b/build/android/pylib/utils/mock_calls.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.utils.mock_calls import *
diff --git a/build/android/pylib/utils/parallelizer.py b/build/android/pylib/utils/parallelizer.py
new file mode 100644
index 0000000..49b18f0
--- /dev/null
+++ b/build/android/pylib/utils/parallelizer.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.utils.parallelizer import *
diff --git a/build/android/pylib/utils/proguard.py b/build/android/pylib/utils/proguard.py
new file mode 100644
index 0000000..89dc4c7
--- /dev/null
+++ b/build/android/pylib/utils/proguard.py
@@ -0,0 +1,291 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import os
+import re
+import tempfile
+
+from devil.utils import cmd_helper
+from pylib import constants
+
+
+_PROGUARD_CLASS_RE = re.compile(r'\s*?- Program class:\s*([\S]+)$')
+_PROGUARD_SUPERCLASS_RE = re.compile(r'\s*? Superclass:\s*([\S]+)$')
+_PROGUARD_SECTION_RE = re.compile(
+ r'^(Interfaces|Constant Pool|Fields|Methods|Class file attributes) '
+ r'\(count = \d+\):$')
+_PROGUARD_METHOD_RE = re.compile(r'\s*?- Method:\s*(\S*)[(].*$')
+_PROGUARD_ANNOTATION_RE = re.compile(r'^(\s*?)- Annotation \[L(\S*);\]:$')
+_ELEMENT_PRIMITIVE = 0
+_ELEMENT_ARRAY = 1
+_ELEMENT_ANNOTATION = 2
+_PROGUARD_ELEMENT_RES = [
+ (_ELEMENT_PRIMITIVE,
+ re.compile(r'^(\s*?)- Constant element value \[(\S*) .*\]$')),
+ (_ELEMENT_ARRAY,
+ re.compile(r'^(\s*?)- Array element value \[(\S*)\]:$')),
+ (_ELEMENT_ANNOTATION,
+ re.compile(r'^(\s*?)- Annotation element value \[(\S*)\]:$'))
+]
+_PROGUARD_INDENT_WIDTH = 2
+_PROGUARD_ANNOTATION_VALUE_RE = re.compile(r'^(\s*?)- \S+? \[(.*)\]$')
+
+_PROGUARD_PATH_SDK = os.path.join(
+ constants.PROGUARD_ROOT, 'lib', 'proguard.jar')
+_PROGUARD_PATH_BUILT = (
+ os.path.join(os.environ['ANDROID_BUILD_TOP'], 'external', 'proguard',
+ 'lib', 'proguard.jar')
+ if 'ANDROID_BUILD_TOP' in os.environ else None)
+_PROGUARD_PATH = (
+ _PROGUARD_PATH_SDK if os.path.exists(_PROGUARD_PATH_SDK)
+ else _PROGUARD_PATH_BUILT)
+
+
+def Dump(jar_path):
+ """Dumps class and method information from a JAR into a dict via proguard.
+
+ Args:
+ jar_path: An absolute path to the JAR file to dump.
+ Returns:
+ A dict in the following format:
+ {
+ 'classes': [
+ {
+ 'class': '',
+ 'superclass': '',
+ 'annotations': {/* dict -- see below */},
+ 'methods': [
+ {
+ 'method': '',
+ 'annotations': {/* dict -- see below */},
+ },
+ ...
+ ],
+ },
+ ...
+ ],
+ }
+
+ Annotations dict format:
+ {
+ 'empty-annotation-class-name': None,
+ 'annotation-class-name': {
+ 'field': 'primitive-value',
+ 'field': [ 'array-item-1', 'array-item-2', ... ],
+ 'field': {
+ /* Object value */
+ 'field': 'primitive-value',
+ 'field': [ 'array-item-1', 'array-item-2', ... ],
+ 'field': { /* Object value */ }
+ }
+ }
+ }
+
+ Note that for top-level annotations their class names are used for
+ identification, whereas for any nested annotations the corresponding
+ field names are used.
+
+ One drawback of this approach is that an array containing empty
+ annotation classes will be represented as an array of 'None' values,
+ thus it will not be possible to find out annotation class names.
+ On the other hand, storing both annotation class name and the field name
+ would produce a very complex JSON.
+ """
+
+ with tempfile.NamedTemporaryFile() as proguard_output:
+ cmd_helper.GetCmdStatusAndOutput([
+ 'java',
+ '-jar', _PROGUARD_PATH,
+ '-injars', jar_path,
+ '-dontshrink', '-dontoptimize', '-dontobfuscate', '-dontpreverify',
+ '-dump', proguard_output.name])
+ return Parse(proguard_output)
+
+class _AnnotationElement(object):
+ def __init__(self, name, ftype, depth):
+ self.ref = None
+ self.name = name
+ self.ftype = ftype
+ self.depth = depth
+
+class _ParseState(object):
+ _INITIAL_VALUES = (lambda: None, list, dict)
+ # Empty annotations are represented as 'None', not as an empty dictionary.
+ _LAZY_INITIAL_VALUES = (lambda: None, list, lambda: None)
+
+ def __init__(self):
+ self._class_result = None
+ self._method_result = None
+ self._parse_annotations = False
+ self._annotation_stack = []
+
+ def ResetPerSection(self, section_name):
+ self.InitMethod(None)
+ self._parse_annotations = (
+ section_name in ['Class file attributes', 'Methods'])
+
+ def ParseAnnotations(self):
+ return self._parse_annotations
+
+ def CreateAndInitClass(self, class_name):
+ self.InitMethod(None)
+ self._class_result = {
+ 'class': class_name,
+ 'superclass': '',
+ 'annotations': {},
+ 'methods': [],
+ }
+ return self._class_result
+
+ def HasCurrentClass(self):
+ return bool(self._class_result)
+
+ def SetSuperClass(self, superclass):
+ assert self.HasCurrentClass()
+ self._class_result['superclass'] = superclass
+
+ def InitMethod(self, method_name):
+ self._annotation_stack = []
+ if method_name:
+ self._method_result = {
+ 'method': method_name,
+ 'annotations': {},
+ }
+ self._class_result['methods'].append(self._method_result)
+ else:
+ self._method_result = None
+
+ def InitAnnotation(self, annotation, depth):
+ if not self._annotation_stack:
+ # Add a fake parent element comprising 'annotations' dictionary,
+ # so we can work uniformly with both top-level and nested annotations.
+ annotations = _AnnotationElement(
+ '<<<top level>>>', _ELEMENT_ANNOTATION, depth - 1)
+ if self._method_result:
+ annotations.ref = self._method_result['annotations']
+ else:
+ annotations.ref = self._class_result['annotations']
+ self._annotation_stack = [annotations]
+ self._BacktrackAnnotationStack(depth)
+ if not self.HasCurrentAnnotation():
+ self._annotation_stack.append(
+ _AnnotationElement(annotation, _ELEMENT_ANNOTATION, depth))
+ self._CreateAnnotationPlaceHolder(self._LAZY_INITIAL_VALUES)
+
+ def HasCurrentAnnotation(self):
+ return len(self._annotation_stack) > 1
+
+ def InitAnnotationField(self, field, field_type, depth):
+ self._BacktrackAnnotationStack(depth)
+ # Create the parent representation, if needed. E.g. annotations
+ # are represented with `None`, not with `{}` until they receive the first
+ # field.
+ self._CreateAnnotationPlaceHolder(self._INITIAL_VALUES)
+ if self._annotation_stack[-1].ftype == _ELEMENT_ARRAY:
+ # Nested arrays are not allowed in annotations.
+ assert not field_type == _ELEMENT_ARRAY
+ # Use array index instead of bogus field name.
+ field = len(self._annotation_stack[-1].ref)
+ self._annotation_stack.append(_AnnotationElement(field, field_type, depth))
+ self._CreateAnnotationPlaceHolder(self._LAZY_INITIAL_VALUES)
+
+ def UpdateCurrentAnnotationFieldValue(self, value, depth):
+ self._BacktrackAnnotationStack(depth)
+ self._InitOrUpdateCurrentField(value)
+
+ def _CreateAnnotationPlaceHolder(self, constructors):
+ assert self.HasCurrentAnnotation()
+ field = self._annotation_stack[-1]
+ if field.ref is None:
+ field.ref = constructors[field.ftype]()
+ self._InitOrUpdateCurrentField(field.ref)
+
+ def _BacktrackAnnotationStack(self, depth):
+ stack = self._annotation_stack
+ while len(stack) > 0 and stack[-1].depth >= depth:
+ stack.pop()
+
+ def _InitOrUpdateCurrentField(self, value):
+ assert self.HasCurrentAnnotation()
+ parent = self._annotation_stack[-2]
+ assert not parent.ref is None
+ # There can be no nested constant element values.
+ assert parent.ftype in [_ELEMENT_ARRAY, _ELEMENT_ANNOTATION]
+ field = self._annotation_stack[-1]
+ if type(value) is str and not field.ftype == _ELEMENT_PRIMITIVE:
+ # The value comes from the output parser via
+ # UpdateCurrentAnnotationFieldValue, and should be a value of a constant
+ # element. If it isn't, just skip it.
+ return
+ if parent.ftype == _ELEMENT_ARRAY and field.name >= len(parent.ref):
+ parent.ref.append(value)
+ else:
+ parent.ref[field.name] = value
+
+
+def _GetDepth(prefix):
+ return len(prefix) // _PROGUARD_INDENT_WIDTH
+
+def Parse(proguard_output):
+ results = {
+ 'classes': [],
+ }
+
+ state = _ParseState()
+
+ for line in proguard_output:
+ line = line.strip('\r\n')
+
+ m = _PROGUARD_CLASS_RE.match(line)
+ if m:
+ results['classes'].append(
+ state.CreateAndInitClass(m.group(1).replace('/', '.')))
+ continue
+
+ if not state.HasCurrentClass():
+ continue
+
+ m = _PROGUARD_SUPERCLASS_RE.match(line)
+ if m:
+ state.SetSuperClass(m.group(1).replace('/', '.'))
+ continue
+
+ m = _PROGUARD_SECTION_RE.match(line)
+ if m:
+ state.ResetPerSection(m.group(1))
+ continue
+
+ m = _PROGUARD_METHOD_RE.match(line)
+ if m:
+ state.InitMethod(m.group(1))
+ continue
+
+ if not state.ParseAnnotations():
+ continue
+
+ m = _PROGUARD_ANNOTATION_RE.match(line)
+ if m:
+ # Ignore the annotation package.
+ state.InitAnnotation(m.group(2).split('/')[-1], _GetDepth(m.group(1)))
+ continue
+
+ if state.HasCurrentAnnotation():
+ m = None
+ for (element_type, element_re) in _PROGUARD_ELEMENT_RES:
+ m = element_re.match(line)
+ if m:
+ state.InitAnnotationField(
+ m.group(2), element_type, _GetDepth(m.group(1)))
+ break
+ if m:
+ continue
+ m = _PROGUARD_ANNOTATION_VALUE_RE.match(line)
+ if m:
+ state.UpdateCurrentAnnotationFieldValue(
+ m.group(2), _GetDepth(m.group(1)))
+ else:
+ state.InitMethod(None)
+
+
+ return results
diff --git a/build/android/pylib/utils/proguard_test.py b/build/android/pylib/utils/proguard_test.py
new file mode 100644
index 0000000..497e12d
--- /dev/null
+++ b/build/android/pylib/utils/proguard_test.py
@@ -0,0 +1,490 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import unittest
+
+from pylib.utils import proguard
+
+class TestParse(unittest.TestCase):
+
+ def setUp(self):
+ self.maxDiff = None
+
+ def testClass(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ ' Superclass: java/lang/Object'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': 'java.lang.Object',
+ 'annotations': {},
+ 'methods': []
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testMethod(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Methods (count = 1):',
+ '- Method: <init>()V'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {},
+ 'methods': [
+ {
+ 'method': '<init>',
+ 'annotations': {}
+ }
+ ]
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testClassAnnotation(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Class file attributes (count = 3):',
+ ' - Annotation [Lorg/example/Annotation;]:',
+ ' - Annotation [Lorg/example/AnnotationWithValue;]:',
+ ' - Constant element value [attr \'13\']',
+ ' - Utf8 [val]',
+ ' - Annotation [Lorg/example/AnnotationWithTwoValues;]:',
+ ' - Constant element value [attr1 \'13\']',
+ ' - Utf8 [val1]',
+ ' - Constant element value [attr2 \'13\']',
+ ' - Utf8 [val2]'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {
+ 'Annotation': None,
+ 'AnnotationWithValue': {'attr': 'val'},
+ 'AnnotationWithTwoValues': {'attr1': 'val1', 'attr2': 'val2'}
+ },
+ 'methods': []
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testClassAnnotationWithArrays(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Class file attributes (count = 3):',
+ ' - Annotation [Lorg/example/AnnotationWithEmptyArray;]:',
+ ' - Array element value [arrayAttr]:',
+ ' - Annotation [Lorg/example/AnnotationWithOneElemArray;]:',
+ ' - Array element value [arrayAttr]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [val]',
+ ' - Annotation [Lorg/example/AnnotationWithTwoElemArray;]:',
+ ' - Array element value [arrayAttr]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [val1]',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [val2]'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {
+ 'AnnotationWithEmptyArray': {'arrayAttr': []},
+ 'AnnotationWithOneElemArray': {'arrayAttr': ['val']},
+ 'AnnotationWithTwoElemArray': {'arrayAttr': ['val1', 'val2']}
+ },
+ 'methods': []
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testNestedClassAnnotations(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Class file attributes (count = 1):',
+ ' - Annotation [Lorg/example/OuterAnnotation;]:',
+ ' - Constant element value [outerAttr \'13\']',
+ ' - Utf8 [outerVal]',
+ ' - Array element value [outerArr]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [outerArrVal1]',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [outerArrVal2]',
+ ' - Annotation element value [emptyAnn]:',
+ ' - Annotation [Lorg/example/EmptyAnnotation;]:',
+ ' - Annotation element value [ann]:',
+ ' - Annotation [Lorg/example/InnerAnnotation;]:',
+ ' - Constant element value [innerAttr \'13\']',
+ ' - Utf8 [innerVal]',
+ ' - Array element value [innerArr]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [innerArrVal1]',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [innerArrVal2]',
+ ' - Annotation element value [emptyInnerAnn]:',
+ ' - Annotation [Lorg/example/EmptyAnnotation;]:'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {
+ 'OuterAnnotation': {
+ 'outerAttr': 'outerVal',
+ 'outerArr': ['outerArrVal1', 'outerArrVal2'],
+ 'emptyAnn': None,
+ 'ann': {
+ 'innerAttr': 'innerVal',
+ 'innerArr': ['innerArrVal1', 'innerArrVal2'],
+ 'emptyInnerAnn': None
+ }
+ }
+ },
+ 'methods': []
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testClassArraysOfAnnotations(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Class file attributes (count = 1):',
+ ' - Annotation [Lorg/example/OuterAnnotation;]:',
+ ' - Array element value [arrayWithEmptyAnnotations]:',
+ ' - Annotation element value [(default)]:',
+ ' - Annotation [Lorg/example/EmptyAnnotation;]:',
+ ' - Annotation element value [(default)]:',
+ ' - Annotation [Lorg/example/EmptyAnnotation;]:',
+ ' - Array element value [outerArray]:',
+ ' - Annotation element value [(default)]:',
+ ' - Annotation [Lorg/example/InnerAnnotation;]:',
+ ' - Constant element value [innerAttr \'115\']',
+ ' - Utf8 [innerVal]',
+ ' - Array element value [arguments]:',
+ ' - Annotation element value [(default)]:',
+ ' - Annotation [Lorg/example/InnerAnnotation$Argument;]:',
+ ' - Constant element value [arg1Attr \'115\']',
+ ' - Utf8 [arg1Val]',
+ ' - Array element value [arg1Array]:',
+ ' - Constant element value [(default) \'73\']',
+ ' - Integer [11]',
+ ' - Constant element value [(default) \'73\']',
+ ' - Integer [12]',
+ ' - Annotation element value [(default)]:',
+ ' - Annotation [Lorg/example/InnerAnnotation$Argument;]:',
+ ' - Constant element value [arg2Attr \'115\']',
+ ' - Utf8 [arg2Val]',
+ ' - Array element value [arg2Array]:',
+ ' - Constant element value [(default) \'73\']',
+ ' - Integer [21]',
+ ' - Constant element value [(default) \'73\']',
+ ' - Integer [22]'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {
+ 'OuterAnnotation': {
+ 'arrayWithEmptyAnnotations': [None, None],
+ 'outerArray': [
+ {
+ 'innerAttr': 'innerVal',
+ 'arguments': [
+ {'arg1Attr': 'arg1Val', 'arg1Array': ['11', '12']},
+ {'arg2Attr': 'arg2Val', 'arg2Array': ['21', '22']}
+ ]
+ }
+ ]
+ }
+ },
+ 'methods': []
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testReadFullClassFileAttributes(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Class file attributes (count = 3):',
+ ' - Source file attribute:',
+ ' - Utf8 [Class.java]',
+ ' - Runtime visible annotations attribute:',
+ ' - Annotation [Lorg/example/IntValueAnnotation;]:',
+ ' - Constant element value [value \'73\']',
+ ' - Integer [19]',
+ ' - Inner classes attribute (count = 1)',
+ ' - InnerClassesInfo:',
+ ' Access flags: 0x9 = public static',
+ ' - Class [org/example/Class1]',
+ ' - Class [org/example/Class2]',
+ ' - Utf8 [OnPageFinishedHelper]'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {
+ 'IntValueAnnotation': {
+ 'value': '19',
+ }
+ },
+ 'methods': []
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testMethodAnnotation(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Methods (count = 1):',
+ '- Method: Test()V',
+ ' - Annotation [Lorg/example/Annotation;]:',
+ ' - Annotation [Lorg/example/AnnotationWithValue;]:',
+ ' - Constant element value [attr \'13\']',
+ ' - Utf8 [val]',
+ ' - Annotation [Lorg/example/AnnotationWithTwoValues;]:',
+ ' - Constant element value [attr1 \'13\']',
+ ' - Utf8 [val1]',
+ ' - Constant element value [attr2 \'13\']',
+ ' - Utf8 [val2]'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {},
+ 'methods': [
+ {
+ 'method': 'Test',
+ 'annotations': {
+ 'Annotation': None,
+ 'AnnotationWithValue': {'attr': 'val'},
+ 'AnnotationWithTwoValues': {'attr1': 'val1', 'attr2': 'val2'}
+ },
+ }
+ ]
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testMethodAnnotationWithArrays(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Methods (count = 1):',
+ '- Method: Test()V',
+ ' - Annotation [Lorg/example/AnnotationWithEmptyArray;]:',
+ ' - Array element value [arrayAttr]:',
+ ' - Annotation [Lorg/example/AnnotationWithOneElemArray;]:',
+ ' - Array element value [arrayAttr]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [val]',
+ ' - Annotation [Lorg/example/AnnotationWithTwoElemArray;]:',
+ ' - Array element value [arrayAttr]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [val1]',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [val2]'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {},
+ 'methods': [
+ {
+ 'method': 'Test',
+ 'annotations': {
+ 'AnnotationWithEmptyArray': {'arrayAttr': []},
+ 'AnnotationWithOneElemArray': {'arrayAttr': ['val']},
+ 'AnnotationWithTwoElemArray': {'arrayAttr': ['val1', 'val2']}
+ },
+ }
+ ]
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testMethodAnnotationWithPrimitivesAndArrays(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Methods (count = 1):',
+ '- Method: Test()V',
+ ' - Annotation [Lorg/example/AnnotationPrimitiveThenArray;]:',
+ ' - Constant element value [attr \'13\']',
+ ' - Utf8 [val]',
+ ' - Array element value [arrayAttr]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [val]',
+ ' - Annotation [Lorg/example/AnnotationArrayThenPrimitive;]:',
+ ' - Array element value [arrayAttr]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [val]',
+ ' - Constant element value [attr \'13\']',
+ ' - Utf8 [val]',
+ ' - Annotation [Lorg/example/AnnotationTwoArrays;]:',
+ ' - Array element value [arrayAttr1]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [val1]',
+ ' - Array element value [arrayAttr2]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [val2]'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {},
+ 'methods': [
+ {
+ 'method': 'Test',
+ 'annotations': {
+ 'AnnotationPrimitiveThenArray': {'attr': 'val',
+ 'arrayAttr': ['val']},
+ 'AnnotationArrayThenPrimitive': {'arrayAttr': ['val'],
+ 'attr': 'val'},
+ 'AnnotationTwoArrays': {'arrayAttr1': ['val1'],
+ 'arrayAttr2': ['val2']}
+ },
+ }
+ ]
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testNestedMethodAnnotations(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Methods (count = 1):',
+ '- Method: Test()V',
+ ' - Annotation [Lorg/example/OuterAnnotation;]:',
+ ' - Constant element value [outerAttr \'13\']',
+ ' - Utf8 [outerVal]',
+ ' - Array element value [outerArr]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [outerArrVal1]',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [outerArrVal2]',
+ ' - Annotation element value [emptyAnn]:',
+ ' - Annotation [Lorg/example/EmptyAnnotation;]:',
+ ' - Annotation element value [ann]:',
+ ' - Annotation [Lorg/example/InnerAnnotation;]:',
+ ' - Constant element value [innerAttr \'13\']',
+ ' - Utf8 [innerVal]',
+ ' - Array element value [innerArr]:',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [innerArrVal1]',
+ ' - Constant element value [(default) \'13\']',
+ ' - Utf8 [innerArrVal2]',
+ ' - Annotation element value [emptyInnerAnn]:',
+ ' - Annotation [Lorg/example/EmptyAnnotation;]:'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {},
+ 'methods': [
+ {
+ 'method': 'Test',
+ 'annotations': {
+ 'OuterAnnotation': {
+ 'outerAttr': 'outerVal',
+ 'outerArr': ['outerArrVal1', 'outerArrVal2'],
+ 'emptyAnn': None,
+ 'ann': {
+ 'innerAttr': 'innerVal',
+ 'innerArr': ['innerArrVal1', 'innerArrVal2'],
+ 'emptyInnerAnn': None
+ }
+ }
+ },
+ }
+ ]
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
+
+ def testMethodArraysOfAnnotations(self):
+ actual = proguard.Parse(
+ ['- Program class: org/example/Test',
+ 'Methods (count = 1):',
+ '- Method: Test()V',
+ ' - Annotation [Lorg/example/OuterAnnotation;]:',
+ ' - Array element value [arrayWithEmptyAnnotations]:',
+ ' - Annotation element value [(default)]:',
+ ' - Annotation [Lorg/example/EmptyAnnotation;]:',
+ ' - Annotation element value [(default)]:',
+ ' - Annotation [Lorg/example/EmptyAnnotation;]:',
+ ' - Array element value [outerArray]:',
+ ' - Annotation element value [(default)]:',
+ ' - Annotation [Lorg/example/InnerAnnotation;]:',
+ ' - Constant element value [innerAttr \'115\']',
+ ' - Utf8 [innerVal]',
+ ' - Array element value [arguments]:',
+ ' - Annotation element value [(default)]:',
+ ' - Annotation [Lorg/example/InnerAnnotation$Argument;]:',
+ ' - Constant element value [arg1Attr \'115\']',
+ ' - Utf8 [arg1Val]',
+ ' - Array element value [arg1Array]:',
+ ' - Constant element value [(default) \'73\']',
+ ' - Integer [11]',
+ ' - Constant element value [(default) \'73\']',
+ ' - Integer [12]',
+ ' - Annotation element value [(default)]:',
+ ' - Annotation [Lorg/example/InnerAnnotation$Argument;]:',
+ ' - Constant element value [arg2Attr \'115\']',
+ ' - Utf8 [arg2Val]',
+ ' - Array element value [arg2Array]:',
+ ' - Constant element value [(default) \'73\']',
+ ' - Integer [21]',
+ ' - Constant element value [(default) \'73\']',
+ ' - Integer [22]'])
+ expected = {
+ 'classes': [
+ {
+ 'class': 'org.example.Test',
+ 'superclass': '',
+ 'annotations': {},
+ 'methods': [
+ {
+ 'method': 'Test',
+ 'annotations': {
+ 'OuterAnnotation': {
+ 'arrayWithEmptyAnnotations': [None, None],
+ 'outerArray': [
+ {
+ 'innerAttr': 'innerVal',
+ 'arguments': [
+ {'arg1Attr': 'arg1Val', 'arg1Array': ['11', '12']},
+ {'arg2Attr': 'arg2Val', 'arg2Array': ['21', '22']}
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ self.assertEquals(expected, actual)
diff --git a/build/android/pylib/utils/repo_utils.py b/build/android/pylib/utils/repo_utils.py
new file mode 100644
index 0000000..5a0efa8
--- /dev/null
+++ b/build/android/pylib/utils/repo_utils.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from devil.utils import cmd_helper
+
+
+def GetGitHeadSHA1(in_directory):
+ """Returns the git hash tag for the given directory.
+
+ Args:
+ in_directory: The directory where git is to be run.
+ """
+ command_line = ['git', 'log', '-1', '--pretty=format:%H']
+ output = cmd_helper.GetCmdOutput(command_line, cwd=in_directory)
+ return output[0:40]
diff --git a/build/android/pylib/utils/reraiser_thread.py b/build/android/pylib/utils/reraiser_thread.py
new file mode 100644
index 0000000..828cd2b
--- /dev/null
+++ b/build/android/pylib/utils/reraiser_thread.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.utils.reraiser_thread import *
diff --git a/build/android/pylib/utils/run_tests_helper.py b/build/android/pylib/utils/run_tests_helper.py
new file mode 100644
index 0000000..5c48668
--- /dev/null
+++ b/build/android/pylib/utils/run_tests_helper.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.utils.run_tests_helper import *
diff --git a/build/android/pylib/utils/test_environment.py b/build/android/pylib/utils/test_environment.py
new file mode 100644
index 0000000..0a1326e
--- /dev/null
+++ b/build/android/pylib/utils/test_environment.py
@@ -0,0 +1,52 @@
+# Copyright 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import psutil
+import signal
+
+from devil.android import device_errors
+from devil.android import device_utils
+
+
+def _KillWebServers():
+ for s in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT, signal.SIGKILL]:
+ signalled = []
+ for server in ['lighttpd', 'webpagereplay']:
+ for p in psutil.process_iter():
+ try:
+ if not server in ' '.join(p.cmdline):
+ continue
+ logging.info('Killing %s %s %s', s, server, p.pid)
+ p.send_signal(s)
+ signalled.append(p)
+ except Exception: # pylint: disable=broad-except
+ logging.exception('Failed killing %s %s', server, p.pid)
+ for p in signalled:
+ try:
+ p.wait(1)
+ except Exception: # pylint: disable=broad-except
+ logging.exception('Failed waiting for %s to die.', p.pid)
+
+
+def CleanupLeftoverProcesses(devices):
+ """Clean up the test environment, restarting fresh adb and HTTP daemons.
+
+ Args:
+ devices: The devices to clean.
+ """
+ _KillWebServers()
+ device_utils.RestartServer()
+
+ def cleanup_device(d):
+ d.WaitUntilFullyBooted()
+ d.RestartAdbd()
+ try:
+ d.EnableRoot()
+ except device_errors.CommandFailedError:
+ logging.exception('Failed to enable root')
+ d.WaitUntilFullyBooted()
+
+ device_utils.DeviceUtils.parallel(devices).pMap(cleanup_device)
+
diff --git a/build/android/pylib/utils/time_profile.py b/build/android/pylib/utils/time_profile.py
new file mode 100644
index 0000000..094799c
--- /dev/null
+++ b/build/android/pylib/utils/time_profile.py
@@ -0,0 +1,45 @@
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import time
+
+
+class TimeProfile(object):
+ """Class for simple profiling of action, with logging of cost."""
+
+ def __init__(self, description='operation'):
+ self._starttime = None
+ self._endtime = None
+ self._description = description
+ self.Start()
+
+ def Start(self):
+ self._starttime = time.time()
+ self._endtime = None
+
+ def GetDelta(self):
+ """Returns the rounded delta.
+
+ Also stops the timer if Stop() has not already been called.
+ """
+ if self._endtime is None:
+ self.Stop(log=False)
+ delta = self._endtime - self._starttime
+ delta = round(delta, 2) if delta < 10 else round(delta, 1)
+ return delta
+
+ def LogResult(self):
+ """Logs the result."""
+ logging.info('%s seconds to perform %s', self.GetDelta(), self._description)
+
+ def Stop(self, log=True):
+ """Stop profiling.
+
+ Args:
+ log: Log the delta (defaults to true).
+ """
+ self._endtime = time.time()
+ if log:
+ self.LogResult()
diff --git a/build/android/pylib/utils/timeout_retry.py b/build/android/pylib/utils/timeout_retry.py
new file mode 100644
index 0000000..e566f45
--- /dev/null
+++ b/build/android/pylib/utils/timeout_retry.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.utils.timeout_retry import *
diff --git a/build/android/pylib/utils/watchdog_timer.py b/build/android/pylib/utils/watchdog_timer.py
new file mode 100644
index 0000000..967794c
--- /dev/null
+++ b/build/android/pylib/utils/watchdog_timer.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.utils.watchdog_timer import *
diff --git a/build/android/pylib/utils/xvfb.py b/build/android/pylib/utils/xvfb.py
new file mode 100644
index 0000000..cb9d50e
--- /dev/null
+++ b/build/android/pylib/utils/xvfb.py
@@ -0,0 +1,58 @@
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=W0702
+
+import os
+import signal
+import subprocess
+import sys
+import time
+
+
+def _IsLinux():
+ """Return True if on Linux; else False."""
+ return sys.platform.startswith('linux')
+
+
+class Xvfb(object):
+ """Class to start and stop Xvfb if relevant. Nop if not Linux."""
+
+ def __init__(self):
+ self._pid = 0
+
+ def Start(self):
+ """Start Xvfb and set an appropriate DISPLAY environment. Linux only.
+
+ Copied from tools/code_coverage/coverage_posix.py
+ """
+ if not _IsLinux():
+ return
+ proc = subprocess.Popen(['Xvfb', ':9', '-screen', '0', '1024x768x24',
+ '-ac'],
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+ self._pid = proc.pid
+ if not self._pid:
+ raise Exception('Could not start Xvfb')
+ os.environ['DISPLAY'] = ':9'
+
+ # Now confirm, giving a chance for it to start if needed.
+ for _ in range(10):
+ proc = subprocess.Popen('xdpyinfo >/dev/null', shell=True)
+ _, retcode = os.waitpid(proc.pid, 0)
+ if retcode == 0:
+ break
+ time.sleep(0.25)
+ if retcode != 0:
+ raise Exception('Could not confirm Xvfb happiness')
+
+ def Stop(self):
+ """Stop Xvfb if needed. Linux only."""
+ if self._pid:
+ try:
+ os.kill(self._pid, signal.SIGKILL)
+ except:
+ pass
+ del os.environ['DISPLAY']
+ self._pid = 0
diff --git a/build/android/pylib/utils/zip_utils.py b/build/android/pylib/utils/zip_utils.py
new file mode 100644
index 0000000..007b34b
--- /dev/null
+++ b/build/android/pylib/utils/zip_utils.py
@@ -0,0 +1,8 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# pylint: disable=unused-wildcard-import
+# pylint: disable=wildcard-import
+
+from devil.utils.zip_utils import *