Initial import of Cobalt 2.8885 2016-07-27
diff --git a/src/build/android/AndroidManifest.xml b/src/build/android/AndroidManifest.xml
new file mode 100644
index 0000000..0822e36
--- /dev/null
+++ b/src/build/android/AndroidManifest.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<!--
+ This dummy manifest is passed to aapt when generating R.java in java.gypi.
+ Nothing in the manifest is used, but it is still required by aapt.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="dummy.package" />
diff --git a/src/build/android/adb_chromium_testshell_command_line b/src/build/android/adb_chromium_testshell_command_line
new file mode 100755
index 0000000..8c09e3f
--- /dev/null
+++ b/src/build/android/adb_chromium_testshell_command_line
@@ -0,0 +1,37 @@
+#!/bin/bash
+#
+# 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.
+
+# If no flags are given, prints the current chromium test shell flags.
+#
+# Otherwise, the given flags are used to REPLACE (not modify) the chromium
+# test shell flags. For example:
+# adb_chromium_testshell_command_line --enable-webgl
+#
+# To remove all chromium test shell flags, pass an empty string for the flags:
+# adb_chromium_testshell_command_line ""
+
+CMD_LINE_FILE=/data/local/tmp/chromium-testshell-command-line
+
+if [ $# -eq 0 ] ; then
+ # If nothing specified, print the command line (stripping off "chromium_testshell")
+ tempfile=$(tempfile)
+ adb pull $CMD_LINE_FILE $tempfile 2>/dev/null
+ if [ $? -eq 0 ] ; then
+ rm $tempfile
+ adb shell cat $CMD_LINE_FILE | cut -d " " -f "2-" 2>/dev/null
+ fi
+elif [ $# -eq 1 ] && [ "$1" = '' ] ; then
+ # If given an empty string, delete the command line.
+ set -x
+ adb shell rm $CMD_LINE_FILE >/dev/null
+else
+ # Else set it.
+ set -x
+ adb shell "echo 'chromium_testshell $*' > $CMD_LINE_FILE"
+ # Prevent other apps from modifying flags -- this can create security issues.
+ adb shell chmod 0664 $CMD_LINE_FILE
+fi
+
diff --git a/src/build/android/adb_content_shell_command_line b/src/build/android/adb_content_shell_command_line
new file mode 100755
index 0000000..f3c1d4f
--- /dev/null
+++ b/src/build/android/adb_content_shell_command_line
@@ -0,0 +1,37 @@
+#!/bin/bash
+#
+# 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.
+
+# If no flags are given, prints the current content shell flags.
+#
+# Otherwise, the given flags are used to REPLACE (not modify) the content shell
+# flags. For example:
+# adb_content_shell_command_line --enable-webgl
+#
+# To remove all content shell flags, pass an empty string for the flags:
+# adb_content_shell_command_line ""
+
+CMD_LINE_FILE=/data/local/tmp/content-shell-command-line
+
+if [ $# -eq 0 ] ; then
+ # If nothing specified, print the command line (stripping off "content_shell")
+ tempfile=$(tempfile)
+ adb pull $CMD_LINE_FILE $tempfile 2>/dev/null
+ if [ $? -eq 0 ] ; then
+ rm $tempfile
+ adb shell cat $CMD_LINE_FILE | cut -d " " -f "2-" 2>/dev/null
+ fi
+elif [ $# -eq 1 ] && [ "$1" = '' ] ; then
+ # If given an empty string, delete the command line.
+ set -x
+ adb shell rm $CMD_LINE_FILE >/dev/null
+else
+ # Else set it.
+ set -x
+ adb shell "echo 'content_shell $*' > $CMD_LINE_FILE"
+ # Prevent other apps from modifying flags -- this can create security issues.
+ adb shell chmod 0664 $CMD_LINE_FILE
+fi
+
diff --git a/src/build/android/adb_device_functions.sh b/src/build/android/adb_device_functions.sh
new file mode 100755
index 0000000..66cc32f
--- /dev/null
+++ b/src/build/android/adb_device_functions.sh
@@ -0,0 +1,139 @@
+#!/bin/bash
+#
+# 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.
+#
+# A collection of functions useful for maintaining android devices
+
+
+# Run an adb command on all connected device in parallel.
+# Usage: adb_all command line to eval. Quoting is optional.
+#
+# Examples:
+# adb_all install Chrome.apk
+# adb_all 'shell cat /path/to/file'
+#
+adb_all() {
+ if [[ $# == 0 ]]; then
+ echo "Usage: adb_all <adb command>. Quoting is optional."
+ echo "Example: adb_all install Chrome.apk"
+ return 1
+ fi
+ local DEVICES=$(adb_get_devices -b)
+ local NUM_DEVICES=$(echo $DEVICES | wc -w)
+ if (( $NUM_DEVICES > 1 )); then
+ echo "Looping over $NUM_DEVICES devices"
+ fi
+ _adb_multi "$DEVICES" "$*"
+}
+
+
+# Run a command on each connected device. Quoting the command is suggested but
+# not required. The script setups up variable DEVICE to correspond to the
+# current serial number. Intended for complex one_liners that don't work in
+# adb_all
+# Usage: adb_device_loop 'command line to eval'
+adb_device_loop() {
+ if [[ $# == 0 ]]; then
+ echo "Intended for more complex one-liners that cannot be done with" \
+ "adb_all."
+ echo 'Usage: adb_device_loop "echo $DEVICE: $(adb root &&' \
+ 'adb shell cat /data/local.prop)"'
+ return 1
+ fi
+ local DEVICES=$(adb_get_devices)
+ if [[ -z $DEVICES ]]; then
+ return
+ fi
+ # Do not change DEVICE variable name - part of api
+ for DEVICE in $DEVICES; do
+ DEV_TYPE=$(adb -s $DEVICE shell getprop ro.product.device | sed 's/\r//')
+ echo "Running on $DEVICE ($DEV_TYPE)"
+ ANDROID_SERIAL=$DEVICE eval "$*"
+ done
+}
+
+# Erases data from any devices visible on adb. To preserve a device,
+# disconnect it or:
+# 1) Reboot it into fastboot with 'adb reboot bootloader'
+# 2) Run wipe_all_devices to wipe remaining devices
+# 3) Restore device it with 'fastboot reboot'
+#
+# Usage: wipe_all_devices [-f]
+#
+wipe_all_devices() {
+ if [[ -z $(which adb) || -z $(which fastboot) ]]; then
+ echo "aborting: adb and fastboot not in path"
+ return 1
+ elif ! $(groups | grep -q 'plugdev'); then
+ echo "If fastboot fails, run: 'sudo adduser $(whoami) plugdev'"
+ fi
+
+ local DEVICES=$(adb_get_devices -b)
+
+ if [[ $1 != '-f' ]]; then
+ echo "This will ERASE ALL DATA from $(echo $DEVICES | wc -w) device."
+ read -p "Hit enter to continue"
+ fi
+
+ _adb_multi "$DEVICES" "reboot bootloader"
+ # Subshell to isolate job list
+ (
+ for DEVICE in $DEVICES; do
+ fastboot_erase $DEVICE &
+ done
+ wait
+ )
+
+ # Reboot devices together
+ for DEVICE in $DEVICES; do
+ fastboot -s $DEVICE reboot
+ done
+}
+
+# Wipe a device in fastboot.
+# Usage fastboot_erase [serial]
+fastboot_erase() {
+ if [[ -n $1 ]]; then
+ echo "Wiping $1"
+ local SERIAL="-s $1"
+ else
+ if [ -z $(fastboot devices) ]; then
+ echo "No devices in fastboot, aborting."
+ echo "Check out wipe_all_devices to see if sufficient"
+ echo "You can put a device in fastboot using adb reboot bootloader"
+ return 1
+ fi
+ local SERIAL=""
+ fi
+ fastboot $SERIAL erase cache
+ fastboot $SERIAL erase userdata
+}
+
+# Get list of devices connected via adb
+# Args: -b block until adb detects a device
+adb_get_devices() {
+ local DEVICES="$(adb devices | grep 'device$')"
+ if [[ -z $DEVICES && $1 == '-b' ]]; then
+ echo '- waiting for device -' >&2
+ local DEVICES="$(adb wait-for-device devices | grep 'device$')"
+ fi
+ echo "$DEVICES" | awk -vORS=' ' '{print $1}' | sed 's/ $/\n/'
+}
+
+###################################################
+## HELPER FUNCTIONS
+###################################################
+
+# Run an adb command in parallel over a device list
+_adb_multi() {
+ local DEVICES=$1
+ local ADB_ARGS=$2
+ (
+ for DEVICE in $DEVICES; do
+ adb -s $DEVICE $ADB_ARGS &
+ done
+ wait
+ )
+}
diff --git a/src/build/android/adb_gdb b/src/build/android/adb_gdb
new file mode 100755
index 0000000..fcbbed6
--- /dev/null
+++ b/src/build/android/adb_gdb
@@ -0,0 +1,895 @@
+#!/bin/bash
+#
+# 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.
+#
+
+# A generic script used to attach to a running Chromium process and
+# debug it. Most users should not use this directly, but one of the
+# wrapper scripts like adb_gdb_content_shell, or adb_gdb_drt
+#
+# Use --help to print full usage instructions.
+#
+
+PROGNAME=$(basename "$0")
+PROGDIR=$(dirname "$0")
+
+# Location of Chromium-top-level sources.
+CHROMIUM_SRC=$(cd "$PROGDIR"/../.. && pwd 2>/dev/null)
+
+TMPDIR=
+GDBSERVER_PIDFILE=
+TARGET_GDBSERVER=
+
+clean_exit () {
+ if [ "$TMPDIR" ]; then
+ GDBSERVER_PID=$(cat $GDBSERVER_PIDFILE 2>/dev/null)
+ if [ "$GDBSERVER_PID" ]; then
+ log "Killing background gdbserver process: $GDBSERVER_PID"
+ kill -9 $GDBSERVER_PID >/dev/null 2>&1
+ fi
+ if [ "$TARGET_GDBSERVER" ]; then
+ log "Removing target gdbserver binary: $TARGET_GDBSERVER."
+ "$ADB" shell rm "$TARGET_GDBSERVER" >/dev/null 2>&1
+ fi
+ log "Cleaning up: $TMPDIR"
+ rm -rf "$TMPDIR"
+ fi
+ exit $1
+}
+
+# Ensure clean exit on Ctrl-C.
+trap "clean_exit 1" INT
+
+panic () {
+ echo "ERROR: $@" >&2
+ clean_exit 1
+}
+
+fail_panic () {
+ if [ $? != 0 ]; then panic "$@"; fi
+}
+
+log () {
+ if [ "$VERBOSE" -gt 0 ]; then
+ echo "$@"
+ fi
+}
+
+DEFAULT_PULL_LIBS_DIR=/tmp/$USER-adb-gdb-libs
+
+# NOTE: Allow wrapper scripts to set various default through ADB_GDB_XXX
+# environment variables. This is only for cosmetic reasons, i.e. to
+# display proper
+
+# Allow wrapper scripts to set the default activity through
+# the ADB_GDB_ACTIVITY variable. Users are still able to change the
+# final activity name through --activity=<name> option.
+#
+# This is only for cosmetic reasons, i.e. to display the proper default
+# in the --help output.
+#
+DEFAULT_ACTIVITY=${ADB_GDB_ACTIVITY:-".Main"}
+
+# Allow wrapper scripts to set the program name through ADB_GDB_PROGNAME
+PROGNAME=${ADB_GDB_PROGNAME:-$(basename "$0")}
+
+ACTIVITY=$DEFAULT_ACTIVITY
+ADB=
+ANNOTATE=
+# Note: Ignore BUILDTYPE variable, because the Ninja build doesn't use it.
+BUILDTYPE=
+FORCE=
+GDBINIT=
+GDBSERVER=
+HELP=
+NDK_DIR=
+NO_PULL_LIBS=
+PACKAGE_NAME=
+PID=
+PROGRAM_NAME="activity"
+PULL_LIBS=
+PULL_LIBS_DIR=
+SANDBOXED=
+SANDBOXED_INDEX=
+START=
+SYMBOL_DIR=
+TARGET_ARCH=
+TOOLCHAIN=
+VERBOSE=0
+
+for opt; do
+ optarg=$(expr "x$opt" : 'x[^=]*=\(.*\)')
+ case $opt in
+ --adb=*)
+ ADB=$optarg
+ ;;
+ --activity=*)
+ ACTIVITY=$optarg
+ ;;
+ --annotate=3)
+ ANNOTATE=$optarg
+ ;;
+ --force)
+ FORCE=true
+ ;;
+ --gdbserver=*)
+ GDBSERVER=$optarg
+ ;;
+ --help|-h|-?)
+ HELP=true
+ ;;
+ --ndk-dir=*)
+ NDK_DIR=$optarg
+ ;;
+ --no-pull-libs)
+ NO_PULL_LIBS=true
+ ;;
+ --package-name=*)
+ PACKAGE_NAME=$optarg
+ ;;
+ --pid=*)
+ PID=$optarg
+ ;;
+ --program-name=*)
+ PROGRAM_NAME=$optarg
+ ;;
+ --pull-libs)
+ PULL_LIBS=true
+ ;;
+ --pull-libs-dir=*)
+ PULL_LIBS_DIR=$optarg
+ ;;
+ --sandboxed)
+ SANDBOXED=true
+ ;;
+ --sandboxed=*)
+ SANDBOXED=true
+ SANDBOXED_INDEX=$optarg
+ ;;
+ --script=*)
+ GDBINIT=$optarg
+ ;;
+ --start)
+ START=true
+ ;;
+ --symbol-dir=*)
+ SYMBOL_DIR=$optarg
+ ;;
+ --target-arch=*)
+ TARGET_ARCH=$optarg
+ ;;
+ --toolchain=*)
+ TOOLCHAIN=$optarg
+ ;;
+ --verbose)
+ VERBOSE=$(( $VERBOSE + 1 ))
+ ;;
+ --debug)
+ BUILDTYPE=Debug
+ ;;
+ --release)
+ BUILDTYPE=Release
+ ;;
+ -*)
+ panic "Unknown option $OPT, see --help." >&2
+ ;;
+ *)
+ if [ "$PACKAGE_NAME" ]; then
+ panic "You can only provide a single package name as argument!\
+ See --help."
+ fi
+ PACKAGE_NAME=$opt
+ ;;
+ esac
+done
+
+print_help_options () {
+ cat <<EOF
+EOF
+}
+
+if [ "$HELP" ]; then
+ if [ "$ADB_GDB_PROGNAME" ]; then
+ # Assume wrapper scripts all provide a default package name.
+ cat <<EOF
+Usage: $PROGNAME [options]
+
+Attach gdb to a running Android $PROGRAM_NAME process.
+EOF
+ else
+ # Assume this is a direct call to adb_gdb
+ cat <<EOF
+Usage: $PROGNAME [options] [<package-name>]
+
+Attach gdb to a running Android $PROGRAM_NAME process.
+
+If provided, <package-name> must be the name of the Android application's
+package name to be debugged. You can also use --package-name=<name> to
+specify it.
+EOF
+ fi
+
+ cat <<EOF
+
+This script is used to debug a running $PROGRAM_NAME process.
+This can be a regular Android application process, or a sandboxed
+service, if you use the --sandboxed or --sandboxed=<num> option.
+
+This script needs several things to work properly. It will try to pick
+them up automatically for you though:
+
+ - target gdbserver binary
+ - host gdb client (e.g. arm-linux-androideabi-gdb)
+ - directory with symbolic version of $PROGRAM_NAME's shared libraries.
+
+If you have sourced Chromium's build/android/envsetup.sh, this script will
+find all of them automatically. This is the recommended way to use it.
+
+Otherwise, if you have ANDROID_NDK_ROOT defined in your environment,
+the script will use it to find the gdb and gdbserver binaries. You can
+also use --ndk-dir=<path> to specify an alternative NDK installation
+directory.
+
+The script tries to find the most recent version of the debug version of
+shared libraries under one of the following directories:
+
+ \$CHROMIUM_SRC/out/Release/lib/ (used by Ninja builds)
+ \$CHROMIUM_SRC/out/Debug/lib/ (used by Ninja builds)
+ \$CHROMIUM_SRC/out/Release/lib.target/ (used by Make builds)
+ \$CHROMIUM_SRC/out/Debug/lib.target/ (used by Make builds)
+
+You can restrict this search by using --release or --debug to specify the
+build type, or simply use --symbol-dir=<path> to specify the file manually.
+
+The script tries to extract the target architecture from your GYP_DEFINES,
+but if this fails, will default to 'arm'. Use --target-arch=<name> to force
+its value.
+
+Otherwise, the script will complain, but you can use the --gdbserver,
+--gdb and --symbol-lib options to specify everything manually.
+
+An alternative to --gdb=<file> is to use --toollchain=<path> to specify
+the path to the host target-specific cross-toolchain.
+
+You will also need the 'adb' tool in your path. Otherwise, use the --adb
+option. The script will complain if there is more than one device connected
+and ANDROID_SERIAL is not defined.
+
+The first time you use it on a device, the script will pull many system
+libraries required by the process into a temporary directory. This
+is done to strongly improve the debugging experience, like allowing
+readable thread stacks and more. The libraries are copied to the following
+directory by default:
+
+ $DEFAULT_PULL_LIBS_DIR/
+
+But you can use the --pull-libs-dir=<path> option to specify an
+alternative. The script can detect when you change the connected device,
+and will re-pull the libraries only in this case. You can however force it
+with the --pull-libs option.
+
+Any local .gdbinit script will be ignored, but it is possible to pass a
+gdb command script with the --script=<file> option. Note that its commands
+will be passed to gdb after the remote connection and library symbol
+loading have completed.
+
+Valid options:
+ --help|-h|-? Print this message.
+ --verbose Increase verbosity.
+
+ --sandboxed Debug first sandboxed process we find.
+ --sandboxed=<num> Debug specific sandboxed process.
+ --symbol-dir=<path> Specify directory with symbol shared libraries.
+ --package-name=<name> Specify package name (alternative to 1st argument).
+ --program-name=<name> Specify program name (cosmetic only).
+ --pid=<pid> Specify application process pid.
+ --force Kill any previous debugging session, if any.
+ --start Start package's activity on device.
+ --activity=<name> Activity name for --start [$DEFAULT_ACTIVITY].
+ --annotate=<num> Enable gdb annotation.
+ --script=<file> Specify extra GDB init script.
+
+ --gdbserver=<file> Specify targer gdbserver binary.
+ --gdb=<program> Specify host gdb client binary.
+ --target-arch=<name> Specify NDK target arch.
+ --adb=<program> Specify host ADB binary.
+
+ --pull-libs Force system libraries extraction.
+ --no-pull-libs Do not extract any system library.
+ --libs-dir=<path> Specify system libraries extraction directory.
+
+ --debug Use libraries under out/Debug.
+ --release Use libraries under out/Release.
+
+EOF
+ exit 0
+fi
+
+if [ -z "$PACKAGE_NAME" ]; then
+ panic "Please specify a package name on the command line. See --help."
+fi
+
+if [ -z "$NDK_DIR" ]; then
+ if [ -z "$ANDROID_NDK_ROOT" ]; then
+ panic "Can't find NDK directory, please source \
+build/android/envsetup.sh!"
+ fi
+else
+ if [ ! -d "$NDK_DIR" ]; then
+ panic "Invalid directory: $NDK_DIR"
+ fi
+ if [ ! -f "$NDK_DIR/ndk-build" ]; then
+ panic "Not a valid NDK directory: $NDK_DIR"
+ fi
+ ANDROID_NDK_ROOT=$NDK_DIR
+fi
+
+if [ "$GDBINIT" -a ! -f "$GDBINIT" ]; then
+ panic "Unknown --script file: $GDBINIT"
+fi
+
+# Find the target architecture from our $GYP_DEFINES
+# This returns an NDK-compatible architecture name.
+# out: NDK Architecture name, or empty string.
+get_gyp_target_arch () {
+ local ARCH=$(echo $GYP_DEFINES | tr ' ' '\n' | grep '^target_arch=' |\
+ cut -d= -f2)
+ case $ARCH in
+ ia32|i?86|x86) echo "x86";;
+ mips|arm) echo "$ARCH";;
+ *) echo "";
+ esac
+}
+
+if [ -z "$TARGET_ARCH" ]; then
+ TARGET_ARCH=$(get_gyp_target_arch)
+ if [ -z "$TARGET_ARCH" ]; then
+ TARGET_ARCH=arm
+ fi
+else
+ # Nit: accept Chromium's 'ia32' as a valid target architecture. This
+ # script prefers the NDK 'x86' name instead because it uses it to find
+ # NDK-specific files (host gdb) with it.
+ if [ "$TARGET_ARCH" = "ia32" ]; then
+ TARGET_ARCH=x86
+ log "Auto-config: --arch=$TARGET_ARCH (equivalent to ia32)"
+ fi
+fi
+
+# Detect the NDK system tag, i.e. the name used to identify the host.
+# out: NDK system tag (e.g. 'linux-x86').
+get_ndk_host_tag () {
+ if [ -z "$NDK_HOST_TAG" ]; then
+ case $(uname -s) in
+ Linux) NDK_HOST_TAG=linux-x86;;
+ Darwin) NDK_HOST_TAG=darwin-x86;;
+ *) panic "You can't run this script on this system: $uname -a";;
+ esac
+ fi
+ echo "$NDK_HOST_TAG"
+}
+
+# Convert an NDK architecture name into a GNU configure triplet.
+# $1: NDK architecture name (e.g. 'arm')
+# Out: Android GNU configure triplet (e.g. 'arm-linux-androideabi')
+get_arch_gnu_config () {
+ case $1 in
+ arm)
+ echo "arm-linux-androideabi"
+ ;;
+ x86)
+ echo "i686-linux-android"
+ ;;
+ mips)
+ echo "mipsel-linux-android"
+ ;;
+ *)
+ echo "$ARCH-linux-android"
+ ;;
+ esac
+}
+
+# Convert an NDK architecture name into a toolchain name prefix
+# $1: NDK architecture name (e.g. 'arm')
+# Out: NDK toolchain name prefix (e.g. 'arm-linux-androideabi')
+get_arch_toolchain_prefix () {
+ # Return the configure triplet, except for x86!
+ if [ "$1" = "x86" ]; then
+ echo "$1"
+ else
+ get_arch_gnu_config $1
+ fi
+}
+
+# Find a NDK toolchain prebuilt file or sub-directory.
+# This will probe the various arch-specific toolchain directories
+# in the NDK for the needed file.
+# $1: NDK install path
+# $2: NDK architecture name
+# $3: prebuilt sub-path to look for.
+# Out: file path, or empty if none is found.
+get_ndk_toolchain_prebuilt () {
+ local NDK_DIR="${1%/}"
+ local ARCH="$2"
+ local SUBPATH="$3"
+ local NAME="$(get_arch_toolchain_prefix $ARCH)"
+ local FILE TARGET
+ FILE=$NDK_DIR/toolchains/$NAME-4.6/prebuilt/$SUBPATH
+ if [ ! -f "$FILE" ]; then
+ FILE=$NDK_DIR/toolchains/$NAME-4.4.3/prebuilt/$SUBPATH
+ if [ ! -f "$FILE" ]; then
+ FILE=
+ fi
+ fi
+ echo "$FILE"
+}
+
+# Find the path to an NDK's toolchain full prefix for a given architecture
+# $1: NDK install path
+# $2: NDK architecture name
+# Out: install path + binary prefix (e.g.
+# ".../path/to/bin/arm-linux-androideabi-")
+get_ndk_toolchain_fullprefix () {
+ local NDK_DIR="$1"
+ local ARCH="$2"
+ local TARGET NAME HOST GCC CONFIG
+
+ # NOTE: This will need to be updated if the NDK changes the names or moves
+ # the location of its prebuilt toolchains.
+ #
+ GCC=
+ HOST=$(get_ndk_host_tag)
+ CONFIG=$(get_arch_gnu_config $ARCH)
+ GCC=$(get_ndk_toolchain_prebuilt \
+ "$NDK_DIR" "$ARCH" "$HOST/bin/$CONFIG-gcc")
+ if [ ! -f "$GCC" -a "$ARCH" = "x86" ]; then
+ # Special case, the x86 toolchain used to be incorrectly
+ # named i686-android-linux-gcc!
+ GCC=$(get_ndk_toolchain_prebuilt \
+ "$NDK_DIR" "$ARCH" "$HOST/bin/i686-android-linux-gcc")
+ fi
+ if [ -z "$GCC" ]; then
+ panic "Cannot find Android NDK toolchain for '$ARCH' architecture. \
+Please verify your NDK installation!"
+ fi
+ echo "${GCC%%gcc}"
+}
+
+# $1: NDK install path
+# $2: target architecture.
+get_ndk_gdbserver () {
+ local NDK_DIR="$1"
+ local ARCH=$2
+ local HOST=$(get_ndk_host_tag)
+ local BINARY
+
+ # The location has moved after NDK r8
+ BINARY=$NDK_DIR/prebuilt/android-$ARCH/gdbserver/gdbserver
+ if [ ! -f "$BINARY" ]; then
+ BINARY=$(get_ndk_toolchain_prebuilt "$NDK_DIR" "$ARCH" gdbserver)
+ fi
+ echo "$BINARY"
+}
+
+# Check/probe the path to the Android toolchain installation. Always
+# use the NDK versions of gdb and gdbserver. They must match to avoid
+# issues when both binaries do not speak the same wire protocol.
+#
+if [ -z "$TOOLCHAIN" ]; then
+ ANDROID_TOOLCHAIN=$(get_ndk_toolchain_fullprefix \
+ "$ANDROID_NDK_ROOT" "$TARGET_ARCH")
+ ANDROID_TOOLCHAIN=$(dirname "$ANDROID_TOOLCHAIN")
+ log "Auto-config: --toolchain=$ANDROID_TOOLCHAIN"
+else
+ # Be flexible, allow one to specify either the install path or the bin
+ # sub-directory in --toolchain:
+ #
+ if [ -d "$TOOLCHAIN/bin" ]; then
+ TOOLCHAIN=$TOOLCHAIN/bin
+ fi
+ ANDROID_TOOLCHAIN=$TOOLCHAIN
+fi
+
+# Cosmetic: Remove trailing directory separator.
+ANDROID_TOOLCHAIN=${ANDROID_TOOLCHAIN%/}
+
+# Find host GDB client binary
+GDB=$(which $ANDROID_TOOLCHAIN/*-gdb 2>/dev/null | head -1)
+if [ -z "$GDB" ]; then
+ panic "Can't find Android gdb client in your path, check your \
+--toolchain path."
+fi
+log "Host gdb client: $GDB"
+
+# Find gdbserver binary, we will later push it to /data/local/tmp
+# This ensures that both gdbserver and $GDB talk the same binary protocol,
+# otherwise weird problems will appear.
+#
+if [ -z "$GDBSERVER" ]; then
+ GDBSERVER=$(get_ndk_gdbserver "$ANDROID_NDK_ROOT" "$TARGET_ARCH")
+ if [ -z "$GDBSERVER" ]; then
+ panic "Can't find NDK gdbserver binary. use --gdbserver to specify \
+valid one!"
+ fi
+ log "Auto-config: --gdbserver=$GDBSERVER"
+fi
+
+
+
+# Check that ADB is in our path
+if [ -z "$ADB" ]; then
+ ADB=$(which adb 2>/dev/null)
+ if [ -z "$ADB" ]; then
+ panic "Can't find 'adb' tool in your path. Install it or use \
+--adb=<file>"
+ fi
+ log "Auto-config: --adb=$ADB"
+fi
+
+# Check that it works minimally
+ADB_VERSION=$($ADB version 2>/dev/null)
+echo "$ADB_VERSION" | fgrep -q -e "Android Debug Bridge"
+if [ $? != 0 ]; then
+ panic "Your 'adb' tool seems invalid, use --adb=<file> to specify a \
+different one: $ADB"
+fi
+
+# If there are more than one device connected, and ANDROID_SERIAL is not
+# defined, print an error message.
+NUM_DEVICES_PLUS2=$($ADB devices 2>/dev/null | wc -l)
+if [ "$NUM_DEVICES_PLUS2" -lt 3 -a -z "$ANDROID_SERIAL" ]; then
+ echo "ERROR: There is more than one Android device connected to ADB."
+ echo "Please define ANDROID_SERIAL to specify which one to use."
+ exit 1
+fi
+
+# A unique ID for this script's session. This needs to be the same in all
+# sub-shell commands we're going to launch, so take the PID of the launcher
+# process.
+TMP_ID=$$
+
+# Temporary directory, will get cleaned up on exit.
+TMPDIR=/tmp/$USER-adb-gdb-tmp-$TMP_ID
+mkdir -p "$TMPDIR" && rm -rf "$TMPDIR"/*
+
+GDBSERVER_PIDFILE="$TMPDIR"/gdbserver-$TMP_ID.pid
+
+# Run a command through adb shell, strip the extra \r from the output
+# and return the correct status code to detect failures. This assumes
+# that the adb shell command prints a final \n to stdout.
+# $1+: command to run
+# Out: command's stdout
+# Return: command's status
+# Note: the command's stderr is lost
+adb_shell () {
+ local TMPOUT="$(mktemp)"
+ local LASTLINE RET
+ local ADB=${ADB:-adb}
+
+ # The weird sed rule is to strip the final \r on each output line
+ # Since 'adb shell' never returns the command's proper exit/status code,
+ # we force it to print it as '%%<status>' in the temporary output file,
+ # which we will later strip from it.
+ $ADB shell $@ ";" echo "%%\$?" 2>/dev/null | \
+ sed -e 's![[:cntrl:]]!!g' > $TMPOUT
+ # Get last line in log, which contains the exit code from the command
+ LASTLINE=$(sed -e '$!d' $TMPOUT)
+ # Extract the status code from the end of the line, which must
+ # be '%%<code>'.
+ RET=$(echo "$LASTLINE" | \
+ awk '{ if (match($0, "%%[0-9]+$")) { print substr($0,RSTART+2); } }')
+ # Remove the status code from the last line. Note that this may result
+ # in an empty line.
+ LASTLINE=$(echo "$LASTLINE" | \
+ awk '{ if (match($0, "%%[0-9]+$")) { print substr($0,1,RSTART-1); } }')
+ # The output itself: all lines except the status code.
+ sed -e '$d' $TMPOUT && printf "%s" "$LASTLINE"
+ # Remove temp file.
+ rm -f $TMPOUT
+ # Exit with the appropriate status.
+ return $RET
+}
+
+# If --force is specified, try to kill any gdbserver process started by the
+# same user on the device. Normally, these are killed automatically by the
+# script on exit, but there are a few corner cases where this would still
+# be needed.
+if [ "$FORCE" ]; then
+ GDBSERVER_PIDS=$(adb_shell ps | awk '$9 ~ /gdbserver/ { print $2; }')
+ for GDB_PID in $GDBSERVER_PIDS; do
+ log "Killing previous gdbserver (PID=$GDB_PID)"
+ adb_shell kill -9 $GDB_PID
+ done
+fi
+
+if [ "$START" ]; then
+ log "Starting $PROGRAM_NAME on device."
+ adb_shell am start -n $PACKAGE_NAME/$ACTIVITY 2>/dev/null
+ adb_shell ps | grep -q $PACKAGE_NAME
+ fail_panic "Could not start $PROGRAM_NAME on device. Are you sure the \
+package is installed?"
+fi
+
+# Return the timestamp of a given time, as number of seconds since epoch.
+# $1: file path
+# Out: file timestamp
+get_file_timestamp () {
+ stat -c %Y "$1" 2>/dev/null
+}
+
+# Detect the build type and symbol directory. This is done by finding
+# the most recent sub-directory containing debug shared libraries under
+# $CHROMIUM_SRC/out/
+#
+# $1: $BUILDTYPE value, can be empty
+# Out: nothing, but this sets SYMBOL_DIR
+#
+detect_symbol_dir () {
+ local SUBDIRS SUBDIR LIST DIR DIR_LIBS TSTAMP
+ # Note: Ninja places debug libraries under out/$BUILDTYPE/lib/, while
+ # Make places then under out/$BUILDTYPE/lib.target.
+ if [ "$1" ]; then
+ SUBDIRS="$1/lib $1/lib.target"
+ else
+ SUBDIRS="Release/lib Debug/lib Release/lib.target Debug/lib.target"
+ fi
+ LIST=$TMPDIR/scan-subdirs-$$.txt
+ printf "" > "$LIST"
+ for SUBDIR in $SUBDIRS; do
+ DIR=$CHROMIUM_SRC/out/$SUBDIR
+ if [ -d "$DIR" ]; then
+ # Ignore build directories that don't contain symbol versions
+ # of the shared libraries.
+ DIR_LIBS=$(ls "$DIR"/lib*.so 2>/dev/null)
+ if [ -z "$DIR_LIBS" ]; then
+ echo "No shared libs: $DIR"
+ continue
+ fi
+ TSTAMP=$(get_file_timestamp "$DIR")
+ printf "%s %s\n" "$TSTAMP" "$SUBDIR" >> "$LIST"
+ fi
+ done
+ SUBDIR=$(cat $LIST | sort -r | head -1 | cut -d" " -f2)
+ rm -f "$LIST"
+
+ if [ -z "$SUBDIR" ]; then
+ if [ -z "$1" ]; then
+ panic "Could not find any build directory under \
+$CHROMIUM_SRC/out. Please build the program first!"
+ else
+ panic "Could not find any $1 directory under \
+$CHROMIUM_SRC/out. Check your build type!"
+ fi
+ fi
+
+ SYMBOL_DIR=$CHROMIUM_SRC/out/$SUBDIR
+ log "Auto-config: --symbol-dir=$SYMBOL_DIR"
+}
+
+if [ -z "$SYMBOL_DIR" ]; then
+ detect_symbol_dir "$BUILDTYPE"
+fi
+
+# Allow several concurrent debugging sessions
+TARGET_GDBSERVER=/data/local/tmp/gdbserver-adb-gdb-$TMP_ID
+
+# Return the build fingerprint contained in a build.prop file.
+# $1: path to build.prop file
+get_build_fingerprint_from () {
+ cat "$1" | grep -e '^ro.build.fingerprint=' | cut -d= -f2
+}
+
+
+ORG_PULL_LIBS_DIR=$PULL_LIBS_DIR
+PULL_LIBS_DIR=${PULL_LIBS_DIR:-$DEFAULT_PULL_LIBS_DIR}
+
+HOST_FINGERPRINT=
+DEVICE_FINGERPRINT=$(adb_shell getprop ro.build.fingerprint)
+log "Device build fingerprint: $DEVICE_FINGERPRINT"
+
+# If --pull-libs-dir is not specified, and this is a platform build, look
+# if we can use the symbolic libraries under $ANDROID_PRODUCT_OUT/symbols/
+# directly, if the build fingerprint matches the device.
+if [ -z "$ORG_PULL_LIBS_DIR" -a \
+ "$ANDROID_PRODUCT_OUT" -a \
+ -f "$ANDROID_PRODUCT_OUT/system/build.prop" ]; then
+ ANDROID_FINGERPRINT=$(get_build_fingerprint_from \
+ "$ANDROID_PRODUCT_OUT"/system/build.prop)
+ log "Android build fingerprint: $ANDROID_FINGERPRINT"
+ if [ "$ANDROID_FINGERPRINT" = "$DEVICE_FINGERPRINT" ]; then
+ log "Perfect match!"
+ PULL_LIBS_DIR=$ANDROID_PRODUCT_OUT/symbols
+ HOST_FINGERPRINT=$ANDROID_FINGERPRINT
+ if [ "$PULL_LIBS" ]; then
+ log "Ignoring --pull-libs since the device and platform build \
+fingerprints match."
+ NO_PULL_LIBS=true
+ fi
+ fi
+fi
+
+# If neither --pull-libs an --no-pull-libs were specified, check the build
+# fingerprints of the device, and the cached system libraries on the host.
+#
+if [ -z "$NO_PULL_LIBS" -a -z "$PULL_LIBS" ]; then
+ if [ ! -f "$PULL_LIBS_DIR/build.prop" ]; then
+ log "Auto-config: --pull-libs (no cached libraries)"
+ PULL_LIBS=true
+ else
+ HOST_FINGERPRINT=$(get_build_fingerprint_from "$PULL_LIBS_DIR/build.prop")
+ log "Host build fingerprint: $HOST_FINGERPRINT"
+ if [ "$HOST_FINGERPRINT" == "$DEVICE_FINGERPRINT" ]; then
+ log "Auto-config: --no-pull-libs (fingerprint match)"
+ NO_PULL_LIBS=true
+ else
+ log "Auto-config: --pull-libs (fingerprint mismatch)"
+ PULL_LIBS=true
+ fi
+ fi
+fi
+
+# Extract the system libraries from the device if necessary.
+if [ "$PULL_LIBS" -a -z "$NO_PULL_LIBS" ]; then
+ echo "Extracting system libraries into: $PULL_LIBS_DIR"
+fi
+
+mkdir -p "$PULL_LIBS_DIR"
+fail_panic "Can't create --libs-dir directory: $PULL_LIBS_DIR"
+
+# If requested, work for M-x gdb. The gdb indirections make it
+# difficult to pass --annotate=3 to the gdb binary itself.
+GDB_ARGS=
+if [ "$ANNOTATE" ]; then
+ GDB_ARGS=$GDB_ARGS" --annotate=$ANNOTATE"
+fi
+
+# Get the PID from the first argument or else find the PID of the
+# browser process.
+if [ -z "$PID" ]; then
+ PROCESSNAME=$PACKAGE_NAME
+ if [ "$SANDBOXED_INDEX" ]; then
+ PROCESSNAME=$PROCESSNAME:sandboxed_process$SANDBOXED_INDEX
+ elif [ "$SANDBOXED" ]; then
+ PROCESSNAME=$PROCESSNAME:sandboxed_process
+ PID=$(adb_shell ps | \
+ awk '$9 ~ /^'$PROCESSNAME'/ { print $2; }' | head -1)
+ fi
+ if [ -z "$PID" ]; then
+ PID=$(adb_shell ps | \
+ awk '$9 == "'$PROCESSNAME'" { print $2; }' | head -1)
+ fi
+ if [ -z "$PID" ]; then
+ if [ "$START" ]; then
+ panic "Can't find application process PID, did it crash?"
+ else
+ panic "Can't find application process PID, are you sure it is \
+running? Try using --start."
+ fi
+ fi
+ log "Found process PID: $PID"
+elif [ "$SANDBOXED" ]; then
+ echo "WARNING: --sandboxed option ignored due to use of --pid."
+fi
+
+# Determine if 'adb shell' runs as root or not.
+# If so, we can launch gdbserver directly, otherwise, we have to
+# use run-as $PACKAGE_NAME ..., which requires the package to be debuggable.
+#
+SHELL_UID=$(adb shell cat /proc/self/status | \
+ awk '$1 == "Uid:" { print $2; }')
+log "Shell UID: $SHELL_UID"
+COMMAND_PREFIX=
+if [ "$SHELL_UID" != 0 -o -n "$NO_ROOT" ]; then
+ log "Using run-as $PACKAGE_NAME to run without root."
+ COMMAND_PREFIX="run-as $PACKAGE_NAME"
+fi
+
+# Pull device's system libraries that are mapped by our process.
+# Pulling all system libraries is too long, so determine which ones
+# we need by looking at /proc/$PID/maps instead
+if [ "$PULL_LIBS" -a -z "$NO_PULL_LIBS" ]; then
+ echo "Extracting system libraries into: $PULL_LIBS_DIR"
+ SYSTEM_LIBS=$(adb_shell $COMMAND_PREFIX cat /proc/$PID/maps | \
+ awk '$6 ~ /\/system\/.*\.so$/ { print $6; }' | sort -u)
+ for SYSLIB in /system/bin/linker $SYSTEM_LIBS; do
+ echo "Pulling from device: $SYSLIB"
+ DST_FILE=$PULL_LIBS_DIR$SYSLIB
+ DST_DIR=$(dirname "$DST_FILE")
+ mkdir -p "$DST_DIR" && adb pull $SYSLIB "$DST_FILE" 2>/dev/null
+ fail_panic "Could not pull $SYSLIB from device !?"
+ done
+ echo "Pulling device build.prop"
+ adb pull /system/build.prop $PULL_LIBS_DIR/build.prop
+ fail_panic "Could not pull device build.prop !?"
+fi
+
+# Find all the sub-directories of $PULL_LIBS_DIR, up to depth 4
+# so we can add them to solib-search-path later.
+SOLIB_DIRS=$(find $PULL_LIBS_DIR -mindepth 1 -maxdepth 4 -type d | \
+ grep -v "^$" | tr '\n' ':')
+
+# This is a re-implementation of gdbclient, where we use compatible
+# versions of gdbserver and $GDBNAME to ensure that everything works
+# properly.
+#
+
+# Push gdbserver to the device
+log "Pushing gdbserver to $TARGET_GDBSERVER"
+adb push $GDBSERVER $TARGET_GDBSERVER &>/dev/null
+fail_panic "Could not copy gdbserver to the device!"
+
+PORT=5039
+HOST_PORT=$PORT
+TARGET_PORT=$PORT
+
+# Pull the app_process binary from the device
+GDBEXEC=app_process
+log "Pulling $GDBEXEC from device"
+adb pull /system/bin/$GDBEXEC "$TMPDIR"/$GDBEXEC &>/dev/null
+fail_panic "Could not retrieve $GDBEXEC from the device!"
+
+# Setup network redirection
+log "Setting network redirection (host:$HOST_PORT -> device:$TARGET_PORT)"
+adb forward tcp:$HOST_PORT tcp:$TARGET_PORT
+fail_panic "Could not setup network redirection from \
+host:localhost:$HOST_PORT to device:localhost:$TARGET_PORT!"
+
+# Start gdbserver in the background
+# Note that using run-as requires the package to be debuggable.
+#
+# If not, this will fail horribly. The alternative is to run the
+# program as root, which requires of course root privileges.
+# Maybe we should add a --root option to enable this?
+#
+log "Starting gdbserver in the background:"
+GDBSERVER_LOG=$TMPDIR/gdbserver-$TMP_ID.log
+log "adb shell $COMMAND_PREFIX $TARGET_GDBSERVER :$TARGET_PORT \
+--attach $PID"
+("$ADB" shell $COMMAND_PREFIX $TARGET_GDBSERVER :$TARGET_PORT \
+ --attach $PID > $GDBSERVER_LOG 2>&1) &
+GDBSERVER_PID=$!
+echo "$GDBSERVER_PID" > $GDBSERVER_PIDFILE
+log "background job pid: $GDBSERVER_PID"
+
+# Check that it is still running after a few seconds. If not, this means we
+# could not properly attach to it
+sleep 2
+log "Job control: $(jobs -l)"
+STATE=$(jobs -l | awk '$2 == "'$GDBSERVER_PID'" { print $3; }')
+if [ "$STATE" != "Running" ]; then
+ echo "ERROR: GDBServer could not attach to PID $PID!"
+ echo "Failure log (use --verbose for more information):"
+ cat $GDBSERVER_LOG
+ exit 1
+fi
+
+# Generate a file containing useful GDB initialization commands
+readonly COMMANDS=$TMPDIR/gdb.init
+log "Generating GDB initialization commands file: $COMMANDS"
+echo -n "" > $COMMANDS
+echo "file $TMPDIR/$GDBEXEC" >> $COMMANDS
+echo "directory $CHROMIUM_SRC" >> $COMMANDS
+echo "set solib-absolute-prefix $PULL_LIBS_DIR" >> $COMMANDS
+echo "set solib-search-path $SOLIB_DIRS:$PULL_LIBS_DIR:$SYMBOL_DIR" \
+ >> $COMMANDS
+echo "echo Attaching and reading symbols, this may take a while.." \
+ >> $COMMANDS
+echo "target remote :$HOST_PORT" >> $COMMANDS
+
+if [ "$GDBINIT" ]; then
+ cat "$GDBINIT" >> $COMMANDS
+fi
+
+if [ "$VERBOSE" -gt 0 ]; then
+ echo "### START $COMMANDS"
+ cat $COMMANDS
+ echo "### END $COMMANDS"
+fi
+
+log "Launching gdb client: $GDB $GDBARGS -x $COMMANDS"
+$GDB $GDBARGS -x $COMMANDS &&
+rm -f "$GDBSERVER_PIDFILE"
+
+clean_exit $?
diff --git a/src/build/android/adb_gdb_chromium_testshell b/src/build/android/adb_gdb_chromium_testshell
new file mode 100755
index 0000000..0f1b4a7
--- /dev/null
+++ b/src/build/android/adb_gdb_chromium_testshell
@@ -0,0 +1,16 @@
+#!/bin/bash
+#
+# 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.
+#
+# Attach to or start a ChromiumTestShell process and debug it.
+# See --help for details.
+#
+PROGDIR=$(dirname "$0")
+export ADB_GDB_PROGNAME=$(basename "$0")
+export ADB_GDB_ACTIVITY=.ChromiumTestShellActivity
+"$PROGDIR"/adb_gdb \
+ --program-name=ChromiumTestShell \
+ --package-name=org.chromium.chrome.testshell \
+ "$@"
diff --git a/src/build/android/adb_gdb_content_shell b/src/build/android/adb_gdb_content_shell
new file mode 100755
index 0000000..d0b6947
--- /dev/null
+++ b/src/build/android/adb_gdb_content_shell
@@ -0,0 +1,16 @@
+#!/bin/bash
+#
+# 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.
+#
+# Attach to or start a ContentShell process and debug it.
+# See --help for details.
+#
+PROGDIR=$(dirname "$0")
+export ADB_GDB_PROGNAME=$(basename "$0")
+export ADB_GDB_ACTIVITY=.ContentShellActivity
+"$PROGDIR"/adb_gdb \
+ --program-name=ContentShell \
+ --package-name=org.chromium.content_shell \
+ "$@"
diff --git a/src/build/android/adb_gdb_drt b/src/build/android/adb_gdb_drt
new file mode 100755
index 0000000..6157361
--- /dev/null
+++ b/src/build/android/adb_gdb_drt
@@ -0,0 +1,16 @@
+#!/bin/bash
+#
+# 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.
+#
+# Attach to or start a DumpRenderTree process and debug it.
+# See --help for details.
+#
+PROGDIR=$(dirname "$0")
+export ADB_GDB_PROGNAME=$(basename "$0")
+export ADB_GDB_ACTIVITY=.ChromeNativeTestActivity
+"$PROGDIR"/adb_gdb \
+ --program-name=DumpRenderTree \
+ --package-name=org.chromium.native_test \
+ "$@"
diff --git a/src/build/android/adb_install_apk.py b/src/build/android/adb_install_apk.py
new file mode 100755
index 0000000..db2c62c
--- /dev/null
+++ b/src/build/android/adb_install_apk.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+#
+# 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 multiprocessing
+import optparse
+import os
+import sys
+
+from pylib import android_commands
+from pylib import apk_info
+from pylib import constants
+from pylib import test_options_parser
+
+
+def _InstallApk(args):
+ apk_path, apk_package, device = args
+ result = android_commands.AndroidCommands(device=device).ManagedInstall(
+ apk_path, False, apk_package)
+ print '----- Installed on %s -----' % device
+ print result
+
+
+def main(argv):
+ parser = optparse.OptionParser()
+ test_options_parser.AddInstallAPKOption(parser)
+ options, args = parser.parse_args(argv)
+ test_options_parser.ValidateInstallAPKOption(parser, options)
+ if len(args) > 1:
+ raise Exception('Error: Unknown argument:', args[1:])
+
+ devices = android_commands.GetAttachedDevices()
+ if not devices:
+ raise Exception('Error: no connected devices')
+
+ if not options.apk_package:
+ options.apk_package = apk_info.GetPackageNameForApk(options.apk)
+
+ pool = multiprocessing.Pool(len(devices))
+ # Send a tuple (apk_path, apk_package, device) per device.
+ pool.map(_InstallApk, zip([options.apk] * len(devices),
+ [options.apk_package] * len(devices),
+ devices))
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/src/build/android/adb_kill_content_shell b/src/build/android/adb_kill_content_shell
new file mode 100755
index 0000000..d24c7a9
--- /dev/null
+++ b/src/build/android/adb_kill_content_shell
@@ -0,0 +1,24 @@
+#!/bin/bash
+#
+# 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.
+#
+# Kill a running content shell.
+#
+# Assumes you have sourced the build/android/envsetup.sh script.
+
+SHELL_PID_LINES=$(adb shell ps | grep ' org.chromium.content_shell')
+VAL=$(echo "$SHELL_PID_LINES" | wc -l)
+if [ $VAL -lt 1 ] ; then
+ echo "Not running Content shell."
+else
+ SHELL_PID=$(echo $SHELL_PID_LINES | awk '{print $2}')
+ if [ "$SHELL_PID" != "" ] ; then
+ set -x
+ adb shell kill $SHELL_PID
+ set -
+ else
+ echo "Content shell does not appear to be running."
+ fi
+fi
diff --git a/src/build/android/adb_logcat_monitor.py b/src/build/android/adb_logcat_monitor.py
new file mode 100755
index 0000000..aeaef0b
--- /dev/null
+++ b/src/build/android/adb_logcat_monitor.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""Saves logcats from all connected devices.
+
+Usage: adb_logcat_monitor.py <base_dir> [<adb_binary_path>]
+
+This script will repeatedly poll adb for new devices and save logcats
+inside the <base_dir> directory, which it attempts to create. The
+script will run until killed by an external signal. To test, run the
+script in a shell and <Ctrl>-C it after a while. It should be
+resilient across phone disconnects and reconnects and start the logcat
+early enough to not miss anything.
+"""
+
+import logging
+import os
+import re
+import shutil
+import signal
+import subprocess
+import sys
+import time
+
+# Map from device_id -> (process, logcat_num)
+devices = {}
+
+
+class TimeoutException(Exception):
+ """Exception used to signal a timeout."""
+ pass
+
+
+class SigtermError(Exception):
+ """Exception used to catch a sigterm."""
+ pass
+
+
+def StartLogcatIfNecessary(device_id, adb_cmd, base_dir):
+ """Spawns a adb logcat process if one is not currently running."""
+ process, logcat_num = devices[device_id]
+ if process:
+ if process.poll() is None:
+ # Logcat process is still happily running
+ return
+ else:
+ logging.info('Logcat for device %s has died', device_id)
+ error_filter = re.compile('- waiting for device -')
+ for line in process.stderr:
+ if not error_filter.match(line):
+ logging.error(device_id + ': ' + line)
+
+ logging.info('Starting logcat %d for device %s', logcat_num,
+ device_id)
+ logcat_filename = 'logcat_%s_%03d' % (device_id, logcat_num)
+ logcat_file = open(os.path.join(base_dir, logcat_filename), 'w')
+ process = subprocess.Popen([adb_cmd, '-s', device_id,
+ 'logcat', '-v', 'threadtime'],
+ stdout=logcat_file,
+ stderr=subprocess.PIPE)
+ devices[device_id] = (process, logcat_num + 1)
+
+
+def GetAttachedDevices(adb_cmd):
+ """Gets the device list from adb.
+
+ We use an alarm in this function to avoid deadlocking from an external
+ dependency.
+
+ Args:
+ adb_cmd: binary to run adb
+
+ Returns:
+ list of devices or an empty list on timeout
+ """
+ signal.alarm(2)
+ try:
+ out, err = subprocess.Popen([adb_cmd, 'devices'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE).communicate()
+ if err:
+ logging.warning('adb device error %s', err.strip())
+ return re.findall('^(\w+)\tdevice$', out, re.MULTILINE)
+ except TimeoutException:
+ logging.warning('"adb devices" command timed out')
+ return []
+ except (IOError, OSError):
+ logging.exception('Exception from "adb devices"')
+ return []
+ finally:
+ signal.alarm(0)
+
+
+def main(base_dir, adb_cmd='adb'):
+ """Monitor adb forever. Expects a SIGINT (Ctrl-C) to kill."""
+ # We create the directory to ensure 'run once' semantics
+ if os.path.exists(base_dir):
+ print 'adb_logcat_monitor: %s already exists? Cleaning' % base_dir
+ shutil.rmtree(base_dir, ignore_errors=True)
+
+ os.makedirs(base_dir)
+ logging.basicConfig(filename=os.path.join(base_dir, 'eventlog'),
+ level=logging.INFO,
+ format='%(asctime)-2s %(levelname)-8s %(message)s')
+
+ # Set up the alarm for calling 'adb devices'. This is to ensure
+ # our script doesn't get stuck waiting for a process response
+ def TimeoutHandler(_, unused_frame):
+ raise TimeoutException()
+ signal.signal(signal.SIGALRM, TimeoutHandler)
+
+ # Handle SIGTERMs to ensure clean shutdown
+ def SigtermHandler(_, unused_frame):
+ raise SigtermError()
+ signal.signal(signal.SIGTERM, SigtermHandler)
+
+ logging.info('Started with pid %d', os.getpid())
+ pid_file_path = os.path.join(base_dir, 'LOGCAT_MONITOR_PID')
+
+ try:
+ with open(pid_file_path, 'w') as f:
+ f.write(str(os.getpid()))
+ while True:
+ for device_id in GetAttachedDevices(adb_cmd):
+ if not device_id in devices:
+ devices[device_id] = (None, 0)
+
+ for device in devices:
+ # This will spawn logcat watchers for any device ever detected
+ StartLogcatIfNecessary(device, adb_cmd, base_dir)
+
+ time.sleep(5)
+ except SigtermError:
+ logging.info('Received SIGTERM, shutting down')
+ except:
+ logging.exception('Unexpected exception in main.')
+ finally:
+ for process, _ in devices.itervalues():
+ if process:
+ try:
+ process.terminate()
+ except OSError:
+ pass
+ os.remove(pid_file_path)
+
+
+if __name__ == '__main__':
+ if 2 <= len(sys.argv) <= 3:
+ print 'adb_logcat_monitor: Initializing'
+ sys.exit(main(*sys.argv[1:3]))
+
+ print 'Usage: %s <base_dir> [<adb_binary_path>]' % sys.argv[0]
diff --git a/src/build/android/adb_logcat_printer.py b/src/build/android/adb_logcat_printer.py
new file mode 100755
index 0000000..5194668
--- /dev/null
+++ b/src/build/android/adb_logcat_printer.py
@@ -0,0 +1,202 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""Shutdown adb_logcat_monitor and print accumulated logs.
+
+To test, call './adb_logcat_printer.py <base_dir>' where
+<base_dir> contains 'adb logcat -v threadtime' files named as
+logcat_<deviceID>_<sequenceNum>
+
+The script will print the files to out, and will combine multiple
+logcats from a single device if there is overlap.
+
+Additionally, if a <base_dir>/LOGCAT_MONITOR_PID exists, the script
+will attempt to terminate the contained PID by sending a SIGINT and
+monitoring for the deletion of the aforementioned file.
+"""
+
+import cStringIO
+import logging
+import os
+import re
+import signal
+import sys
+import time
+
+
+# Set this to debug for more verbose output
+LOG_LEVEL = logging.INFO
+
+
+def CombineLogFiles(list_of_lists, logger):
+ """Splices together multiple logcats from the same device.
+
+ Args:
+ list_of_lists: list of pairs (filename, list of timestamped lines)
+ logger: handler to log events
+
+ Returns:
+ list of lines with duplicates removed
+ """
+ cur_device_log = ['']
+ for cur_file, cur_file_lines in list_of_lists:
+ # Ignore files with just the logcat header
+ if len(cur_file_lines) < 2:
+ continue
+ common_index = 0
+ # Skip this step if list just has empty string
+ if len(cur_device_log) > 1:
+ try:
+ line = cur_device_log[-1]
+ # Used to make sure we only splice on a timestamped line
+ if re.match('^\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} ', line):
+ common_index = cur_file_lines.index(line)
+ else:
+ logger.warning('splice error - no timestamp in "%s"?', line.strip())
+ except ValueError:
+ # The last line was valid but wasn't found in the next file
+ cur_device_log += ['***** POSSIBLE INCOMPLETE LOGCAT *****']
+ logger.info('Unable to splice %s. Incomplete logcat?', cur_file)
+
+ cur_device_log += ['*'*30 + ' %s' % cur_file]
+ cur_device_log.extend(cur_file_lines[common_index:])
+
+ return cur_device_log
+
+
+def FindLogFiles(base_dir):
+ """Search a directory for logcat files.
+
+ Args:
+ base_dir: directory to search
+
+ Returns:
+ Mapping of device_id to a sorted list of file paths for a given device
+ """
+ logcat_filter = re.compile('^logcat_(\w+)_(\d+)$')
+ # list of tuples (<device_id>, <seq num>, <full file path>)
+ filtered_list = []
+ for cur_file in os.listdir(base_dir):
+ matcher = logcat_filter.match(cur_file)
+ if matcher:
+ filtered_list += [(matcher.group(1), int(matcher.group(2)),
+ os.path.join(base_dir, cur_file))]
+ filtered_list.sort()
+ file_map = {}
+ for device_id, _, cur_file in filtered_list:
+ if not device_id in file_map:
+ file_map[device_id] = []
+
+ file_map[device_id] += [cur_file]
+ return file_map
+
+
+def GetDeviceLogs(log_filenames, logger):
+ """Read log files, combine and format.
+
+ Args:
+ log_filenames: mapping of device_id to sorted list of file paths
+ logger: logger handle for logging events
+
+ Returns:
+ list of formatted device logs, one for each device.
+ """
+ device_logs = []
+
+ for device, device_files in log_filenames.iteritems():
+ logger.debug('%s: %s', device, str(device_files))
+ device_file_lines = []
+ for cur_file in device_files:
+ with open(cur_file) as f:
+ device_file_lines += [(cur_file, f.read().splitlines())]
+ combined_lines = CombineLogFiles(device_file_lines, logger)
+ # Prepend each line with a short unique ID so it's easy to see
+ # when the device changes. We don't use the start of the device
+ # ID because it can be the same among devices. Example lines:
+ # AB324: foo
+ # AB324: blah
+ device_logs += [('\n' + device[-5:] + ': ').join(combined_lines)]
+ return device_logs
+
+
+def ShutdownLogcatMonitor(base_dir, logger):
+ """Attempts to shutdown adb_logcat_monitor and blocks while waiting."""
+ try:
+ monitor_pid_path = os.path.join(base_dir, 'LOGCAT_MONITOR_PID')
+ with open(monitor_pid_path) as f:
+ monitor_pid = int(f.readline())
+
+ logger.info('Sending SIGTERM to %d', monitor_pid)
+ os.kill(monitor_pid, signal.SIGTERM)
+ i = 0
+ while True:
+ time.sleep(.2)
+ if not os.path.exists(monitor_pid_path):
+ return
+ if not os.path.exists('/proc/%d' % monitor_pid):
+ logger.warning('Monitor (pid %d) terminated uncleanly?', monitor_pid)
+ return
+ logger.info('Waiting for logcat process to terminate.')
+ i += 1
+ if i >= 10:
+ logger.warning('Monitor pid did not terminate. Continuing anyway.')
+ return
+
+ except (ValueError, IOError, OSError):
+ logger.exception('Error signaling logcat monitor - continuing')
+
+
+def main(base_dir, output_file):
+ log_stringio = cStringIO.StringIO()
+ logger = logging.getLogger('LogcatPrinter')
+ logger.setLevel(LOG_LEVEL)
+ sh = logging.StreamHandler(log_stringio)
+ sh.setFormatter(logging.Formatter('%(asctime)-2s %(levelname)-8s'
+ ' %(message)s'))
+ logger.addHandler(sh)
+
+ try:
+ # Wait at least 5 seconds after base_dir is created before printing.
+ #
+ # The idea is that 'adb logcat > file' output consists of 2 phases:
+ # 1 Dump all the saved logs to the file
+ # 2 Stream log messages as they are generated
+ #
+ # We want to give enough time for phase 1 to complete. There's no
+ # good method to tell how long to wait, but it usually only takes a
+ # second. On most bots, this code path won't occur at all, since
+ # adb_logcat_monitor.py command will have spawned more than 5 seconds
+ # prior to called this shell script.
+ try:
+ sleep_time = 5 - (time.time() - os.path.getctime(base_dir))
+ except OSError:
+ sleep_time = 5
+ if sleep_time > 0:
+ logger.warning('Monitor just started? Sleeping %.1fs', sleep_time)
+ time.sleep(sleep_time)
+
+ assert os.path.exists(base_dir), '%s does not exist' % base_dir
+ ShutdownLogcatMonitor(base_dir, logger)
+ separator = '\n' + '*' * 80 + '\n\n'
+ for log in GetDeviceLogs(FindLogFiles(base_dir), logger):
+ output_file.write(log)
+ output_file.write(separator)
+ with open(os.path.join(base_dir, 'eventlog')) as f:
+ output_file.write('\nLogcat Monitor Event Log\n')
+ output_file.write(f.read())
+ except:
+ logger.exception('Unexpected exception')
+
+ logger.info('Done.')
+ sh.flush()
+ output_file.write('\nLogcat Printer Event Log\n')
+ output_file.write(log_stringio.getvalue())
+
+if __name__ == '__main__':
+ if len(sys.argv) == 1:
+ print 'Usage: %s <base_dir>' % sys.argv[0]
+ sys.exit(1)
+ sys.exit(main(sys.argv[1], sys.stdout))
diff --git a/src/build/android/adb_run_chromium_testshell b/src/build/android/adb_run_chromium_testshell
new file mode 100755
index 0000000..b17482c
--- /dev/null
+++ b/src/build/android/adb_run_chromium_testshell
@@ -0,0 +1,14 @@
+#!/bin/bash
+#
+# 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.
+
+if [ $# -gt 0 ] ; then
+ INTENT_ARGS="-d \"$1\"" # e.g. a URL
+fi
+
+adb shell am start \
+ -a android.intent.action.VIEW \
+ -n org.chromium.chrome.testshell/.ChromiumTestShellActivity \
+ $INTENT_ARGS
diff --git a/src/build/android/adb_run_content_shell b/src/build/android/adb_run_content_shell
new file mode 100755
index 0000000..ea92f5a
--- /dev/null
+++ b/src/build/android/adb_run_content_shell
@@ -0,0 +1,14 @@
+#!/bin/bash
+#
+# 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.
+
+if [ $# -gt 0 ] ; then
+ INTENT_ARGS="-d \"$1\"" # e.g. a URL
+fi
+
+adb shell am start \
+ -a android.intent.action.VIEW \
+ -n org.chromium.content_shell/.ContentShellActivity \
+ $INTENT_ARGS
diff --git a/src/build/android/ant/chromium-apk.xml b/src/build/android/ant/chromium-apk.xml
new file mode 100644
index 0000000..2e24084
--- /dev/null
+++ b/src/build/android/ant/chromium-apk.xml
@@ -0,0 +1,102 @@
+<!--
+ 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.
+-->
+<project default="error">
+ <property name="ant.project.name" value="${APK_NAME}"/>
+ <!--
+ Gyp will pass CONFIGURATION_NAME as the target for ant to build. These targets will call the
+ appropriate sdk tools target.
+ -->
+ <target name="Debug" depends="debug"/>
+ <target name="Release" depends="release"/>
+ <!-- __LB_SHELL__
+ Map LbShell configurations into Android build system configurations
+ -->
+ <target name="Android_Debug" depends="debug"/>
+ <target name="Android_Devel" depends="debug"/>
+ <target name="Android_QA" depends="debug"/>
+ <target name="Android_Gold" depends="release"/>
+ <target name="error">
+ <fail message="CONFIGURATION_NAME should be passed as a target to ant."/>
+ </target>
+
+ <description>
+ Building ${ant.project.name}.apk
+ </description>
+ <import file="common.xml"/>
+
+ <property-location
+ name="out.dir"
+ location="${PRODUCT_DIR}/${PACKAGE_NAME}"
+ check-exists="false"
+ />
+
+ <path id="javac.srcdirs.additional">
+ <filelist files="${ADDITIONAL_SRC_DIRS}"/>
+ <filelist files="${GENERATED_SRC_DIRS}"/>
+ </path>
+
+ <!--
+ Include additional resource folders in the apk, e.g. content/.../res. We
+ list the res folders in project.library.res.folder.path and the
+ corresponding java packages in project.library.packages, which must be
+ semicolon-delimited while ADDITIONAL_RES_PACKAGES is space-delimited, hence
+ the javascript task.
+ -->
+
+ <path id="project.library.res.folder.path">
+ <filelist files="${ADDITIONAL_RES_DIRS}"/>
+ </path>
+ <!-- __LB_SHELL__
+ javascript isn't supported in stock Apache Ant(TM) version 1.8.2
+ so we are going to disable this section for now.
+ <script language="javascript">
+ var before = project.getProperty("ADDITIONAL_RES_PACKAGES");
+ project.setProperty("project.library.packages", before.replaceAll(" ", ";"));
+ </script>
+ -->
+ <property-value name="target.abi" value="${APP_ABI}"/>
+ <property name="resource.absolute.dir" value="${RESOURCE_DIR}"/>
+ <property-value name="gen.absolute.dir" value="${out.dir}/gen"/>
+ <property-location name="native.libs.absolute.dir" location="${out.dir}/libs"
+ check-exists="false"/>
+
+ <property-value name="version.code" value="${APP_MANIFEST_VERSION_CODE}"/>
+ <property-value name="version.name" value="${APP_MANIFEST_VERSION_NAME}"/>
+
+ <!--
+ We use the PROGUARD_ENABLED flag for enabling proguard. By default proguard is enabled for
+ Release builds if proguard.config is set. Setting proguard.config even to an empty string will
+ enable proguard. Set this property only when we have explicitly enabled proguard.
+ -->
+ <condition property="proguard.config" value="${PROGUARD_FLAGS}">
+ <istrue value="${PROGUARD_ENABLED}"/>
+ </condition>
+ <!-- TODO(shashishekhar): Enable emma and code-coverage filters. -->
+
+ <condition property="asset.absolute.dir"
+ value="${out.dir}/assets"
+ else="${ASSET_DIR}">
+ <equals arg1="${ASSET_DIR}" arg2=""/>
+ </condition>
+
+ <!-- Set the output directory for the final apk to the ${apks.dir}. -->
+ <property-location name="out.final.file"
+ location="${apks.dir}/${ant.project.name}.apk"
+ check-exists="false"/>
+
+ <!-- Classpath for javac -->
+ <path id="javac.custom.classpath">
+ <filelist files="${INPUT_JARS_PATHS}"/>
+ </path>
+
+ <path id="out.dex.jar.input.ref">
+ <path refid="javac.custom.classpath"/>
+ </path>
+
+ <import file="sdk-targets.xml"/>
+ <import file="${sdk.dir}/tools/ant/build.xml"/>
+</project>
+
diff --git a/src/build/android/ant/chromium-debug.keystore b/src/build/android/ant/chromium-debug.keystore
new file mode 100644
index 0000000..67eb0aa
--- /dev/null
+++ b/src/build/android/ant/chromium-debug.keystore
Binary files differ
diff --git a/src/build/android/ant/chromium-jars.xml b/src/build/android/ant/chromium-jars.xml
new file mode 100644
index 0000000..d13c927
--- /dev/null
+++ b/src/build/android/ant/chromium-jars.xml
@@ -0,0 +1,116 @@
+<!--
+ 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.
+-->
+<project name="chromium-jars" default="dist">
+ <!--
+ Common ant build file for for chromium_*.jars.
+ For creating a new chromium_*.jar :
+ 1. Use build/java.gypi action.
+ The jar will be created as chromium_${PACKAGE_NAME} in
+ ${PRODUCT_DIR}/lib.java.
+ -->
+ <description>
+ Building ${PROJECT_NAME}/ java source code with ant.
+ </description>
+
+ <import file="common.xml"/>
+
+ <path id="javac.custom.classpath">
+ <filelist files="${INPUT_JARS_PATHS}"/>
+ <pathelement location="${ANDROID_SDK}/android.jar"/>
+ </path>
+
+ <path id="javac.srcdirs.additional">
+ <filelist files="${ADDITIONAL_SRC_DIRS}"/>
+ <filelist files="${GENERATED_SRC_DIRS}"/>
+ </path>
+
+ <property-value
+ name="javac.srcdir"
+ value="src:${toString:javac.srcdirs.additional}"
+ />
+
+ <property-location
+ name="dest.dir"
+ location="${PRODUCT_DIR}/java/${PACKAGE_NAME}"
+ check-exists="false"
+ />
+
+ <condition property="javac_includes_message"
+ value=""
+ else="Include filter: ${JAVAC_INCLUDES}">
+ <equals arg1="${JAVAC_INCLUDES}" arg2=""/>
+ </condition>
+
+ <target name="init">
+ <!-- Create the time stamp -->
+ <tstamp/>
+ <!-- Create the build directory structure used by compile -->
+ <mkdir dir="${dest.dir}"/>
+
+ <!-- Remove all .class files from dest.dir. This prevents inclusion of
+ incorrect .class files in the final .jar. For example, if a .java file
+ was deleted, the .jar should not contain the .class files for that
+ .java from previous builds.
+ -->
+ <delete>
+ <fileset dir="${dest.dir}" includes="**/*.class"/>
+ </delete>
+ </target>
+
+ <target name="compile" depends="init" description="Compiles source.">
+ <fail message="Error: javac.custom.classpath is not set. Please set it to
+ classpath for javac.">
+ <condition>
+ <not><isreference refid="javac.custom.classpath"/></not>
+ </condition>
+ </fail>
+
+ <echo>
+ Compiling ${javac.srcdir}, classpath: ${toString:javac.custom.classpath}
+ ${javac_includes_message}
+ </echo>
+
+ <!-- __LB_SHELL__
+ Android SDK build toolchain doesn't support Java 1.7 so we force
+ the source and target versions to 1.6. Failure to do will result
+ in .class files that can't be parsed by the dexer.
+ -->
+ <javac
+ source="1.6"
+ target="1.6"
+ srcdir="${javac.srcdir}"
+ destdir="${dest.dir}"
+ classpathref="javac.custom.classpath"
+ debug="true"
+ includeantruntime="false"
+ includes="${JAVAC_INCLUDES}">
+ <compilerarg value="-Xlint:unchecked"/>
+ </javac>
+ </target>
+
+ <target name="dist" depends="compile"
+ description="Generate chromium_${PACKAGE_NAME}.jar.">
+ <!-- Create the distribution directory. We exclude R.class and R$*.class
+ files since new versions of these files with the correct resource -> ID
+ mapping will be provided when we build each individual apk. -->
+ <jar
+ jarfile="${lib.java.dir}/chromium_${PACKAGE_NAME}.jar"
+ excludes="**/R.class **/R$*.class"
+ basedir="${dest.dir}"
+ />
+
+ <!-- If Gyp thinks this output is stale but Ant doesn't, the modification
+ time should still be updated. Otherwise, this target will continue to
+ be rebuilt in future builds.
+ -->
+ <touch file="${lib.java.dir}/chromium_${PACKAGE_NAME}.jar"/>
+ </target>
+
+ <target name="clean" description="clean up">
+ <!-- Delete the appropriate directory trees -->
+ <delete dir="${dest.dir}"/>
+ </target>
+</project>
diff --git a/src/build/android/ant/common.xml b/src/build/android/ant/common.xml
new file mode 100644
index 0000000..1001f19
--- /dev/null
+++ b/src/build/android/ant/common.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project name="chrome_common_defines">
+ <!-- Common build properties for Chrome for android. -->
+
+ <!--
+ Macro for checking that a property is correctly set. Performs checks for:
+ 1. Property is set and not null.
+ 2. String value of property does not contains any '$' signs.
+ -->
+ <macrodef name="check-property-value">
+ <attribute name="property"/>
+ <sequential>
+ <fail message ="Property @{property} is not set.">
+ <condition>
+ <or>
+ <not><isset property="@{property}"/></not>
+ <length string="${@{property}}" trim="true" when="less" length="1"/>
+ </or>
+ </condition>
+ </fail>
+ <!--
+ Check for $ signs. This catches errors when properties are initialized from environment
+ variables. E.g. if we have <property name="foo" value="${env.bar}" /> but env.bar is
+ not set then foo will have the literal value of '${env.bar}'.
+ -->
+ <fail message="Value checked failed for property: @{property} : ${@{property}}.
+ Property value contains an uninitialized environment variable.">
+ <condition>
+ <contains string="${@{property}}" substring="$"/>
+ </condition>
+ </fail>
+ </sequential>
+ </macrodef>
+
+ <!--
+ A safe setter for location properties. Checks that a location is not
+ empty and actually exists. For specifying output directories, location
+ check can be disabled by specifying check-exists="false".
+ -->
+ <macrodef name="property-location">
+ <attribute name="name"/>
+ <attribute name="location"/>
+ <attribute name="check-exists" default="true"/>
+ <sequential>
+ <property name="@{name}" location="@{location}"/>
+ <check-property-value property="@{name}"/>
+ <fail message="Location specified for @{name} : @{location} does not exist.">
+ <condition>
+ <and>
+ <equals arg1="@{check-exists}" arg2="true"/>
+ <not><available file="@{location}"/></not>
+ </and>
+ </condition>
+ </fail>
+ </sequential>
+ </macrodef>
+
+ <!-- A safe setter for property values -->
+ <macrodef name="property-value">
+ <attribute name="name"/>
+ <attribute name="value"/>
+ <sequential>
+ <property name="@{name}" value="@{value}"/>
+ <check-property-value property="@{name}"/>
+ </sequential>
+ </macrodef>
+
+ <!-- Common environment properties. -->
+ <property-location name="sdk.dir" location="${ANDROID_SDK_ROOT}"/>
+ <property-value name="target" value="android-${ANDROID_SDK_VERSION}"/>
+ <property name="source.dir" location="src"/>
+ <property-location name="android.gdbserver" location="${ANDROID_GDBSERVER}"/>
+ <!--
+ Common directories used by SDK Build, when making changes here
+ make sure to update gyp files and test scripts constants in
+ build/android/pylib/constants.py
+ -->
+ <!-- Common directory for chromium_*.jars. -->
+ <property-location name="lib.java.dir" location="${PRODUCT_DIR}/lib.java"/>
+ <!-- Common directory for test jars. -->
+ <property-location name="test.lib.java.dir"
+ location="${PRODUCT_DIR}/test.lib.java"/>
+ <!-- Common directory for apks. -->
+ <property-location name="apks.dir" location="${PRODUCT_DIR}/apks"/>
+</project>
diff --git a/src/build/android/ant/create-test-jar.js b/src/build/android/ant/create-test-jar.js
new file mode 100644
index 0000000..d22c003
--- /dev/null
+++ b/src/build/android/ant/create-test-jar.js
@@ -0,0 +1,70 @@
+// 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.
+
+/**
+ * Combines classes from javac.custom.classpath property and ${out.dir}/classes
+ * into a single jar file ${ant.project.name}.jar and places the file in
+ * ${lib.java.dir}.
+ */
+
+importClass(java.io.File);
+importClass(org.apache.tools.ant.types.Reference);
+importClass(org.apache.tools.ant.types.FileSet);
+importClass(org.apache.tools.ant.types.ZipFileSet);
+importClass(org.apache.tools.ant.taskdefs.Zip);
+
+var echo = project.createTask("echo");
+var jarTask = project.createTask("jar");
+
+// Do not allow duplicates in the jar, the default behavior of Jar task
+// is "add" which means duplicates are allowed.
+// This can cause a class file to be included multiple times, setting the
+// duplicate to "preserve" ensures that only the first definition is included.
+
+var duplicate = Zip.Duplicate();
+duplicate.setValue("preserve");
+jarTask.setDuplicate(duplicate);
+
+var destFile = project.getProperty("ant.project.name") + ".jar";
+var destPath = File(project.getProperty("test.lib.java.dir") + "/" + destFile);
+jarTask.setDestFile(destPath);
+
+// Include all the jars in the classpath.
+var javacCustomClasspath =
+ project.getReference("javac.custom.classpath").list();
+
+for (var i in javacCustomClasspath) {
+ var fileName = javacCustomClasspath[i]
+ var fileExtension = fileName.split("\\.").pop();
+ if(fileExtension == "jar")
+ {
+ var zipFileSet = ZipFileSet();
+ zipFileSet.setIncludes("**/*.class");
+ zipFileSet.setSrc(File(fileName));
+ jarTask.addFileset(zipFileSet);
+ }
+}
+
+// Add the compiled classes in ${out.dir}/classes.
+var projectClasses = FileSet();
+projectClasses.setIncludes("**/*.class");
+projectClasses.setDir(File(project.getProperty("out.dir") + "/classes"));
+jarTask.addFileset(projectClasses);
+
+// Exclude manifest and resource classes.
+var appPackagePath =
+ (project.getProperty("project.app.package")).replace('.','/');
+var excludedClasses = ["R.class", "R$*.class", "Manifest.class",
+ "Manifest$*.class", "BuildConfig.class"]
+
+var exclusionString = "";
+for (var i in excludedClasses) {
+ exclusionString += appPackagePath+ "/" + excludedClasses[i] + " ";
+}
+
+jarTask.setExcludes(exclusionString);
+echo.setMessage("Creating test jar: " +
+ jarTask.getDestFile().getAbsolutePath());
+echo.perform();
+jarTask.perform();
diff --git a/src/build/android/ant/empty/res/.keep b/src/build/android/ant/empty/res/.keep
new file mode 100644
index 0000000..1fd038b
--- /dev/null
+++ b/src/build/android/ant/empty/res/.keep
@@ -0,0 +1,2 @@
+# This empty res folder can be passed to aapt while building Java libraries or
+# APKs that don't have any resources.
diff --git a/src/build/android/ant/sdk-targets.xml b/src/build/android/ant/sdk-targets.xml
new file mode 100644
index 0000000..91a0220
--- /dev/null
+++ b/src/build/android/ant/sdk-targets.xml
@@ -0,0 +1,289 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project name="chrome_sdk_overrides" >
+ <!--
+ Redefinition of targets used by SDK tools.
+ Supported version: SDK tools revision 20.
+
+ SDK tools do not allow easy way of extending classpaths
+ for aidl and javac. This file defines targets which can be used to
+ override targets used by tools.
+ -->
+ <target name="-pre-compile">
+ <!--
+ Remove all .class files from the output directory. This prevents inclusion of incorrect .class
+ files in the final apk. For example, if a .java file was deleted, the apk should not contain
+ the .class files for that .java from previous builds.
+ -->
+ <delete>
+ <fileset dir="${out.classes.absolute.dir}" includes="**/*.class"/>
+ </delete>
+ </target>
+
+ <!--
+ Override the -compile target.
+ This target requires 'javac.custom.classpath' to be set to reference
+ of classpath to be used for javac. Also accepts custom path for
+ sources: 'javac.custom.sourcepath'.
+ -->
+ <target
+ name="-compile"
+ depends="-build-setup, -pre-build, -code-gen, -pre-compile">
+ <do-only-if-manifest-hasCode elseText="hasCode = false. Skipping..." >
+ <!-- If javac.srcdirs.additional isn't set, set it to an empty path. -->
+ <if>
+ <condition>
+ <not>
+ <isreference refid="javac.srcdirs.additional"/>
+ </not>
+ </condition>
+ <then>
+ <path id="javac.srcdirs.additional"/>
+ </then>
+ </if>
+ <javac
+ bootclasspathref="project.target.class.path"
+ classpathref="javac.custom.classpath"
+ debug="true"
+ destdir="${out.classes.absolute.dir}"
+ encoding="${java.encoding}"
+ extdirs=""
+ fork="${need.javac.fork}"
+ includeantruntime="false"
+ source="${java.source}"
+ target="${java.target}"
+ verbose="${verbose}">
+ <src path="${source.absolute.dir}"/>
+ <src path="${gen.absolute.dir}"/>
+ <src>
+ <path refid="javac.srcdirs.additional"/>
+ </src>
+ <compilerarg value="-Xlint:unchecked"/>
+ <compilerarg value="-Xlint:deprecation"/>
+ <compilerarg line="${java.compilerargs}"/>
+ </javac>
+ <!--
+ If the project is instrumented, then instrument the classes
+ TODO(shashishekhar): Add option to override emma filter.
+ -->
+ <if condition="${build.is.instrumented}">
+ <then>
+ <echo level="info">
+ Instrumenting classes from ${out.absolute.dir}/classes...
+ </echo>
+ <!-- build the default filter to remove R, Manifest, BuildConfig -->
+ <getemmafilter
+ appPackage="${project.app.package}"
+ filterOut="emma.default.filter"
+ libraryPackagesRefId="project.library.packages"/>
+ <!--
+ Define where the .em file is output.
+ This may have been setup already if this is a library.
+ -->
+ <property name="emma.coverage.absolute.file"
+ location="${out.absolute.dir}/coverage.em"/>
+ <!-- It only instruments class files, not any external libs -->
+
+ <emma enabled="true">
+ <instr
+ instrpath="${out.absolute.dir}/classes"
+ metadatafile="${emma.coverage.absolute.file}"
+ mode="overwrite"
+ outdir="${out.absolute.dir}/classes"
+ verbosity="${verbosity}">
+ <filter excludes="${emma.default.filter}"/>
+ <filter value="${emma.filter}"/>
+ </instr>
+ </emma>
+ </then>
+ </if>
+ <!--
+ If the project needs a test jar then generate a jar containing
+ all compiled classes and referenced jars.
+ project.is.testapp is set by Android's ant build system based on the
+ target's manifest. It is true only for instrumentation apks.
+ -->
+ <if condition="${project.is.testapp}">
+ <then>
+ <echo level="info">Creating test jar file:
+ ${ant.project.name}.jar</echo>
+ <property-location name="create.test.jar.file"
+ location="${CHROMIUM_SRC}/build/android/ant/create-test-jar.js"/>
+ <script language="javascript" src="${create.test.jar.file}"/>
+ </then>
+ </if>
+
+ </do-only-if-manifest-hasCode>
+ </target>
+
+ <!--
+ For debug builds, the Android SDK tools create a key in ~/.android and sign the build with it.
+ This has caused all kinds of issues. Instead, the debug build should be signed with a key in
+ build/android/ant. The SDK tools do not provide any support for overriding that behavior and so
+ instead one must use the hack below.
+ -->
+
+ <!-- Disables automatic signing. -->
+ <property name="build.is.signing.debug" value="false"/>
+
+ <!-- TODO(cjhopman): Remove this property when all gyp files define the CHROMIUM_SRC property. -->
+ <!-- __LB_SHELL__
+ The path needs to be adjusted since LbShell is using a different directory
+ configuration.
+ -->
+ <property name="CHROMIUM_SRC" value="${PRODUCT_DIR}/../../../external/chromium" />
+ <property name="LBSHELL_SRC" value="${PRODUCT_DIR}/../../../lbshell" />
+ <!-- __LB_SHELL__
+ Use our custom key for signing debug packages.
+ -->
+ <property name="key.store" value="${LBSHELL_SRC}/build/lbshell-debug.keystore"/>
+ <property name="key.store.password" value="lbshell"/>
+ <property name="key.alias" value="lbshelldebugkey"/>
+ <property name="key.alias.password" value="lbshell"/>
+
+ <!-- SDK tools assume that out.packaged.file is signed and name it "...-unaligned" -->
+ <property name="out.packaged.file"
+ value="${apks.dir}/${ant.project.name}-unsigned.apk" />
+ <property name="out.unaligned.file"
+ value="${apks.dir}/${ant.project.name}-unaligned.apk" />
+
+ <!-- By default, the SDK tools build only aligns the APK in the -do-debug target. -->
+ <target name="-do-debug"
+ depends="-set-debug-mode, -debug-obfuscation-check, -package, -post-package">
+ <!-- only create apk if *not* a library project -->
+ <do-only-if-not-library elseText="Library project: do not create apk..." >
+ <sequential>
+ <!-- Signs the APK -->
+ <echo level="info">Signing final apk...</echo>
+ <signapk
+ input="${out.packaged.file}"
+ output="${out.unaligned.file}"
+ keystore="${key.store}"
+ storepass="${key.store.password}"
+ alias="${key.alias}"
+ keypass="${key.alias.password}"/>
+
+ <!-- Zip aligns the APK -->
+ <zipalign-helper
+ in.package="${out.unaligned.file}"
+ out.package="${out.final.file}" />
+ <echo level="info">Release Package: ${out.final.file}</echo>
+ </sequential>
+ </do-only-if-not-library>
+ <record-build-info />
+ </target>
+
+ <path id="native.libs.gdbserver">
+ <fileset file="${android.gdbserver}"/>
+ </path>
+
+ <target name="-post-compile">
+ <!--
+ Copy gdbserver to main libs directory if building a non-instrumentation debug apk.
+ -->
+ <if>
+ <condition>
+ <and>
+ <equals arg1="${build.target}" arg2="debug"/>
+ <isfalse value="${project.is.testapp}"/>
+ </and>
+ </condition>
+ <then>
+ <echo message="Copying gdbserver to the apk to enable native debugging"/>
+ <copy todir="${out.dir}/libs/${target.abi}">
+ <path refid="native.libs.gdbserver"/>
+ </copy>
+ </then>
+ </if>
+
+ <!-- Package all the compiled .class files into a .jar. -->
+ <jar
+ jarfile="${lib.java.dir}/${JAR_NAME}"
+ basedir="${out.classes.absolute.dir}"
+ />
+ </target>
+
+ <!--
+ Override obfuscate target to pass javac.custom.classpath to Proguard. SDK tools do not provide
+ any way to pass custom class paths to Proguard.
+ -->
+ <target name="-obfuscate">
+ <if condition="${proguard.enabled}">
+ <then>
+ <property name="obfuscate.absolute.dir" location="${out.absolute.dir}/proguard"/>
+ <property name="preobfuscate.jar.file" value="${obfuscate.absolute.dir}/original.jar"/>
+ <property name="obfuscated.jar.file" value="${obfuscate.absolute.dir}/obfuscated.jar"/>
+ <!-- input for dex will be proguard's output -->
+ <property name="out.dex.input.absolute.dir" value="${obfuscated.jar.file}"/>
+
+ <!-- Add Proguard Tasks -->
+ <property name="proguard.jar" location="${android.tools.dir}/proguard/lib/proguard.jar"/>
+ <taskdef name="proguard" classname="proguard.ant.ProGuardTask" classpath="${proguard.jar}"/>
+
+ <!-- Set the android classpath Path object into a single property. It'll be
+ all the jar files separated by a platform path-separator.
+ Each path must be quoted if it contains spaces.
+ -->
+ <pathconvert property="project.target.classpath.value" refid="project.target.class.path">
+ <firstmatchmapper>
+ <regexpmapper from='^([^ ]*)( .*)$$' to='"\1\2"'/>
+ <identitymapper/>
+ </firstmatchmapper>
+ </pathconvert>
+
+ <!-- Build a path object with all the jar files that must be obfuscated.
+ This include the project compiled source code and any 3rd party jar
+ files. -->
+ <path id="project.all.classes.path">
+ <pathelement location="${preobfuscate.jar.file}"/>
+ <path refid="project.all.jars.path"/>
+ <!-- Pass javac.custom.classpath for apks. -->
+ <path refid="javac.custom.classpath"/>
+ </path>
+ <!-- Set the project jar files Path object into a single property. It'll be
+ all the jar files separated by a platform path-separator.
+ Each path must be quoted if it contains spaces.
+ -->
+ <pathconvert property="project.all.classes.value" refid="project.all.classes.path">
+ <firstmatchmapper>
+ <regexpmapper from='^([^ ]*)( .*)$$' to='"\1\2"'/>
+ <identitymapper/>
+ </firstmatchmapper>
+ </pathconvert>
+
+ <!-- Turn the path property ${proguard.config} from an A:B:C property
+ into a series of includes: -include A -include B -include C
+ suitable for processing by the ProGuard task. Note - this does
+ not include the leading '-include "' or the closing '"'; those
+ are added under the <proguard> call below.
+ -->
+ <path id="proguard.configpath">
+ <pathelement path="${proguard.config}"/>
+ </path>
+ <pathconvert pathsep='" -include "' property="proguard.configcmd"
+ refid="proguard.configpath"/>
+
+ <mkdir dir="${obfuscate.absolute.dir}"/>
+ <delete file="${preobfuscate.jar.file}"/>
+ <delete file="${obfuscated.jar.file}"/>
+ <jar basedir="${out.classes.absolute.dir}"
+ destfile="${preobfuscate.jar.file}"/>
+ <proguard>
+ -include "${proguard.configcmd}"
+ -include "${out.absolute.dir}/proguard.txt"
+ -injars ${project.all.classes.value}
+ -outjars "${obfuscated.jar.file}"
+ -libraryjars ${project.target.classpath.value}
+ -dump "${obfuscate.absolute.dir}/dump.txt"
+ -printseeds "${obfuscate.absolute.dir}/seeds.txt"
+ -printusage "${obfuscate.absolute.dir}/usage.txt"
+ -printmapping "${obfuscate.absolute.dir}/mapping.txt"
+ </proguard>
+ </then>
+ </if>
+ </target>
+</project>
diff --git a/src/build/android/arm-linux-androideabi-gold/arm-linux-androideabi-ld b/src/build/android/arm-linux-androideabi-gold/arm-linux-androideabi-ld
new file mode 120000
index 0000000..5b178e9
--- /dev/null
+++ b/src/build/android/arm-linux-androideabi-gold/arm-linux-androideabi-ld
@@ -0,0 +1 @@
+../../../third_party/android_tools/ndk/toolchains/arm-linux-androideabi-4.6/prebuilt/linux-x86_64/bin/arm-linux-androideabi-ld.gold
\ No newline at end of file
diff --git a/src/build/android/arm-linux-androideabi-gold/ld b/src/build/android/arm-linux-androideabi-gold/ld
new file mode 120000
index 0000000..2366dda
--- /dev/null
+++ b/src/build/android/arm-linux-androideabi-gold/ld
@@ -0,0 +1 @@
+../../../third_party/android_tools/ndk/toolchains/arm-linux-androideabi-4.6/prebuilt/linux-x86_64/arm-linux-androideabi/bin/ld.gold
\ No newline at end of file
diff --git a/src/build/android/bb_run_sharded_steps.py b/src/build/android/bb_run_sharded_steps.py
new file mode 100755
index 0000000..9010d77
--- /dev/null
+++ b/src/build/android/bb_run_sharded_steps.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""Helper script to shard build bot steps and save results to disk.
+
+Our buildbot infrastructure requires each slave to run steps serially.
+This is sub-optimal for android, where these steps can run independently on
+multiple connected devices.
+
+The buildbots will run this script multiple times per cycle:
+- First, without params: all steps will be executed in parallel using all
+connected devices. Step results will be pickled to disk (each step has a unique
+name).
+The buildbot will treat this step as a regular step, and will not process any
+graph data.
+
+- Then, with -p STEP_NAME: at this stage, we'll simply print the file with the
+step results previously saved. The buildbot will then process the graph data
+accordingly.
+
+The JSON config contains is a file containing a dictionary in the format:
+{
+ 'step_name_foo': 'script_to_execute foo',
+ 'step_name_bar': 'script_to_execute bar'
+}
+
+Note that script_to_execute necessarily have to take at least the following
+options:
+ --device: the serial number to be passed to all adb commands.
+ --keep_test_server_ports: indicates it's being run as a shard, and shouldn't
+ reset test server port allocation.
+"""
+
+
+import datetime
+import json
+import logging
+import multiprocessing
+import optparse
+import pexpect
+import pickle
+import os
+import signal
+import shutil
+import sys
+
+from pylib import android_commands
+from pylib import cmd_helper
+from pylib import constants
+from pylib import ports
+
+
+_OUTPUT_DIR = os.path.join(constants.CHROME_DIR, 'out', 'step_results')
+
+
+def _SaveResult(result):
+ with file(os.path.join(_OUTPUT_DIR, result['name']), 'w') as f:
+ f.write(pickle.dumps(result))
+
+
+def _RunStepsPerDevice(steps):
+ results = []
+ for step in steps:
+ start_time = datetime.datetime.now()
+ print 'Starting %s: %s %s at %s' % (step['name'], step['cmd'],
+ start_time, step['device'])
+ output, exit_code = pexpect.run(
+ step['cmd'], cwd=os.path.abspath(constants.CHROME_DIR),
+ withexitstatus=True, logfile=sys.stdout, timeout=1800,
+ env=os.environ)
+ end_time = datetime.datetime.now()
+ print 'Finished %s: %s %s at %s' % (step['name'], step['cmd'],
+ end_time, step['device'])
+ result = {'name': step['name'],
+ 'output': output,
+ 'exit_code': exit_code or 0,
+ 'total_time': (end_time - start_time).seconds,
+ 'device': step['device']}
+ _SaveResult(result)
+ results += [result]
+ return results
+
+
+def _RunShardedSteps(steps, devices):
+ assert steps
+ assert devices, 'No devices connected?'
+ if os.path.exists(_OUTPUT_DIR):
+ assert '/step_results' in _OUTPUT_DIR
+ shutil.rmtree(_OUTPUT_DIR)
+ if not os.path.exists(_OUTPUT_DIR):
+ os.makedirs(_OUTPUT_DIR)
+ step_names = sorted(steps.keys())
+ all_params = []
+ num_devices = len(devices)
+ shard_size = (len(steps) + num_devices - 1) / num_devices
+ for i, device in enumerate(devices):
+ steps_per_device = []
+ for s in steps.keys()[i * shard_size:(i + 1) * shard_size]:
+ steps_per_device += [{'name': s,
+ 'device': device,
+ 'cmd': steps[s] + ' --device ' + device +
+ ' --keep_test_server_ports'}]
+ all_params += [steps_per_device]
+ print 'Start sharding (note: output is not synchronized...)'
+ print '*' * 80
+ start_time = datetime.datetime.now()
+ pool = multiprocessing.Pool(processes=num_devices)
+ async_results = pool.map_async(_RunStepsPerDevice, all_params)
+ results_per_device = async_results.get(999999)
+ end_time = datetime.datetime.now()
+ print '*' * 80
+ print 'Finished sharding.'
+ print 'Summary'
+ total_time = 0
+ for results in results_per_device:
+ for result in results:
+ print('%s : exit_code=%d in %d secs at %s' %
+ (result['name'], result['exit_code'], result['total_time'],
+ result['device']))
+ total_time += result['total_time']
+ print 'Step time: %d secs' % ((end_time - start_time).seconds)
+ print 'Bots time: %d secs' % total_time
+ # No exit_code for the sharding step: the individual _PrintResults step
+ # will return the corresponding exit_code.
+ return 0
+
+
+def _PrintStepOutput(step_name):
+ file_name = os.path.join(_OUTPUT_DIR, step_name)
+ if not os.path.exists(file_name):
+ print 'File not found ', file_name
+ return 1
+ with file(file_name, 'r') as f:
+ result = pickle.loads(f.read())
+ print result['output']
+ return result['exit_code']
+
+
+def _KillPendingServers():
+ for retry in range(5):
+ for server in ['lighttpd', 'web-page-replay']:
+ pids = cmd_helper.GetCmdOutput(['pgrep', '-f', server])
+ pids = [pid.strip() for pid in pids.split('\n') if pid.strip()]
+ for pid in pids:
+ try:
+ logging.warning('Killing %s %s', server, pid)
+ os.kill(int(pid), signal.SIGQUIT)
+ except Exception as e:
+ logging.warning('Failed killing %s %s %s', server, pid, e)
+
+
+def main(argv):
+ parser = optparse.OptionParser()
+ parser.add_option('-s', '--steps',
+ help='A JSON file containing all the steps to be '
+ 'sharded.')
+ parser.add_option('-p', '--print_results',
+ help='Only prints the results for the previously '
+ 'executed step, do not run it again.')
+ options, urls = parser.parse_args(argv)
+ if options.print_results:
+ return _PrintStepOutput(options.print_results)
+
+ # At this point, we should kill everything that may have been left over from
+ # previous runs.
+ _KillPendingServers()
+
+ # Reset the test port allocation. It's important to do it before starting
+ # to dispatch any step.
+ if not ports.ResetTestServerPortAllocation():
+ raise Exception('Failed to reset test server port.')
+
+ # Sort the devices so that we'll try to always run a step in the same device.
+ devices = sorted(android_commands.GetAttachedDevices())
+ if not devices:
+ print 'You must attach a device'
+ return 1
+
+ with file(options.steps, 'r') as f:
+ steps = json.load(f)
+ return _RunShardedSteps(steps, devices)
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/src/build/android/buildbot/bb_asan_builder.sh b/src/build/android/buildbot/bb_asan_builder.sh
new file mode 100755
index 0000000..57679e0
--- /dev/null
+++ b/src/build/android/buildbot/bb_asan_builder.sh
@@ -0,0 +1,15 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for the fyi waterfall and fyi trybots.
+# Compile and zip the build.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_compile
+bb_zip_build
diff --git a/src/build/android/buildbot/bb_asan_tests.sh b/src/build/android/buildbot/bb_asan_tests.sh
new file mode 100755
index 0000000..122fe40
--- /dev/null
+++ b/src/build/android/buildbot/bb_asan_tests.sh
@@ -0,0 +1,19 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for the fyi waterfall and fyi trybots.
+# Downloads and extracts a build from the builder and runs tests.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_spawn_logcat_monitor_and_status
+bb_extract_build
+bb_reboot_phones
+bb_run_unit_tests
+bb_run_instrumentation_tests
+bb_print_logcat
diff --git a/src/build/android/buildbot/bb_clang_builder.sh b/src/build/android/buildbot/bb_clang_builder.sh
new file mode 100755
index 0000000..3bf88ae
--- /dev/null
+++ b/src/build/android/buildbot/bb_clang_builder.sh
@@ -0,0 +1,13 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for clang.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_compile
diff --git a/src/build/android/buildbot/bb_fyi_builder.sh b/src/build/android/buildbot/bb_fyi_builder.sh
new file mode 100755
index 0000000..d76598b
--- /dev/null
+++ b/src/build/android/buildbot/bb_fyi_builder.sh
@@ -0,0 +1,18 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for the fyi waterfall and fyi trybots.
+# Compile and zip the build.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_check_webview_licenses
+bb_compile
+bb_compile_experimental
+bb_run_findbugs
+bb_zip_build
diff --git a/src/build/android/buildbot/bb_fyi_tester.sh b/src/build/android/buildbot/bb_fyi_tester.sh
new file mode 100755
index 0000000..e50a32b
--- /dev/null
+++ b/src/build/android/buildbot/bb_fyi_tester.sh
@@ -0,0 +1,21 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for the fyi waterfall and fyi trybots.
+# Downloads and extracts a build from the builder and runs tests.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_spawn_logcat_monitor_and_status
+bb_extract_build
+bb_reboot_phones
+bb_run_unit_tests
+bb_run_instrumentation_tests
+bb_run_experimental_unit_tests
+bb_run_experimental_instrumentation_tests
+bb_print_logcat
diff --git a/src/build/android/buildbot/bb_main_builder.sh b/src/build/android/buildbot/bb_main_builder.sh
new file mode 100755
index 0000000..13df0c4
--- /dev/null
+++ b/src/build/android/buildbot/bb_main_builder.sh
@@ -0,0 +1,20 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for the main waterfall. Compile only.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+# SHERIFF: if you need to quickly turn the main waterfall android bots
+# green (preventing tree closures), uncomment the next line (and send
+# appropriate email out):
+## bb_force_bot_green_and_exit
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_compile
+bb_run_findbugs
+bb_zip_build
diff --git a/src/build/android/buildbot/bb_main_clobber.sh b/src/build/android/buildbot/bb_main_clobber.sh
new file mode 100755
index 0000000..8c3df90
--- /dev/null
+++ b/src/build/android/buildbot/bb_main_clobber.sh
@@ -0,0 +1,19 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for the main waterfall. Compile only.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+# SHERIFF: if you need to quickly turn the main waterfall android bots
+# green (preventing tree closures), uncomment the next line (and send
+# appropriate email out):
+## bb_force_bot_green_and_exit
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_compile
+
diff --git a/src/build/android/buildbot/bb_main_tester.sh b/src/build/android/buildbot/bb_main_tester.sh
new file mode 100755
index 0000000..287d281
--- /dev/null
+++ b/src/build/android/buildbot/bb_main_tester.sh
@@ -0,0 +1,23 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for the main waterfall. Tester only.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+# SHERIFF: if you need to quickly turn the main waterfall android bots
+# green (preventing tree closures), uncomment the next line (and send
+# appropriate email out):
+## bb_force_bot_green_and_exit
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_spawn_logcat_monitor_and_status
+bb_extract_build
+bb_reboot_phones
+bb_run_unit_tests
+bb_run_instrumentation_tests
+bb_print_logcat
diff --git a/src/build/android/buildbot/bb_perf_builder.sh b/src/build/android/buildbot/bb_perf_builder.sh
new file mode 100755
index 0000000..0907362
--- /dev/null
+++ b/src/build/android/buildbot/bb_perf_builder.sh
@@ -0,0 +1,14 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for chromium.perf.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_compile
+bb_zip_build
diff --git a/src/build/android/buildbot/bb_perf_gn_tests.sh b/src/build/android/buildbot/bb_perf_gn_tests.sh
new file mode 100755
index 0000000..ba36b29
--- /dev/null
+++ b/src/build/android/buildbot/bb_perf_gn_tests.sh
@@ -0,0 +1,17 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for chromium.perf.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_spawn_logcat_monitor_and_status
+bb_extract_build
+bb_reboot_phones
+bb_install_apk "ContentShell.apk" "org.chromium.content_shell"
+bb_print_logcat
diff --git a/src/build/android/buildbot/bb_try_builder.sh b/src/build/android/buildbot/bb_try_builder.sh
new file mode 100755
index 0000000..c26b546
--- /dev/null
+++ b/src/build/android/buildbot/bb_try_builder.sh
@@ -0,0 +1,20 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for trybots. Compile only.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+# SHERIFF: if you need to quickly turn "android_dbg" trybots green,
+# uncomment the next line (and send appropriate email out):
+## bb_force_bot_green_and_exit
+# You will also need to change buildbot_try_tester.sh
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_compile
+bb_run_findbugs
+bb_zip_build
diff --git a/src/build/android/buildbot/bb_try_clang_builder.sh b/src/build/android/buildbot/bb_try_clang_builder.sh
new file mode 100755
index 0000000..75ff9f4
--- /dev/null
+++ b/src/build/android/buildbot/bb_try_clang_builder.sh
@@ -0,0 +1,13 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for trybots. Compile only.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_compile
diff --git a/src/build/android/buildbot/bb_try_fyi_builder.sh b/src/build/android/buildbot/bb_try_fyi_builder.sh
new file mode 100755
index 0000000..3041ccc
--- /dev/null
+++ b/src/build/android/buildbot/bb_try_fyi_builder.sh
@@ -0,0 +1,7 @@
+#!/bin/bash -ex
+# 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.
+
+# Buildbot annotator entry for trybot mirroring fyi builder
+exec "$(dirname $0)/bb_fyi_builder.sh" "$@"
diff --git a/src/build/android/buildbot/bb_try_fyi_tester.sh b/src/build/android/buildbot/bb_try_fyi_tester.sh
new file mode 100755
index 0000000..f3ea8e0
--- /dev/null
+++ b/src/build/android/buildbot/bb_try_fyi_tester.sh
@@ -0,0 +1,7 @@
+#!/bin/bash -ex
+# 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.
+
+# Buildbot annotator entry for trybot mirroring fyi tester
+exec "$(dirname $0)/bb_fyi_tester.sh" "$@"
diff --git a/src/build/android/buildbot/bb_try_tester.sh b/src/build/android/buildbot/bb_try_tester.sh
new file mode 100755
index 0000000..bfbff27
--- /dev/null
+++ b/src/build/android/buildbot/bb_try_tester.sh
@@ -0,0 +1,22 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for tester half of android trybots
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+# SHERIFF: if you need to quickly turn "android" trybots green,
+# uncomment the next line (and send appropriate email out):
+## bb_force_bot_green_and_exit
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_spawn_logcat_monitor_and_status
+bb_extract_build
+bb_reboot_phones
+bb_run_unit_tests
+bb_run_instrumentation_tests
+bb_print_logcat
diff --git a/src/build/android/buildbot/bb_webkit_latest_builder.sh b/src/build/android/buildbot/bb_webkit_latest_builder.sh
new file mode 100755
index 0000000..982f857
--- /dev/null
+++ b/src/build/android/buildbot/bb_webkit_latest_builder.sh
@@ -0,0 +1,19 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for the WebKit builder on the Canary waterfall.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+# SHERIFF: if you need to quickly turn the main waterfall android bots
+# green (preventing tree closures), uncomment the next line (and send
+# appropriate email out):
+## bb_force_bot_green_and_exit
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_compile
+bb_zip_build
diff --git a/src/build/android/buildbot/bb_webkit_latest_tester.sh b/src/build/android/buildbot/bb_webkit_latest_tester.sh
new file mode 100755
index 0000000..581fbea
--- /dev/null
+++ b/src/build/android/buildbot/bb_webkit_latest_tester.sh
@@ -0,0 +1,22 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for the WebKit tester on the Canary waterfall.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+# SHERIFF: if you need to quickly turn the main waterfall android bots
+# green (preventing tree closures), uncomment the next line (and send
+# appropriate email out):
+## bb_force_bot_green_and_exit
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_spawn_logcat_monitor_and_status
+bb_extract_build
+bb_reboot_phones
+bb_run_unit_tests
+bb_print_logcat
diff --git a/src/build/android/buildbot/bb_webkit_latest_webkit_tester.sh b/src/build/android/buildbot/bb_webkit_latest_webkit_tester.sh
new file mode 100755
index 0000000..a49509f
--- /dev/null
+++ b/src/build/android/buildbot/bb_webkit_latest_webkit_tester.sh
@@ -0,0 +1,20 @@
+#!/bin/bash -ex
+# 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.
+#
+# Buildbot annotator script for the WebKit latest WebKit tester on the
+# WebKit canary waterfall.
+
+BB_DIR="$(dirname $0)"
+BB_SRC_ROOT="$(cd "$BB_DIR/../../.."; pwd)"
+. "$BB_DIR/buildbot_functions.sh"
+
+bb_baseline_setup "$BB_SRC_ROOT" "$@"
+bb_spawn_logcat_monitor_and_status
+bb_extract_build
+bb_reboot_phones
+bb_run_webkit_unit_tests
+bb_lint_webkit_expectation_files
+bb_run_webkit_layout_tests
+bb_print_logcat
diff --git a/src/build/android/buildbot/buildbot_functions.sh b/src/build/android/buildbot/buildbot_functions.sh
new file mode 100755
index 0000000..8efeb15
--- /dev/null
+++ b/src/build/android/buildbot/buildbot_functions.sh
@@ -0,0 +1,427 @@
+#!/bin/bash
+# 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.
+#
+# Bash functions used by buildbot annotator scripts for the android
+# build of chromium. Executing this script should not perform actions
+# other than setting variables and defining of functions.
+
+# Number of jobs on the compile line; e.g. make -j"${JOBS}"
+JOBS="${JOBS:-4}"
+
+# Parse named arguments passed into the annotator script
+# and assign them global variable names.
+function bb_parse_args {
+ while [[ $1 ]]; do
+ case "$1" in
+ --factory-properties=*)
+ FACTORY_PROPERTIES="$(echo "$1" | sed 's/^[^=]*=//')"
+ BUILDTYPE=$(bb_get_json_prop "$FACTORY_PROPERTIES" target)
+ ;;
+ --build-properties=*)
+ BUILD_PROPERTIES="$(echo "$1" | sed 's/^[^=]*=//')"
+ ;;
+ *)
+ echo "@@@STEP_WARNINGS@@@"
+ echo "Warning, unparsed input argument: '$1'"
+ ;;
+ esac
+ shift
+ done
+}
+
+# Function to force-green a bot.
+function bb_force_bot_green_and_exit {
+ echo "@@@BUILD_STEP Bot forced green.@@@"
+ exit 0
+}
+
+# Basic setup for all bots to run after a source tree checkout.
+# Args:
+# $1: source root.
+# $2 and beyond: key value pairs which are parsed by bb_parse_args.
+function bb_baseline_setup {
+ SRC_ROOT="$1"
+ # Remove SRC_ROOT param
+ shift
+ cd $SRC_ROOT
+
+ echo "@@@BUILD_STEP Environment setup@@@"
+ bb_parse_args "$@"
+
+ local BUILDTOOL=$(bb_get_json_prop "$FACTORY_PROPERTIES" buildtool)
+ if [[ $BUILDTOOL = ninja ]]; then
+ export GYP_GENERATORS=ninja
+ fi
+ export GOMA_DIR=/b/build/goma
+ . build/android/envsetup.sh
+
+ local extra_gyp_defines="$(bb_get_json_prop "$FACTORY_PROPERTIES" \
+ extra_gyp_defines)"
+ export GYP_DEFINES+=" fastbuild=1 $extra_gyp_defines"
+ if echo $extra_gyp_defines | grep -qE 'clang|asan'; then
+ unset CXX_target
+ fi
+
+ adb kill-server
+ adb start-server
+
+ local build_path="${SRC_ROOT}/out/${BUILDTYPE}"
+ local landmines_triggered_path="$build_path/.landmines_triggered"
+ python "$SRC_ROOT/build/landmines.py"
+
+ if [[ $BUILDBOT_CLOBBER || -f "$landmines_triggered_path" ]]; then
+ echo "@@@BUILD_STEP Clobber@@@"
+
+ if [[ -z $BUILDBOT_CLOBBER ]]; then
+ echo "Clobbering due to triggered landmines: "
+ cat "$landmines_triggered_path"
+ else
+ # Also remove all the files under out/ on an explicit clobber
+ find "${SRC_ROOT}/out" -maxdepth 1 -type f -exec rm -f {} +
+ fi
+
+ # Sdk key expires, delete android folder.
+ # crbug.com/145860
+ rm -rf ~/.android
+ rm -rf "$build_path"
+ if [[ -e $build_path ]] ; then
+ echo "Clobber appeared to fail? $build_path still exists."
+ echo "@@@STEP_WARNINGS@@@"
+ fi
+ fi
+}
+
+function bb_compile_setup {
+ bb_setup_goma_internal
+ # Should be called only after envsetup is done.
+ gclient runhooks
+}
+
+# Setup goma. Used internally to buildbot_functions.sh.
+function bb_setup_goma_internal {
+ export GOMA_API_KEY_FILE=${GOMA_DIR}/goma.key
+ export GOMA_COMPILER_PROXY_DAEMON_MODE=true
+ export GOMA_COMPILER_PROXY_RPC_TIMEOUT_SECS=300
+
+ echo "Killing old goma processes"
+ ${GOMA_DIR}/goma_ctl.sh stop || true
+ killall -9 compiler_proxy || true
+
+ echo "Starting goma"
+ ${GOMA_DIR}/goma_ctl.sh start
+ trap bb_stop_goma_internal SIGHUP SIGINT SIGTERM
+}
+
+# Stop goma.
+function bb_stop_goma_internal {
+ echo "Stopping goma"
+ ${GOMA_DIR}/goma_ctl.sh stop
+}
+
+# $@: make args.
+# Use goma if possible; degrades to non-Goma if needed.
+function bb_goma_make {
+ if [ "${GOMA_DIR}" = "" ]; then
+ make -j${JOBS} "$@"
+ return
+ fi
+
+ HOST_CC=$GOMA_DIR/gcc
+ HOST_CXX=$GOMA_DIR/g++
+ TARGET_CC=$(/bin/ls $ANDROID_TOOLCHAIN/*-gcc | head -n1)
+ TARGET_CXX=$(/bin/ls $ANDROID_TOOLCHAIN/*-g++ | head -n1)
+ TARGET_CC="$GOMA_DIR/gomacc $TARGET_CC"
+ TARGET_CXX="$GOMA_DIR/gomacc $TARGET_CXX"
+ COMMON_JAVAC="$GOMA_DIR/gomacc /usr/bin/javac -J-Xmx512M \
+ -target 1.5 -Xmaxerrs 9999999"
+
+ command make \
+ -j100 \
+ -l20 \
+ HOST_CC="$HOST_CC" \
+ HOST_CXX="$HOST_CXX" \
+ TARGET_CC="$TARGET_CC" \
+ TARGET_CXX="$TARGET_CXX" \
+ CC.host="$HOST_CC" \
+ CXX.host="$HOST_CXX" \
+ CC.target="$TARGET_CC" \
+ CXX.target="$TARGET_CXX" \
+ LINK.target="$TARGET_CXX" \
+ COMMON_JAVAC="$COMMON_JAVAC" \
+ BUILDTYPE="$BUILDTYPE" \
+ "$@"
+
+ local make_exit_code=$?
+ return $make_exit_code
+}
+
+# Build using ninja.
+function bb_goma_ninja {
+ echo "Using ninja to build."
+ local TARGET=$1
+ ninja -C out/$BUILDTYPE -j120 -l20 $TARGET
+}
+
+# Compile step
+function bb_compile {
+ # This must be named 'compile', not 'Compile', for CQ interaction.
+ # Talk to maruel for details.
+ echo "@@@BUILD_STEP compile@@@"
+ bb_compile_setup
+
+ BUILDTOOL=$(bb_get_json_prop "$FACTORY_PROPERTIES" buildtool)
+ if [[ $BUILDTOOL = ninja ]]; then
+ bb_goma_ninja All
+ else
+ bb_goma_make
+ fi
+
+ bb_stop_goma_internal
+}
+
+# Experimental compile step; does not turn the tree red if it fails.
+function bb_compile_experimental {
+ # Linking DumpRenderTree appears to hang forever?
+ EXPERIMENTAL_TARGETS="android_experimental"
+ for target in ${EXPERIMENTAL_TARGETS} ; do
+ echo "@@@BUILD_STEP Experimental Compile $target @@@"
+ set +e
+ if [[ $BUILDTOOL = ninja ]]; then
+ bb_goma_ninja "${target}"
+ else
+ bb_goma_make -k "${target}"
+ fi
+ if [ $? -ne 0 ] ; then
+ echo "@@@STEP_WARNINGS@@@"
+ fi
+ set -e
+ done
+}
+
+# Run tests on an emulator.
+function bb_run_tests_emulator {
+ echo "@@@BUILD_STEP Run Tests on an Emulator@@@"
+ build/android/run_tests.py -e --xvfb --verbose
+}
+
+function bb_spawn_logcat_monitor_and_status {
+ python build/android/device_status_check.py
+ LOGCAT_DUMP_DIR="$CHROME_SRC/out/logcat"
+ rm -rf "$LOGCAT_DUMP_DIR"
+ python build/android/adb_logcat_monitor.py "$LOGCAT_DUMP_DIR" &
+}
+
+function bb_print_logcat {
+ echo "@@@BUILD_STEP Logcat dump@@@"
+ python build/android/adb_logcat_printer.py "$LOGCAT_DUMP_DIR"
+}
+
+# Run tests on an actual device. (Better have one plugged in!)
+function bb_run_unit_tests {
+ build/android/run_tests.py --xvfb --verbose
+}
+
+# Run WebKit's test suites: webkit_unit_tests and TestWebKitAPI
+function bb_run_webkit_unit_tests {
+ if [[ $BUILDTYPE = Release ]]; then
+ local BUILDFLAG="--release"
+ fi
+ bb_run_step build/android/run_tests.py --xvfb --verbose $BUILDFLAG \
+ -s webkit_unit_tests
+ bb_run_step build/android/run_tests.py --xvfb --verbose $BUILDFLAG \
+ -s TestWebKitAPI
+}
+
+# Lint WebKit's TestExpectation files.
+function bb_lint_webkit_expectation_files {
+ echo "@@@BUILD_STEP webkit_lint@@@"
+ bb_run_step python webkit/tools/layout_tests/run_webkit_tests.py \
+ --lint-test-files \
+ --chromium
+}
+
+# Run layout tests on an actual device.
+function bb_run_webkit_layout_tests {
+ echo "@@@BUILD_STEP webkit_tests@@@"
+ local BUILDERNAME="$(bb_get_json_prop "$BUILD_PROPERTIES" buildername)"
+ local BUILDNUMBER="$(bb_get_json_prop "$BUILD_PROPERTIES" buildnumber)"
+ local MASTERNAME="$(bb_get_json_prop "$BUILD_PROPERTIES" mastername)"
+ local RESULTSERVER=\
+"$(bb_get_json_prop "$FACTORY_PROPERTIES" test_results_server)"
+
+ bb_run_step python webkit/tools/layout_tests/run_webkit_tests.py \
+ --no-show-results \
+ --no-new-test-results \
+ --full-results-html \
+ --clobber-old-results \
+ --exit-after-n-failures 5000 \
+ --exit-after-n-crashes-or-timeouts 100 \
+ --debug-rwt-logging \
+ --results-directory "../layout-test-results" \
+ --target "$BUILDTYPE" \
+ --builder-name "$BUILDERNAME" \
+ --build-number "$BUILDNUMBER" \
+ --master-name "$MASTERNAME" \
+ --build-name "$BUILDERNAME" \
+ --platform=chromium-android \
+ --test-results-server "$RESULTSERVER"
+}
+
+# Run experimental unittest bundles.
+function bb_run_experimental_unit_tests {
+ build/android/run_tests.py --xvfb --verbose -s android_webview_unittests
+}
+
+# Run findbugs.
+function bb_run_findbugs {
+ echo "@@@BUILD_STEP findbugs@@@"
+ if [[ $BUILDTYPE = Release ]]; then
+ local BUILDFLAG="--release-build"
+ fi
+ bb_run_step build/android/findbugs_diff.py $BUILDFLAG
+ bb_run_step tools/android/findbugs_plugin/test/run_findbugs_plugin_tests.py \
+ $BUILDFLAG
+}
+
+# Run a buildbot step and handle failure (failure will not halt build).
+function bb_run_step {
+ (
+ set +e
+ "$@"
+ if [[ $? != 0 ]]; then
+ echo "@@@STEP_FAILURE@@@"
+ fi
+ )
+}
+
+# Install a specific APK.
+# Args:
+# $1: APK to be installed.
+# $2: APK_PACKAGE for the APK to be installed.
+function bb_install_apk {
+ local APK=${1}
+ local APK_PACKAGE=${2}
+ if [[ $BUILDTYPE = Release ]]; then
+ local BUILDFLAG="--release"
+ fi
+
+ echo "@@@BUILD_STEP Install ${APK}@@@"
+ python build/android/adb_install_apk.py --apk ${APK} \
+ --apk_package ${APK_PACKAGE} ${BUILDFLAG}
+}
+
+# Run instrumentation tests for a specific APK.
+# Args:
+# $1: APK to be installed.
+# $2: APK_PACKAGE for the APK to be installed.
+# $3: TEST_APK to run the tests against.
+# $4: TEST_DATA in format destination:source
+function bb_run_all_instrumentation_tests_for_apk {
+ local APK=${1}
+ local APK_PACKAGE=${2}
+ local TEST_APK=${3}
+ local TEST_DATA=${4}
+
+ # Install application APK.
+ bb_install_apk ${APK} ${APK_PACKAGE}
+
+ # Run instrumentation tests. Using -I to install the test apk.
+ echo "@@@BUILD_STEP Run instrumentation tests ${TEST_APK}@@@"
+ bb_run_step python build/android/run_instrumentation_tests.py \
+ -vvv --test-apk ${TEST_APK} -I --test_data ${TEST_DATA}
+}
+
+# Run instrumentation tests for all relevant APKs on device.
+function bb_run_instrumentation_tests {
+ bb_run_all_instrumentation_tests_for_apk "ContentShell.apk" \
+ "org.chromium.content_shell" "ContentShellTest" \
+ "content:content/test/data/android/device_files"
+ bb_run_all_instrumentation_tests_for_apk "ChromiumTestShell.apk" \
+ "org.chromium.chrome.testshell" "ChromiumTestShellTest" \
+ "chrome:chrome/test/data/android/device_files"
+ bb_run_all_instrumentation_tests_for_apk "AndroidWebView.apk" \
+ "org.chromium.android_webview" "AndroidWebViewTest" \
+ "webview:android_webview/test/data/device_files"
+}
+
+# Run instrumentation tests for experimental APKs on device.
+function bb_run_experimental_instrumentation_tests {
+ echo "" # Can't have empty functions in bash.
+}
+
+# Zip and archive a build.
+function bb_zip_build {
+ echo "@@@BUILD_STEP Zip build@@@"
+ python ../../../../scripts/slave/zip_build.py \
+ --src-dir "$SRC_ROOT" \
+ --exclude-files "lib.target,gen,android_webview,jingle_unittests" \
+ --factory-properties "$FACTORY_PROPERTIES" \
+ --build-properties "$BUILD_PROPERTIES"
+}
+
+# Download and extract a build.
+function bb_extract_build {
+ echo "@@@BUILD_STEP Download and extract build@@@"
+ if [[ -z $FACTORY_PROPERTIES || -z $BUILD_PROPERTIES ]]; then
+ return 1
+ fi
+
+ # When extract_build.py downloads an unversioned build it
+ # issues a warning by exiting with large numbered return code
+ # When it fails to download it build, it exits with return
+ # code 1. We disable halt on error mode and return normally
+ # unless the python tool returns 1.
+ (
+ set +e
+ python ../../../../scripts/slave/extract_build.py \
+ --build-dir "$SRC_ROOT/build" \
+ --build-output-dir "../out" \
+ --factory-properties "$FACTORY_PROPERTIES" \
+ --build-properties "$BUILD_PROPERTIES"
+ local extract_exit_code=$?
+ if (( $extract_exit_code > 1 )); then
+ echo "@@@STEP_WARNINGS@@@"
+ return
+ fi
+ return $extract_exit_code
+ )
+}
+
+# Reboot all phones and wait for them to start back up
+# Does not break build if a phone fails to restart
+function bb_reboot_phones {
+ echo "@@@BUILD_STEP Rebooting phones@@@"
+ (
+ set +e
+ cd $CHROME_SRC/build/android/pylib;
+ for DEVICE in $(adb_get_devices); do
+ python -c "import android_commands;\
+ android_commands.AndroidCommands(device='$DEVICE').Reboot(True)" &
+ done
+ wait
+ )
+}
+
+# Runs the license checker for the WebView build.
+function bb_check_webview_licenses {
+ echo "@@@BUILD_STEP Check licenses for WebView@@@"
+ (
+ set +e
+ cd "${SRC_ROOT}"
+ python android_webview/tools/webview_licenses.py scan
+ if [[ $? -ne 0 ]]; then
+ echo "@@@STEP_WARNINGS@@@"
+ fi
+ return 0
+ )
+}
+
+# Retrieve a packed json property using python
+function bb_get_json_prop {
+ local JSON="$1"
+ local PROP="$2"
+
+ python -c "import json; print json.loads('$JSON').get('$PROP', '')"
+}
diff --git a/src/build/android/cpufeatures.gypi b/src/build/android/cpufeatures.gypi
new file mode 100644
index 0000000..17b262c
--- /dev/null
+++ b/src/build/android/cpufeatures.gypi
@@ -0,0 +1,20 @@
+# 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.
+
+# Depend on the Android NDK's cpu feature detection. The WebView build is part
+# of the system and the library already exists; for the normal build there is a
+# gyp file in the checked-in NDK to build it.
+{
+ 'conditions': [
+ ['android_build_type != 0', {
+ 'libraries': [
+ 'cpufeatures.a'
+ ],
+ }, {
+ 'dependencies': [
+ '<(android_ndk_root)/android_tools_ndk.gyp:cpu_features',
+ ],
+ }],
+ ],
+}
diff --git a/src/build/android/device_stats_monitor.py b/src/build/android/device_stats_monitor.py
new file mode 100755
index 0000000..181c3db
--- /dev/null
+++ b/src/build/android/device_stats_monitor.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+#
+# 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 iotop/top style profiling for android.
+
+Usage:
+ ./device_stats_monitor.py --hz=20 --duration=5 --outfile=/tmp/foo
+"""
+
+import optparse
+import os
+import sys
+import time
+
+from pylib import android_commands
+from pylib import device_stats_monitor
+from pylib import test_options_parser
+
+
+def main(argv):
+ option_parser = optparse.OptionParser()
+ option_parser.add_option('--hz', type='int', default=20,
+ help='Number of samples/sec.')
+ option_parser.add_option('--duration', type='int', default=5,
+ help='Seconds to monitor.')
+ option_parser.add_option('--outfile', default='/tmp/devicestatsmonitor',
+ help='Location to start output file.')
+ test_options_parser.AddBuildTypeOption(option_parser)
+ options, args = option_parser.parse_args(argv)
+
+ monitor = device_stats_monitor.DeviceStatsMonitor(
+ android_commands.AndroidCommands(), options.hz, options.build_type)
+ monitor.Start()
+ print 'Waiting for %d seconds while profiling.' % options.duration
+ time.sleep(options.duration)
+ url = monitor.StopAndCollect(options.outfile)
+ print 'View results in browser at %s' % url
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/src/build/android/device_status_check.py b/src/build/android/device_status_check.py
new file mode 100755
index 0000000..3d695a2
--- /dev/null
+++ b/src/build/android/device_status_check.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""A class to keep track of devices across builds and report state."""
+import logging
+import optparse
+import os
+import smtplib
+import sys
+
+from pylib import buildbot_report
+from pylib.android_commands import GetAttachedDevices
+from pylib.cmd_helper import GetCmdOutput
+
+
+def DeviceInfo(serial):
+ """Gathers info on a device via various adb calls.
+
+ Args:
+ serial: The serial of the attached device to construct info about.
+
+ Returns:
+ Tuple of device type, build id and report as a string.
+ """
+
+ def AdbShellCmd(cmd):
+ return GetCmdOutput('adb -s %s shell %s' % (serial, cmd),
+ shell=True).strip()
+
+ device_type = AdbShellCmd('getprop ro.build.product')
+ device_build = AdbShellCmd('getprop ro.build.id')
+
+ report = ['Device %s (%s)' % (serial, device_type),
+ ' Build: %s (%s)' % (device_build,
+ AdbShellCmd('getprop ro.build.fingerprint')),
+ ' Battery: %s%%' % AdbShellCmd('dumpsys battery | grep level '
+ "| awk '{print $2}'"),
+ ' Battery temp: %s' % AdbShellCmd('dumpsys battery'
+ '| grep temp '
+ "| awk '{print $2}'"),
+ ' IMEI slice: %s' % AdbShellCmd('dumpsys iphonesubinfo '
+ '| grep Device'
+ "| awk '{print $4}'")[-6:],
+ ' Wifi IP: %s' % AdbShellCmd('getprop dhcp.wlan0.ipaddress'),
+ '']
+
+ return device_type, device_build, '\n'.join(report)
+
+
+def CheckForMissingDevices(options, adb_online_devs):
+ """Uses file of previous online devices to detect broken phones.
+
+ Args:
+ options: out_dir parameter of options argument is used as the base
+ directory to load and update the cache file.
+ adb_online_devs: A list of serial numbers of the currently visible
+ and online attached devices.
+ """
+ # TODO(navabi): remove this once the bug that causes different number
+ # of devices to be detected between calls is fixed.
+ logger = logging.getLogger()
+ logger.setLevel(logging.INFO)
+
+ out_dir = os.path.abspath(options.out_dir)
+
+ def ReadDeviceList(file_name):
+ devices_path = os.path.join(out_dir, file_name)
+ devices = []
+ try:
+ with open(devices_path) as f:
+ devices = f.read().splitlines()
+ except IOError:
+ # Ignore error, file might not exist
+ pass
+ return devices
+
+ def WriteDeviceList(file_name, device_list):
+ path = os.path.join(out_dir, file_name)
+ if not os.path.exists(out_dir):
+ os.makedirs(out_dir)
+ with open(path, 'w') as f:
+ # Write devices currently visible plus devices previously seen.
+ f.write('\n'.join(set(device_list)))
+
+ last_devices_path = os.path.join(out_dir, '.last_devices')
+ last_devices = ReadDeviceList('.last_devices')
+
+ missing_devs = list(set(last_devices) - set(adb_online_devs))
+ if missing_devs:
+ from_address = 'buildbot@chromium.org'
+ to_address = 'chromium-android-device-alerts@google.com'
+ bot_name = os.environ['BUILDBOT_BUILDERNAME']
+ slave_name = os.environ['BUILDBOT_SLAVENAME']
+ num_online_devs = len(adb_online_devs)
+ subject = 'Devices offline on %s, %s (%d remaining).' % (slave_name,
+ bot_name,
+ num_online_devs)
+ buildbot_report.PrintWarning()
+ devices_missing_msg = '%d devices not detected.' % len(missing_devs)
+ buildbot_report.PrintSummaryText(devices_missing_msg)
+
+ # TODO(navabi): Debug by printing both output from GetCmdOutput and
+ # GetAttachedDevices to compare results.
+ body = '\n'.join(
+ ['Current online devices: %s' % adb_online_devs,
+ '%s are no longer visible. Were they removed?\n' % missing_devs,
+ 'SHERIFF: See go/chrome_device_monitor',
+ 'Cache file: %s\n\n' % last_devices_path,
+ 'adb devices: %s' % GetCmdOutput(['adb', 'devices']),
+ 'adb devices(GetAttachedDevices): %s' % GetAttachedDevices()])
+
+ print body
+
+ # Only send email if the first time a particular device goes offline
+ last_missing = ReadDeviceList('.last_missing')
+ new_missing_devs = set(missing_devs) - set(last_missing)
+
+ if new_missing_devs:
+ msg_body = '\r\n'.join(
+ ['From: %s' % from_address,
+ 'To: %s' % to_address,
+ 'Subject: %s' % subject,
+ '', body])
+ try:
+ server = smtplib.SMTP('localhost')
+ server.sendmail(from_address, [to_address], msg_body)
+ server.quit()
+ except Exception as e:
+ print 'Failed to send alert email. Error: %s' % e
+ else:
+ new_devs = set(adb_online_devs) - set(last_devices)
+ if new_devs and os.path.exists(last_devices_path):
+ buildbot_report.PrintWarning()
+ buildbot_report.PrintSummaryText(
+ '%d new devices detected' % len(new_devs))
+ print ('New devices detected %s. And now back to your '
+ 'regularly scheduled program.' % list(new_devs))
+ WriteDeviceList('.last_devices', (adb_online_devs + last_devices))
+ WriteDeviceList('.last_missing', missing_devs)
+
+
+def main():
+ parser = optparse.OptionParser()
+ parser.add_option('', '--out-dir',
+ help='Directory where the device path is stored',
+ default=os.path.join(os.path.dirname(__file__), '..',
+ '..', 'out'))
+
+ options, args = parser.parse_args()
+ if args:
+ parser.error('Unknown options %s' % args)
+ buildbot_report.PrintNamedStep('Device Status Check')
+ devices = GetAttachedDevices()
+ types, builds, reports = [], [], []
+ if devices:
+ types, builds, reports = zip(*[DeviceInfo(dev) for dev in devices])
+
+ unique_types = list(set(types))
+ unique_builds = list(set(builds))
+
+ buildbot_report.PrintMsg('Online devices: %d. Device types %s, builds %s'
+ % (len(devices), unique_types, unique_builds))
+ print '\n'.join(reports)
+ CheckForMissingDevices(options, devices)
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/src/build/android/empty/src/.keep b/src/build/android/empty/src/.keep
new file mode 100644
index 0000000..0f710b6
--- /dev/null
+++ b/src/build/android/empty/src/.keep
@@ -0,0 +1,6 @@
+This is a file that needs to live here until http://crbug.com/158155 has
+been fixed.
+
+The ant build system requires that a src folder is always present, and for
+some of our targets that is not the case. Giving it an empty src-folder works
+nicely though.
diff --git a/src/build/android/emulator.py b/src/build/android/emulator.py
new file mode 100755
index 0000000..77c9a75
--- /dev/null
+++ b/src/build/android/emulator.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python
+#
+# 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.
+
+Assumes system environment ANDROID_NDK_ROOT has been set.
+
+ 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 sys
+import time
+
+from pylib import android_commands
+from pylib import cmd_helper
+
+# adb_interface.py is under ../../third_party/android_testrunner/
+sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..',
+ '..', 'third_party', 'android_testrunner'))
+import adb_interface
+import errors
+import run_command
+
+class EmulatorLaunchException(Exception):
+ """Emulator failed to launch."""
+ pass
+
+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 and
+ we're out of room forever.
+ """
+ emulators = android_commands.GetEmulators()
+ if not emulators:
+ return
+ for emu_name in emulators:
+ cmd_helper.GetCmdOutput(['adb', '-s', emu_name, 'emu', 'kill'])
+ logging.info('Emulator killing is async; give a few seconds for all to die.')
+ for i in range(5):
+ if not android_commands.GetEmulators():
+ 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.
+ """
+ avds = android_commands.GetAVDs()
+ if not avds:
+ return
+ for avd_name in avds:
+ if 'run_tests_avd' in avd_name:
+ cmd = ['android', '-s', 'delete', 'avd', '--name', avd_name]
+ cmd_helper.GetCmdOutput(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 = android_commands.GetEmulators()
+ for emulator in emulators:
+ used_ports.append(emulator.split('-')[1])
+ for port in PortPool.port_range():
+ if str(port) not in used_ports:
+ return port
+
+
+class Emulator(object):
+ """Provides the methods to lanuch/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, new_avd_name, fast_and_loose):
+ """Init an Emulator.
+
+ Args:
+ nwe_avd_name: If set, will create a new temporary AVD.
+ fast_and_loose: Loosen up the rules for reliable running for speed.
+ Intended for quick testing or re-testing.
+
+ """
+ try:
+ android_sdk_root = os.environ['ANDROID_SDK_ROOT']
+ except KeyError:
+ logging.critical('The ANDROID_SDK_ROOT must be set to run the test on '
+ 'emulator.')
+ raise
+ self.emulator = os.path.join(android_sdk_root, 'tools', 'emulator')
+ self.android = os.path.join(android_sdk_root, 'tools', 'android')
+ self.popen = None
+ self.device = None
+ self.default_avd = True
+ self.fast_and_loose = fast_and_loose
+ self.abi = 'armeabi-v7a'
+ self.avd = 'avd_armeabi'
+ if 'x86' in os.environ.get('TARGET_PRODUCT', ''):
+ self.abi = 'x86'
+ self.avd = 'avd_x86'
+ if new_avd_name:
+ self.default_avd = False
+ self.avd = self._CreateAVD(new_avd_name)
+
+ def _DeviceName(self):
+ """Return our device name."""
+ port = _GetAvailablePort()
+ return ('emulator-%d' % port, port)
+
+ def _CreateAVD(self, avd_name):
+ """Creates an AVD with the given name.
+
+ Return avd_name.
+ """
+ avd_command = [
+ self.android,
+ '--silent',
+ 'create', 'avd',
+ '--name', avd_name,
+ '--abi', self.abi,
+ '--target', 'android-16',
+ '-c', '128M',
+ '--force',
+ ]
+ avd_process = subprocess.Popen(args=avd_command,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+ avd_process.stdin.write('no\n')
+ avd_process.wait()
+ logging.info('Create AVD command: %s', ' '.join(avd_command))
+ return avd_name
+
+ def _DeleteAVD(self):
+ """Delete the AVD of this emulator."""
+ avd_command = [
+ self.android,
+ '--silent',
+ 'delete',
+ 'avd',
+ '--name', self.avd,
+ ]
+ avd_process = subprocess.Popen(args=avd_command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT)
+ logging.info('Delete AVD command: %s', ' '.join(avd_command))
+ avd_process.wait()
+
+ 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
+ if not self.fast_and_loose:
+ self._AggressiveImageCleanup()
+ (self.device, port) = self._DeviceName()
+ emulator_command = [
+ self.emulator,
+ # Speed up emulator launch by 40%. Really.
+ '-no-boot-anim',
+ # The default /data size is 64M.
+ # That's not enough for 8 unit test bundles and their data.
+ '-partition-size', '512',
+ # Enable GPU by default.
+ '-gpu', 'on',
+ # Use a familiar name and port.
+ '-avd', self.avd,
+ '-port', str(port)]
+ if not self.fast_and_loose:
+ emulator_command.extend([
+ # Wipe the data. We've seen cases where an emulator
+ # gets 'stuck' if we don't do this (every thousand runs or
+ # so).
+ '-wipe-data',
+ ])
+ logging.info('Emulator launch command: %s', ' '.join(emulator_command))
+ self.popen = subprocess.Popen(args=emulator_command,
+ stderr=subprocess.STDOUT)
+ self._InstallKillHandler()
+
+ def _AggressiveImageCleanup(self):
+ """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
+ adb_cmd = "adb -s %s %s" % (self.device, 'wait-for-device')
+ while seconds_waited < self._LAUNCH_TIMEOUT:
+ try:
+ run_command.RunCommand(adb_cmd,
+ timeout_time=self._WAITFORDEVICE_TIMEOUT,
+ retry_count=1)
+ number_of_waits -= 1
+ if not number_of_waits:
+ break
+ except errors.WaitForResponseTimedOutError as e:
+ seconds_waited += self._WAITFORDEVICE_TIMEOUT
+ adb_cmd = "adb -s %s %s" % (self.device, 'kill-server')
+ run_command.RunCommand(adb_cmd)
+ 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.
+ a = android_commands.AndroidCommands(self.device)
+ a.WaitForSystemBootCompleted(self._WAITFORBOOT_TIMEOUT)
+
+ def Shutdown(self):
+ """Shuts down the process started by launch."""
+ if not self.default_avd:
+ 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)
+
+def main(argv):
+ Emulator(None, True).Launch(True)
+
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/src/build/android/enable_asserts.py b/src/build/android/enable_asserts.py
new file mode 100755
index 0000000..5659e9e
--- /dev/null
+++ b/src/build/android/enable_asserts.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""Enables dalvik vm asserts in the android device."""
+
+from pylib import android_commands
+import optparse
+import sys
+
+
+def main(argv):
+ option_parser = optparse.OptionParser()
+ option_parser.add_option('--enable_asserts', dest='set_asserts',
+ action='store_true', default=None,
+ help='Sets the dalvik.vm.enableassertions property to "all"')
+ option_parser.add_option('--disable_asserts', dest='set_asserts',
+ action='store_false', default=None,
+ help='Removes the dalvik.vm.enableassertions property')
+ options, _ = option_parser.parse_args(argv)
+
+ commands = android_commands.AndroidCommands()
+ if options.set_asserts != None:
+ if commands.SetJavaAssertsEnabled(options.set_asserts):
+ commands.Reboot(full_reboot=False)
+
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/src/build/android/envsetup.sh b/src/build/android/envsetup.sh
new file mode 100644
index 0000000..10d9ec8
--- /dev/null
+++ b/src/build/android/envsetup.sh
@@ -0,0 +1,138 @@
+# 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.
+
+# Sets up environment for building Chromium on Android. It can either be
+# compiled with the Android tree or using the Android SDK/NDK. To build with
+# NDK/SDK: ". build/android/envsetup.sh". Environment variable
+# ANDROID_SDK_BUILD=1 will then be defined and used in the rest of the setup to
+# specifiy build type.
+
+# Source functions script. The file is in the same directory as this script.
+. "$(dirname $BASH_SOURCE)"/envsetup_functions.sh
+
+export ANDROID_SDK_BUILD=1 # Default to SDK build.
+
+process_options "$@"
+
+# When building WebView as part of Android we can't use the SDK. Other builds
+# default to using the SDK.
+if [[ "${CHROME_ANDROID_BUILD_WEBVIEW}" -eq 1 ]]; then
+ export ANDROID_SDK_BUILD=0
+fi
+
+if [[ "${ANDROID_SDK_BUILD}" -eq 1 ]]; then
+ echo "Using SDK build"
+fi
+
+# Get host architecture, and abort if it is 32-bit, unless --try-32
+# is also used.
+host_arch=$(uname -m)
+case "${host_arch}" in
+ x86_64) # pass
+ ;;
+ i?86)
+ if [[ -z "${try_32bit_host_build}" ]]; then
+ echo "ERROR: Android build requires a 64-bit host build machine."
+ echo "If you really want to try it on this machine, use the \
+--try-32bit-host flag."
+ echo "Be warned that this may fail horribly at link time, due \
+very large binaries."
+ return 1
+ else
+ echo "WARNING: 32-bit host build enabled. Here be dragons!"
+ host_arch=x86
+ fi
+ ;;
+ *)
+ echo "ERROR: Unsupported host architecture (${host_arch})."
+ echo "Try running this script on a Linux/x86_64 machine instead."
+ return 1
+esac
+
+host_os=$(uname -s | sed -e 's/Linux/linux/;s/Darwin/mac/')
+
+case "${host_os}" in
+ "linux")
+ toolchain_dir="linux-${host_arch}"
+ ;;
+ "mac")
+ toolchain_dir="darwin-${host_arch}"
+ ;;
+ *)
+ echo "Host platform ${host_os} is not supported" >& 2
+ return 1
+esac
+
+CURRENT_DIR="$(readlink -f "$(dirname $BASH_SOURCE)/../../")"
+if [[ -z "${CHROME_SRC}" ]]; then
+ # If $CHROME_SRC was not set, assume current directory is CHROME_SRC.
+ export CHROME_SRC="${CURRENT_DIR}"
+fi
+
+if [[ "${CURRENT_DIR/"${CHROME_SRC}"/}" == "${CURRENT_DIR}" ]]; then
+ # If current directory is not in $CHROME_SRC, it might be set for other
+ # source tree. If $CHROME_SRC was set correctly and we are in the correct
+ # directory, "${CURRENT_DIR/"${CHROME_SRC}"/}" will be "".
+ # Otherwise, it will equal to "${CURRENT_DIR}"
+ echo "Warning: Current directory is out of CHROME_SRC, it may not be \
+the one you want."
+ echo "${CHROME_SRC}"
+fi
+
+# Android sdk platform version to use
+export ANDROID_SDK_VERSION=16
+
+if [[ "${ANDROID_SDK_BUILD}" -eq 1 ]]; then
+ if [[ -z "${TARGET_ARCH}" ]]; then
+ return 1
+ fi
+ sdk_build_init
+# Sets up environment for building Chromium for Android with source. Expects
+# android environment setup and lunch.
+elif [[ -z "$ANDROID_BUILD_TOP" || \
+ -z "$ANDROID_TOOLCHAIN" || \
+ -z "$ANDROID_PRODUCT_OUT" ]]; then
+ echo "Android build environment variables must be set."
+ echo "Please cd to the root of your Android tree and do: "
+ echo " . build/envsetup.sh"
+ echo " lunch"
+ echo "Then try this again."
+ echo "Or did you mean NDK/SDK build. Run envsetup.sh without any arguments."
+ return 1
+elif [[ -n "$CHROME_ANDROID_BUILD_WEBVIEW" ]]; then
+ webview_build_init
+fi
+
+# Workaround for valgrind build
+if [[ -n "$CHROME_ANDROID_VALGRIND_BUILD" ]]; then
+# arm_thumb=0 is a workaround for https://bugs.kde.org/show_bug.cgi?id=270709
+ DEFINES+=" arm_thumb=0 release_extra_cflags='-fno-inline\
+ -fno-omit-frame-pointer -fno-builtin' release_valgrind_build=1\
+ release_optimize=1"
+fi
+
+# Source a bunch of helper functions
+. ${CHROME_SRC}/build/android/adb_device_functions.sh
+
+ANDROID_GOMA_WRAPPER=""
+if [[ -d $GOMA_DIR ]]; then
+ ANDROID_GOMA_WRAPPER="$GOMA_DIR/gomacc"
+fi
+export ANDROID_GOMA_WRAPPER
+
+# Declare Android are cross compile.
+export GYP_CROSSCOMPILE=1
+
+# Performs a gyp_chromium run to convert gyp->Makefile for android code.
+android_gyp() {
+ # This is just a simple wrapper of gyp_chromium, please don't add anything
+ # in this function.
+ echo "GYP_GENERATORS set to '$GYP_GENERATORS'"
+ (
+ "${CHROME_SRC}/build/gyp_chromium" --depth="${CHROME_SRC}" --check "$@"
+ )
+}
+
+# FLOCK needs to be null on system that has no flock
+which flock > /dev/null || export FLOCK=
diff --git a/src/build/android/envsetup_functions.sh b/src/build/android/envsetup_functions.sh
new file mode 100755
index 0000000..99eddd4
--- /dev/null
+++ b/src/build/android/envsetup_functions.sh
@@ -0,0 +1,306 @@
+#!/bin/bash
+
+# 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.
+
+# Defines functions for envsetup.sh which sets up environment for building
+# Chromium on Android. The build can be either use the Android NDK/SDK or
+# android source tree. Each has a unique init function which calls functions
+# prefixed with "common_" that is common for both environment setups.
+
+################################################################################
+# Check to make sure the toolchain exists for the NDK version.
+################################################################################
+common_check_toolchain() {
+ if [[ ! -d "${ANDROID_TOOLCHAIN}" ]]; then
+ echo "Can not find Android toolchain in ${ANDROID_TOOLCHAIN}." >& 2
+ echo "The NDK version might be wrong." >& 2
+ return 1
+ fi
+}
+
+################################################################################
+# Exports environment variables common to both sdk and non-sdk build (e.g. PATH)
+# based on CHROME_SRC and ANDROID_TOOLCHAIN, along with DEFINES for GYP_DEFINES.
+################################################################################
+common_vars_defines() {
+ # Set toolchain path according to product architecture.
+ case "${TARGET_ARCH}" in
+ "arm")
+ toolchain_arch="arm-linux-androideabi"
+ ;;
+ "x86")
+ toolchain_arch="x86"
+ ;;
+ *)
+ echo "TARGET_ARCH: ${TARGET_ARCH} is not supported." >& 2
+ print_usage
+ return 1
+ ;;
+ esac
+
+ toolchain_version="4.6"
+ # We directly set the gcc_version since we know what we use, and it should
+ # be set to xx instead of x.x. Refer the output of compiler_version.py.
+ gcc_version="46"
+ toolchain_target=$(basename \
+ ${ANDROID_NDK_ROOT}/toolchains/${toolchain_arch}-${toolchain_version})
+ toolchain_path="${ANDROID_NDK_ROOT}/toolchains/${toolchain_target}"\
+"/prebuilt/${toolchain_dir}/bin/"
+
+ # Set only if not already set.
+ # Don't override ANDROID_TOOLCHAIN if set by Android configuration env.
+ export ANDROID_TOOLCHAIN=${ANDROID_TOOLCHAIN:-${toolchain_path}}
+
+ common_check_toolchain
+
+ # Add Android SDK/NDK tools to system path.
+ export PATH=$PATH:${ANDROID_NDK_ROOT}
+ export PATH=$PATH:${ANDROID_SDK_ROOT}/tools
+ export PATH=$PATH:${ANDROID_SDK_ROOT}/platform-tools
+
+ # This must be set before ANDROID_TOOLCHAIN, so that clang could find the
+ # gold linker.
+ # TODO(michaelbai): Remove this path once the gold linker become the default
+ # linker.
+ export PATH=$PATH:${CHROME_SRC}/build/android/${toolchain_arch}-gold
+
+ # Must have tools like arm-linux-androideabi-gcc on the path for ninja
+ export PATH=$PATH:${ANDROID_TOOLCHAIN}
+
+ # Add Chromium Android development scripts to system path.
+ # Must be after CHROME_SRC is set.
+ export PATH=$PATH:${CHROME_SRC}/build/android
+
+ # TODO(beverloo): Remove these once all consumers updated to --strip-binary.
+ export OBJCOPY=$(echo ${ANDROID_TOOLCHAIN}/*-objcopy)
+ export STRIP=$(echo ${ANDROID_TOOLCHAIN}/*-strip)
+
+ # The set of GYP_DEFINES to pass to gyp. Use 'readlink -e' on directories
+ # to canonicalize them (remove double '/', remove trailing '/', etc).
+ DEFINES="OS=android"
+ DEFINES+=" host_os=${host_os}"
+ DEFINES+=" gcc_version=${gcc_version}"
+
+ if [[ -n "$CHROME_ANDROID_OFFICIAL_BUILD" ]]; then
+ DEFINES+=" branding=Chrome"
+ DEFINES+=" buildtype=Official"
+
+ # These defines are used by various chrome build scripts to tag the binary's
+ # version string as 'official' in linux builds (e.g. in
+ # chrome/trunk/src/chrome/tools/build/version.py).
+ export OFFICIAL_BUILD=1
+ export CHROMIUM_BUILD="_google_chrome"
+ export CHROME_BUILD_TYPE="_official"
+
+ # Used by chrome_version_info_posix.cc to display the channel name.
+ # Valid values: "unstable", "stable", "dev", "beta".
+ export CHROME_VERSION_EXTRA="beta"
+ fi
+
+ # The order file specifies the order of symbols in the .text section of the
+ # shared library, libchromeview.so. The file is an order list of section
+ # names and the library is linked with option
+ # --section-ordering-file=<orderfile>. The order file is updated by profiling
+ # startup after compiling with the order_profiling=1 GYP_DEFINES flag.
+ ORDER_DEFINES="order_text_section=${CHROME_SRC}/orderfiles/orderfile.out"
+
+ # The following defines will affect ARM code generation of both C/C++ compiler
+ # and V8 mksnapshot.
+ case "${TARGET_ARCH}" in
+ "arm")
+ DEFINES+=" arm_neon=0 armv7=1 arm_thumb=1 arm_fpu=vfpv3-d16"
+ DEFINES+=" arm_neon_optional=1" # Enable dynamic NEON support.
+ DEFINES+=" ${ORDER_DEFINES}"
+ DEFINES+=" target_arch=arm"
+ ;;
+ "x86")
+ # TODO(tedbo): The ia32 build fails on ffmpeg, so we disable it here.
+ DEFINES+=" use_libffmpeg=0"
+
+ host_arch=$(uname -m | sed -e \
+ 's/i.86/ia32/;s/x86_64/x64/;s/amd64/x64/;s/arm.*/arm/;s/i86pc/ia32/')
+ DEFINES+=" host_arch=${host_arch}"
+ DEFINES+=" target_arch=ia32"
+ ;;
+ *)
+ echo "TARGET_ARCH: ${TARGET_ARCH} is not supported." >& 2
+ print_usage
+ return 1
+ esac
+
+ DEFINES+=" android_gdbserver=${ANDROID_NDK_ROOT}/prebuilt/\
+android-${TARGET_ARCH}/gdbserver/gdbserver"
+}
+
+
+################################################################################
+# Exports common GYP variables based on variable DEFINES and CHROME_SRC.
+################################################################################
+common_gyp_vars() {
+ export GYP_DEFINES="${DEFINES}"
+
+ # Set GYP_GENERATORS to make-android if it's currently unset or null.
+ export GYP_GENERATORS="${GYP_GENERATORS:-make-android}"
+
+ # Use our All target as the default
+ export GYP_GENERATOR_FLAGS="${GYP_GENERATOR_FLAGS} default_target=All"
+
+ # We want to use our version of "all" targets.
+ export CHROMIUM_GYP_FILE="${CHROME_SRC}/build/all_android.gyp"
+}
+
+
+################################################################################
+# Prints out help message on usage.
+################################################################################
+print_usage() {
+ echo "usage: ${0##*/} [--target-arch=value] [--help]" >& 2
+ echo "--target-arch=value target CPU architecture (arm=default, x86)" >& 2
+ echo "--try-32bit-host try building a 32-bit host architecture" >&2
+ echo "--help this help" >& 2
+}
+
+################################################################################
+# Process command line options.
+# --target-arch= Specifices target CPU architecture. Currently supported
+# architectures are "arm" (default), and "x86".
+# --help Prints out help message.
+################################################################################
+process_options() {
+ try_32bit_host_build=
+ while [[ $1 ]]; do
+ case "$1" in
+ --target-arch=*)
+ target_arch="$(echo "$1" | sed 's/^[^=]*=//')"
+ ;;
+ --try-32bit-host)
+ try_32bit_host_build=true
+ ;;
+ --help)
+ print_usage
+ return 1
+ ;;
+ *)
+ # Ignore other command line options
+ echo "Unknown option: $1"
+ ;;
+ esac
+ shift
+ done
+
+ # Sets TARGET_ARCH. Defaults to arm if not specified.
+ TARGET_ARCH=${target_arch:-arm}
+}
+
+################################################################################
+# Initializes environment variables for NDK/SDK build. Only Android NDK Revision
+# 7 on Linux or Mac is offically supported. To run this script, the system
+# environment ANDROID_NDK_ROOT must be set to Android NDK's root path. The
+# ANDROID_SDK_ROOT only needs to be set to override the default SDK which is in
+# the tree under $ROOT/src/third_party/android_tools/sdk.
+# To build Chromium for Android with NDK/SDK follow the steps below:
+# > export ANDROID_NDK_ROOT=<android ndk root>
+# > export ANDROID_SDK_ROOT=<android sdk root> # to override the default sdk
+# > . build/android/envsetup.sh
+# > make
+################################################################################
+sdk_build_init() {
+ # If ANDROID_NDK_ROOT is set when envsetup is run, use the ndk pointed to by
+ # the environment variable. Otherwise, use the default ndk from the tree.
+ if [[ -z "${ANDROID_NDK_ROOT}" || ! -d "${ANDROID_NDK_ROOT}" ]]; then
+ export ANDROID_NDK_ROOT="${CHROME_SRC}/third_party/android_tools/ndk/"
+ fi
+
+ # If ANDROID_SDK_ROOT is set when envsetup is run, and if it has the
+ # right SDK-compatible directory layout, use the sdk pointed to by the
+ # environment variable. Otherwise, use the default sdk from the tree.
+ local sdk_suffix=platforms/android-${ANDROID_SDK_VERSION}
+ if [[ -z "${ANDROID_SDK_ROOT}" || \
+ ! -d "${ANDROID_SDK_ROOT}/${sdk_suffix}" ]]; then
+ export ANDROID_SDK_ROOT="${CHROME_SRC}/third_party/android_tools/sdk/"
+ fi
+
+ unset ANDROID_BUILD_TOP
+
+ # Set default target.
+ export TARGET_PRODUCT="${TARGET_PRODUCT:-trygon}"
+
+ # Unset toolchain so that it can be set based on TARGET_PRODUCT.
+ # This makes it easy to switch between architectures.
+ unset ANDROID_TOOLCHAIN
+
+ common_vars_defines
+
+ DEFINES+=" sdk_build=1"
+
+ # Sets android specific directories to NOT_SDK_COMPLIANT. This will allow
+ # android_gyp to generate make files, but will cause errors when (and only
+ # when) building targets that depend on these directories.
+ DEFINES+=" android_src='NOT_SDK_COMPLIANT'"
+ DEFINES+=" android_product_out=${CHROME_SRC}/out/android"
+ DEFINES+=" android_lib='NOT_SDK_COMPLIANT'"
+ DEFINES+=" android_static_lib='NOT_SDK_COMPLIANT'"
+ DEFINES+=" android_sdk=${ANDROID_SDK_ROOT}/${sdk_suffix}"
+ DEFINES+=" android_sdk_root=${ANDROID_SDK_ROOT}"
+ DEFINES+=" android_sdk_tools=${ANDROID_SDK_ROOT}/platform-tools"
+ DEFINES+=" android_sdk_version=${ANDROID_SDK_VERSION}"
+ DEFINES+=" android_toolchain=${ANDROID_TOOLCHAIN}"
+
+ common_gyp_vars
+
+ if [[ -n "$CHROME_ANDROID_BUILD_WEBVIEW" ]]; then
+ # Can not build WebView with NDK/SDK because it needs the Android build
+ # system and build inside an Android source tree.
+ echo "Can not build WebView with NDK/SDK. Requires android source tree." \
+ >& 2
+ echo "Try . build/android/envsetup.sh instead." >& 2
+ return 1
+ fi
+
+}
+
+################################################################################
+# To build WebView, we use the Android build system and build inside an Android
+# source tree. This method is called from non_sdk_build_init() and adds to the
+# settings specified there.
+#############################################################################
+webview_build_init() {
+ # For the WebView build we always use the NDK and SDK in the Android tree,
+ # and we don't touch ANDROID_TOOLCHAIN which is already set by Android.
+ export ANDROID_NDK_ROOT=${ANDROID_BUILD_TOP}/prebuilts/ndk/8
+ export ANDROID_SDK_ROOT=${ANDROID_BUILD_TOP}/prebuilts/sdk/\
+${ANDROID_SDK_VERSION}
+
+ common_vars_defines
+
+ # We need to supply SDK paths relative to the top of the Android tree to make
+ # sure the generated Android makefiles are portable, as they will be checked
+ # into the Android tree.
+ ANDROID_SDK=$(python -c \
+ "import os.path; print os.path.relpath('${ANDROID_SDK_ROOT}', \
+ '${ANDROID_BUILD_TOP}')")
+ ANDROID_SDK_TOOLS=$(python -c \
+ "import os.path; \
+ print os.path.relpath('${ANDROID_SDK_ROOT}/../tools/linux', \
+ '${ANDROID_BUILD_TOP}')")
+ DEFINES+=" android_build_type=1"
+ DEFINES+=" sdk_build=0"
+ DEFINES+=" android_src=\$(GYP_ABS_ANDROID_TOP_DIR)"
+ DEFINES+=" android_product_out=NOT_USED_ON_WEBVIEW"
+ DEFINES+=" android_sdk=\$(GYP_ABS_ANDROID_TOP_DIR)/${ANDROID_SDK}"
+ DEFINES+=" android_sdk_root=\$(GYP_ABS_ANDROID_TOP_DIR)/${ANDROID_SDK}"
+ DEFINES+=" android_sdk_tools=\$(GYP_ABS_ANDROID_TOP_DIR)/${ANDROID_SDK_TOOLS}"
+ DEFINES+=" android_sdk_version=${ANDROID_SDK_VERSION}"
+ DEFINES+=" android_toolchain=${ANDROID_TOOLCHAIN}"
+ export GYP_DEFINES="${DEFINES}"
+
+ export GYP_GENERATORS="android"
+
+ export GYP_GENERATOR_FLAGS="${GYP_GENERATOR_FLAGS} default_target=All"
+ export GYP_GENERATOR_FLAGS="${GYP_GENERATOR_FLAGS} limit_to_target_all=1"
+ export GYP_GENERATOR_FLAGS="${GYP_GENERATOR_FLAGS} auto_regeneration=0"
+
+ export CHROMIUM_GYP_FILE="${CHROME_SRC}/android_webview/all_webview.gyp"
+}
diff --git a/src/build/android/findbugs_diff.py b/src/build/android/findbugs_diff.py
new file mode 100755
index 0000000..eb49824
--- /dev/null
+++ b/src/build/android/findbugs_diff.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""Runs findbugs, and returns an error code if there are new warnings.
+This runs findbugs with an additional flag to exclude known bugs.
+To update the list of known bugs, do this:
+
+ findbugs_diff.py --rebaseline
+
+Note that this is separate from findbugs_exclude.xml. The "exclude" file has
+false positives that we do not plan to fix. The "known bugs" file has real
+bugs that we *do* plan to fix (but haven't done so yet).
+
+Other options
+ --only-analyze used to only analyze the class you are interested.
+ --relase-build analyze the classes in out/Release directory.
+ --findbugs-args used to passin other findbugs's options.
+
+Run
+ $CHROM_SRC/third_party/findbugs/bin/findbugs -textui for details.
+
+"""
+
+import optparse
+import os
+import sys
+
+from pylib import findbugs
+
+
+def main(argv):
+ if not findbugs.CheckEnvironment():
+ return 1
+
+ parser = findbugs.GetCommonParser()
+
+ options, _ = parser.parse_args()
+
+ chrome_src = os.getenv('CHROME_SRC')
+
+ if not options.base_dir:
+ options.base_dir = os.path.join(chrome_src, 'build', 'android',
+ 'findbugs_filter')
+ if not options.only_analyze:
+ options.only_analyze = 'org.chromium.-'
+
+ return findbugs.Run(options)
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/src/build/android/findbugs_filter/findbugs_exclude.xml b/src/build/android/findbugs_filter/findbugs_exclude.xml
new file mode 100644
index 0000000..49fa811
--- /dev/null
+++ b/src/build/android/findbugs_filter/findbugs_exclude.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+
+<!--
+Documentation: http://findbugs.sourceforge.net/manual/filter.html
+In particular, ~ at the start of a string means it's a regex.
+-->
+<FindBugsFilter>
+ <!-- Skip the generated resource classes (including nested classes). -->
+ <Match>
+ <Class name="~org\.chromium\..*\.R(\$\w+)?" />
+ </Match>
+ <Match>
+ <Class name="~org\.chromium\..*\.Manifest(\$\w+)?" />
+ </Match>
+ <!-- Ignore "reliance on default String encoding" warnings, as we're not multi-platform -->
+ <Bug pattern="DM_DEFAULT_ENCODING" />
+</FindBugsFilter>
diff --git a/src/build/android/findbugs_filter/findbugs_known_bugs.txt b/src/build/android/findbugs_filter/findbugs_known_bugs.txt
new file mode 100644
index 0000000..cf91422
--- /dev/null
+++ b/src/build/android/findbugs_filter/findbugs_known_bugs.txt
@@ -0,0 +1,142 @@
+H B Nm: The class name org.chromium.content.browser.test.util.TouchUtils shadows the simple name of the superclass android.test.TouchUtils At TouchUtils.java
+H C EC: Using pointer equality to compare a JavaBridgeCoercionTest$CustomType with a JavaBridgeCoercionTest$CustomType2 in org.chromium.content.browser.JavaBridgeCoercionTest.testPassJavaObject() At JavaBridgeCoercionTest.java
+H D RCN: Redundant nullcheck of org.chromium.content.browser.SandboxedProcessConnection.mConnectionParams, which is known to be non-null in org.chromium.content.browser.SandboxedProcessConnection.doConnectionSetup() Redundant null check at SandboxedProcessConnection.java
+M D ST: Write to static field org.chromium.content.app.SandboxedProcessService.sContext from instance method org.chromium.content.app.SandboxedProcessService.onCreate() At SandboxedProcessService.java
+H D ST: Write to static field org.chromium.net.test.util.TestWebServer.sInstance from instance method org.chromium.net.test.util.TestWebServer.shutdown() At TestWebServer.java
+H V MS: org.chromium.android_webview.test.AndroidWebViewTestBase.WAIT_TIMEOUT_SECONDS isn't final but should be At AndroidWebViewTestBase.java
+H V MS: org.chromium.android_webview.test.LoadDataWithBaseUrlTest.WAIT_TIMEOUT_SECONDS isn't final but should be At LoadDataWithBaseUrlTest.java
+H V MS: org.chromium.content.browser.ContentViewTestBase.WAIT_TIMEOUT_SECONDS isn't final but should be At ContentViewTestBase.java
+H V MS: org.chromium.content.browser.test.util.CallbackHelper.WAIT_TIMEOUT_SECONDS isn't final but should be At CallbackHelper.java
+H V MS: org.chromium.content.browser.test.util.HistoryUtils.WAIT_TIMEOUT_SECONDS isn't final but should be At HistoryUtils.java
+M B DE: org.chromium.net.X509Util.clearTestRootCertificates() might ignore java.io.IOException At X509Util.java
+M B Nm: The method name org.chromium.base.test.util.ScalableTimeout.ScaleTimeout(long) doesn't start with a lower case letter At ScalableTimeout.java
+M B RV: exceptional return value of java.io.File.delete() ignored in org.chromium.android_webview.test.ArchiveTest.doArchiveTest(AwContents, String, boolean, String) At ArchiveTest.java
+M B RV: exceptional return value of java.io.File.delete() ignored in org.chromium.android_webview.test.ArchiveTest.testAutoBadPath() At ArchiveTest.java
+M B RV: exceptional return value of java.io.File.delete() ignored in org.chromium.android_webview.test.ArchiveTest.testExplicitBadPath() At ArchiveTest.java
+M B RV: exceptional return value of java.io.File.delete() ignored in org.chromium.android_webview.test.ArchiveTest.testExplicitGoodPath() At ArchiveTest.java
+M B RV: exceptional return value of java.io.File.delete() ignored in org.chromium.base.test.util.TestFileUtil.deleteFile(String) At TestFileUtil.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At HttpAuthDatabase.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At JavaBridgeArrayCoercionTest.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At JavaBridgeArrayTest.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At JavaBridgeBasicsTest.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At JavaBridgeChildFrameTest.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At JavaBridgeCoercionTest.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At JavaBridgeFieldsTest.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At JavaBridgeReturnValuesTest.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At JavaBridgeTestBase.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At PerfTraceEvent.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At SandboxedProcessConnection.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At SandboxedProcessLauncher.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At SimpleSynchronizedMethod.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At SimpleSynchronizedStaticMethod.java
+M C CSM: Shouldn't use synchronized method, please narrow down the synchronization scope. At TraceEvent.java
+M C CST: Shouldn't use synchronized(this), please narrow down the synchronization scope. At HttpAuthDatabase.java
+M C CST: Shouldn't use synchronized(this), please narrow down the synchronization scope. At SimpleSynchronizedThis.java
+M C IJU: TestCase org.chromium.android_webview.test.AndroidWebViewTestBase defines setUp that doesn't call super.setUp() At AndroidWebViewTestBase.java
+M C IJU: TestCase org.chromium.android_webview.test.ArchiveTest defines setUp that doesn't call super.setUp() At ArchiveTest.java
+M C IJU: TestCase org.chromium.android_webview.test.HttpAuthDatabaseTest defines setUp that doesn't call super.setUp() At HttpAuthDatabaseTest.java
+M C IJU: TestCase org.chromium.android_webview.test.HttpAuthDatabaseTest defines tearDown that doesn't call super.tearDown() At HttpAuthDatabaseTest.java
+M C IJU: TestCase org.chromium.chrome.testshell.ProviderBookmarkNodeTest defines setUp that doesn't call super.setUp() At ProviderBookmarkNodeTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$10.method() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$11.method() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$11.method(int) defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$11.method(int, int) defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$14$1.method(int) defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$14.getInnerObject() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$15.getInnerObject() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$17.captureThreadId() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$19.method() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$1Base.method(int) defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$1InnerObject.method() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$1Test.safe() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$1Test.unsafe() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$1TestObject.method() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$1TestReturner.getTest() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$20.method() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$24.allowed() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$25.allowed() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$25.disallowed() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$2Test.checkJavascriptInterfaceFoo() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$2Test.checkTestAnnotationFoo() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$3Base.method() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$3Test.allowed() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$3Test.blocked() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$4Base.base() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$7.method() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$8.method2() defined in anonymous class At JavaBridgeBasicsTest.java
+M C UMAC: Uncallable method org.chromium.content.browser.JavaBridgeBasicsTest$9.method() defined in anonymous class At JavaBridgeBasicsTest.java
+M C USELESS_STRING: Invocation of toString on certChain in org.chromium.net.X509Util.verifyServerCertificates(byte[][], String) At X509Util.java
+M D DLS: Dead store to context in org.chromium.android_webview.test.AndroidWebViewTestBase.createAwTestContainerViewOnMainSync(boolean, AwContentsClient) At AndroidWebViewTestBase.java
+M D DLS: Dead store to eventTime in org.chromium.content.browser.LongPressDetectorTest$1.run() At LongPressDetectorTest.java
+M D DLS: Dead store to prevEditableLength in org.chromium.content.browser.ImeAdapter$AdapterInputConnection.setEditableText(String, int, int, int, int) At ImeAdapter.java
+M D DLS: Dead store to testUrl in org.chromium.android_webview.test.ClientOnPageFinishedTest.testOnPageFinishedNotCalledForValidSubresources() At ClientOnPageFinishedTest.java
+M D DLS: Dead store to time in org.chromium.net.test.util.TestWebServer.setDateHeaders(HttpResponse) At TestWebServer.java
+M D DMI: Hard coded reference to an absolute pathname in org.chromium.android_webview.test.ArchiveTest.testAutoBadPath() At ArchiveTest.java
+M D DMI: Hard coded reference to an absolute pathname in org.chromium.android_webview.test.ArchiveTest.testExplicitBadPath() At ArchiveTest.java
+M D ICAST: integral division result cast to double or float in org.chromium.content.browser.HandleView.setOrientation(int) At HandleView.java
+M D REC: Exception is caught when Exception is not thrown in org.chromium.content.browser.test.util.UiUtils.findParentViewForIdAcrossActivities(int) At UiUtils.java
+M D SF: Switch statement found in org.chromium.chrome.browser.ChromeBrowserProvider.insert(Uri, ContentValues) where one case falls through to the next case At ChromeBrowserProvider.java
+M D SF: Switch statement found in org.chromium.chrome.browser.database.SQLiteCursor.fillWindow(int, CursorWindow) where default case is missing At SQLiteCursor.java
+M D SF: Switch statement found in org.chromium.content.browser.ContentSettings$EventHandler$1.handleMessage(Message) where default case is missing At ContentSettings.java
+M D SF: Switch statement found in org.chromium.content.browser.HandleView.onTouchEvent(MotionEvent) where default case is missing At HandleView.java
+M D SF: Switch statement found in org.chromium.content.browser.ImeAdapter$AdapterInputConnection.performEditorAction(int) where default case is missing At ImeAdapter.java
+M D ST: Write to static field org.chromium.net.test.util.TestWebServer.sInstance from instance method new org.chromium.net.test.util.TestWebServer(boolean) At TestWebServer.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeBasicsTest$21.field At JavaBridgeBasicsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.booleanField At JavaBridgeFieldsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.byteField At JavaBridgeFieldsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.charField At JavaBridgeFieldsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.customTypeField At JavaBridgeFieldsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.doubleField At JavaBridgeFieldsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.floatField At JavaBridgeFieldsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.intField At JavaBridgeFieldsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.longField At JavaBridgeFieldsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.objectField At JavaBridgeFieldsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.shortField At JavaBridgeFieldsTest.java
+M D UrF: Unread public/protected field: org.chromium.content.browser.JavaBridgeFieldsTest$TestObject.stringField At JavaBridgeFieldsTest.java
+M D UuF: Unused public or protected field: org.chromium.content.browser.JavaBridgeBasicsTest$19.field In JavaBridgeBasicsTest.java
+M M IS: Inconsistent synchronization of org.chromium.content.browser.SandboxedProcessConnection.mPID; locked 66% of time Unsynchronized access at SandboxedProcessConnection.java
+M M IS: Inconsistent synchronization of org.chromium.content.browser.SandboxedProcessConnection.mService; locked 55% of time Unsynchronized access at SandboxedProcessConnection.java
+M M IS: Inconsistent synchronization of org.chromium.content.browser.SandboxedProcessConnection.mServiceConnectComplete; locked 60% of time Unsynchronized access at SandboxedProcessConnection.java
+M M LI: Incorrect lazy initialization and update of static field org.chromium.base.SystemMonitor.sInstance in org.chromium.base.SystemMonitor.create(Context) At SystemMonitor.java
+M M LI: Incorrect lazy initialization and update of static field org.chromium.content.browser.ContentVideoView.sContentVideoView in org.chromium.content.browser.ContentVideoView.createContentVideoView(int) At ContentVideoView.java
+M M LI: Incorrect lazy initialization and update of static field org.chromium.net.test.util.TestWebServer.sReasons in org.chromium.net.test.util.TestWebServer.createResponse(int) At TestWebServer.java
+M M LI: Incorrect lazy initialization of static field org.chromium.net.NetworkChangeNotifier.sInstance in org.chromium.net.NetworkChangeNotifier.init(Context) At NetworkChangeNotifier.java
+M M UG: org.chromium.content.browser.JavaBridgeReturnValuesTest$TestObject.getBooleanValue() is unsynchronized, org.chromium.content.browser.JavaBridgeReturnValuesTest$TestObject.setBooleanValue(boolean) is synchronized At JavaBridgeReturnValuesTest.java
+M M UG: org.chromium.content.browser.JavaBridgeReturnValuesTest$TestObject.getStringValue() is unsynchronized, org.chromium.content.browser.JavaBridgeReturnValuesTest$TestObject.setStringValue(String) is synchronized At JavaBridgeReturnValuesTest.java
+M P SIC: Should org.chromium.android_webview.test.TestAwContentsClient$AddMessageToConsoleHelper be a _static_ inner class? At TestAwContentsClient.java
+M P SIC: Should org.chromium.android_webview.test.TestContentProvider$ProviderStateCursor be a _static_ inner class? At TestContentProvider.java
+M P SIC: Should org.chromium.content.browser.ContentViewGestureHandlerTest$GestureRecordingMotionEventDelegate$GestureEvent be a _static_ inner class? At ContentViewGestureHandlerTest.java
+M P SIC: Should org.chromium.content.browser.JavaBridgeArrayCoercionTest$CustomType be a _static_ inner class? At JavaBridgeArrayCoercionTest.java
+M P SIC: Should org.chromium.content.browser.JavaBridgeFieldsTest$CustomType be a _static_ inner class? At JavaBridgeFieldsTest.java
+M P SIC: Should org.chromium.content.browser.JavaBridgeReturnValuesTest$CustomType be a _static_ inner class? At JavaBridgeReturnValuesTest.java
+M P SIC: Should org.chromium.content.browser.PopupZoomerTest$CustomCanvasPopupZoomer be a _static_ inner class? At PopupZoomerTest.java
+M P SS: Unread field: org.chromium.android_webview.test.util.ImagePageGenerator.IMAGE_PREFIX; should this field be static? At ImagePageGenerator.java
+M P SS: Unread field: org.chromium.android_webview.test.util.ImagePageGenerator.IMAGE_SUFFIX; should this field be static? At ImagePageGenerator.java
+M P SS: Unread field: org.chromium.native_test.ChromeNativeTestActivity.EXTRA_RUN_IN_SUB_THREAD; should this field be static? At ChromeNativeTestActivity.java
+M P SS: Unread field: org.chromium.native_test.ChromeNativeTestActivity.TAG; should this field be static? At ChromeNativeTestActivity.java
+M P UrF: Unread field: org.chromium.content.browser.ContentViewGestureHandlerTest$MockListener.mLastFling2 At ContentViewGestureHandlerTest.java
+M P UrF: Unread field: org.chromium.content.browser.ContentViewGestureHandlerTest$MockListener.mLastScroll1 At ContentViewGestureHandlerTest.java
+M P UrF: Unread field: org.chromium.content.browser.ContentViewGestureHandlerTest$MockListener.mLastScroll2 At ContentViewGestureHandlerTest.java
+M P UrF: Unread field: org.chromium.content.browser.ContentViewGestureHandlerTest$MockListener.mLastScrollDistanceX At ContentViewGestureHandlerTest.java
+M P UrF: Unread field: org.chromium.content.browser.ContentViewGestureHandlerTest$MockListener.mLastScrollDistanceY At ContentViewGestureHandlerTest.java
+M P UrF: Unread field: org.chromium.content.browser.HandleView.mHeight At HandleView.java
+M P UuF: Unused field: org.chromium.content.browser.HandleView.mLongPressCallback In HandleView.java
+M P UuF: Unused field: org.chromium.content.browser.JavaBridgeBasicsTest$19.privateField In JavaBridgeBasicsTest.java
+M P UuF: Unused field: org.chromium.content.browser.JavaBridgeBasicsTest$23.field In JavaBridgeBasicsTest.java
+M V EI2: new org.chromium.chrome.browser.FindMatchRectsDetails(int, RectF[], RectF) may expose internal representation by storing an externally mutable object into FindMatchRectsDetails.rects At FindMatchRectsDetails.java
+M V EI2: org.chromium.chrome.browser.ChromeBrowserProvider$BookmarkNode.setFavicon(byte[]) may expose internal representation by storing an externally mutable object into ChromeBrowserProvider$BookmarkNode.mFavicon At ChromeBrowserProvider.java
+M V EI2: org.chromium.chrome.browser.ChromeBrowserProvider$BookmarkNode.setThumbnail(byte[]) may expose internal representation by storing an externally mutable object into ChromeBrowserProvider$BookmarkNode.mThumbnail At ChromeBrowserProvider.java
+M V EI2: org.chromium.content.browser.LoadUrlParams.setPostData(byte[]) may expose internal representation by storing an externally mutable object into LoadUrlParams.mPostData At LoadUrlParams.java
+M V EI: org.chromium.chrome.browser.ChromeBrowserProvider$BookmarkNode.favicon() may expose internal representation by returning ChromeBrowserProvider$BookmarkNode.mFavicon At ChromeBrowserProvider.java
+M V EI: org.chromium.chrome.browser.ChromeBrowserProvider$BookmarkNode.thumbnail() may expose internal representation by returning ChromeBrowserProvider$BookmarkNode.mThumbnail At ChromeBrowserProvider.java
+M V MS: org.chromium.android_webview.AwResource.RAW_LOAD_ERROR isn't final and can't be protected from malicious code In AwResource.java
+M V MS: org.chromium.android_webview.AwResource.RAW_NO_DOMAIN isn't final and can't be protected from malicious code In AwResource.java
+M V MS: org.chromium.android_webview.AwResource.STRING_DEFAULT_TEXT_ENCODING isn't final and can't be protected from malicious code In AwResource.java
+M V MS: org.chromium.content.browser.LoadUrlParams.LOAD_TYPE_BROWSER_INITIATED_HTTP_POST should be package protected In LoadUrlParams.java
+M V MS: org.chromium.content.browser.LoadUrlParams.LOAD_TYPE_DATA isn't final and can't be protected from malicious code In LoadUrlParams.java
+M V MS: org.chromium.content.browser.LoadUrlParams.LOAD_TYPE_DEFAULT should be package protected In LoadUrlParams.java
+M V MS: org.chromium.content.browser.LoadUrlParams.UA_OVERRIDE_INHERIT should be package protected In LoadUrlParams.java
+M V MS: org.chromium.content.browser.LoadUrlParams.UA_OVERRIDE_TRUE should be package protected In LoadUrlParams.java
+M C RCN: Nullcheck of GestureDetector.mVelocityTracker at line 630 of value previously dereferenced in org.chromium.content.browser.third_party.GestureDetector.onTouchEvent(MotionEvent) At GestureDetector.java
+M D SF: Switch statement found in org.chromium.content.browser.third_party.GestureDetector.onTouchEvent(MotionEvent) where default case is missing At GestureDetector.java
+M D ST: Write to static field org.chromium.content.browser.ContentSettings.sAppCachePathIsSet from instance method org.chromium.content.browser.ContentSettings.setAppCachePath(String) At ContentSettings.java
diff --git a/src/build/android/gdb_apk b/src/build/android/gdb_apk
new file mode 100755
index 0000000..7e657d6
--- /dev/null
+++ b/src/build/android/gdb_apk
@@ -0,0 +1,171 @@
+#!/bin/bash
+#
+# 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.
+#
+# Attach gdb to a running android application. Similar to ndk-gdb.
+# Run with --annotate=3 if running under emacs (M-x gdb).
+#
+# By default it is used to debug content shell, if it is used to
+# debug other piceces, '-p' and '-l' options are needed.
+# For *unittests_apk (like base_unittests_apk), run with:
+# "gdb_apk -p org.chromium.native_test -l out/Release/lib.target -r"
+
+# Run a command through adb shell, strip the extra \r from the output
+# and return the correct status code to detect failures. This assumes
+# that the adb shell command prints a final \n to stdout.
+# args: command to run
+# Prints the command's stdout on stdout
+# Returns the command's status
+# Note: the command's stderr is lost
+adb_shell () {
+ local TMPOUT="$(mktemp)"
+ local LASTLINE RET
+ local ADB=${ADB:-adb}
+
+ # The weird sed rule is to strip the final \r on each output line
+ # Since 'adb shell' never returns the command's proper exit/status code,
+ # we force it to print it as '%%<status>' in the temporary output file,
+ # which we will later strip from it.
+ $ADB shell $@ ";" echo "%%\$?" 2>/dev/null | sed -e 's![[:cntrl:]]!!g' > $TMPOUT
+ # Get last line in log, which contains the exit code from the command
+ LASTLINE=$(sed -e '$!d' $TMPOUT)
+ # Extract the status code from the end of the line, which must be '%%<code>'
+ RET=$(echo "$LASTLINE" | awk '{ if (match($0, "%%[0-9]+$")) { print substr($0,RSTART+2); } }')
+ # Remove the status code from the last line. Note that this may result in an empty line
+ LASTLINE=$(echo "$LASTLINE" | awk '{ if (match($0, "%%[0-9]+$")) { print substr($0,1,RSTART-1); } }')
+ # The output itself: all lines except the status code
+ sed -e '$d' $TMPOUT && echo -n "$LASTLINE"
+ # Remove temp file
+ rm -f $TMPOUT
+ # Exit with the appropriate status
+ return $RET
+}
+
+adb=$(which adb)
+if [[ "$adb" = "" ]] ; then
+ echo "Need adb in your path"
+ exit 1
+fi
+
+usage() {
+ echo "usage: ${0##*/} [-p package_name] [-l shared_lib_dir] [-g gdb] [-r]"
+ echo "-p package_name the android APK package to be debugged"
+ echo "-l shared_lib_dir directory containes native shared library"
+ echo "-g gdb_args agruments for gdb, eg: -g '-n -write'"
+ echo "-r the target device is rooted"
+}
+
+process_options() {
+ local OPTNAME OPTIND OPTERR OPTARG
+ while getopts ":p:l:g:r" OPTNAME; do
+ case "$OPTNAME" in
+ p)
+ package_name="$OPTARG"
+ ;;
+ l)
+ shared_lib_dir="$OPTARG"
+ ;;
+ g)
+ gdb_args="$OPTARG"
+ ;;
+ r)
+ rooted_phone=1
+ ;;
+ \:)
+ echo "'-$OPTARG' needs an argument."
+ usage
+ exit 1
+ ;;
+ *)
+ echo "invalid command line option: $OPTARG"
+ usage
+ exit 1
+ ;;
+ esac
+ done
+
+ if [ $# -ge ${OPTIND} ]; then
+ eval echo "Unexpected command line argument: \${${OPTIND}}"
+ usage
+ exit 1
+ fi
+}
+
+rooted_phone=0
+
+root=$(dirname $0)/../..
+package_name=org.chromium.content_shell
+shared_lib_dir=$root/out/${BUILDTYPE:-Debug}/lib.target
+gdb_args=''
+
+#process options
+process_options "$@"
+echo "Debug package $package_name"
+echo "Assume native shared library is under $shared_lib_dir"
+
+data_dir=/data/data/$package_name
+gdb_server_on_device=$data_dir/lib/gdbserver
+
+# Kill any running gdbserver
+pid=$(adb shell ps | awk '/gdbserver/ {print $2}')
+if [[ "$pid" != "" ]] ; then
+ if [[ $rooted_phone -eq 1 ]] ; then
+ adb shell kill $pid
+ else
+ adb shell run-as $package_name kill $pid
+ fi
+fi
+
+pid=$(adb_shell ps | awk "/$package_name$/ {print \$2}")
+if [[ "$pid" = "" ]] ; then
+ echo "No $package_name running?"
+ echo "Try this: adb shell am start -a android.intent.action.VIEW " \
+ "-n $package_name/.SomethingActivity (Something might be ContentShell)"
+ exit 2
+fi
+
+no_gdb_server=$(adb shell ls $gdb_server_on_device | grep 'No such file')
+if [[ "$no_gdb_server" != "" ]] ; then
+ echo "No gdb server on device at $gdb_server_on_device"
+ echo "Please install a debug build."
+ exit 3
+fi
+
+if [[ $rooted_phone -eq 1 ]] ; then
+ adb shell $gdb_server_on_device :4321 --attach $pid &
+ adb forward tcp:4321 tcp:4321
+else
+ adb shell run-as $package_name lib/gdbserver +debug-socket --attach $pid &
+ adb forward tcp:4321 localfilesystem:$data_dir/debug-socket
+fi
+sleep 2
+
+# Pull app_process and C libraries from device if needed
+app_process=${shared_lib_dir}/app_process
+if [[ ! -f ${app_process} ]] ; then
+ adb pull /system/bin/app_process ${app_process}
+ adb pull /system/lib/libc.so ${shared_lib_dir}
+fi
+
+# gdb commands
+cmdfile=$(mktemp /tmp/gdb_android_XXXXXXXX)
+cat >$cmdfile<<EOF
+# set solib-absolute-prefix null
+set solib-search-path ${shared_lib_dir}
+file ${app_process}
+target remote :4321
+EOF
+
+gdb=$(echo $ANDROID_TOOLCHAIN/../../linux-x86/bin/*gdb)
+if [[ ! -f ${gdb} ]] ; then
+ echo "Wow no gdb in env var ANDROID_TOOLCHAIN which is $ANDROID_TOOLCHAIN"
+ exit 4
+else
+ echo Using $gdb
+fi
+
+# ${gdb} -x $cmdfile $* $app_process
+${gdb} -x $cmdfile $gdb_args
+rm $cmdfile
diff --git a/src/build/android/gdb_content_shell b/src/build/android/gdb_content_shell
new file mode 100755
index 0000000..c8cb88f
--- /dev/null
+++ b/src/build/android/gdb_content_shell
@@ -0,0 +1,16 @@
+#!/bin/bash
+#
+# 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.
+#
+# Attach gdb to a running content shell. Redirect to the shell gdb_apk
+
+ROOT=$(cd "$(dirname $0)"; pwd)
+echo "WARNING: This script is deprecated, consider using \
+adb_gdb_content_shell instead!"
+if [ $# -gt 0 ]; then
+ exec ${ROOT}/gdb_apk -r -g "$*"
+else
+ exec ${ROOT}/gdb_apk -r -p org.chromium.content_shell
+fi
diff --git a/src/build/android/gtest_filter/base_unittests_disabled b/src/build/android/gtest_filter/base_unittests_disabled
new file mode 100644
index 0000000..37bf65b
--- /dev/null
+++ b/src/build/android/gtest_filter/base_unittests_disabled
@@ -0,0 +1,27 @@
+# List of suppressions
+
+# Android will not support StackTrace.
+StackTrace.*
+#
+# Sometimes this is automatically generated by run_tests.py
+VerifyPathControlledByUserTest.Symlinks
+
+# http://crbug.com/138845
+MessagePumpLibeventTest.TestWatchingFromBadThread
+
+StringPrintfTest.StringPrintfMisc
+StringPrintfTest.StringAppendfString
+StringPrintfTest.StringAppendfInt
+StringPrintfTest.StringPrintfBounds
+ProcessUtilTest.GetAppOutputRestrictedSIGPIPE
+# TODO(jrg): Fails on bots. Works locally. Figure out why. 2/6/12
+FieldTrialTest.*
+# TODO(zhenghao): Fail from build 6102 r123270. http://crbug.com/115612
+StackContainer.BufferAlignment
+# Failed on bot since it was first introduced.
+FileUtilProxyTest.Touch
+# Flaky?
+ScopedJavaRefTest.RefCounts
+# Death tests are not supported with apks.
+*DeathTest*
+
diff --git a/src/build/android/gtest_filter/base_unittests_emulator_additional_disabled b/src/build/android/gtest_filter/base_unittests_emulator_additional_disabled
new file mode 100644
index 0000000..85e8fd6
--- /dev/null
+++ b/src/build/android/gtest_filter/base_unittests_emulator_additional_disabled
@@ -0,0 +1,10 @@
+# Addtional list of suppressions from emulator
+#
+# Automatically generated by run_tests.py
+PathServiceTest.Get
+SharedMemoryTest.OpenClose
+StringPrintfTest.StringAppendfInt
+StringPrintfTest.StringAppendfString
+StringPrintfTest.StringPrintfBounds
+StringPrintfTest.StringPrintfMisc
+VerifyPathControlledByUserTest.Symlinks
diff --git a/src/build/android/gtest_filter/breakpad_unittests_disabled b/src/build/android/gtest_filter/breakpad_unittests_disabled
new file mode 100644
index 0000000..32b07c9
--- /dev/null
+++ b/src/build/android/gtest_filter/breakpad_unittests_disabled
@@ -0,0 +1,5 @@
+FileIDStripTest.StripSelf
+# crbug.com/165916
+ExceptionHandlerTest.InstructionPointerMemory
+ExceptionHandlerTest.InstructionPointerMemoryMinBound
+ExceptionHandlerTest.InstructionPointerMemoryMaxBound
diff --git a/src/build/android/gtest_filter/content_unittests_disabled b/src/build/android/gtest_filter/content_unittests_disabled
new file mode 100644
index 0000000..61f0627
--- /dev/null
+++ b/src/build/android/gtest_filter/content_unittests_disabled
@@ -0,0 +1,21 @@
+# List of suppressions
+
+AudioRendererHostTest.CreateAndClose
+AudioRendererHostTest.CreateAndShutdown
+AudioRendererHostTest.CreatePlayAndClose
+AudioRendererHostTest.CreatePlayPauseAndClose
+AudioRendererHostTest.SetVolume
+AudioRendererHostTest.CreatePlayAndShutdown
+AudioRendererHostTest.CreatePlayPauseAndShutdown
+AudioRendererHostTest.SimulateError
+AudioRendererHostTest.SimulateErrorAndClose
+# crbug.com/104950
+DeviceOrientationProviderTest.ObserverNotRemoved
+DeviceOrientationProviderTest.StartFailing
+# crbug.com/139095
+RenderWidgetTest.OnMsgPaintAtSize
+# crbug.com/147549
+GamepadProviderTest.PollingAccess
+PepperGamepadHostTest.WaitForReply
+# crbug.com/159234
+WebContentsVideoCaptureDeviceTest.*
diff --git a/src/build/android/gtest_filter/ipc_tests_disabled b/src/build/android/gtest_filter/ipc_tests_disabled
new file mode 100644
index 0000000..e6d5f2d
--- /dev/null
+++ b/src/build/android/gtest_filter/ipc_tests_disabled
@@ -0,0 +1,15 @@
+# Times out
+IPCSyncChannelTest.ChattyServer
+
+# MultiProcessTest related failures. These tests fail if DCHECK is enabled.
+IPCChannelPosixTest.AdvancedConnected
+IPCChannelPosixTest.ResetState
+IPCChannelPosixTest.MultiConnection
+IPCFuzzingTest.SanityTest
+IPCFuzzingTest.MsgBadPayloadArgs
+IPCFuzzingTest.MsgBadPayloadShort
+IPCChannelTest.DescriptorTest
+IPCChannelTest.ChannelTest
+IPCChannelTest.ChannelProxyTest
+IPCChannelTest.SendMessageInChannelConnected
+SyncSocketTest.SanityTest
diff --git a/src/build/android/gtest_filter/media_unittests_disabled b/src/build/android/gtest_filter/media_unittests_disabled
new file mode 100644
index 0000000..2690683
--- /dev/null
+++ b/src/build/android/gtest_filter/media_unittests_disabled
@@ -0,0 +1,11 @@
+# List of suppressions
+
+# Death tests are not supported on APK
+# http://crbug.com/138855
+CompositeFilterDeathTest.*
+
+# http://crbug.com/138833
+AesDecryptorTest.*
+
+# crbug.com/138930
+SkCanvasVideoRendererTest.*
diff --git a/src/build/android/gtest_filter/net_unittests_disabled b/src/build/android/gtest_filter/net_unittests_disabled
new file mode 100644
index 0000000..7828aec
--- /dev/null
+++ b/src/build/android/gtest_filter/net_unittests_disabled
@@ -0,0 +1,51 @@
+# List of suppressions.
+CertVerifyProcTest.ExtraneousMD5RootCert
+CertVerifyProcTest.IntermediateCARequireExplicitPolicy
+CertVerifyProcTest.PublicKeyHashes
+CertVerifyProcTest.RejectWeakKeys
+CertVerifyProcTest.VerifyReturnChainBasic
+CertVerifyProcTest.VerifyReturnChainFiltersUnrelatedCerts
+CertVerifyProcTest.VerifyReturnChainProperlyOrdered
+HTTPSCRLSetTest.ExpiredCRLSet
+HTTPSEVCRLSetTest.FreshCRLSet
+HTTPSRequestTest.ClientAuthTest
+HTTPSRequestTest.ResumeTest
+HTTPSRequestTest.SSLSessionCacheShardTest
+PythonUtils.PythonRunTime
+TransportSecurityStateTest.ValidPinsHeadersSHA1
+TransportSecurityStateTest.ValidPinsHeadersSHA256
+URLRequestTestHTTP.ProcessSTS
+URLRequestTestHTTP.ProcessSTSOnce
+VerifyEndEntity/CertVerifyProcWeakDigestTest.Verify/0
+VerifyEndEntity/CertVerifyProcWeakDigestTest.Verify/1
+VerifyEndEntity/CertVerifyProcWeakDigestTest.Verify/2
+VerifyIncompleteEndEntity/CertVerifyProcWeakDigestTest.Verify/0
+VerifyIncompleteEndEntity/CertVerifyProcWeakDigestTest.Verify/1
+VerifyIncompleteEndEntity/CertVerifyProcWeakDigestTest.Verify/2
+VerifyIncompleteIntermediate/CertVerifyProcWeakDigestTest.Verify/0
+VerifyIncompleteIntermediate/CertVerifyProcWeakDigestTest.Verify/1
+VerifyIncompleteIntermediate/CertVerifyProcWeakDigestTest.Verify/2
+VerifyIntermediate/CertVerifyProcWeakDigestTest.Verify/0
+VerifyIntermediate/CertVerifyProcWeakDigestTest.Verify/1
+VerifyIntermediate/CertVerifyProcWeakDigestTest.Verify/2
+VerifyMixed/CertVerifyProcWeakDigestTest.Verify/0
+VerifyMixed/CertVerifyProcWeakDigestTest.Verify/1
+VerifyMixed/CertVerifyProcWeakDigestTest.Verify/2
+VerifyRoot/CertVerifyProcWeakDigestTest.Verify/0
+VerifyRoot/CertVerifyProcWeakDigestTest.Verify/1
+VerifyRoot/CertVerifyProcWeakDigestTest.Verify/2
+# Fail only on bots.
+CertVerifyProcTest.TestKnownRoot
+CertVerifyProcTest.WithoutRevocationChecking
+HttpCache.RangeGET_Cancel
+HttpCache.RangeGET_Cancel2
+HttpCache.RangeGET_OK
+HttpCache.RangeGET_Previous200
+HttpCache.RangeGET_Revalidate2
+HttpCache.RangeGET_SyncOK
+HttpCache.TypicalGET_ConditionalRequest
+# Death tests are not supported with apks.
+*DeathTest*
+# These are death tests and thus also disabled.
+PrioritizedDispatcherTest.CancelNull
+PrioritizedDispatcherTest.CancelMissing
diff --git a/src/build/android/gtest_filter/sync_unit_tests_disabled b/src/build/android/gtest_filter/sync_unit_tests_disabled
new file mode 100644
index 0000000..cc4b72d
--- /dev/null
+++ b/src/build/android/gtest_filter/sync_unit_tests_disabled
@@ -0,0 +1,4 @@
+SyncHttpBridgeTest.*
+
+# crbug.com/144422
+OnDiskSyncableDirectory.FailInitialWrite
diff --git a/src/build/android/gtest_filter/ui_unittests_disabled b/src/build/android/gtest_filter/ui_unittests_disabled
new file mode 100644
index 0000000..6515b19
--- /dev/null
+++ b/src/build/android/gtest_filter/ui_unittests_disabled
@@ -0,0 +1,40 @@
+# List of suppressions
+# This file was automatically generated by build/android/run_tests.py
+CanvasTest.StringSizeEmptyString
+CanvasTest.StringWidth
+ClipboardTest.RTFTest
+FontListTest.FontDescString_FromFont
+FontListTest.FontDescString_FromFontVector
+FontListTest.FontDescString_FromFontWithNonNormalStyle
+FontListTest.Fonts_DeriveFontList
+FontListTest.Fonts_DeriveFontListWithSize
+FontListTest.Fonts_DescStringWithStyleInFlexibleFormat_RoundTrip
+FontListTest.Fonts_FontVector_RoundTrip
+FontListTest.Fonts_FromDescString
+FontListTest.Fonts_FromDescStringInFlexibleFormat
+FontListTest.Fonts_FromDescStringWithStyleInFlexibleFormat
+FontListTest.Fonts_FromFont
+FontListTest.Fonts_FromFontVector
+FontListTest.Fonts_FromFontWithNonNormalStyle
+FontListTest.Fonts_GetStyle
+FontTest.Ascent
+FontTest.AvgCharWidth
+FontTest.AvgWidths
+FontTest.Height
+FontTest.LoadArial
+FontTest.LoadArialBold
+FontTest.Widths
+ResourceBundleTest.DelegateGetFont
+TextEliderTest.ElideEmail
+TextEliderTest.ElideEmailMoreSpace
+TextEliderTest.ElideRectangleText
+TextEliderTest.ElideRectangleTextLongWords
+TextEliderTest.ElideRectangleTextPunctuation
+TextEliderTest.ElideTextLongStrings
+TextEliderTest.ElideTextSurrogatePairs
+TextEliderTest.ElideTextTruncate
+TextEliderTest.TestFileURLEliding
+TextEliderTest.TestFilenameEliding
+TextEliderTest.TestGeneralEliding
+TextEliderTest.TestMoreEliding
+TextEliderTest.TestTrailingEllipsisSlashEllipsisHack
diff --git a/src/build/android/gtest_filter/unit_tests_disabled b/src/build/android/gtest_filter/unit_tests_disabled
new file mode 100644
index 0000000..9228542
--- /dev/null
+++ b/src/build/android/gtest_filter/unit_tests_disabled
@@ -0,0 +1,121 @@
+# List of suppressions
+
+# crbug.com/139429
+BrowserMainTest.WarmConnectionFieldTrial_Invalid
+BrowserMainTest.WarmConnectionFieldTrial_Random
+BrowserMainTest.WarmConnectionFieldTrial_WarmestSocket
+
+# The UDP related tests currently do not work on Android because
+# we lack a UDP forwarder tool.
+NetworkStatsTestUDP.*
+
+# Missing test resource of 16MB.
+HistoryProfileTest.TypicalProfileVersion
+
+# crbug.com/139408
+SQLitePersistentCookieStoreTest.TestDontLoadOldSessionCookies
+SQLitePersistentCookieStoreTest.PersistIsPersistent
+
+# crbug.com/139433
+AutofillTableTest.AutofillProfile*
+AutofillTableTest.UpdateAutofillProfile
+
+# crbug.com/139400
+AutofillProfileTest.*
+CreditCardTest.SetInfoExpirationMonth
+
+# crbug.com/139398
+DownloadItemModelTest.InterruptTooltip
+
+# Tests crashing in the APK
+# l10n_util.cc(655)] Check failed: std::string::npos != pos
+DownloadItemModelTest.InterruptStatus
+# l10n_util.cc(655)] Check failed: std::string::npos != pos
+WebsiteSettingsTest.OnSiteDataAccessed
+
+# crbug.com/139423
+ValueStoreFrontendTest.GetExistingData
+
+# crbug.com/139421
+ChromeSelectFilePolicyTest.ExpectAsynchronousListenerCall
+
+# http://crbug.com/139033
+ChromeDownloadManagerDelegateTest.StartDownload_PromptAlways
+
+# Extension support is limited on Android.
+# Some of these can be enabled if we register extension related prefs in
+# browser_prefs.cc
+ExtensionTest.*
+ExtensionAPI.*
+ExtensionFileUtil.*
+ExtensionPermissionsTest.*
+ExtensionUnpackerTest.*
+ActiveTabTest.*
+ExtensionAppsPromo.*
+ComponentLoaderTest.*
+ExtensionFromUserScript.*
+ExtensionFromWebApp.*
+ExtensionIconManagerTest.*
+ExtensionServiceTest.*
+ExtensionServiceTestSimple.*
+ExtensionSourcePriorityTest.*
+ExtensionSpecialStoragePolicyTest.*
+ExternalPolicyProviderTest.*
+MenuManagerTest.*
+PageActionControllerTest.*
+PermissionsUpdaterTest.*
+ImageLoaderTest.*
+ImageLoadingTrackerTest.*
+ScriptBadgeControllerTest.*
+ExtensionSettingsFrontendTest.*
+ExtensionSettingsSyncTest.*
+ExtensionUpdaterTest.*
+UserScriptListenerTest.*
+WebApplicationTest.GetShortcutInfoForTab
+ExtensionActionIconFactoryTest.*
+
+# crbug.com/139411
+AutocompleteProviderTest.*
+HistoryContentsProviderBodyOnlyTest.*
+HistoryContentsProviderTest.*
+HQPOrderingTest.*
+SearchProviderTest.*
+
+ProtocolHandlerRegistryTest.TestOSRegistrationFailure
+
+# crbug.com/139418
+SQLiteServerBoundCertStoreTest.TestUpgradeV1
+SQLiteServerBoundCertStoreTest.TestUpgradeV2
+
+ProfileSyncComponentsFactoryImplTest.*
+PermissionsTest.GetWarningMessages_Plugins
+ImageOperations.ResizeShouldAverageColors
+
+# crbug.com/138275
+PrerenderTest.*
+RenderWidgetTest.OnMsgPaintAtSize
+
+# crbug.com/139643
+VariationsUtilTest.DisableAfterInitialization
+VariationsUtilTest.AssociateGoogleVariationID
+VariationsUtilTest.NoAssociation
+
+# crbug.com/141473
+AutofillManagerTest.UpdatePasswordSyncState
+AutofillManagerTest.UpdatePasswordGenerationState
+
+# crbug.com/144227
+ExtensionIconImageTest.*
+
+# crbug.com/145843
+EntropyProviderTest.UseOneTimeRandomizationSHA1
+EntropyProviderTest.UseOneTimeRandomizationPermuted
+
+# crbug.com/147500
+ManifestTest.RestrictedKeys
+
+# crbug.com/152599
+SyncSearchEngineDataTypeControllerTest.*
+
+# Death tests are not supported with apks.
+*DeathTest*
diff --git a/src/build/android/gtest_filter/webkit_unit_tests_disabled b/src/build/android/gtest_filter/webkit_unit_tests_disabled
new file mode 100644
index 0000000..a0864d3
--- /dev/null
+++ b/src/build/android/gtest_filter/webkit_unit_tests_disabled
@@ -0,0 +1,8 @@
+# List of suppressions
+
+# crbug.com/159935
+ScrollingCoordinatorChromiumTest.nonFastScrollableRegion
+WebCompositorInputHandlerImplTest.gestureFlingAnimates
+WebCompositorInputHandlerImplTest.gestureFlingTransferResets
+WebPageSerializerTest.HTMLNodes
+
diff --git a/src/build/android/java_cpp_template.gypi b/src/build/android/java_cpp_template.gypi
new file mode 100644
index 0000000..3c5704e
--- /dev/null
+++ b/src/build/android/java_cpp_template.gypi
@@ -0,0 +1,70 @@
+# 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.
+
+# This file is meant to be included into a target to provide a rule
+# to generate Java source files from templates that are processed
+# through the host C pre-processor.
+#
+# This assumes a GNU-compatible pre-processor installed as 'cpp'.
+# Only tested on Linux.
+#
+# To use this, create a gyp target with the following form:
+# {
+# 'target_name': 'android_net_java_constants',
+# 'type': 'none',
+# 'sources': [
+# 'net/android/NetError.template',
+# ],
+# 'variables': {
+# 'package_name': 'org.chromium.net',
+# 'template_deps': ['net/base/certificate_mime_type_list.h'],
+# },
+# 'includes': [ '../build/android/java_constants.gypi' ],
+# },
+#
+# The 'sources' entry should only list template file. The template file
+# itself should use the 'ClassName.template' format, and will generate
+# 'gen/templates/<package-name>/ClassName.java. The files which template
+# dependents on and typically included by the template should be listed
+# in template_deps variables. Any change to them will force a rebuild of
+# the template, and hence of any source that depends on it.
+#
+
+{
+ # Location where all generated Java sources will be placed.
+ 'variables': {
+ 'output_dir': '<(SHARED_INTERMEDIATE_DIR)/templates/<(package_name)'
+ },
+ # Ensure that the output directory is used in the class path
+ # when building targets that depend on this one.
+ 'direct_dependent_settings': {
+ 'variables': {
+ 'generated_src_dirs': [
+ '<(output_dir)/',
+ ],
+ },
+ },
+ # Define a single rule that will be apply to each .template file
+ # listed in 'sources'.
+ 'rules': [
+ {
+ 'rule_name': 'generate_java_constants',
+ 'extension': 'template',
+ # Set template_deps as additional dependencies.
+ 'inputs': ['<@(template_deps)'],
+ 'outputs': [
+ '<(output_dir)/<(RULE_INPUT_ROOT).java'
+ ],
+ 'action': [
+ 'cpp', # invoke host pre-processor.
+ '-x', 'c-header', # treat sources as C header files
+ '-P', # disable line markers, i.e. '#line 309'
+ '-I', '<(DEPTH)', # Add project top-level to include path
+ '-o', '<@(_outputs)', # Specify output file
+ '<(RULE_INPUT_PATH)', # Specify input file
+ ],
+ 'message': 'Generating Java from cpp template <(RULE_INPUT_PATH)',
+ }
+ ],
+}
diff --git a/src/build/android/lighttpd_server.py b/src/build/android/lighttpd_server.py
new file mode 100755
index 0000000..11ae794
--- /dev/null
+++ b/src/build/android/lighttpd_server.py
@@ -0,0 +1,253 @@
+#!/usr/bin/env python
+#
+# 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 a convenient wrapper for spawning a test lighttpd instance.
+
+Usage:
+ lighttpd_server PATH_TO_DOC_ROOT
+"""
+
+import codecs
+import contextlib
+import httplib
+import os
+import random
+import shutil
+import socket
+import subprocess
+import sys
+import tempfile
+import time
+
+from pylib import constants
+from pylib import pexpect
+
+class LighttpdServer(object):
+ """Wraps lighttpd server, providing robust startup.
+
+ Args:
+ document_root: Path to root of this server's hosted files.
+ port: TCP port on the _host_ machine that the server will listen on. If
+ ommitted it will attempt to use 9000, or if unavailable it will find
+ a free port from 8001 - 8999.
+ lighttpd_path, lighttpd_module_path: Optional paths to lighttpd binaries.
+ base_config_path: If supplied this file will replace the built-in default
+ lighttpd config file.
+ extra_config_contents: If specified, this string will be appended to the
+ base config (default built-in, or from base_config_path).
+ config_path, error_log, access_log: Optional paths where the class should
+ place temprary files for this session.
+ """
+
+ def __init__(self, document_root, port=None,
+ lighttpd_path=None, lighttpd_module_path=None,
+ base_config_path=None, extra_config_contents=None,
+ config_path=None, error_log=None, access_log=None):
+ self.temp_dir = tempfile.mkdtemp(prefix='lighttpd_for_chrome_android')
+ self.document_root = os.path.abspath(document_root)
+ self.fixed_port = port
+ self.port = port or constants.LIGHTTPD_DEFAULT_PORT
+ self.server_tag = 'LightTPD ' + str(random.randint(111111, 999999))
+ self.lighttpd_path = lighttpd_path or '/usr/sbin/lighttpd'
+ self.lighttpd_module_path = lighttpd_module_path or '/usr/lib/lighttpd'
+ self.base_config_path = base_config_path
+ self.extra_config_contents = extra_config_contents
+ self.config_path = config_path or self._Mktmp('config')
+ self.error_log = error_log or self._Mktmp('error_log')
+ self.access_log = access_log or self._Mktmp('access_log')
+ self.pid_file = self._Mktmp('pid_file')
+ self.process = None
+
+ def _Mktmp(self, name):
+ return os.path.join(self.temp_dir, name)
+
+ def _GetRandomPort(self):
+ # The ports of test server is arranged in constants.py.
+ return random.randint(constants.LIGHTTPD_RANDOM_PORT_FIRST,
+ constants.LIGHTTPD_RANDOM_PORT_LAST)
+
+ def StartupHttpServer(self):
+ """Starts up a http server with specified document root and port."""
+ # If we want a specific port, make sure no one else is listening on it.
+ if self.fixed_port:
+ self._KillProcessListeningOnPort(self.fixed_port)
+ while True:
+ if self.base_config_path:
+ # Read the config
+ with codecs.open(self.base_config_path, 'r', 'utf-8') as f:
+ config_contents = f.read()
+ else:
+ config_contents = self._GetDefaultBaseConfig()
+ if self.extra_config_contents:
+ config_contents += self.extra_config_contents
+ # Write out the config, filling in placeholders from the members of |self|
+ with codecs.open(self.config_path, 'w', 'utf-8') as f:
+ f.write(config_contents % self.__dict__)
+ if (not os.path.exists(self.lighttpd_path) or
+ not os.access(self.lighttpd_path, os.X_OK)):
+ raise EnvironmentError(
+ 'Could not find lighttpd at %s.\n'
+ 'It may need to be installed (e.g. sudo apt-get install lighttpd)'
+ % self.lighttpd_path)
+ self.process = pexpect.spawn(self.lighttpd_path,
+ ['-D', '-f', self.config_path,
+ '-m', self.lighttpd_module_path],
+ cwd=self.temp_dir)
+ client_error, server_error = self._TestServerConnection()
+ if not client_error:
+ assert int(open(self.pid_file, 'r').read()) == self.process.pid
+ break
+ self.process.close()
+
+ if self.fixed_port or not 'in use' in server_error:
+ print 'Client error:', client_error
+ print 'Server error:', server_error
+ return False
+ self.port = self._GetRandomPort()
+ return True
+
+ def ShutdownHttpServer(self):
+ """Shuts down our lighttpd processes."""
+ if self.process:
+ self.process.terminate()
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
+
+ def _TestServerConnection(self):
+ # Wait for server to start
+ server_msg = ''
+ for timeout in xrange(1, 5):
+ client_error = None
+ try:
+ with contextlib.closing(httplib.HTTPConnection(
+ '127.0.0.1', self.port, timeout=timeout)) as http:
+ http.set_debuglevel(timeout > 3)
+ http.request('HEAD', '/')
+ r = http.getresponse()
+ r.read()
+ if (r.status == 200 and r.reason == 'OK' and
+ r.getheader('Server') == self.server_tag):
+ return (None, server_msg)
+ client_error = ('Bad response: %s %s version %s\n ' %
+ (r.status, r.reason, r.version) +
+ '\n '.join([': '.join(h) for h in r.getheaders()]))
+ except (httplib.HTTPException, socket.error) as client_error:
+ pass # Probably too quick connecting: try again
+ # Check for server startup error messages
+ ix = self.process.expect([pexpect.TIMEOUT, pexpect.EOF, '.+'],
+ timeout=timeout)
+ if ix == 2: # stdout spew from the server
+ server_msg += self.process.match.group(0)
+ elif ix == 1: # EOF -- server has quit so giveup.
+ client_error = client_error or 'Server exited'
+ break
+ return (client_error or 'Timeout', server_msg)
+
+ def _KillProcessListeningOnPort(self, port):
+ """Checks if there is a process listening on port number |port| and
+ terminates it if found.
+
+ Args:
+ port: Port number to check.
+ """
+ if subprocess.call(['fuser', '-kv', '%d/tcp' % port]) == 0:
+ # Give the process some time to terminate and check that it is gone.
+ time.sleep(2)
+ assert subprocess.call(['fuser', '-v', '%d/tcp' % port]) != 0, \
+ 'Unable to kill process listening on port %d.' % port
+
+ def _GetDefaultBaseConfig(self):
+ return """server.tag = "%(server_tag)s"
+server.modules = ( "mod_access",
+ "mod_accesslog",
+ "mod_alias",
+ "mod_cgi",
+ "mod_rewrite" )
+
+# default document root required
+#server.document-root = "."
+
+# files to check for if .../ is requested
+index-file.names = ( "index.php", "index.pl", "index.cgi",
+ "index.html", "index.htm", "default.htm" )
+# mimetype mapping
+mimetype.assign = (
+ ".gif" => "image/gif",
+ ".jpg" => "image/jpeg",
+ ".jpeg" => "image/jpeg",
+ ".png" => "image/png",
+ ".svg" => "image/svg+xml",
+ ".css" => "text/css",
+ ".html" => "text/html",
+ ".htm" => "text/html",
+ ".xhtml" => "application/xhtml+xml",
+ ".xhtmlmp" => "application/vnd.wap.xhtml+xml",
+ ".js" => "application/x-javascript",
+ ".log" => "text/plain",
+ ".conf" => "text/plain",
+ ".text" => "text/plain",
+ ".txt" => "text/plain",
+ ".dtd" => "text/xml",
+ ".xml" => "text/xml",
+ ".manifest" => "text/cache-manifest",
+ )
+
+# Use the "Content-Type" extended attribute to obtain mime type if possible
+mimetype.use-xattr = "enable"
+
+##
+# which extensions should not be handle via static-file transfer
+#
+# .php, .pl, .fcgi are most often handled by mod_fastcgi or mod_cgi
+static-file.exclude-extensions = ( ".php", ".pl", ".cgi" )
+
+server.bind = "127.0.0.1"
+server.port = %(port)s
+
+## virtual directory listings
+dir-listing.activate = "enable"
+#dir-listing.encoding = "iso-8859-2"
+#dir-listing.external-css = "style/oldstyle.css"
+
+## enable debugging
+#debug.log-request-header = "enable"
+#debug.log-response-header = "enable"
+#debug.log-request-handling = "enable"
+#debug.log-file-not-found = "enable"
+
+#### SSL engine
+#ssl.engine = "enable"
+#ssl.pemfile = "server.pem"
+
+# Autogenerated test-specific config follows.
+
+cgi.assign = ( ".cgi" => "/usr/bin/env",
+ ".pl" => "/usr/bin/env",
+ ".asis" => "/bin/cat",
+ ".php" => "/usr/bin/php-cgi" )
+
+server.errorlog = "%(error_log)s"
+accesslog.filename = "%(access_log)s"
+server.upload-dirs = ( "/tmp" )
+server.pid-file = "%(pid_file)s"
+server.document-root = "%(document_root)s"
+
+"""
+
+
+def main(argv):
+ server = LighttpdServer(*argv[1:])
+ try:
+ if server.StartupHttpServer():
+ raw_input('Server running at http://127.0.0.1:%s -'
+ ' press Enter to exit it.' % server.port)
+ else:
+ print 'Server exit code:', server.process.exitstatus
+ finally:
+ server.ShutdownHttpServer()
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/src/build/android/prepare_library_for_apk b/src/build/android/prepare_library_for_apk
new file mode 100755
index 0000000..ce414b3
--- /dev/null
+++ b/src/build/android/prepare_library_for_apk
@@ -0,0 +1,19 @@
+#!/bin/bash
+# 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.
+
+# Creates a stripped copy of a library for inclusion in an apk.
+
+if [[ $# -ne 3 ]]
+then
+ echo "Usage: prepare_library_for_apk android_strip path/to/library stripped/library/output/path"
+ exit 1
+fi
+
+ANDROID_STRIP=$1
+LIBRARY=$2
+STRIPPED=$3
+
+set -ex
+$ANDROID_STRIP --strip-unneeded $LIBRARY -o $STRIPPED
diff --git a/src/build/android/process_resources.py b/src/build/android/process_resources.py
new file mode 100755
index 0000000..8a46e5c
--- /dev/null
+++ b/src/build/android/process_resources.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""Process Android library resources to generate R.java and crunched images."""
+
+import optparse
+import os
+import subprocess
+
+
+BUILD_ANDROID_DIR = os.path.dirname(__file__)
+
+
+def ParseArgs():
+ """Parses command line options.
+
+ Returns:
+ An options object as from optparse.OptionsParser.parse_args()
+ """
+ parser = optparse.OptionParser()
+ parser.add_option('--android-sdk', help='path to the Android SDK folder')
+ parser.add_option('--android-sdk-tools',
+ help='path to the Android SDK platform tools folder')
+ parser.add_option('--R-package', help='Java package for generated R.java')
+ parser.add_option('--R-dir', help='directory to hold generated R.java')
+ parser.add_option('--res-dir', help='directory containing resources')
+ parser.add_option('--crunched-res-dir',
+ help='directory to hold crunched resources')
+ (options, args) = parser.parse_args()
+
+ if args:
+ parser.error('No positional arguments should be given.')
+
+ # Check that required options have been provided.
+ required_options = ('android_sdk', 'android_sdk_tools', 'R_package',
+ 'R_dir', 'res_dir', 'crunched_res_dir')
+ for option_name in required_options:
+ if getattr(options, option_name) is None:
+ parser.error('--%s is required' % option_name.replace('_', '-'))
+
+ return options
+
+
+def main():
+ options = ParseArgs()
+ android_jar = os.path.join(options.android_sdk, 'android.jar')
+ aapt = os.path.join(options.android_sdk_tools, 'aapt')
+ dummy_manifest = os.path.join(BUILD_ANDROID_DIR, 'AndroidManifest.xml')
+
+ # Generate R.java. This R.java contains non-final constants and is used only
+ # while compiling the library jar (e.g. chromium_content.jar). When building
+ # an apk, a new R.java file with the correct resource -> ID mappings will be
+ # generated by merging the resources from all libraries and the main apk
+ # project.
+ subprocess.check_call([aapt,
+ 'package',
+ '-m',
+ '--non-constant-id',
+ '--custom-package', options.R_package,
+ '-M', dummy_manifest,
+ '-S', options.res_dir,
+ '-I', android_jar,
+ '-J', options.R_dir])
+
+ # Crunch image resources. This shrinks png files and is necessary for 9-patch
+ # images to display correctly.
+ subprocess.check_call([aapt,
+ 'crunch',
+ '-S', options.res_dir,
+ '-C', options.crunched_res_dir])
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/build/android/pylib/__init__.py b/src/build/android/pylib/__init__.py
new file mode 100644
index 0000000..727e987
--- /dev/null
+++ b/src/build/android/pylib/__init__.py
@@ -0,0 +1,4 @@
+# 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.
+
diff --git a/src/build/android/pylib/android_commands.py b/src/build/android/pylib/android_commands.py
new file mode 100644
index 0000000..66f705f
--- /dev/null
+++ b/src/build/android/pylib/android_commands.py
@@ -0,0 +1,1121 @@
+# 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 communicate with the device via the adb command.
+
+Assumes adb binary is currently on system path.
+"""
+
+import collections
+import datetime
+import logging
+import os
+import re
+import shlex
+import subprocess
+import sys
+import tempfile
+import time
+
+import io_stats_parser
+try:
+ import pexpect
+except:
+ pexpect = None
+
+CHROME_SRC = os.path.join(
+ os.path.abspath(os.path.dirname(__file__)), '..', '..', '..')
+
+sys.path.append(os.path.join(CHROME_SRC, 'third_party', 'android_testrunner'))
+import adb_interface
+
+import cmd_helper
+import errors # is under ../../../third_party/android_testrunner/errors.py
+
+
+# Pattern to search for the next whole line of pexpect output and capture it
+# into a match group. We can't use ^ and $ for line start end with pexpect,
+# see http://www.noah.org/python/pexpect/#doc for explanation why.
+PEXPECT_LINE_RE = re.compile('\n([^\r]*)\r')
+
+# Set the adb shell prompt to be a unique marker that will [hopefully] not
+# appear at the start of any line of a command's output.
+SHELL_PROMPT = '~+~PQ\x17RS~+~'
+
+# Java properties file
+LOCAL_PROPERTIES_PATH = '/data/local.prop'
+
+# Property in /data/local.prop that controls Java assertions.
+JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions'
+
+MEMORY_INFO_RE = re.compile('^(?P<key>\w+):\s+(?P<usage_kb>\d+) kB$')
+NVIDIA_MEMORY_INFO_RE = re.compile('^\s*(?P<user>\S+)\s*(?P<name>\S+)\s*'
+ '(?P<pid>\d+)\s*(?P<usage_bytes>\d+)$')
+
+# Keycode "enum" suitable for passing to AndroidCommands.SendKey().
+KEYCODE_HOME = 3
+KEYCODE_BACK = 4
+KEYCODE_DPAD_UP = 19
+KEYCODE_DPAD_DOWN = 20
+KEYCODE_DPAD_RIGHT = 22
+KEYCODE_ENTER = 66
+KEYCODE_MENU = 82
+
+MD5SUM_DEVICE_PATH = '/data/local/tmp/md5sum_bin'
+
+def GetEmulators():
+ """Returns a list of emulators. Does not filter by status (e.g. offline).
+
+ Both devices starting with 'emulator' will be returned in below output:
+
+ * daemon not running. starting it now on port 5037 *
+ * daemon started successfully *
+ List of devices attached
+ 027c10494100b4d7 device
+ emulator-5554 offline
+ emulator-5558 device
+ """
+ re_device = re.compile('^emulator-[0-9]+', re.MULTILINE)
+ devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices']))
+ return devices
+
+
+def GetAVDs():
+ """Returns a list of AVDs."""
+ re_avd = re.compile('^[ ]+Name: ([a-zA-Z0-9_:.-]+)', re.MULTILINE)
+ avds = re_avd.findall(cmd_helper.GetCmdOutput(['android', 'list', 'avd']))
+ return avds
+
+
+def GetAttachedDevices():
+ """Returns a list of attached, online android devices.
+
+ If a preferred device has been set with ANDROID_SERIAL, it will be first in
+ the returned list.
+
+ Example output:
+
+ * daemon not running. starting it now on port 5037 *
+ * daemon started successfully *
+ List of devices attached
+ 027c10494100b4d7 device
+ emulator-5554 offline
+ """
+ re_device = re.compile('^([a-zA-Z0-9_:.-]+)\tdevice$', re.MULTILINE)
+ devices = re_device.findall(cmd_helper.GetCmdOutput(['adb', 'devices']))
+ preferred_device = os.environ.get('ANDROID_SERIAL')
+ if preferred_device in devices:
+ devices.remove(preferred_device)
+ devices.insert(0, preferred_device)
+ return devices
+
+def IsDeviceAttached(device):
+ return device in GetAttachedDevices()
+
+def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None):
+ """Gets a list of files from `ls` command output.
+
+ Python's os.walk isn't used because it doesn't work over adb shell.
+
+ Args:
+ path: The path to list.
+ ls_output: A list of lines returned by an `ls -lR` command.
+ re_file: A compiled regular expression which parses a line into named groups
+ consisting of at minimum "filename", "date", "time", "size" and
+ optionally "timezone".
+ utc_offset: A 5-character string of the form +HHMM or -HHMM, where HH is a
+ 2-digit string giving the number of UTC offset hours, and MM is a
+ 2-digit string giving the number of UTC offset minutes. If the input
+ utc_offset is None, will try to look for the value of "timezone" if it
+ is specified in re_file.
+
+ Returns:
+ A dict of {"name": (size, lastmod), ...} where:
+ name: The file name relative to |path|'s directory.
+ size: The file size in bytes (0 for directories).
+ lastmod: The file last modification date in UTC.
+ """
+ re_directory = re.compile('^%s/(?P<dir>[^:]+):$' % re.escape(path))
+ path_dir = os.path.dirname(path)
+
+ current_dir = ''
+ files = {}
+ for line in ls_output:
+ directory_match = re_directory.match(line)
+ if directory_match:
+ current_dir = directory_match.group('dir')
+ continue
+ file_match = re_file.match(line)
+ if file_match:
+ filename = os.path.join(current_dir, file_match.group('filename'))
+ if filename.startswith(path_dir):
+ filename = filename[len(path_dir)+1:]
+ lastmod = datetime.datetime.strptime(
+ file_match.group('date') + ' ' + file_match.group('time')[:5],
+ '%Y-%m-%d %H:%M')
+ if not utc_offset and 'timezone' in re_file.groupindex:
+ utc_offset = file_match.group('timezone')
+ if isinstance(utc_offset, str) and len(utc_offset) == 5:
+ utc_delta = datetime.timedelta(hours=int(utc_offset[1:3]),
+ minutes=int(utc_offset[3:5]))
+ if utc_offset[0:1] == '-':
+ utc_delta = -utc_delta
+ lastmod -= utc_delta
+ files[filename] = (int(file_match.group('size')), lastmod)
+ return files
+
+def _ComputeFileListHash(md5sum_output):
+ """Returns a list of MD5 strings from the provided md5sum output."""
+ return [line.split(' ')[0] for line in md5sum_output]
+
+def _HasAdbPushSucceeded(command_output):
+ """Returns whether adb push has succeeded from the provided output."""
+ if not command_output:
+ return False
+ # Success looks like this: "3035 KB/s (12512056 bytes in 4.025s)"
+ # Errors look like this: "failed to copy ... "
+ if not re.search('^[0-9]', command_output.splitlines()[-1]):
+ logging.critical('PUSH FAILED: ' + command_output)
+ return False
+ return True
+
+def GetLogTimestamp(log_line, year):
+ """Returns the timestamp of the given |log_line| in the given year."""
+ try:
+ return datetime.datetime.strptime('%s-%s' % (year, log_line[:18]),
+ '%Y-%m-%d %H:%M:%S.%f')
+ except (ValueError, IndexError):
+ logging.critical('Error reading timestamp from ' + log_line)
+ return None
+
+
+class AndroidCommands(object):
+ """Helper class for communicating with Android device via adb.
+
+ Args:
+ device: If given, adb commands are only send to the device of this ID.
+ Otherwise commands are sent to all attached devices.
+ """
+
+ def __init__(self, device=None):
+ self._adb = adb_interface.AdbInterface()
+ if device:
+ self._adb.SetTargetSerial(device)
+ self._device = device
+ self._logcat = None
+ self.logcat_process = None
+ self._pushed_files = []
+ self._device_utc_offset = self.RunShellCommand('date +%z')[0]
+ self._md5sum_path = ''
+ self._external_storage = ''
+
+ def Adb(self):
+ """Returns our AdbInterface to avoid us wrapping all its methods."""
+ return self._adb
+
+ def IsRootEnabled(self):
+ """Checks if root is enabled on the device."""
+ root_test_output = self.RunShellCommand('ls /root') or ['']
+ return not 'Permission denied' in root_test_output[0]
+
+ def EnableAdbRoot(self):
+ """Enables adb root on the device.
+
+ Returns:
+ True: if output from executing adb root was as expected.
+ False: otherwise.
+ """
+ return_value = self._adb.EnableAdbRoot()
+ # EnableAdbRoot inserts a call for wait-for-device only when adb logcat
+ # output matches what is expected. Just to be safe add a call to
+ # wait-for-device.
+ self._adb.SendCommand('wait-for-device')
+ return return_value
+
+ def GetDeviceYear(self):
+ """Returns the year information of the date on device."""
+ return self.RunShellCommand('date +%Y')[0]
+
+ def GetExternalStorage(self):
+ if not self._external_storage:
+ self._external_storage = self.RunShellCommand('echo $EXTERNAL_STORAGE')[0]
+ assert self._external_storage, 'Unable to find $EXTERNAL_STORAGE'
+ return self._external_storage
+
+ def WaitForDevicePm(self):
+ """Blocks until the device's package manager is available.
+
+ To workaround http://b/5201039, we restart the shell and retry if the
+ package manager isn't back after 120 seconds.
+
+ Raises:
+ errors.WaitForResponseTimedOutError after max retries reached.
+ """
+ last_err = None
+ retries = 3
+ while retries:
+ try:
+ self._adb.WaitForDevicePm()
+ return # Success
+ except errors.WaitForResponseTimedOutError as e:
+ last_err = e
+ logging.warning('Restarting and retrying after timeout: %s', e)
+ retries -= 1
+ self.RestartShell()
+ raise last_err # Only reached after max retries, re-raise the last error.
+
+ def RestartShell(self):
+ """Restarts the shell on the device. Does not block for it to return."""
+ self.RunShellCommand('stop')
+ self.RunShellCommand('start')
+
+ def Reboot(self, full_reboot=True):
+ """Reboots the device and waits for the package manager to return.
+
+ Args:
+ full_reboot: Whether to fully reboot the device or just restart the shell.
+ """
+ # TODO(torne): hive can't reboot the device either way without breaking the
+ # connection; work out if we can handle this better
+ if os.environ.get('USING_HIVE'):
+ logging.warning('Ignoring reboot request as we are on hive')
+ return
+ if full_reboot or not self.IsRootEnabled():
+ self._adb.SendCommand('reboot')
+ timeout = 300
+ else:
+ self.RestartShell()
+ timeout = 120
+ # To run tests we need at least the package manager and the sd card (or
+ # other external storage) to be ready.
+ self.WaitForDevicePm()
+ self.WaitForSdCardReady(timeout)
+
+ def Uninstall(self, package):
+ """Uninstalls the specified package from the device.
+
+ Args:
+ package: Name of the package to remove.
+
+ Returns:
+ A status string returned by adb uninstall
+ """
+ uninstall_command = 'uninstall %s' % package
+
+ logging.info('>>> $' + uninstall_command)
+ return self._adb.SendCommand(uninstall_command, timeout_time=60)
+
+ def Install(self, package_file_path, reinstall=False):
+ """Installs the specified package to the device.
+
+ Args:
+ package_file_path: Path to .apk file to install.
+ reinstall: Reinstall an existing apk, keeping the data.
+
+ Returns:
+ A status string returned by adb install
+ """
+ assert os.path.isfile(package_file_path), ('<%s> is not file' %
+ package_file_path)
+
+ install_cmd = ['install']
+
+ if reinstall:
+ install_cmd.append('-r')
+
+ install_cmd.append(package_file_path)
+ install_cmd = ' '.join(install_cmd)
+
+ logging.info('>>> $' + install_cmd)
+ return self._adb.SendCommand(install_cmd, timeout_time=2*60, retry_count=0)
+
+ def ManagedInstall(self, apk_path, keep_data=False, package_name=None,
+ reboots_on_failure=2):
+ """Installs specified package and reboots device on timeouts.
+
+ Args:
+ apk_path: Path to .apk file to install.
+ keep_data: Reinstalls instead of uninstalling first, preserving the
+ application data.
+ package_name: Package name (only needed if keep_data=False).
+ reboots_on_failure: number of time to reboot if package manager is frozen.
+
+ Returns:
+ A status string returned by adb install
+ """
+ reboots_left = reboots_on_failure
+ while True:
+ try:
+ if not keep_data:
+ assert package_name
+ self.Uninstall(package_name)
+ install_status = self.Install(apk_path, reinstall=keep_data)
+ if 'Success' in install_status:
+ return install_status
+ except errors.WaitForResponseTimedOutError:
+ print '@@@STEP_WARNINGS@@@'
+ logging.info('Timeout on installing %s' % apk_path)
+
+ if reboots_left <= 0:
+ raise Exception('Install failure')
+
+ # Force a hard reboot on last attempt
+ self.Reboot(full_reboot=(reboots_left == 1))
+ reboots_left -= 1
+
+ def MakeSystemFolderWritable(self):
+ """Remounts the /system folder rw."""
+ out = self._adb.SendCommand('remount')
+ if out.strip() != 'remount succeeded':
+ raise errors.MsgException('Remount failed: %s' % out)
+
+ def RestartAdbServer(self):
+ """Restart the adb server."""
+ self.KillAdbServer()
+ self.StartAdbServer()
+
+ def KillAdbServer(self):
+ """Kill adb server."""
+ adb_cmd = ['adb', 'kill-server']
+ return cmd_helper.RunCmd(adb_cmd)
+
+ def StartAdbServer(self):
+ """Start adb server."""
+ adb_cmd = ['adb', 'start-server']
+ return cmd_helper.RunCmd(adb_cmd)
+
+ def WaitForSystemBootCompleted(self, wait_time):
+ """Waits for targeted system's boot_completed flag to be set.
+
+ Args:
+ wait_time: time in seconds to wait
+
+ Raises:
+ WaitForResponseTimedOutError if wait_time elapses and flag still not
+ set.
+ """
+ logging.info('Waiting for system boot completed...')
+ self._adb.SendCommand('wait-for-device')
+ # Now the device is there, but system not boot completed.
+ # Query the sys.boot_completed flag with a basic command
+ boot_completed = False
+ attempts = 0
+ wait_period = 5
+ while not boot_completed and (attempts * wait_period) < wait_time:
+ output = self._adb.SendShellCommand('getprop sys.boot_completed',
+ retry_count=1)
+ output = output.strip()
+ if output == '1':
+ boot_completed = True
+ else:
+ # If 'error: xxx' returned when querying the flag, it means
+ # adb server lost the connection to the emulator, so restart the adb
+ # server.
+ if 'error:' in output:
+ self.RestartAdbServer()
+ time.sleep(wait_period)
+ attempts += 1
+ if not boot_completed:
+ raise errors.WaitForResponseTimedOutError(
+ 'sys.boot_completed flag was not set after %s seconds' % wait_time)
+
+ def WaitForSdCardReady(self, timeout_time):
+ """Wait for the SD card ready before pushing data into it."""
+ logging.info('Waiting for SD card ready...')
+ sdcard_ready = False
+ attempts = 0
+ wait_period = 5
+ external_storage = self.GetExternalStorage()
+ while not sdcard_ready and attempts * wait_period < timeout_time:
+ output = self.RunShellCommand('ls ' + external_storage)
+ if output:
+ sdcard_ready = True
+ else:
+ time.sleep(wait_period)
+ attempts += 1
+ if not sdcard_ready:
+ raise errors.WaitForResponseTimedOutError(
+ 'SD card not ready after %s seconds' % timeout_time)
+
+ # It is tempting to turn this function into a generator, however this is not
+ # possible without using a private (local) adb_shell instance (to ensure no
+ # other command interleaves usage of it), which would defeat the main aim of
+ # being able to reuse the adb shell instance across commands.
+ def RunShellCommand(self, command, timeout_time=20, log_result=False):
+ """Send a command to the adb shell and return the result.
+
+ Args:
+ command: String containing the shell command to send. Must not include
+ the single quotes as we use them to escape the whole command.
+ timeout_time: Number of seconds to wait for command to respond before
+ retrying, used by AdbInterface.SendShellCommand.
+ log_result: Boolean to indicate whether we should log the result of the
+ shell command.
+
+ Returns:
+ list containing the lines of output received from running the command
+ """
+ logging.info('>>> $' + command)
+ if "'" in command: logging.warning(command + " contains ' quotes")
+ result = self._adb.SendShellCommand(
+ "'%s'" % command, timeout_time).splitlines()
+ if ['error: device not found'] == result:
+ raise errors.DeviceUnresponsiveError('device not found')
+ if log_result:
+ logging.info('\n>>> '.join(result))
+ return result
+
+ def GetShellCommandStatusAndOutput(self, command, timeout_time=20,
+ log_result=False):
+ """See RunShellCommand() above.
+
+ Returns:
+ The tuple (exit code, list of output lines).
+ """
+ lines = self.RunShellCommand(
+ command + '; echo %$?', timeout_time, log_result)
+ last_line = lines[-1]
+ status_pos = last_line.rfind('%')
+ assert status_pos >= 0
+ status = int(last_line[status_pos + 1:])
+ if status_pos == 0:
+ lines = lines[:-1]
+ else:
+ lines = lines[:-1] + last_line[:status_pos]
+ return (status, lines)
+
+ def KillAll(self, process):
+ """Android version of killall, connected via adb.
+
+ Args:
+ process: name of the process to kill off
+
+ Returns:
+ the number of processes killed
+ """
+ pids = self.ExtractPid(process)
+ if pids:
+ self.RunShellCommand('kill ' + ' '.join(pids))
+ return len(pids)
+
+ def KillAllBlocking(self, process, timeout_sec):
+ """Blocking version of killall, connected via adb.
+
+ This waits until no process matching the corresponding name appears in ps'
+ output anymore.
+
+ Args:
+ process: name of the process to kill off
+ timeout_sec: the timeout in seconds
+
+ Returns:
+ the number of processes killed
+ """
+ processes_killed = self.KillAll(process)
+ if processes_killed:
+ elapsed = 0
+ wait_period = 0.1
+ # Note that this doesn't take into account the time spent in ExtractPid().
+ while self.ExtractPid(process) and elapsed < timeout_sec:
+ time.sleep(wait_period)
+ elapsed += wait_period
+ if elapsed >= timeout_sec:
+ return 0
+ return processes_killed
+
+ def StartActivity(self, package, activity, wait_for_completion=False,
+ action='android.intent.action.VIEW',
+ category=None, data=None,
+ extras=None, trace_file_name=None,
+ force_stop=False):
+ """Starts |package|'s activity on the device.
+
+ Args:
+ package: Name of package to start (e.g. 'com.google.android.apps.chrome').
+ activity: Name of activity (e.g. '.Main' or
+ 'com.google.android.apps.chrome.Main').
+ wait_for_completion: wait for the activity to finish launching (-W flag).
+ action: string (e.g. "android.intent.action.MAIN"). Default is VIEW.
+ category: string (e.g. "android.intent.category.HOME")
+ data: Data string to pass to activity (e.g. 'http://www.example.com/').
+ extras: Dict of extras to pass to activity. Values are significant.
+ trace_file_name: If used, turns on and saves the trace to this file name.
+ force_stop: force stop the target app before starting the activity (-S
+ flag).
+ """
+ cmd = 'am start -a %s' % action
+ if force_stop:
+ cmd += ' -S'
+ if wait_for_completion:
+ cmd += ' -W'
+ if category:
+ cmd += ' -c %s' % category
+ if package and activity:
+ cmd += ' -n %s/%s' % (package, activity)
+ if data:
+ cmd += ' -d "%s"' % data
+ if extras:
+ for key in extras:
+ value = extras[key]
+ if isinstance(value, str):
+ cmd += ' --es'
+ elif isinstance(value, bool):
+ cmd += ' --ez'
+ elif isinstance(value, int):
+ cmd += ' --ei'
+ else:
+ raise NotImplementedError(
+ 'Need to teach StartActivity how to pass %s extras' % type(value))
+ cmd += ' %s %s' % (key, value)
+ if trace_file_name:
+ cmd += ' --start-profiler ' + trace_file_name
+ self.RunShellCommand(cmd)
+
+ def GoHome(self):
+ """Tell the device to return to the home screen. Blocks until completion."""
+ self.RunShellCommand('am start -W '
+ '-a android.intent.action.MAIN -c android.intent.category.HOME')
+
+ def CloseApplication(self, package):
+ """Attempt to close down the application, using increasing violence.
+
+ Args:
+ package: Name of the process to kill off, e.g.
+ com.google.android.apps.chrome
+ """
+ self.RunShellCommand('am force-stop ' + package)
+
+ def ClearApplicationState(self, package):
+ """Closes and clears all state for the given |package|."""
+ self.CloseApplication(package)
+ self.RunShellCommand('rm -r /data/data/%s/app_*' % package)
+ self.RunShellCommand('rm -r /data/data/%s/cache/*' % package)
+ self.RunShellCommand('rm -r /data/data/%s/files/*' % package)
+ self.RunShellCommand('rm -r /data/data/%s/shared_prefs/*' % package)
+
+ def SendKeyEvent(self, keycode):
+ """Sends keycode to the device.
+
+ Args:
+ keycode: Numeric keycode to send (see "enum" at top of file).
+ """
+ self.RunShellCommand('input keyevent %d' % keycode)
+
+ def PushIfNeeded(self, local_path, device_path):
+ """Pushes |local_path| to |device_path|.
+
+ Works for files and directories. This method skips copying any paths in
+ |test_data_paths| that already exist on the device with the same hash.
+
+ All pushed files can be removed by calling RemovePushedFiles().
+ """
+ assert os.path.exists(local_path), 'Local path not found %s' % local_path
+
+ if not self._md5sum_path:
+ default_build_type = os.environ.get('BUILD_TYPE', 'Debug')
+ md5sum_path = '%s/%s/md5sum_bin' % (cmd_helper.OutDirectory.get(),
+ default_build_type)
+ if not os.path.exists(md5sum_path):
+ md5sum_path = '%s/Release/md5sum_bin' % cmd_helper.OutDirectory.get()
+ if not os.path.exists(md5sum_path):
+ print >> sys.stderr, 'Please build md5sum.'
+ sys.exit(1)
+ command = 'push %s %s' % (md5sum_path, MD5SUM_DEVICE_PATH)
+ assert _HasAdbPushSucceeded(self._adb.SendCommand(command))
+ self._md5sum_path = md5sum_path
+
+ self._pushed_files.append(device_path)
+ hashes_on_device = _ComputeFileListHash(
+ self.RunShellCommand(MD5SUM_DEVICE_PATH + ' ' + device_path))
+ assert os.path.exists(local_path), 'Local path not found %s' % local_path
+ hashes_on_host = _ComputeFileListHash(
+ subprocess.Popen(
+ '%s_host %s' % (self._md5sum_path, local_path),
+ stdout=subprocess.PIPE, shell=True).stdout)
+ if hashes_on_device == hashes_on_host:
+ return
+
+ # They don't match, so remove everything first and then create it.
+ if os.path.isdir(local_path):
+ self.RunShellCommand('rm -r %s' % device_path, timeout_time=2*60)
+ self.RunShellCommand('mkdir -p %s' % device_path)
+
+ # NOTE: We can't use adb_interface.Push() because it hardcodes a timeout of
+ # 60 seconds which isn't sufficient for a lot of users of this method.
+ push_command = 'push %s %s' % (local_path, device_path)
+ logging.info('>>> $' + push_command)
+ output = self._adb.SendCommand(push_command, timeout_time=30*60)
+ assert _HasAdbPushSucceeded(output)
+
+
+ def GetFileContents(self, filename, log_result=False):
+ """Gets contents from the file specified by |filename|."""
+ return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' +
+ filename + '"; fi', log_result=log_result)
+
+ def SetFileContents(self, filename, contents):
+ """Writes |contents| to the file specified by |filename|."""
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(contents)
+ f.flush()
+ self._adb.Push(f.name, filename)
+
+ def RemovePushedFiles(self):
+ """Removes all files pushed with PushIfNeeded() from the device."""
+ for p in self._pushed_files:
+ self.RunShellCommand('rm -r %s' % p, timeout_time=2*60)
+
+ def ListPathContents(self, path):
+ """Lists files in all subdirectories of |path|.
+
+ Args:
+ path: The path to list.
+
+ Returns:
+ A dict of {"name": (size, lastmod), ...}.
+ """
+ # Example output:
+ # /foo/bar:
+ # -rw-r----- 1 user group 102 2011-05-12 12:29:54.131623387 +0100 baz.txt
+ re_file = re.compile('^-(?P<perms>[^\s]+)\s+'
+ '(?P<user>[^\s]+)\s+'
+ '(?P<group>[^\s]+)\s+'
+ '(?P<size>[^\s]+)\s+'
+ '(?P<date>[^\s]+)\s+'
+ '(?P<time>[^\s]+)\s+'
+ '(?P<filename>[^\s]+)$')
+ return _GetFilesFromRecursiveLsOutput(
+ path, self.RunShellCommand('ls -lR %s' % path), re_file,
+ self._device_utc_offset)
+
+ def SetJavaAssertsEnabled(self, enable):
+ """Sets or removes the device java assertions property.
+
+ Args:
+ enable: If True the property will be set.
+
+ Returns:
+ True if the file was modified (reboot is required for it to take effect).
+ """
+ # First ensure the desired property is persisted.
+ temp_props_file = tempfile.NamedTemporaryFile()
+ properties = ''
+ if self._adb.Pull(LOCAL_PROPERTIES_PATH, temp_props_file.name):
+ properties = file(temp_props_file.name).read()
+ re_search = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) +
+ r'\s*=\s*all\s*$', re.MULTILINE)
+ if enable != bool(re.search(re_search, properties)):
+ re_replace = re.compile(r'^\s*' + re.escape(JAVA_ASSERT_PROPERTY) +
+ r'\s*=\s*\w+\s*$', re.MULTILINE)
+ properties = re.sub(re_replace, '', properties)
+ if enable:
+ properties += '\n%s=all\n' % JAVA_ASSERT_PROPERTY
+
+ file(temp_props_file.name, 'w').write(properties)
+ self._adb.Push(temp_props_file.name, LOCAL_PROPERTIES_PATH)
+
+ # Next, check the current runtime value is what we need, and
+ # if not, set it and report that a reboot is required.
+ was_set = 'all' in self.RunShellCommand('getprop ' + JAVA_ASSERT_PROPERTY)
+ if was_set == enable:
+ return False
+
+ self.RunShellCommand('setprop %s "%s"' % (JAVA_ASSERT_PROPERTY,
+ enable and 'all' or ''))
+ return True
+
+ def GetBuildId(self):
+ """Returns the build ID of the system (e.g. JRM79C)."""
+ build_id = self.RunShellCommand('getprop ro.build.id')[0]
+ assert build_id
+ return build_id
+
+ def GetBuildType(self):
+ """Returns the build type of the system (e.g. eng)."""
+ build_type = self.RunShellCommand('getprop ro.build.type')[0]
+ assert build_type
+ return build_type
+
+ def StartMonitoringLogcat(self, clear=True, timeout=10, logfile=None,
+ filters=None):
+ """Starts monitoring the output of logcat, for use with WaitForLogMatch.
+
+ Args:
+ clear: If True the existing logcat output will be cleared, to avoiding
+ matching historical output lurking in the log.
+ timeout: How long WaitForLogMatch will wait for the given match
+ filters: A list of logcat filters to be used.
+ """
+ if clear:
+ self.RunShellCommand('logcat -c')
+ args = []
+ if self._adb._target_arg:
+ args += shlex.split(self._adb._target_arg)
+ args += ['logcat', '-v', 'threadtime']
+ if filters:
+ args.extend(filters)
+ else:
+ args.append('*:v')
+
+ if logfile:
+ logfile = NewLineNormalizer(logfile)
+
+ # Spawn logcat and syncronize with it.
+ for _ in range(4):
+ self._logcat = pexpect.spawn('adb', args, timeout=timeout,
+ logfile=logfile)
+ self.RunShellCommand('log startup_sync')
+ if self._logcat.expect(['startup_sync', pexpect.EOF,
+ pexpect.TIMEOUT]) == 0:
+ break
+ self._logcat.close(force=True)
+ else:
+ logging.critical('Error reading from logcat: ' + str(self._logcat.match))
+ sys.exit(1)
+
+ def GetMonitoredLogCat(self):
+ """Returns an "adb logcat" command as created by pexpected.spawn."""
+ if not self._logcat:
+ self.StartMonitoringLogcat(clear=False)
+ return self._logcat
+
+ def WaitForLogMatch(self, success_re, error_re, clear=False):
+ """Blocks until a matching line is logged or a timeout occurs.
+
+ Args:
+ success_re: A compiled re to search each line for.
+ error_re: A compiled re which, if found, terminates the search for
+ |success_re|. If None is given, no error condition will be detected.
+ clear: If True the existing logcat output will be cleared, defaults to
+ false.
+
+ Raises:
+ pexpect.TIMEOUT upon the timeout specified by StartMonitoringLogcat().
+
+ Returns:
+ The re match object if |success_re| is matched first or None if |error_re|
+ is matched first.
+ """
+ logging.info('<<< Waiting for logcat:' + str(success_re.pattern))
+ t0 = time.time()
+ while True:
+ if not self._logcat:
+ self.StartMonitoringLogcat(clear)
+ try:
+ while True:
+ # Note this will block for upto the timeout _per log line_, so we need
+ # to calculate the overall timeout remaining since t0.
+ time_remaining = t0 + self._logcat.timeout - time.time()
+ if time_remaining < 0: raise pexpect.TIMEOUT(self._logcat)
+ self._logcat.expect(PEXPECT_LINE_RE, timeout=time_remaining)
+ line = self._logcat.match.group(1)
+ if error_re:
+ error_match = error_re.search(line)
+ if error_match:
+ return None
+ success_match = success_re.search(line)
+ if success_match:
+ return success_match
+ logging.info('<<< Skipped Logcat Line:' + str(line))
+ except pexpect.TIMEOUT:
+ raise pexpect.TIMEOUT(
+ 'Timeout (%ds) exceeded waiting for pattern "%s" (tip: use -vv '
+ 'to debug)' %
+ (self._logcat.timeout, success_re.pattern))
+ except pexpect.EOF:
+ # It seems that sometimes logcat can end unexpectedly. This seems
+ # to happen during Chrome startup after a reboot followed by a cache
+ # clean. I don't understand why this happens, but this code deals with
+ # getting EOF in logcat.
+ logging.critical('Found EOF in adb logcat. Restarting...')
+ # Rerun spawn with original arguments. Note that self._logcat.args[0] is
+ # the path of adb, so we don't want it in the arguments.
+ self._logcat = pexpect.spawn('adb',
+ self._logcat.args[1:],
+ timeout=self._logcat.timeout,
+ logfile=self._logcat.logfile)
+
+ def StartRecordingLogcat(self, clear=True, filters=['*:v']):
+ """Starts recording logcat output to eventually be saved as a string.
+
+ This call should come before some series of tests are run, with either
+ StopRecordingLogcat or SearchLogcatRecord following the tests.
+
+ Args:
+ clear: True if existing log output should be cleared.
+ filters: A list of logcat filters to be used.
+ """
+ if clear:
+ self._adb.SendCommand('logcat -c')
+ logcat_command = 'adb %s logcat -v threadtime %s' % (self._adb._target_arg,
+ ' '.join(filters))
+ self.logcat_process = subprocess.Popen(logcat_command, shell=True,
+ stdout=subprocess.PIPE)
+
+ def StopRecordingLogcat(self):
+ """Stops an existing logcat recording subprocess and returns output.
+
+ Returns:
+ The logcat output as a string or an empty string if logcat was not
+ being recorded at the time.
+ """
+ if not self.logcat_process:
+ return ''
+ # Cannot evaluate directly as 0 is a possible value.
+ # Better to read the self.logcat_process.stdout before killing it,
+ # Otherwise the communicate may return incomplete output due to pipe break.
+ if self.logcat_process.poll() is None:
+ self.logcat_process.kill()
+ (output, _) = self.logcat_process.communicate()
+ self.logcat_process = None
+ return output
+
+ def SearchLogcatRecord(self, record, message, thread_id=None, proc_id=None,
+ log_level=None, component=None):
+ """Searches the specified logcat output and returns results.
+
+ This method searches through the logcat output specified by record for a
+ certain message, narrowing results by matching them against any other
+ specified criteria. It returns all matching lines as described below.
+
+ Args:
+ record: A string generated by Start/StopRecordingLogcat to search.
+ message: An output string to search for.
+ thread_id: The thread id that is the origin of the message.
+ proc_id: The process that is the origin of the message.
+ log_level: The log level of the message.
+ component: The name of the component that would create the message.
+
+ Returns:
+ A list of dictionaries represeting matching entries, each containing keys
+ thread_id, proc_id, log_level, component, and message.
+ """
+ if thread_id:
+ thread_id = str(thread_id)
+ if proc_id:
+ proc_id = str(proc_id)
+ results = []
+ reg = re.compile('(\d+)\s+(\d+)\s+([A-Z])\s+([A-Za-z]+)\s*:(.*)$',
+ re.MULTILINE)
+ log_list = reg.findall(record)
+ for (tid, pid, log_lev, comp, msg) in log_list:
+ if ((not thread_id or thread_id == tid) and
+ (not proc_id or proc_id == pid) and
+ (not log_level or log_level == log_lev) and
+ (not component or component == comp) and msg.find(message) > -1):
+ match = dict({'thread_id': tid, 'proc_id': pid,
+ 'log_level': log_lev, 'component': comp,
+ 'message': msg})
+ results.append(match)
+ return results
+
+ def ExtractPid(self, process_name):
+ """Extracts Process Ids for a given process name from Android Shell.
+
+ Args:
+ process_name: name of the process on the device.
+
+ Returns:
+ List of all the process ids (as strings) that match the given name.
+ If the name of a process exactly matches the given name, the pid of
+ that process will be inserted to the front of the pid list.
+ """
+ pids = []
+ for line in self.RunShellCommand('ps', log_result=False):
+ data = line.split()
+ try:
+ if process_name in data[-1]: # name is in the last column
+ if process_name == data[-1]:
+ pids.insert(0, data[1]) # PID is in the second column
+ else:
+ pids.append(data[1])
+ except IndexError:
+ pass
+ return pids
+
+ def GetIoStats(self):
+ """Gets cumulative disk IO stats since boot (for all processes).
+
+ Returns:
+ Dict of {num_reads, num_writes, read_ms, write_ms} or None if there
+ was an error.
+ """
+ for line in self.GetFileContents('/proc/diskstats', log_result=False):
+ stats = io_stats_parser.ParseIoStatsLine(line)
+ if stats.device == 'mmcblk0':
+ return {
+ 'num_reads': stats.num_reads_issued,
+ 'num_writes': stats.num_writes_completed,
+ 'read_ms': stats.ms_spent_reading,
+ 'write_ms': stats.ms_spent_writing,
+ }
+ logging.warning('Could not find disk IO stats.')
+ return None
+
+ def GetMemoryUsageForPid(self, pid):
+ """Returns the memory usage for given pid.
+
+ Args:
+ pid: The pid number of the specific process running on device.
+
+ Returns:
+ A tuple containg:
+ [0]: Dict of {metric:usage_kb}, for the process which has specified pid.
+ The metric keys which may be included are: Size, Rss, Pss, Shared_Clean,
+ Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap,
+ KernelPageSize, MMUPageSize, Nvidia (tablet only).
+ [1]: Detailed /proc/[PID]/smaps information.
+ """
+ usage_dict = collections.defaultdict(int)
+ smaps = collections.defaultdict(dict)
+ current_smap = ''
+ for line in self.GetFileContents('/proc/%s/smaps' % pid, log_result=False):
+ items = line.split()
+ # See man 5 proc for more details. The format is:
+ # address perms offset dev inode pathname
+ if len(items) > 5:
+ current_smap = ' '.join(items[5:])
+ elif len(items) > 3:
+ current_smap = ' '.join(items[3:])
+ match = re.match(MEMORY_INFO_RE, line)
+ if match:
+ key = match.group('key')
+ usage_kb = int(match.group('usage_kb'))
+ usage_dict[key] += usage_kb
+ if key not in smaps[current_smap]:
+ smaps[current_smap][key] = 0
+ smaps[current_smap][key] += usage_kb
+ if not usage_dict or not any(usage_dict.values()):
+ # Presumably the process died between ps and calling this method.
+ logging.warning('Could not find memory usage for pid ' + str(pid))
+
+ for line in self.GetFileContents('/d/nvmap/generic-0/clients',
+ log_result=False):
+ match = re.match(NVIDIA_MEMORY_INFO_RE, line)
+ if match and match.group('pid') == pid:
+ usage_bytes = int(match.group('usage_bytes'))
+ usage_dict['Nvidia'] = int(round(usage_bytes / 1000.0)) # kB
+ break
+
+ return (usage_dict, smaps)
+
+ def GetMemoryUsageForPackage(self, package):
+ """Returns the memory usage for all processes whose name contains |pacakge|.
+
+ Args:
+ package: A string holding process name to lookup pid list for.
+
+ Returns:
+ A tuple containg:
+ [0]: Dict of {metric:usage_kb}, summed over all pids associated with
+ |name|.
+ The metric keys which may be included are: Size, Rss, Pss, Shared_Clean,
+ Shared_Dirty, Private_Clean, Private_Dirty, Referenced, Swap,
+ KernelPageSize, MMUPageSize, Nvidia (tablet only).
+ [1]: a list with detailed /proc/[PID]/smaps information.
+ """
+ usage_dict = collections.defaultdict(int)
+ pid_list = self.ExtractPid(package)
+ smaps = collections.defaultdict(dict)
+
+ for pid in pid_list:
+ usage_dict_per_pid, smaps_per_pid = self.GetMemoryUsageForPid(pid)
+ smaps[pid] = smaps_per_pid
+ for (key, value) in usage_dict_per_pid.items():
+ usage_dict[key] += value
+
+ return usage_dict, smaps
+
+ def ProcessesUsingDevicePort(self, device_port):
+ """Lists processes using the specified device port on loopback interface.
+
+ Args:
+ device_port: Port on device we want to check.
+
+ Returns:
+ A list of (pid, process_name) tuples using the specified port.
+ """
+ tcp_results = self.RunShellCommand('cat /proc/net/tcp', log_result=False)
+ tcp_address = '0100007F:%04X' % device_port
+ pids = []
+ for single_connect in tcp_results:
+ connect_results = single_connect.split()
+ # Column 1 is the TCP port, and Column 9 is the inode of the socket
+ if connect_results[1] == tcp_address:
+ socket_inode = connect_results[9]
+ socket_name = 'socket:[%s]' % socket_inode
+ lsof_results = self.RunShellCommand('lsof', log_result=False)
+ for single_process in lsof_results:
+ process_results = single_process.split()
+ # Ignore the line if it has less than nine columns in it, which may
+ # be the case when a process stops while lsof is executing.
+ if len(process_results) <= 8:
+ continue
+ # Column 0 is the executable name
+ # Column 1 is the pid
+ # Column 8 is the Inode in use
+ if process_results[8] == socket_name:
+ pids.append((int(process_results[1]), process_results[0]))
+ break
+ logging.info('PidsUsingDevicePort: %s', pids)
+ return pids
+
+ def FileExistsOnDevice(self, file_name):
+ """Checks whether the given file exists on the device.
+
+ Args:
+ file_name: Full path of file to check.
+
+ Returns:
+ True if the file exists, False otherwise.
+ """
+ assert '"' not in file_name, 'file_name cannot contain double quotes'
+ try:
+ status = self._adb.SendShellCommand(
+ '\'test -e "%s"; echo $?\'' % (file_name))
+ if 'test: not found' not in status:
+ return int(status) == 0
+
+ status = self._adb.SendShellCommand(
+ '\'ls "%s" >/dev/null 2>&1; echo $?\'' % (file_name))
+ return int(status) == 0
+ except ValueError:
+ if IsDeviceAttached(self._device):
+ raise errors.DeviceUnresponsiveError('Device may be offline.')
+
+ return False
+
+ def TakeScreenshot(self, host_file):
+ """Saves a screenshot image to |host_file| on the host.
+
+ Args:
+ host_file: Absolute path to the image file to store on the host.
+ """
+ host_dir = os.path.dirname(host_file)
+ if not os.path.exists(host_dir):
+ os.makedirs(host_dir)
+ device_file = '%s/screenshot.png' % self.GetExternalStorage()
+ self.RunShellCommand('/system/bin/screencap -p %s' % device_file)
+ assert self._adb.Pull(device_file, host_file)
+ assert os.path.exists(host_file)
+
+
+class NewLineNormalizer(object):
+ """A file-like object to normalize EOLs to '\n'.
+
+ Pexpect runs adb within a pseudo-tty device (see
+ http://www.noah.org/wiki/pexpect), so any '\n' printed by adb is written
+ as '\r\n' to the logfile. Since adb already uses '\r\n' to terminate
+ lines, the log ends up having '\r\r\n' at the end of each line. This
+ filter replaces the above with a single '\n' in the data stream.
+ """
+ def __init__(self, output):
+ self._output = output
+
+ def write(self, data):
+ data = data.replace('\r\r\n', '\n')
+ self._output.write(data)
+
+ def flush(self):
+ self._output.flush()
diff --git a/src/build/android/pylib/apk_info.py b/src/build/android/pylib/apk_info.py
new file mode 100644
index 0000000..00b30dd
--- /dev/null
+++ b/src/build/android/pylib/apk_info.py
@@ -0,0 +1,186 @@
+# 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.
+
+"""Gathers information about APKs."""
+
+import collections
+import logging
+import os
+import pickle
+import re
+
+import cmd_helper
+
+# If you change the cached output of proguard, increment this number
+PICKLE_FORMAT_VERSION = 1
+
+def GetPackageNameForApk(apk_path):
+ """Returns the package name of the apk file."""
+ aapt_output = cmd_helper.GetCmdOutput(
+ ['aapt', 'dump', 'badging', apk_path]).split('\n')
+ package_name_re = re.compile(r'package: .*name=\'(\S*)\'')
+ for line in aapt_output:
+ m = package_name_re.match(line)
+ if m:
+ return m.group(1)
+ raise Exception('Failed to determine package name of %s' % apk_path)
+
+
+class ApkInfo(object):
+ """Helper class for inspecting APKs."""
+
+ def __init__(self, apk_path, jar_path):
+ self._PROGUARD_PATH = os.path.join(os.environ['ANDROID_SDK_ROOT'],
+ 'tools/proguard/bin/proguard.sh')
+ if not os.path.exists(self._PROGUARD_PATH):
+ self._PROGUARD_PATH = os.path.join(os.environ['ANDROID_BUILD_TOP'],
+ 'external/proguard/bin/proguard.sh')
+ self._PROGUARD_CLASS_RE = re.compile(r'\s*?- Program class:\s*([\S]+)$')
+ self._PROGUARD_METHOD_RE = re.compile(r'\s*?- Method:\s*(\S*)[(].*$')
+ self._PROGUARD_ANNOTATION_RE = re.compile(r'\s*?- Annotation \[L(\S*);\]:$')
+ self._PROGUARD_ANNOTATION_CONST_RE = (
+ re.compile(r'\s*?- Constant element value.*$'))
+ self._PROGUARD_ANNOTATION_VALUE_RE = re.compile(r'\s*?- \S+? \[(.*)\]$')
+
+ if not os.path.exists(apk_path):
+ raise Exception('%s not found, please build it' % apk_path)
+ self._apk_path = apk_path
+ if not os.path.exists(jar_path):
+ raise Exception('%s not found, please build it' % jar_path)
+ self._jar_path = jar_path
+ self._annotation_map = collections.defaultdict(list)
+ self._pickled_proguard_name = self._jar_path + '-proguard.pickle'
+ self._test_methods = []
+ self._Initialize()
+
+ def _Initialize(self):
+ if not self._GetCachedProguardData():
+ self._GetProguardData()
+
+ def _GetCachedProguardData(self):
+ if (os.path.exists(self._pickled_proguard_name) and
+ (os.path.getmtime(self._pickled_proguard_name) >
+ os.path.getmtime(self._jar_path))):
+ logging.info('Loading cached proguard output from %s',
+ self._pickled_proguard_name)
+ try:
+ with open(self._pickled_proguard_name, 'r') as r:
+ d = pickle.loads(r.read())
+ if d['VERSION'] == PICKLE_FORMAT_VERSION:
+ self._annotation_map = d['ANNOTATION_MAP']
+ self._test_methods = d['TEST_METHODS']
+ return True
+ except:
+ logging.warning('PICKLE_FORMAT_VERSION has changed, ignoring cache')
+ return False
+
+ def _GetProguardData(self):
+ proguard_output = cmd_helper.GetCmdOutput([self._PROGUARD_PATH,
+ '-injars', self._jar_path,
+ '-dontshrink',
+ '-dontoptimize',
+ '-dontobfuscate',
+ '-dontpreverify',
+ '-dump',
+ ]).split('\n')
+ clazz = None
+ method = None
+ annotation = None
+ has_value = False
+ qualified_method = None
+ for line in proguard_output:
+ m = self._PROGUARD_CLASS_RE.match(line)
+ if m:
+ clazz = m.group(1).replace('/', '.') # Change package delim.
+ annotation = None
+ continue
+
+ m = self._PROGUARD_METHOD_RE.match(line)
+ if m:
+ method = m.group(1)
+ annotation = None
+ qualified_method = clazz + '#' + method
+ if method.startswith('test') and clazz.endswith('Test'):
+ self._test_methods += [qualified_method]
+ continue
+
+ if not qualified_method:
+ # Ignore non-method annotations.
+ continue
+
+ m = self._PROGUARD_ANNOTATION_RE.match(line)
+ if m:
+ annotation = m.group(1).split('/')[-1] # Ignore the annotation package.
+ self._annotation_map[qualified_method].append(annotation)
+ has_value = False
+ continue
+ if annotation:
+ if not has_value:
+ m = self._PROGUARD_ANNOTATION_CONST_RE.match(line)
+ if m:
+ has_value = True
+ else:
+ m = self._PROGUARD_ANNOTATION_VALUE_RE.match(line)
+ if m:
+ value = m.group(1)
+ self._annotation_map[qualified_method].append(
+ annotation + ':' + value)
+ has_value = False
+
+ logging.info('Storing proguard output to %s', self._pickled_proguard_name)
+ d = {'VERSION': PICKLE_FORMAT_VERSION,
+ 'ANNOTATION_MAP': self._annotation_map,
+ 'TEST_METHODS': self._test_methods}
+ with open(self._pickled_proguard_name, 'w') as f:
+ f.write(pickle.dumps(d))
+
+ def _GetAnnotationMap(self):
+ return self._annotation_map
+
+ def _IsTestMethod(self, test):
+ class_name, method = test.split('#')
+ return class_name.endswith('Test') and method.startswith('test')
+
+ def GetApkPath(self):
+ return self._apk_path
+
+ def GetPackageName(self):
+ """Returns the package name of this APK."""
+ return GetPackageNameForApk(self._apk_path)
+
+ def GetTestAnnotations(self, test):
+ """Returns a list of all annotations for the given |test|. May be empty."""
+ if not self._IsTestMethod(test):
+ return []
+ return self._GetAnnotationMap()[test]
+
+ def _AnnotationsMatchFilters(self, annotation_filter_list, annotations):
+ """Checks if annotations match any of the filters."""
+ if not annotation_filter_list:
+ return True
+ for annotation_filter in annotation_filter_list:
+ filters = annotation_filter.split('=')
+ if len(filters) == 2:
+ key = filters[0]
+ value_list = filters[1].split(',')
+ for value in value_list:
+ if key + ':' + value in annotations:
+ return True
+ elif annotation_filter in annotations:
+ return True
+ return False
+
+ def GetAnnotatedTests(self, annotation_filter_list):
+ """Returns a list of all tests that match the given annotation filters."""
+ return [test for test, annotations in self._GetAnnotationMap().iteritems()
+ if self._IsTestMethod(test) and self._AnnotationsMatchFilters(
+ annotation_filter_list, annotations)]
+
+ def GetTestMethods(self):
+ """Returns a list of all test methods in this apk as Class#testMethod."""
+ return self._test_methods
+
+ @staticmethod
+ def IsPythonDrivenTest(test):
+ return 'pythonDrivenTests' in test
diff --git a/src/build/android/pylib/base_test_runner.py b/src/build/android/pylib/base_test_runner.py
new file mode 100644
index 0000000..5355633
--- /dev/null
+++ b/src/build/android/pylib/base_test_runner.py
@@ -0,0 +1,210 @@
+# 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 contextlib
+import httplib
+import logging
+import os
+import tempfile
+import time
+
+import android_commands
+import constants
+from chrome_test_server_spawner import SpawningServer
+import constants
+from flag_changer import FlagChanger
+from forwarder import Forwarder
+import lighttpd_server
+import ports
+from valgrind_tools import CreateTool
+
+
+# A file on device to store ports of net test server. The format of the file is
+# test-spawner-server-port:test-server-port
+NET_TEST_SERVER_PORT_INFO_FILE = 'net-test-server-ports'
+
+
+class BaseTestRunner(object):
+ """Base class for running tests on a single device.
+
+ A subclass should implement RunTests() with no parameter, so that calling
+ the Run() method will set up tests, run them and tear them down.
+ """
+
+ def __init__(self, device, tool, shard_index, build_type):
+ """
+ Args:
+ device: Tests will run on the device of this ID.
+ shard_index: Index number of the shard on which the test suite will run.
+ build_type: 'Release' or 'Debug'.
+ """
+ self.device = device
+ self.adb = android_commands.AndroidCommands(device=device)
+ self.tool = CreateTool(tool, self.adb)
+ self._http_server = None
+ self._forwarder = None
+ self._forwarder_device_port = 8000
+ self.forwarder_base_url = ('http://localhost:%d' %
+ self._forwarder_device_port)
+ self.flags = FlagChanger(self.adb)
+ self.shard_index = shard_index
+ self.flags.AddFlags(['--disable-fre'])
+ self._spawning_server = None
+ self._spawner_forwarder = None
+ # We will allocate port for test server spawner when calling method
+ # LaunchChromeTestServerSpawner and allocate port for test server when
+ # starting it in TestServerThread.
+ self.test_server_spawner_port = 0
+ self.test_server_port = 0
+ self.build_type = build_type
+
+ def _PushTestServerPortInfoToDevice(self):
+ """Pushes the latest port information to device."""
+ self.adb.SetFileContents(self.adb.GetExternalStorage() + '/' +
+ NET_TEST_SERVER_PORT_INFO_FILE,
+ '%d:%d' % (self.test_server_spawner_port,
+ self.test_server_port))
+
+ def Run(self):
+ """Calls subclass functions to set up tests, run them and tear them down.
+
+ Returns:
+ Test results returned from RunTests().
+ """
+ if not self.HasTests():
+ return True
+ self.SetUp()
+ try:
+ return self.RunTests()
+ finally:
+ self.TearDown()
+
+ def SetUp(self):
+ """Called before tests run."""
+ Forwarder.KillDevice(self.adb)
+
+ def HasTests(self):
+ """Whether the test suite has tests to run."""
+ return True
+
+ def RunTests(self):
+ """Runs the tests. Need to be overridden."""
+ raise NotImplementedError
+
+ def TearDown(self):
+ """Called when tests finish running."""
+ self.ShutdownHelperToolsForTestSuite()
+
+ def CopyTestData(self, test_data_paths, dest_dir):
+ """Copies |test_data_paths| list of files/directories to |dest_dir|.
+
+ Args:
+ test_data_paths: A list of files or directories relative to |dest_dir|
+ which should be copied to the device. The paths must exist in
+ |CHROME_DIR|.
+ dest_dir: Absolute path to copy to on the device.
+ """
+ for p in test_data_paths:
+ self.adb.PushIfNeeded(
+ os.path.join(constants.CHROME_DIR, p),
+ os.path.join(dest_dir, p))
+
+ def LaunchTestHttpServer(self, document_root, port=None,
+ extra_config_contents=None):
+ """Launches an HTTP server to serve HTTP tests.
+
+ Args:
+ document_root: Document root of the HTTP server.
+ port: port on which we want to the http server bind.
+ extra_config_contents: Extra config contents for the HTTP server.
+ """
+ self._http_server = lighttpd_server.LighttpdServer(
+ document_root, port=port, extra_config_contents=extra_config_contents)
+ if self._http_server.StartupHttpServer():
+ logging.info('http server started: http://localhost:%s',
+ self._http_server.port)
+ else:
+ logging.critical('Failed to start http server')
+ self.StartForwarderForHttpServer()
+ return (self._forwarder_device_port, self._http_server.port)
+
+ def _CreateAndRunForwarder(
+ self, adb, port_pairs, tool, host_name, build_type):
+ """Creates and run a forwarder."""
+ forwarder = Forwarder(adb, build_type)
+ forwarder.Run(port_pairs, tool, host_name)
+ return forwarder
+
+ def StartForwarder(self, port_pairs):
+ """Starts TCP traffic forwarding for the given |port_pairs|.
+
+ Args:
+ host_port_pairs: A list of (device_port, local_port) tuples to forward.
+ """
+ if self._forwarder:
+ self._forwarder.Close()
+ self._forwarder = self._CreateAndRunForwarder(
+ self.adb, port_pairs, self.tool, '127.0.0.1', self.build_type)
+
+ def StartForwarderForHttpServer(self):
+ """Starts a forwarder for the HTTP server.
+
+ The forwarder forwards HTTP requests and responses between host and device.
+ """
+ self.StartForwarder([(self._forwarder_device_port, self._http_server.port)])
+
+ def RestartHttpServerForwarderIfNecessary(self):
+ """Restarts the forwarder if it's not open."""
+ # Checks to see if the http server port is being used. If not forwards the
+ # request.
+ # TODO(dtrainor): This is not always reliable because sometimes the port
+ # will be left open even after the forwarder has been killed.
+ if not ports.IsDevicePortUsed(self.adb,
+ self._forwarder_device_port):
+ self.StartForwarderForHttpServer()
+
+ def ShutdownHelperToolsForTestSuite(self):
+ """Shuts down the server and the forwarder."""
+ # Forwarders should be killed before the actual servers they're forwarding
+ # to as they are clients potentially with open connections and to allow for
+ # proper hand-shake/shutdown.
+ Forwarder.KillDevice(self.adb)
+ if self._http_server:
+ self._http_server.ShutdownHttpServer()
+ if self._spawning_server:
+ self._spawning_server.Stop()
+ self.flags.Restore()
+
+ def LaunchChromeTestServerSpawner(self):
+ """Launches test server spawner."""
+ server_ready = False
+ error_msgs = []
+ # Try 3 times to launch test spawner server.
+ for i in xrange(0, 3):
+ # Do not allocate port for test server here. We will allocate
+ # different port for individual test in TestServerThread.
+ self.test_server_spawner_port = ports.AllocateTestServerPort()
+ self._spawning_server = SpawningServer(self.test_server_spawner_port,
+ self.adb,
+ self.tool,
+ self.build_type)
+ self._spawning_server.Start()
+ server_ready, error_msg = ports.IsHttpServerConnectable(
+ '127.0.0.1', self.test_server_spawner_port, path='/ping',
+ expected_read='ready')
+ if server_ready:
+ break
+ else:
+ error_msgs.append(error_msg)
+ self._spawning_server.Stop()
+ # Wait for 2 seconds then restart.
+ time.sleep(2)
+ if not server_ready:
+ logging.error(';'.join(error_msgs))
+ raise Exception('Can not start the test spawner server.')
+ self._PushTestServerPortInfoToDevice()
+ self._spawner_forwarder = self._CreateAndRunForwarder(
+ self.adb,
+ [(self.test_server_spawner_port, self.test_server_spawner_port)],
+ self.tool, '127.0.0.1', self.build_type)
diff --git a/src/build/android/pylib/base_test_sharder.py b/src/build/android/pylib/base_test_sharder.py
new file mode 100644
index 0000000..1c4559a
--- /dev/null
+++ b/src/build/android/pylib/base_test_sharder.py
@@ -0,0 +1,150 @@
+# 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 android_commands
+import logging
+import multiprocessing
+
+from android_commands import errors
+from forwarder import Forwarder
+from test_result import TestResults
+
+
+def _ShardedTestRunnable(test):
+ """Standalone function needed by multiprocessing.Pool."""
+ log_format = '[' + test.device + '] # %(asctime)-15s: %(message)s'
+ if logging.getLogger().handlers:
+ logging.getLogger().handlers[0].setFormatter(logging.Formatter(log_format))
+ else:
+ logging.basicConfig(format=log_format)
+ # Handle SystemExit here since python has a bug to exit current process
+ try:
+ return test.Run()
+ except SystemExit:
+ return TestResults()
+
+
+def SetTestsContainer(tests_container):
+ """Sets tests container.
+
+ multiprocessing.Queue can't be pickled across processes, so we need to set
+ this as a 'global', per process, via multiprocessing.Pool.
+ """
+ BaseTestSharder.tests_container = tests_container
+
+
+class BaseTestSharder(object):
+ """Base class for sharding tests across multiple devices.
+
+ Args:
+ attached_devices: A list of attached devices.
+ """
+ # See more in SetTestsContainer.
+ tests_container = None
+
+ def __init__(self, attached_devices, build_type='Debug'):
+ self.attached_devices = attached_devices
+ # Worst case scenario: a device will drop offline per run, so we need
+ # to retry until we're out of devices.
+ self.retries = len(self.attached_devices)
+ self.tests = []
+ self.build_type = build_type
+
+ def CreateShardedTestRunner(self, device, index):
+ """Factory function to create a suite-specific test runner.
+
+ Args:
+ device: Device serial where this shard will run
+ index: Index of this device in the pool.
+
+ Returns:
+ An object of BaseTestRunner type (that can provide a "Run()" method).
+ """
+ pass
+
+ def SetupSharding(self, tests):
+ """Called before starting the shards."""
+ pass
+
+ def OnTestsCompleted(self, test_runners, test_results):
+ """Notifies that we completed the tests."""
+ pass
+
+ def _KillHostForwarder(self):
+ Forwarder.KillHost(self.build_type)
+
+ def RunShardedTests(self):
+ """Runs the tests in all connected devices.
+
+ Returns:
+ A TestResults object.
+ """
+ logging.warning('*' * 80)
+ logging.warning('Sharding in ' + str(len(self.attached_devices)) +
+ ' devices.')
+ logging.warning('Note that the output is not synchronized.')
+ logging.warning('Look for the "Final result" banner in the end.')
+ logging.warning('*' * 80)
+ final_results = TestResults()
+ self._KillHostForwarder()
+ for retry in xrange(self.retries):
+ logging.warning('Try %d of %d', retry + 1, self.retries)
+ self.SetupSharding(self.tests)
+ test_runners = []
+
+ # Try to create N shards, and retrying on failure.
+ try:
+ for index, device in enumerate(self.attached_devices):
+ logging.warning('*' * 80)
+ logging.warning('Creating shard %d for %s', index, device)
+ logging.warning('*' * 80)
+ test_runner = self.CreateShardedTestRunner(device, index)
+ test_runners += [test_runner]
+ except errors.DeviceUnresponsiveError as e:
+ logging.critical('****Failed to create a shard: [%s]', e)
+ self.attached_devices.remove(device)
+ continue
+
+ logging.warning('Starting...')
+ pool = multiprocessing.Pool(len(self.attached_devices),
+ SetTestsContainer,
+ [BaseTestSharder.tests_container])
+ # map can't handle KeyboardInterrupt exception. It's a python bug.
+ # So use map_async instead.
+ async_results = pool.map_async(_ShardedTestRunnable, test_runners)
+ try:
+ results_lists = async_results.get(999999)
+ except errors.DeviceUnresponsiveError as e:
+ logging.critical('****Failed to run test: [%s]', e)
+ self.attached_devices = android_commands.GetAttachedDevices()
+ continue
+ test_results = TestResults.FromTestResults(results_lists)
+ # Re-check the attached devices for some devices may
+ # become offline
+ retry_devices = set(android_commands.GetAttachedDevices())
+ # Remove devices that had exceptions.
+ retry_devices -= TestResults.DeviceExceptions(results_lists)
+ # Retry on devices that didn't have any exception.
+ self.attached_devices = list(retry_devices)
+ if (retry == self.retries - 1 or
+ len(self.attached_devices) == 0):
+ all_passed = final_results.ok + test_results.ok
+ final_results = test_results
+ final_results.ok = all_passed
+ break
+ else:
+ final_results.ok += test_results.ok
+ self.tests = []
+ for t in test_results.GetAllBroken():
+ self.tests += [t.name]
+ if not self.tests:
+ break
+ else:
+ # We ran out retries, possibly out of healthy devices.
+ # There's no recovery at this point.
+ raise Exception('Unrecoverable error while retrying test runs.')
+ self.OnTestsCompleted(test_runners, final_results)
+ self._KillHostForwarder()
+ return final_results
diff --git a/src/build/android/pylib/buildbot_report.py b/src/build/android/pylib/buildbot_report.py
new file mode 100644
index 0000000..8e7db8d
--- /dev/null
+++ b/src/build/android/pylib/buildbot_report.py
@@ -0,0 +1,54 @@
+# 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.
+
+"""Helper functions to print buildbot messages."""
+
+def PrintLink(label, url):
+ """Adds a link with name |label| linking to |url| to current buildbot step.
+
+ Args:
+ label: A string with the name of the label.
+ url: A string of the URL.
+ """
+ print '@@@STEP_LINK@%s@%s@@@' % (label, url)
+
+
+def PrintMsg(msg):
+ """Appends |msg| to the current buildbot step text.
+
+ Args:
+ msg: String to be appended.
+ """
+ print '@@@STEP_TEXT@%s@@@' % msg
+
+
+def PrintSummaryText(msg):
+ """Appends |msg| to main build summary. Visible from waterfall.
+
+ Args:
+ msg: String to be appended.
+ """
+ print '@@@STEP_SUMMARY_TEXT@%s@@@' % msg
+
+
+def PrintError():
+ """Marks the current step as failed."""
+ print '@@@STEP_FAILURE@@@'
+
+
+def PrintWarning():
+ """Marks the current step with a warning."""
+ print '@@@STEP_WARNINGS@@@'
+
+
+def PrintNamedStep(step):
+ print '@@@BUILD_STEP %s@@@' % step
+
+
+def PrintStepResultIfNeeded(options, result):
+ if result:
+ if options.buildbot_step_failure:
+ PrintError()
+ else:
+ PrintWarning()
diff --git a/src/build/android/pylib/chrome_test_server_spawner.py b/src/build/android/pylib/chrome_test_server_spawner.py
new file mode 100644
index 0000000..8206ca0
--- /dev/null
+++ b/src/build/android/pylib/chrome_test_server_spawner.py
@@ -0,0 +1,402 @@
+# 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.
+
+"""A "Test Server Spawner" that handles killing/stopping per-test test servers.
+
+It's used to accept requests from the device to spawn and kill instances of the
+chrome test server on the host.
+"""
+
+import BaseHTTPServer
+import json
+import logging
+import os
+import select
+import struct
+import subprocess
+import threading
+import time
+import urlparse
+
+import constants
+from forwarder import Forwarder
+import ports
+
+
+# Path that are needed to import necessary modules when running testserver.py.
+os.environ['PYTHONPATH'] = os.environ.get('PYTHONPATH', '') + ':%s:%s:%s:%s' % (
+ os.path.join(constants.CHROME_DIR, 'third_party'),
+ os.path.join(constants.CHROME_DIR, 'third_party', 'tlslite'),
+ os.path.join(constants.CHROME_DIR, 'third_party', 'pyftpdlib', 'src'),
+ os.path.join(constants.CHROME_DIR, 'net', 'tools', 'testserver'))
+
+
+SERVER_TYPES = {
+ 'http': '',
+ 'ftp': '-f',
+ 'sync': '--sync',
+ 'tcpecho': '--tcp-echo',
+ 'udpecho': '--udp-echo',
+}
+
+
+# The timeout (in seconds) of starting up the Python test server.
+TEST_SERVER_STARTUP_TIMEOUT = 10
+
+
+def _CheckPortStatus(port, expected_status):
+ """Returns True if port has expected_status.
+
+ Args:
+ port: the port number.
+ expected_status: boolean of expected status.
+
+ Returns:
+ Returns True if the status is expected. Otherwise returns False.
+ """
+ for timeout in range(1, 5):
+ if ports.IsHostPortUsed(port) == expected_status:
+ return True
+ time.sleep(timeout)
+ return False
+
+
+def _GetServerTypeCommandLine(server_type):
+ """Returns the command-line by the given server type.
+
+ Args:
+ server_type: the server type to be used (e.g. 'http').
+
+ Returns:
+ A string containing the command-line argument.
+ """
+ if server_type not in SERVER_TYPES:
+ raise NotImplementedError('Unknown server type: %s' % server_type)
+ if server_type == 'udpecho':
+ raise Exception('Please do not run UDP echo tests because we do not have '
+ 'a UDP forwarder tool.')
+ return SERVER_TYPES[server_type]
+
+
+class TestServerThread(threading.Thread):
+ """A thread to run the test server in a separate process."""
+
+ def __init__(self, ready_event, arguments, adb, tool, build_type):
+ """Initialize TestServerThread with the following argument.
+
+ Args:
+ ready_event: event which will be set when the test server is ready.
+ arguments: dictionary of arguments to run the test server.
+ adb: instance of AndroidCommands.
+ tool: instance of runtime error detection tool.
+ build_type: 'Release' or 'Debug'.
+ """
+ threading.Thread.__init__(self)
+ self.wait_event = threading.Event()
+ self.stop_flag = False
+ self.ready_event = ready_event
+ self.ready_event.clear()
+ self.arguments = arguments
+ self.adb = adb
+ self.tool = tool
+ self.test_server_process = None
+ self.is_ready = False
+ self.host_port = self.arguments['port']
+ assert isinstance(self.host_port, int)
+ self._test_server_forwarder = None
+ # The forwarder device port now is dynamically allocated.
+ self.forwarder_device_port = 0
+ # Anonymous pipe in order to get port info from test server.
+ self.pipe_in = None
+ self.pipe_out = None
+ self.command_line = []
+ self.build_type = build_type
+
+ def _WaitToStartAndGetPortFromTestServer(self):
+ """Waits for the Python test server to start and gets the port it is using.
+
+ The port information is passed by the Python test server with a pipe given
+ by self.pipe_out. It is written as a result to |self.host_port|.
+
+ Returns:
+ Whether the port used by the test server was successfully fetched.
+ """
+ assert self.host_port == 0 and self.pipe_out and self.pipe_in
+ (in_fds, _, _) = select.select([self.pipe_in, ], [], [],
+ TEST_SERVER_STARTUP_TIMEOUT)
+ if len(in_fds) == 0:
+ logging.error('Failed to wait to the Python test server to be started.')
+ return False
+ # First read the data length as an unsigned 4-byte value. This
+ # is _not_ using network byte ordering since the Python test server packs
+ # size as native byte order and all Chromium platforms so far are
+ # configured to use little-endian.
+ # TODO(jnd): Change the Python test server and local_test_server_*.cc to
+ # use a unified byte order (either big-endian or little-endian).
+ data_length = os.read(self.pipe_in, struct.calcsize('=L'))
+ if data_length:
+ (data_length,) = struct.unpack('=L', data_length)
+ assert data_length
+ if not data_length:
+ logging.error('Failed to get length of server data.')
+ return False
+ port_json = os.read(self.pipe_in, data_length)
+ if not port_json:
+ logging.error('Failed to get server data.')
+ return False
+ logging.info('Got port json data: %s', port_json)
+ port_json = json.loads(port_json)
+ if port_json.has_key('port') and isinstance(port_json['port'], int):
+ self.host_port = port_json['port']
+ return _CheckPortStatus(self.host_port, True)
+ logging.error('Failed to get port information from the server data.')
+ return False
+
+ def _GenerateCommandLineArguments(self):
+ """Generates the command line to run the test server.
+
+ Note that all options are processed by following the definitions in
+ testserver.py.
+ """
+ if self.command_line:
+ return
+ # The following arguments must exist.
+ type_cmd = _GetServerTypeCommandLine(self.arguments['server-type'])
+ if type_cmd:
+ self.command_line.append(type_cmd)
+ self.command_line.append('--port=%d' % self.host_port)
+ # Use a pipe to get the port given by the instance of Python test server
+ # if the test does not specify the port.
+ if self.host_port == 0:
+ (self.pipe_in, self.pipe_out) = os.pipe()
+ self.command_line.append('--startup-pipe=%d' % self.pipe_out)
+ self.command_line.append('--host=%s' % self.arguments['host'])
+ data_dir = self.arguments['data-dir'] or 'chrome/test/data'
+ if not os.path.isabs(data_dir):
+ data_dir = os.path.join(constants.CHROME_DIR, data_dir)
+ self.command_line.append('--data-dir=%s' % data_dir)
+ # The following arguments are optional depending on the individual test.
+ if self.arguments.has_key('log-to-console'):
+ self.command_line.append('--log-to-console')
+ if self.arguments.has_key('auth-token'):
+ self.command_line.append('--auth-token=%s' % self.arguments['auth-token'])
+ if self.arguments.has_key('https'):
+ self.command_line.append('--https')
+ if self.arguments.has_key('cert-and-key-file'):
+ self.command_line.append('--cert-and-key-file=%s' % os.path.join(
+ constants.CHROME_DIR, self.arguments['cert-and-key-file']))
+ if self.arguments.has_key('ocsp'):
+ self.command_line.append('--ocsp=%s' % self.arguments['ocsp'])
+ if self.arguments.has_key('https-record-resume'):
+ self.command_line.append('--https-record-resume')
+ if self.arguments.has_key('ssl-client-auth'):
+ self.command_line.append('--ssl-client-auth')
+ if self.arguments.has_key('tls-intolerant'):
+ self.command_line.append('--tls-intolerant=%s' %
+ self.arguments['tls-intolerant'])
+ if self.arguments.has_key('ssl-client-ca'):
+ for ca in self.arguments['ssl-client-ca']:
+ self.command_line.append('--ssl-client-ca=%s' %
+ os.path.join(constants.CHROME_DIR, ca))
+ if self.arguments.has_key('ssl-bulk-cipher'):
+ for bulk_cipher in self.arguments['ssl-bulk-cipher']:
+ self.command_line.append('--ssl-bulk-cipher=%s' % bulk_cipher)
+
+ def run(self):
+ logging.info('Start running the thread!')
+ self.wait_event.clear()
+ self._GenerateCommandLineArguments()
+ command = [os.path.join(constants.CHROME_DIR, 'net', 'tools',
+ 'testserver', 'testserver.py')] + self.command_line
+ logging.info('Running: %s', command)
+ self.process = subprocess.Popen(command)
+ if self.process:
+ if self.pipe_out:
+ self.is_ready = self._WaitToStartAndGetPortFromTestServer()
+ else:
+ self.is_ready = _CheckPortStatus(self.host_port, True)
+ if self.is_ready:
+ self._test_server_forwarder = Forwarder(self.adb, self.build_type)
+ self._test_server_forwarder.Run(
+ [(0, self.host_port)], self.tool, '127.0.0.1')
+ # Check whether the forwarder is ready on the device.
+ self.is_ready = False
+ device_port = self._test_server_forwarder.DevicePortForHostPort(
+ self.host_port)
+ if device_port:
+ for timeout in range(1, 5):
+ if ports.IsDevicePortUsed(self.adb, device_port, 'LISTEN'):
+ self.is_ready = True
+ self.forwarder_device_port = device_port
+ break
+ time.sleep(timeout)
+ # Wake up the request handler thread.
+ self.ready_event.set()
+ # Keep thread running until Stop() gets called.
+ while not self.stop_flag:
+ time.sleep(1)
+ if self.process.poll() is None:
+ self.process.kill()
+ if self._test_server_forwarder:
+ self._test_server_forwarder.Close()
+ self.process = None
+ self.is_ready = False
+ if self.pipe_out:
+ os.close(self.pipe_in)
+ os.close(self.pipe_out)
+ self.pipe_in = None
+ self.pipe_out = None
+ logging.info('Test-server has died.')
+ self.wait_event.set()
+
+ def Stop(self):
+ """Blocks until the loop has finished.
+
+ Note that this must be called in another thread.
+ """
+ if not self.process:
+ return
+ self.stop_flag = True
+ self.wait_event.wait()
+
+
+class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+ """A handler used to process http GET/POST request."""
+
+ def _SendResponse(self, response_code, response_reason, additional_headers,
+ contents):
+ """Generates a response sent to the client from the provided parameters.
+
+ Args:
+ response_code: number of the response status.
+ response_reason: string of reason description of the response.
+ additional_headers: dict of additional headers. Each key is the name of
+ the header, each value is the content of the header.
+ contents: string of the contents we want to send to client.
+ """
+ self.send_response(response_code, response_reason)
+ self.send_header('Content-Type', 'text/html')
+ # Specify the content-length as without it the http(s) response will not
+ # be completed properly (and the browser keeps expecting data).
+ self.send_header('Content-Length', len(contents))
+ for header_name in additional_headers:
+ self.send_header(header_name, additional_headers[header_name])
+ self.end_headers()
+ self.wfile.write(contents)
+ self.wfile.flush()
+
+ def _StartTestServer(self):
+ """Starts the test server thread."""
+ logging.info('Handling request to spawn a test server.')
+ content_type = self.headers.getheader('content-type')
+ if content_type != 'application/json':
+ raise Exception('Bad content-type for start request.')
+ content_length = self.headers.getheader('content-length')
+ if not content_length:
+ content_length = 0
+ try:
+ content_length = int(content_length)
+ except:
+ raise Exception('Bad content-length for start request.')
+ logging.info(content_length)
+ test_server_argument_json = self.rfile.read(content_length)
+ logging.info(test_server_argument_json)
+ assert not self.server.test_server_instance
+ ready_event = threading.Event()
+ self.server.test_server_instance = TestServerThread(
+ ready_event,
+ json.loads(test_server_argument_json),
+ self.server.adb,
+ self.server.tool,
+ self.server.build_type)
+ self.server.test_server_instance.setDaemon(True)
+ self.server.test_server_instance.start()
+ ready_event.wait()
+ if self.server.test_server_instance.is_ready:
+ self._SendResponse(200, 'OK', {}, json.dumps(
+ {'port': self.server.test_server_instance.forwarder_device_port,
+ 'message': 'started'}))
+ logging.info('Test server is running on port: %d.',
+ self.server.test_server_instance.host_port)
+ else:
+ self.server.test_server_instance.Stop()
+ self.server.test_server_instance = None
+ self._SendResponse(500, 'Test Server Error.', {}, '')
+ logging.info('Encounter problem during starting a test server.')
+
+ def _KillTestServer(self):
+ """Stops the test server instance."""
+ # There should only ever be one test server at a time. This may do the
+ # wrong thing if we try and start multiple test servers.
+ if not self.server.test_server_instance:
+ return
+ port = self.server.test_server_instance.host_port
+ logging.info('Handling request to kill a test server on port: %d.', port)
+ self.server.test_server_instance.Stop()
+ # Make sure the status of test server is correct before sending response.
+ if _CheckPortStatus(port, False):
+ self._SendResponse(200, 'OK', {}, 'killed')
+ logging.info('Test server on port %d is killed', port)
+ else:
+ self._SendResponse(500, 'Test Server Error.', {}, '')
+ logging.info('Encounter problem during killing a test server.')
+ self.server.test_server_instance = None
+
+ def do_POST(self):
+ parsed_path = urlparse.urlparse(self.path)
+ action = parsed_path.path
+ logging.info('Action for POST method is: %s.', action)
+ if action == '/start':
+ self._StartTestServer()
+ else:
+ self._SendResponse(400, 'Unknown request.', {}, '')
+ logging.info('Encounter unknown request: %s.', action)
+
+ def do_GET(self):
+ parsed_path = urlparse.urlparse(self.path)
+ action = parsed_path.path
+ params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1)
+ logging.info('Action for GET method is: %s.', action)
+ for param in params:
+ logging.info('%s=%s', param, params[param][0])
+ if action == '/kill':
+ self._KillTestServer()
+ elif action == '/ping':
+ # The ping handler is used to check whether the spawner server is ready
+ # to serve the requests. We don't need to test the status of the test
+ # server when handling ping request.
+ self._SendResponse(200, 'OK', {}, 'ready')
+ logging.info('Handled ping request and sent response.')
+ else:
+ self._SendResponse(400, 'Unknown request', {}, '')
+ logging.info('Encounter unknown request: %s.', action)
+
+
+class SpawningServer(object):
+ """The class used to start/stop a http server."""
+
+ def __init__(self, test_server_spawner_port, adb, tool, build_type):
+ logging.info('Creating new spawner on port: %d.', test_server_spawner_port)
+ self.server = BaseHTTPServer.HTTPServer(('', test_server_spawner_port),
+ SpawningServerRequestHandler)
+ self.port = test_server_spawner_port
+ self.server.adb = adb
+ self.server.tool = tool
+ self.server.test_server_instance = None
+ self.server.build_type = build_type
+
+ def _Listen(self):
+ logging.info('Starting test server spawner')
+ self.server.serve_forever()
+
+ def Start(self):
+ listener_thread = threading.Thread(target=self._Listen)
+ listener_thread.setDaemon(True)
+ listener_thread.start()
+ time.sleep(1)
+
+ def Stop(self):
+ if self.server.test_server_instance:
+ self.server.test_server_instance.Stop()
+ self.server.shutdown()
diff --git a/src/build/android/pylib/cmd_helper.py b/src/build/android/pylib/cmd_helper.py
new file mode 100644
index 0000000..b6bbac7
--- /dev/null
+++ b/src/build/android/pylib/cmd_helper.py
@@ -0,0 +1,77 @@
+# 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.
+
+"""A wrapper for subprocess to make calling shell commands easier."""
+
+import os
+import logging
+import subprocess
+
+import constants
+
+def RunCmd(args, cwd=None):
+ """Opens a subprocess to execute a program and returns its return value.
+
+ Args:
+ args: A string or a sequence of program arguments. The program to execute is
+ the string or the first item in the args sequence.
+ cwd: If not None, the subprocess's current directory will be changed to
+ |cwd| before it's executed.
+
+ Returns:
+ Return code from the command execution.
+ """
+ logging.info(str(args) + ' ' + (cwd or ''))
+ p = subprocess.Popen(args=args, cwd=cwd)
+ return p.wait()
+
+
+def GetCmdOutput(args, cwd=None, shell=False):
+ """Open a subprocess to execute a program and returns its output.
+
+ Args:
+ args: A string or a sequence of program arguments. The program to execute is
+ the string or the first item in the args sequence.
+ cwd: If not None, the subprocess's current directory will be changed to
+ |cwd| before it's executed.
+ shell: Whether to execute args as a shell command.
+
+ Returns:
+ Captures and returns the command's stdout.
+ Prints the command's stderr to logger (which defaults to stdout).
+ """
+ (_, output) = GetCmdStatusAndOutput(args, cwd, shell)
+ return output
+
+def GetCmdStatusAndOutput(args, cwd=None, shell=False):
+ """Executes a subprocess and returns its exit code and output.
+
+ Args:
+ args: A string or a sequence of program arguments. The program to execute is
+ the string or the first item in the args sequence.
+ cwd: If not None, the subprocess's current directory will be changed to
+ |cwd| before it's executed.
+ shell: Whether to execute args as a shell command.
+
+ Returns:
+ The tuple (exit code, output).
+ """
+ logging.info(str(args) + ' ' + (cwd or ''))
+ p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, shell=shell)
+ stdout, stderr = p.communicate()
+ exit_code = p.returncode
+ if stderr:
+ logging.critical(stderr)
+ logging.info(stdout[:4096]) # Truncate output longer than 4k.
+ return (exit_code, stdout)
+
+class OutDirectory(object):
+ _out_directory = os.path.join(constants.CHROME_DIR, 'out')
+ @staticmethod
+ def set(out_directory):
+ OutDirectory._out_directory = out_directory
+ @staticmethod
+ def get():
+ return OutDirectory._out_directory
diff --git a/src/build/android/pylib/constants.py b/src/build/android/pylib/constants.py
new file mode 100644
index 0000000..69c9f30
--- /dev/null
+++ b/src/build/android/pylib/constants.py
@@ -0,0 +1,50 @@
+# 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.
+
+"""Defines a set of constants shared by test runners and other scripts."""
+
+import os
+
+
+CHROME_PACKAGE = 'com.google.android.apps.chrome'
+CHROME_ACTIVITY = 'com.google.android.apps.chrome.Main'
+CHROME_TESTS_PACKAGE = 'com.google.android.apps.chrome.tests'
+LEGACY_BROWSER_PACKAGE = 'com.google.android.browser'
+LEGACY_BROWSER_ACTIVITY = 'com.android.browser.BrowserActivity'
+CONTENT_SHELL_PACKAGE = "org.chromium.content_shell"
+CONTENT_SHELL_ACTIVITY = "org.chromium.content_shell.ContentShellActivity"
+CHROME_SHELL_PACKAGE = 'org.chromium.chrome.browser.test'
+CHROMIUM_TEST_SHELL_PACKAGE = 'org.chromium.chrome.testshell'
+
+CHROME_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '..', '..', '..'))
+
+# Ports arrangement for various test servers used in Chrome for Android.
+# Lighttpd server will attempt to use 9000 as default port, if unavailable it
+# will find a free port from 8001 - 8999.
+LIGHTTPD_DEFAULT_PORT = 9000
+LIGHTTPD_RANDOM_PORT_FIRST = 8001
+LIGHTTPD_RANDOM_PORT_LAST = 8999
+TEST_SYNC_SERVER_PORT = 9031
+
+# The net test server is started from 10000. Reserve 20000 ports for the all
+# test-server based tests should be enough for allocating different port for
+# individual test-server based test.
+TEST_SERVER_PORT_FIRST = 10000
+TEST_SERVER_PORT_LAST = 30000
+# A file to record next valid port of test server.
+TEST_SERVER_PORT_FILE = '/tmp/test_server_port'
+TEST_SERVER_PORT_LOCKFILE = '/tmp/test_server_port.lock'
+
+TEST_EXECUTABLE_DIR = '/data/local/tmp'
+# Directories for common java libraries for SDK build.
+# These constants are defined in build/android/ant/common.xml
+SDK_BUILD_TEST_JAVALIB_DIR = 'test.lib.java'
+SDK_BUILD_APKS_DIR = 'apks'
+
+# The directory on the device where perf test output gets saved to.
+DEVICE_PERF_OUTPUT_DIR = '/data/data/' + CHROME_PACKAGE + '/files'
+
+# Directory on host where screensohts are saved.
+SCREENSHOTS_DIR = os.path.join(CHROME_DIR, 'out_screenshots')
diff --git a/src/build/android/pylib/debug_info.py b/src/build/android/pylib/debug_info.py
new file mode 100644
index 0000000..6f0f55a
--- /dev/null
+++ b/src/build/android/pylib/debug_info.py
@@ -0,0 +1,196 @@
+# 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.
+
+"""Collect debug info for a test."""
+
+import datetime
+import logging
+import os
+import re
+import shutil
+import string
+import subprocess
+import tempfile
+
+import cmd_helper
+
+
+TOMBSTONE_DIR = '/data/tombstones/'
+
+
+class GTestDebugInfo(object):
+ """A helper class to collect related debug information for a gtest.
+
+ Debug info is collected in two steps:
+ - first, object(s) of this class (one per device), accumulate logs
+ and screenshots in tempdir.
+ - once the test has finished, call ZipAndCleanResults to create
+ a zip containing the logs from all devices, and clean them up.
+
+ Args:
+ adb: ADB interface the tests are using.
+ device: Serial# of the Android device in which the specified gtest runs.
+ testsuite_name: Name of the specified gtest.
+ gtest_filter: Test filter used by the specified gtest.
+ """
+
+ def __init__(self, adb, device, testsuite_name, gtest_filter):
+ """Initializes the DebugInfo class for a specified gtest."""
+ self.adb = adb
+ self.device = device
+ self.testsuite_name = testsuite_name
+ self.gtest_filter = gtest_filter
+ self.logcat_process = None
+ self.has_storage = False
+ self.log_dir = os.path.join(tempfile.gettempdir(),
+ 'gtest_debug_info',
+ self.testsuite_name,
+ self.device)
+ if not os.path.exists(self.log_dir):
+ os.makedirs(self.log_dir)
+ self.log_file_name = os.path.join(self.log_dir,
+ self._GeneratePrefixName() + '_log.txt')
+ self.old_crash_files = self._ListCrashFiles()
+
+ def _GetSignatureFromGTestFilter(self):
+ """Gets a signature from gtest_filter.
+
+ Signature is used to identify the tests from which we collect debug
+ information.
+
+ Returns:
+ A signature string. Returns 'all' if there is no gtest filter.
+ """
+ if not self.gtest_filter:
+ return 'all'
+ filename_chars = "-_()%s%s" % (string.ascii_letters, string.digits)
+ signature = ''.join(c for c in self.gtest_filter if c in filename_chars)
+ if len(signature) > 64:
+ # The signature can't be too long, as it'll be part of a file name.
+ signature = signature[:64]
+ return signature
+
+ def _GeneratePrefixName(self):
+ """Generates a prefix name for debug information of the test.
+
+ The prefix name consists of the following:
+ (1) root name of test_suite_base.
+ (2) device serial number.
+ (3) prefix of filter signature generate from gtest_filter.
+ (4) date & time when calling this method.
+
+ Returns:
+ Name of the log file.
+ """
+ return (os.path.splitext(self.testsuite_name)[0] + '_' + self.device + '_' +
+ self._GetSignatureFromGTestFilter() + '_' +
+ datetime.datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S-%f'))
+
+ def StartRecordingLog(self, clear=True, filters=['*:v']):
+ """Starts recording logcat output to a file.
+
+ This call should come before running test, with calling StopRecordingLog
+ following the tests.
+
+ Args:
+ clear: True if existing log output should be cleared.
+ filters: A list of logcat filters to be used.
+ """
+ self.StopRecordingLog()
+ if clear:
+ cmd_helper.RunCmd(['adb', '-s', self.device, 'logcat', '-c'])
+ logging.info('Start dumping log to %s ...', self.log_file_name)
+ command = 'adb -s %s logcat -v threadtime %s > %s' % (self.device,
+ ' '.join(filters),
+ self.log_file_name)
+ self.logcat_process = subprocess.Popen(command, shell=True)
+
+ def StopRecordingLog(self):
+ """Stops an existing logcat recording subprocess."""
+ if not self.logcat_process:
+ return
+ # Cannot evaluate directly as 0 is a possible value.
+ if self.logcat_process.poll() is None:
+ self.logcat_process.kill()
+ self.logcat_process = None
+ logging.info('Finish log dump.')
+
+ def TakeScreenshot(self, identifier_mark):
+ """Takes a screen shot from current specified device.
+
+ Args:
+ identifier_mark: A string to identify the screen shot DebugInfo will take.
+ It will be part of filename of the screen shot. Empty
+ string is acceptable.
+ Returns:
+ Returns the file name on the host of the screenshot if successful,
+ None otherwise.
+ """
+ assert isinstance(identifier_mark, str)
+ screenshot_path = os.path.join(os.getenv('ANDROID_HOST_OUT', ''),
+ 'bin',
+ 'screenshot2')
+ if not os.path.exists(screenshot_path):
+ logging.error('Failed to take screen shot from device %s', self.device)
+ return None
+ shot_path = os.path.join(self.log_dir, ''.join([self._GeneratePrefixName(),
+ identifier_mark,
+ '_screenshot.png']))
+ re_success = re.compile(re.escape('Success.'), re.MULTILINE)
+ if re_success.findall(cmd_helper.GetCmdOutput([screenshot_path, '-s',
+ self.device, shot_path])):
+ logging.info('Successfully took a screen shot to %s', shot_path)
+ return shot_path
+ logging.error('Failed to take screen shot from device %s', self.device)
+ return None
+
+ def _ListCrashFiles(self):
+ """Collects crash files from current specified device.
+
+ Returns:
+ A dict of crash files in format {"name": (size, lastmod), ...}.
+ """
+ return self.adb.ListPathContents(TOMBSTONE_DIR)
+
+ def ArchiveNewCrashFiles(self):
+ """Archives the crash files newly generated until calling this method."""
+ current_crash_files = self._ListCrashFiles()
+ files = []
+ for f in current_crash_files:
+ if f not in self.old_crash_files:
+ files += [f]
+ elif current_crash_files[f] != self.old_crash_files[f]:
+ # Tombstones dir can only have maximum 10 files, so we need to compare
+ # size and timestamp information of file if the file exists.
+ files += [f]
+ if files:
+ logging.info('New crash file(s):%s' % ' '.join(files))
+ for f in files:
+ self.adb.Adb().Pull(TOMBSTONE_DIR + f,
+ os.path.join(self.log_dir, f))
+
+ @staticmethod
+ def ZipAndCleanResults(dest_dir, dump_file_name):
+ """A helper method to zip all debug information results into a dump file.
+
+ Args:
+ dest_dir: Dir path in where we put the dump file.
+ dump_file_name: Desired name of the dump file. This method makes sure
+ '.zip' will be added as ext name.
+ """
+ if not dest_dir or not dump_file_name:
+ return
+ cmd_helper.RunCmd(['mkdir', '-p', dest_dir])
+ log_basename = os.path.basename(dump_file_name)
+ log_zip_file = os.path.join(dest_dir,
+ os.path.splitext(log_basename)[0] + '.zip')
+ logging.info('Zipping debug dumps into %s ...', log_zip_file)
+ # Add new dumps into the zip file. The zip may exist already if previous
+ # gtest also dumps the debug information. It's OK since we clean up the old
+ # dumps in each build step.
+ log_src_dir = os.path.join(tempfile.gettempdir(), 'gtest_debug_info')
+ cmd_helper.RunCmd(['zip', '-q', '-r', log_zip_file, log_src_dir])
+ assert os.path.exists(log_zip_file)
+ assert os.path.exists(log_src_dir)
+ shutil.rmtree(log_src_dir)
diff --git a/src/build/android/pylib/device_stats_monitor.html b/src/build/android/pylib/device_stats_monitor.html
new file mode 100644
index 0000000..b3abbb0
--- /dev/null
+++ b/src/build/android/pylib/device_stats_monitor.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<!--
+ * 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.
+-->
+<html>
+<head>
+ <title>Device Stats Monitor</title>
+ <script type="text/javascript" src="http://www.google.com/jsapi"></script>
+ <style>
+ body {
+ font-family: sans-serif
+ }
+ </style>
+</head>
+<body>
+<h2>Device Stats Monitor</h2>
+<ul>
+<li>Pass path to trace data via the <code>results</code> querystring param.
+<li>Combine charts with the <code>combine</code> querystring param (e.g. <code>&combine=sectors_read,sectors_written</code>).
+<li>Use <code>stacked=true</code> to stack combined charts instead of overlaying (default).
+</ul>
+</body>
+<script>
+google.load("visualization", "1", {packages:["corechart"]});
+
+/**
+ * @returns The querystring param value for |name| or an empty string.
+ */
+function getQuerystringParam(name) {
+ name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
+ var regexS = "[\\?&]" + name + "=([^&#]*)";
+ var regex = new RegExp(regexS);
+ var results = regex.exec(window.location.search);
+ if (results == null)
+ return "";
+ else
+ return decodeURIComponent(results[1].replace(/\+/g, " "));
+}
+
+/**
+ * @returns An array of keys in |obj| sorted by value.
+ */
+function sortedKeys(obj) {
+ var keys = [];
+ for (var key in obj) {
+ keys.push(key);
+ }
+ keys.sort();
+ return keys;
+}
+
+/**
+ * Removes by value all params from array.
+ */
+Array.prototype.remove = function() {
+ var what, a = arguments, l = a.length, ax;
+ while (l && this.length) {
+ what = a[--l];
+ while ((ax = this.indexOf(what)) != -1) {
+ this.splice(ax, 1);
+ }
+ }
+ return this;
+}
+
+/**
+ * Displays a new chart.
+ *
+ * @param {Number} hz Number of sample per second of the data.
+ * @param {String} name Name to display on top of chart.
+ * @param {Number[][]} values Array of value arrays to display.
+ * @param {Boolean} stacked Whether to display values as stacked.
+ */
+function displayChart(hz, name, values, units, stacked) {
+ var data = new google.visualization.DataTable();
+ data.addColumn('number', 'ms');
+ var names = name.split(',');
+ for (var i = 0; i < names.length; i++) {
+ data.addColumn('number', names[i]);
+ }
+
+ var rows = [];
+ var interval = 1000.0 / hz;
+ for (var i = 0; i < values[0].length; i++) {
+ var row = [i*interval];
+ for (var j = 0; j < values.length; j++) {
+ row.push(values[j][i]);
+ }
+ rows.push(row);
+ }
+ data.addRows(rows);
+
+ var options = {
+ hAxis: {title: 'ms (' + hz + 'hz)'},
+ isStacked: stacked,
+ legend: {position: 'top'},
+ vAxis: {title: units},
+ };
+
+ var elem = document.createElement('DIV');
+ elem.style = 'width:100%;height:500px';
+ document.body.appendChild(elem);
+ var chart = new google.visualization.AreaChart(elem);
+ chart.draw(data, options);
+}
+
+/**
+ * Displays all charts.
+ *
+ * Invoked by the results script. JSONP is used to avoid security
+ * restrictions on XHRs for file:// URLs.
+ */
+function display(hz, results, units) {
+ var combine = getQuerystringParam('combine');
+ var keys = sortedKeys(results);
+ for (var i = 0; i < keys.length; i++) {
+ var key = keys[i];
+ var name = key;
+ var values = [results[key]];
+ var unit = units[key];
+ if (combine.indexOf(key) >= 0) {
+ i--;
+ name = combine;
+ values = [];
+ var combined_keys = combine.split(',');
+ for (var j = 0; j < combined_keys.length; j++) {
+ values.push(results[combined_keys[j]]);
+ keys.remove(combined_keys[j]);
+ }
+ }
+ displayChart(hz, name, values, unit, !!getQuerystringParam('stacked'));
+ }
+}
+
+var resultsPath = getQuerystringParam('results');
+if (resultsPath)
+ document.write("<script src='" + resultsPath + "'></"+"script>");
+else
+ document.write("Please specify results querystring param.");
+</script>
+</html>
diff --git a/src/build/android/pylib/device_stats_monitor.py b/src/build/android/pylib/device_stats_monitor.py
new file mode 100644
index 0000000..8be4efa
--- /dev/null
+++ b/src/build/android/pylib/device_stats_monitor.py
@@ -0,0 +1,116 @@
+# 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.
+
+"""Utilities for iotop/top style profiling for android."""
+
+import collections
+import json
+import os
+import subprocess
+import sys
+import urllib
+
+import constants
+import io_stats_parser
+
+
+class DeviceStatsMonitor(object):
+ """Class for collecting device stats such as IO/CPU usage.
+
+ Args:
+ adb: Instance of AndroidComannds.
+ hz: Frequency at which to sample device stats.
+ """
+
+ DEVICE_PATH = constants.TEST_EXECUTABLE_DIR + '/device_stats_monitor'
+ PROFILE_PATH = (constants.DEVICE_PERF_OUTPUT_DIR +
+ '/device_stats_monitor.profile')
+ RESULT_VIEWER_PATH = os.path.abspath(os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'device_stats_monitor.html'))
+
+ def __init__(self, adb, hz, build_type):
+ self._adb = adb
+ host_path = os.path.abspath(os.path.join(
+ constants.CHROME_DIR, 'out', build_type, 'device_stats_monitor'))
+ self._adb.PushIfNeeded(host_path, DeviceStatsMonitor.DEVICE_PATH)
+ self._hz = hz
+
+ def Start(self):
+ """Starts device stats monitor on the device."""
+ self._adb.SetFileContents(DeviceStatsMonitor.PROFILE_PATH, '')
+ self._process = subprocess.Popen(
+ ['adb', 'shell', '%s --hz=%d %s' % (
+ DeviceStatsMonitor.DEVICE_PATH, self._hz,
+ DeviceStatsMonitor.PROFILE_PATH)])
+
+ def StopAndCollect(self, output_path):
+ """Stops monitoring and saves results.
+
+ Args:
+ output_path: Path to save results.
+
+ Returns:
+ String of URL to load results in browser.
+ """
+ assert self._process
+ self._adb.KillAll(DeviceStatsMonitor.DEVICE_PATH)
+ self._process.wait()
+ profile = self._adb.GetFileContents(DeviceStatsMonitor.PROFILE_PATH)
+
+ results = collections.defaultdict(list)
+ last_io_stats = None
+ last_cpu_stats = None
+ for line in profile:
+ if ' mmcblk0 ' in line:
+ stats = io_stats_parser.ParseIoStatsLine(line)
+ if last_io_stats:
+ results['sectors_read'].append(stats.num_sectors_read -
+ last_io_stats.num_sectors_read)
+ results['sectors_written'].append(stats.num_sectors_written -
+ last_io_stats.num_sectors_written)
+ last_io_stats = stats
+ elif line.startswith('cpu '):
+ stats = self._ParseCpuStatsLine(line)
+ if last_cpu_stats:
+ results['user'].append(stats.user - last_cpu_stats.user)
+ results['nice'].append(stats.nice - last_cpu_stats.nice)
+ results['system'].append(stats.system - last_cpu_stats.system)
+ results['idle'].append(stats.idle - last_cpu_stats.idle)
+ results['iowait'].append(stats.iowait - last_cpu_stats.iowait)
+ results['irq'].append(stats.irq - last_cpu_stats.irq)
+ results['softirq'].append(stats.softirq- last_cpu_stats.softirq)
+ last_cpu_stats = stats
+ units = {
+ 'sectors_read': 'sectors',
+ 'sectors_written': 'sectors',
+ 'user': 'jiffies',
+ 'nice': 'jiffies',
+ 'system': 'jiffies',
+ 'idle': 'jiffies',
+ 'iowait': 'jiffies',
+ 'irq': 'jiffies',
+ 'softirq': 'jiffies',
+ }
+ with open(output_path, 'w') as f:
+ f.write('display(%d, %s, %s);' % (self._hz, json.dumps(results), units))
+ return 'file://%s?results=file://%s' % (
+ DeviceStatsMonitor.RESULT_VIEWER_PATH, urllib.quote(output_path))
+
+
+ @staticmethod
+ def _ParseCpuStatsLine(line):
+ """Parses a line of cpu stats into a CpuStats named tuple."""
+ # Field definitions: http://www.linuxhowtos.org/System/procstat.htm
+ cpu_stats = collections.namedtuple('CpuStats',
+ ['device',
+ 'user',
+ 'nice',
+ 'system',
+ 'idle',
+ 'iowait',
+ 'irq',
+ 'softirq',
+ ])
+ fields = line.split()
+ return cpu_stats._make([fields[0]] + [int(f) for f in fields[1:8]])
diff --git a/src/build/android/pylib/fake_dns.py b/src/build/android/pylib/fake_dns.py
new file mode 100644
index 0000000..1c64490
--- /dev/null
+++ b/src/build/android/pylib/fake_dns.py
@@ -0,0 +1,63 @@
+# 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 android_commands
+import constants
+import logging
+import os
+import subprocess
+import time
+
+
+class FakeDns(object):
+ """Wrapper class for the fake_dns tool."""
+ _FAKE_DNS_PATH = constants.TEST_EXECUTABLE_DIR + '/fake_dns'
+
+ def __init__(self, adb, build_type):
+ """
+ Args:
+ adb: the AndroidCommands to use.
+ build_type: 'Release' or 'Debug'.
+ """
+ self._adb = adb
+ self._build_type = build_type
+ self._fake_dns = None
+ self._original_dns = None
+
+ def _PushAndStartFakeDns(self):
+ """Starts the fake_dns server that replies all name queries 127.0.0.1.
+
+ Returns:
+ subprocess instance connected to the fake_dns process on the device.
+ """
+ self._adb.PushIfNeeded(
+ os.path.join(constants.CHROME_DIR, 'out', self._build_type, 'fake_dns'),
+ FakeDns._FAKE_DNS_PATH)
+ return subprocess.Popen(
+ ['adb', '-s', self._adb._adb.GetSerialNumber(),
+ 'shell', '%s -D' % FakeDns._FAKE_DNS_PATH])
+
+ def SetUp(self):
+ """Configures the system to point to a DNS server that replies 127.0.0.1.
+
+ This can be used in combination with the forwarder to forward all web
+ traffic to a replay server.
+
+ The TearDown() method will perform all cleanup.
+ """
+ self._adb.RunShellCommand('ip route add 8.8.8.0/24 via 127.0.0.1 dev lo')
+ self._fake_dns = self._PushAndStartFakeDns()
+ self._original_dns = self._adb.RunShellCommand('getprop net.dns1')[0]
+ self._adb.RunShellCommand('setprop net.dns1 127.0.0.1')
+ time.sleep(2) # Time for server to start and the setprop to take effect.
+
+ def TearDown(self):
+ """Shuts down the fake_dns."""
+ if self._fake_dns:
+ if not self._original_dns or self._original_dns == '127.0.0.1':
+ logging.warning('Bad original DNS, falling back to Google DNS.')
+ self._original_dns = '8.8.8.8'
+ self._adb.RunShellCommand('setprop net.dns1 %s' % self._original_dns)
+ self._fake_dns.kill()
+ self._adb.RunShellCommand('ip route del 8.8.8.0/24 via 127.0.0.1 dev lo')
diff --git a/src/build/android/pylib/findbugs.py b/src/build/android/pylib/findbugs.py
new file mode 100755
index 0000000..996c0ee
--- /dev/null
+++ b/src/build/android/pylib/findbugs.py
@@ -0,0 +1,240 @@
+#!/usr/bin/env python
+#
+# 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 optparse
+import os
+import re
+import shlex
+import subprocess
+import sys
+
+
+def _PrintMessage(warnings, title, action, known_bugs_file):
+ if warnings:
+ print
+ print '*' * 80
+ print '%s warnings.' % title
+ print '%s %s' % (action, known_bugs_file)
+ print '-' * 80
+ for warning in warnings:
+ print warning
+ print '-' * 80
+ print
+
+
+def _StripLineNumbers(current_warnings):
+ re_line = r':\[line.*?\]$'
+ return [re.sub(re_line, '', x) for x in current_warnings]
+
+
+def _DiffKnownWarnings(current_warnings_set, known_bugs_file):
+ with open(known_bugs_file, 'r') as known_bugs:
+ known_bugs_set = set(known_bugs.read().splitlines())
+
+ new_warnings = current_warnings_set - known_bugs_set
+ _PrintMessage(sorted(new_warnings), 'New', 'Please fix, or perhaps add to',
+ known_bugs_file)
+
+ obsolete_warnings = known_bugs_set - current_warnings_set
+ _PrintMessage(sorted(obsolete_warnings), 'Obsolete', 'Please remove from',
+ known_bugs_file)
+
+ count = len(new_warnings) + len(obsolete_warnings)
+ if count:
+ print '*** %d FindBugs warning%s! ***' % (count, 's' * (count > 1))
+ if len(new_warnings):
+ print '*** %d: new ***' % len(new_warnings)
+ if len(obsolete_warnings):
+ print '*** %d: obsolete ***' % len(obsolete_warnings)
+ print
+ print 'Alternatively, rebaseline with --rebaseline command option'
+ print
+ else:
+ print 'No new FindBugs warnings.'
+ print
+ return count
+
+
+def _Rebaseline(current_warnings_set, known_bugs_file):
+ with file(known_bugs_file, 'w') as known_bugs:
+ for warning in sorted(current_warnings_set):
+ print >>known_bugs, warning
+ return 0
+
+
+def _GetChromeClasses(release_version):
+ chrome_src = os.getenv('CHROME_SRC')
+ version = 'Debug'
+ if release_version:
+ version = 'Release'
+ path = os.path.join(chrome_src, 'out', version)
+ cmd = 'find %s -name "*.class"' % path
+ proc = subprocess.Popen(shlex.split(cmd),
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ out, err = proc.communicate()
+
+ if not out:
+ print 'No classes found in %s' % path
+ return out
+
+
+def _Run(exclude, known_bugs, classes_to_analyze, auxiliary_classes,
+ rebaseline, release_version, findbug_args):
+ """Run the FindBugs.
+
+ Args:
+ exclude: the exclude xml file, refer to FindBugs's -exclude command option.
+ known_bugs: the text file of known bugs. The bugs in it will not be
+ reported.
+ 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.
+ rebaseline: True if the known_bugs file needs rebaseline.
+ release_version: True if the release version needs check, otherwise check
+ debug version.
+ findbug_args: addtional command line options needs pass to Findbugs.
+ """
+
+ chrome_src = os.getenv('CHROME_SRC')
+ sdk_root = os.getenv('ANDROID_SDK_ROOT')
+ sdk_version = os.getenv('ANDROID_SDK_VERSION')
+
+ system_classes = []
+ system_classes.append(os.path.join(sdk_root, 'platforms',
+ 'android-%s' % sdk_version, 'android.jar'))
+ if auxiliary_classes:
+ for classes in auxiliary_classes:
+ system_classes.append(os.path.abspath(classes))
+
+ cmd = '%s -textui -sortByClass ' % os.path.join(chrome_src, 'third_party',
+ 'findbugs', 'bin', 'findbugs')
+ cmd = '%s -pluginList %s' % (cmd, os.path.join(chrome_src, 'tools', 'android',
+ 'findbugs_plugin', 'lib',
+ 'chromiumPlugin.jar'))
+ if len(system_classes):
+ cmd = '%s -auxclasspath %s ' % (cmd, ':'.join(system_classes))
+
+ if classes_to_analyze:
+ cmd = '%s -onlyAnalyze %s ' % (cmd, classes_to_analyze)
+
+ if exclude:
+ cmd = '%s -exclude %s ' % (cmd, os.path.abspath(exclude))
+
+ if findbug_args:
+ cmd = '%s %s ' % (cmd, fingbug_args)
+
+
+ chrome_classes = _GetChromeClasses(release_version)
+ if not chrome_classes:
+ return 1
+ cmd = '%s %s ' % (cmd, chrome_classes)
+
+ proc = subprocess.Popen(shlex.split(cmd),
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ out, err = proc.communicate()
+ current_warnings_set = set(_StripLineNumbers(filter(None, out.splitlines())))
+
+ if rebaseline:
+ return _Rebaseline(current_warnings_set, known_bugs)
+ else:
+ return _DiffKnownWarnings(current_warnings_set, known_bugs)
+
+def Run(options):
+ exclude_file = None
+ known_bugs_file = None
+
+ if options.exclude:
+ exclude_file = options.exclude
+ elif options.base_dir:
+ exclude_file = os.path.join(options.base_dir, 'findbugs_exclude.xml')
+
+ if options.known_bugs:
+ known_bugs_file = options.known_bugs
+ elif options.base_dir:
+ known_bugs_file = os.path.join(options.base_dir, 'findbugs_known_bugs.txt')
+
+ auxclasspath = None
+ if options.auxclasspath:
+ auxclasspath = options.auxclasspath.split(':')
+ return _Run(exclude_file, known_bugs_file, options.only_analyze, auxclasspath,
+ options.rebaseline, options.release_build, options.findbug_args)
+
+
+def GetCommonParser():
+ parser = optparse.OptionParser()
+ parser.add_option('-r',
+ '--rebaseline',
+ action='store_true',
+ dest='rebaseline',
+ help='Rebaseline known findbugs issues.')
+
+ parser.add_option('-a',
+ '--auxclasspath',
+ action='store',
+ default=None,
+ dest='auxclasspath',
+ help='Set aux classpath for analysis.')
+
+ parser.add_option('-o',
+ '--only-analyze',
+ action='store',
+ default=None,
+ dest='only_analyze',
+ help='Only analyze the given classes and packages.')
+
+ parser.add_option('-e',
+ '--exclude',
+ action='store',
+ default=None,
+ dest='exclude',
+ help='Exclude bugs matching given filter.')
+
+ parser.add_option('-k',
+ '--known-bugs',
+ action='store',
+ default=None,
+ dest='known_bugs',
+ help='Not report the bugs in the given file.')
+
+ parser.add_option('-l',
+ '--release-build',
+ action='store_true',
+ dest='release_build',
+ help='Analyze release build instead of debug.')
+
+ parser.add_option('-f',
+ '--findbug-args',
+ action='store',
+ default=None,
+ dest='findbug_args',
+ help='Additional findbug arguments.')
+
+ parser.add_option('-b',
+ '--base-dir',
+ action='store',
+ default=None,
+ dest='base_dir',
+ help='Base directory for configuration file.')
+
+ return parser
+
+def CheckEnvironment():
+ if not (os.getenv('CHROME_SRC') and os.getenv('ANDROID_SDK_ROOT') and
+ os.getenv('ANDROID_SDK_VERSION')):
+ print 'Your build environment is not set up correctly.'
+ print 'Please source build/android/envsetup.sh.'
+ return False
+ return True
+
+def main(argv):
+ parser = GetCommonParser()
+ options, _ = parser.parse_args()
+
+ return Run(options)
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/src/build/android/pylib/flag_changer.py b/src/build/android/pylib/flag_changer.py
new file mode 100644
index 0000000..8b8dbca
--- /dev/null
+++ b/src/build/android/pylib/flag_changer.py
@@ -0,0 +1,144 @@
+# 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 constants
+import traceback
+import warnings
+
+
+# Location where chrome reads command line flags from
+CHROME_COMMAND_FILE = '/data/local/chrome-command-line'
+
+class FlagChanger(object):
+ """Changes the flags Chrome runs with.
+
+ There are two different use cases for this file:
+ * Flags are permanently set by calling Set().
+ * Flags can be temporarily set for a particular set of unit tests. These
+ tests should call Restore() to revert the flags to their original state
+ once the tests have completed.
+ """
+
+ def __init__(self, android_cmd):
+ self._android_cmd = android_cmd
+
+ # Save the original flags.
+ self._orig_line = self._android_cmd.GetFileContents(CHROME_COMMAND_FILE)
+ if self._orig_line:
+ self._orig_line = self._orig_line[0].strip()
+
+ # Parse out the flags into a list to facilitate adding and removing flags.
+ self._current_flags = self._TokenizeFlags(self._orig_line)
+
+ def Get(self):
+ """Returns list of current flags."""
+ return self._current_flags
+
+ def Set(self, flags):
+ """Replaces all flags on the current command line with the flags given.
+
+ Args:
+ flags: A list of flags to set, eg. ['--single-process'].
+ """
+ if flags:
+ assert flags[0] != 'chrome'
+
+ self._current_flags = flags
+ self._UpdateCommandLineFile()
+
+ def AddFlags(self, flags):
+ """Appends flags to the command line if they aren't already there.
+
+ Args:
+ flags: A list of flags to add on, eg. ['--single-process'].
+ """
+ if flags:
+ assert flags[0] != 'chrome'
+
+ # Avoid appending flags that are already present.
+ for flag in flags:
+ if flag not in self._current_flags:
+ self._current_flags.append(flag)
+ self._UpdateCommandLineFile()
+
+ def RemoveFlags(self, flags):
+ """Removes flags from the command line, if they exist.
+
+ Args:
+ flags: A list of flags to remove, eg. ['--single-process']. Note that we
+ expect a complete match when removing flags; if you want to remove
+ a switch with a value, you must use the exact string used to add
+ it in the first place.
+ """
+ if flags:
+ assert flags[0] != 'chrome'
+
+ for flag in flags:
+ if flag in self._current_flags:
+ self._current_flags.remove(flag)
+ self._UpdateCommandLineFile()
+
+ def Restore(self):
+ """Restores the flags to their original state."""
+ self._current_flags = self._TokenizeFlags(self._orig_line)
+ self._UpdateCommandLineFile()
+
+ def _UpdateCommandLineFile(self):
+ """Writes out the command line to the file, or removes it if empty."""
+ print "Current flags: ", self._current_flags
+
+ if self._current_flags:
+ self._android_cmd.SetFileContents(CHROME_COMMAND_FILE,
+ 'chrome ' +
+ ' '.join(self._current_flags))
+ else:
+ self._android_cmd.RunShellCommand('rm ' + CHROME_COMMAND_FILE)
+
+ def _TokenizeFlags(self, line):
+ """Changes the string containing the command line into a list of flags.
+
+ Follows similar logic to CommandLine.java::tokenizeQuotedArguments:
+ * Flags are split using whitespace, unless the whitespace is within a
+ pair of quotation marks.
+ * Unlike the Java version, we keep the quotation marks around switch
+ values since we need them to re-create the file when new flags are
+ appended.
+
+ Args:
+ line: A string containing the entire command line. The first token is
+ assumed to be the program name.
+ """
+ if not line:
+ return []
+
+ tokenized_flags = []
+ current_flag = ""
+ within_quotations = False
+
+ # Move through the string character by character and build up each flag
+ # along the way.
+ for c in line.strip():
+ if c is '"':
+ if len(current_flag) > 0 and current_flag[-1] == '\\':
+ # Last char was a backslash; pop it, and treat this " as a literal.
+ current_flag = current_flag[0:-1] + '"'
+ else:
+ within_quotations = not within_quotations
+ current_flag += c
+ elif not within_quotations and (c is ' ' or c is '\t'):
+ if current_flag is not "":
+ tokenized_flags.append(current_flag)
+ current_flag = ""
+ else:
+ current_flag += c
+
+ # Tack on the last flag.
+ if not current_flag:
+ if within_quotations:
+ warnings.warn("Unterminated quoted string: " + current_flag)
+ else:
+ tokenized_flags.append(current_flag)
+
+ # Return everything but the program name.
+ return tokenized_flags[1:]
diff --git a/src/build/android/pylib/flakiness_dashboard_results_uploader.py b/src/build/android/pylib/flakiness_dashboard_results_uploader.py
new file mode 100644
index 0000000..900af4c
--- /dev/null
+++ b/src/build/android/pylib/flakiness_dashboard_results_uploader.py
@@ -0,0 +1,158 @@
+# 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.
+
+"""Uploads the results to the flakiness dashboard server."""
+
+import logging
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+sys.path.append(os.path.join(sys.path[0], '..', '..', 'third_party',
+ 'WebKit', 'Tools', 'Scripts'))
+from webkitpy.common.system import executive, filesystem
+from webkitpy.layout_tests.layout_package import json_results_generator
+
+
+# The JSONResultsGenerator gets the filesystem.join operation from the Port
+# object. Creating a Port object requires specifying information that only
+# makes sense for running WebKit layout tests, so we provide a dummy object
+# that contains the fields required by the generator.
+class PortDummy(object):
+ def __init__(self):
+ self._executive = executive.Executive()
+ self._filesystem = filesystem.FileSystem()
+
+
+class JSONResultsGenerator(json_results_generator.JSONResultsGeneratorBase):
+ """Writes test results to a JSON file and handles uploading that file to
+ the test results server.
+ """
+ def __init__(self, port, builder_name, build_name, build_number, tmp_folder,
+ test_results_map, test_results_server, test_type, master_name):
+ super(JSONResultsGenerator, self).__init__(
+ port=port,
+ builder_name=builder_name,
+ build_name=build_name,
+ build_number=build_number,
+ results_file_base_path=tmp_folder,
+ builder_base_url=None,
+ test_results_map=test_results_map,
+ svn_repositories=(('webkit', 'third_party/WebKit'),
+ ('chrome', '.')),
+ test_results_server=test_results_server,
+ test_type=test_type,
+ master_name=master_name)
+
+ #override
+ def _get_modifier_char(self, test_name):
+ if test_name not in self._test_results_map:
+ return self.__class__.NO_DATA_RESULT
+
+ return self._test_results_map[test_name].modifier
+
+ #override
+ def _get_svn_revision(self, in_directory):
+ """Returns the git revision for the given directory.
+
+ Args:
+ in_directory: The directory where git is to be run.
+ """
+ git_dir = self._filesystem.join(os.environ.get('CHROME_SRC'),
+ in_directory,
+ '.git')
+ if self._filesystem.exists(git_dir):
+ # Note: Not thread safe: http://bugs.python.org/issue2320
+ output = subprocess.Popen(
+ ['git', '--git-dir=%s' % git_dir, 'show-ref', '--head',
+ '--hash=10', 'HEAD'],
+ stdout=subprocess.PIPE).communicate()[0].strip()
+ return output
+ return ''
+
+
+class ResultsUploader(object):
+ """Handles uploading buildbot tests results to the flakiness dashboard."""
+ def __init__(self, tests_type):
+ self._build_number = os.environ.get('BUILDBOT_BUILDNUMBER')
+ self._builder_name = os.environ.get('BUILDBOT_BUILDERNAME')
+ self._tests_type = tests_type
+ self._build_name = 'chromium-android'
+
+ if not self._builder_name:
+ raise Exception('You should not be uploading tests results to the server'
+ 'from your local machine.')
+
+ buildbot_branch = os.environ.get('BUILDBOT_BRANCH')
+ if not buildbot_branch:
+ buildbot_branch = 'master'
+ self._master_name = '%s-%s' % (self._build_name, buildbot_branch)
+ self._test_results_map = {}
+
+ def AddResults(self, test_results):
+ conversion_map = [
+ (test_results.ok, False,
+ json_results_generator.JSONResultsGeneratorBase.PASS_RESULT),
+ (test_results.failed, True,
+ json_results_generator.JSONResultsGeneratorBase.FAIL_RESULT),
+ (test_results.crashed, True,
+ "C"),
+ (test_results.unknown, True,
+ json_results_generator.JSONResultsGeneratorBase.NO_DATA_RESULT),
+ ]
+
+ for results_list, failed, modifier in conversion_map:
+ for single_test_result in results_list:
+ test_result = json_results_generator.TestResult(
+ test=single_test_result.name,
+ failed=failed,
+ elapsed_time=single_test_result.dur / 1000)
+ # The WebKit TestResult object sets the modifier it based on test name.
+ # Since we don't use the same test naming convention as WebKit the
+ # modifier will be wrong, so we need to overwrite it.
+ test_result.modifier = modifier
+
+ self._test_results_map[single_test_result.name] = test_result
+
+ def Upload(self, test_results_server):
+ if not self._test_results_map:
+ return
+
+ tmp_folder = tempfile.mkdtemp()
+
+ try:
+ results_generator = JSONResultsGenerator(
+ port=PortDummy(),
+ builder_name=self._builder_name,
+ build_name=self._build_name,
+ build_number=self._build_number,
+ tmp_folder=tmp_folder,
+ test_results_map=self._test_results_map,
+ test_results_server=test_results_server,
+ test_type=self._tests_type,
+ master_name=self._master_name)
+
+ json_files = ["incremental_results.json", "times_ms.json"]
+ results_generator.generate_json_output()
+ results_generator.generate_times_ms_file()
+ results_generator.upload_json_files(json_files)
+ except Exception as e:
+ logging.error("Uploading results to test server failed: %s." % e);
+ finally:
+ shutil.rmtree(tmp_folder)
+
+
+def Upload(flakiness_dashboard_server, test_type, results):
+ """Reports test results to the flakiness dashboard for Chrome for Android.
+
+ Args:
+ flakiness_dashboard_server: the server to upload the results to.
+ test_type: the type of the tests (as displayed by the flakiness dashboard).
+ results: test results.
+ """
+ uploader = ResultsUploader(test_type)
+ uploader.AddResults(results)
+ uploader.Upload(flakiness_dashboard_server)
diff --git a/src/build/android/pylib/forwarder.py b/src/build/android/pylib/forwarder.py
new file mode 100644
index 0000000..460a3dc
--- /dev/null
+++ b/src/build/android/pylib/forwarder.py
@@ -0,0 +1,144 @@
+# 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 re
+import sys
+import time
+
+import android_commands
+import cmd_helper
+import constants
+import ports
+
+from pylib import pexpect
+
+
+def _MakeBinaryPath(build_type, binary_name):
+ return os.path.join(cmd_helper.OutDirectory.get(), build_type, binary_name)
+
+
+class Forwarder(object):
+ """Class to manage port forwards from the device to the host."""
+
+ # Unix Abstract socket path:
+ _DEVICE_ADB_CONTROL_PORT = 'chrome_device_forwarder'
+ _TIMEOUT_SECS = 30
+
+ _DEVICE_FORWARDER_PATH = constants.TEST_EXECUTABLE_DIR + '/device_forwarder'
+
+ def __init__(self, adb, build_type):
+ """Forwards TCP ports on the device back to the host.
+
+ Works like adb forward, but in reverse.
+
+ Args:
+ adb: Instance of AndroidCommands for talking to the device.
+ build_type: 'Release' or 'Debug'.
+ """
+ assert build_type in ('Release', 'Debug')
+ self._adb = adb
+ self._host_to_device_port_map = dict()
+ self._device_process = None
+ self._host_forwarder_path = _MakeBinaryPath(build_type, 'host_forwarder')
+ self._device_forwarder_path = _MakeBinaryPath(
+ build_type, 'device_forwarder')
+
+ def Run(self, port_pairs, tool, host_name):
+ """Runs the forwarder.
+
+ Args:
+ port_pairs: A list of tuples (device_port, host_port) to forward. Note
+ that you can specify 0 as a device_port, in which case a
+ port will by dynamically assigned on the device. You can
+ get the number of the assigned port using the
+ DevicePortForHostPort method.
+ tool: Tool class to use to get wrapper, if necessary, for executing the
+ forwarder (see valgrind_tools.py).
+ host_name: Address to forward to, must be addressable from the
+ host machine. Usually use loopback '127.0.0.1'.
+
+ Raises:
+ Exception on failure to forward the port.
+ """
+ host_adb_control_port = ports.AllocateTestServerPort()
+ if not host_adb_control_port:
+ raise Exception('Failed to allocate a TCP port in the host machine.')
+ self._adb.PushIfNeeded(
+ self._device_forwarder_path, Forwarder._DEVICE_FORWARDER_PATH)
+ redirection_commands = [
+ '%d:%d:%d:%s' % (host_adb_control_port, device, host,
+ host_name) for device, host in port_pairs]
+ logging.info('Command format: <ADB port>:<Device port>' +
+ '[:<Forward to port>:<Forward to address>]')
+ logging.info('Forwarding using commands: %s', redirection_commands)
+ if cmd_helper.RunCmd(
+ ['adb', '-s', self._adb._adb.GetSerialNumber(), 'forward',
+ 'tcp:%s' % host_adb_control_port,
+ 'localabstract:%s' % Forwarder._DEVICE_ADB_CONTROL_PORT]) != 0:
+ raise Exception('Error while running adb forward.')
+
+ (exit_code, output) = self._adb.GetShellCommandStatusAndOutput(
+ '%s %s' % (Forwarder._DEVICE_FORWARDER_PATH,
+ Forwarder._DEVICE_ADB_CONTROL_PORT))
+ if exit_code != 0:
+ raise Exception(
+ 'Failed to start device forwarder:\n%s' % '\n'.join(output))
+
+ for redirection_command in redirection_commands:
+ (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
+ [self._host_forwarder_path, redirection_command])
+ if exit_code != 0:
+ raise Exception('%s exited with %d:\n%s' % (
+ self._host_forwarder_path, exit_code, '\n'.join(output)))
+ tokens = output.split(':')
+ if len(tokens) != 2:
+ raise Exception('Unexpected host forwarder output "%s", ' +
+ 'expected "device_port:host_port"' % output)
+ device_port = int(tokens[0])
+ host_port = int(tokens[1])
+ self._host_to_device_port_map[host_port] = device_port
+ logging.info('Forwarding device port: %d to host port: %d.', device_port,
+ host_port)
+
+ @staticmethod
+ def KillHost(build_type):
+ logging.info('Killing host_forwarder.')
+ host_forwarder_path = _MakeBinaryPath(build_type, 'host_forwarder')
+ (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
+ [host_forwarder_path, 'kill-server'])
+ if exit_code != 0:
+ (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
+ ['pkill', 'host_forwarder'])
+ if exit_code != 0:
+ raise Exception('%s exited with %d:\n%s' % (
+ host_forwarder_path, exit_code, '\n'.join(output)))
+
+ @staticmethod
+ def KillDevice(adb):
+ logging.info('Killing device_forwarder.')
+ if not adb.FileExistsOnDevice(Forwarder._DEVICE_FORWARDER_PATH):
+ return
+ (exit_code, output) = adb.GetShellCommandStatusAndOutput(
+ '%s kill-server' % Forwarder._DEVICE_FORWARDER_PATH)
+ # TODO(pliard): Remove the following call to KillAllBlocking() when we are
+ # sure that the old version of device_forwarder (not supporting
+ # 'kill-server') is not running on the bots anymore.
+ timeout_sec = 5
+ processes_killed = adb.KillAllBlocking('device_forwarder', timeout_sec)
+ if not processes_killed:
+ pids = adb.ExtractPid('device_forwarder')
+ if pids:
+ raise Exception('Timed out while killing device_forwarder')
+
+ def DevicePortForHostPort(self, host_port):
+ """Get the device port that corresponds to a given host port."""
+ return self._host_to_device_port_map.get(host_port)
+
+ def Close(self):
+ """Terminate the forwarder process."""
+ if self._device_process:
+ self._device_process.close()
+ self._device_process = None
diff --git a/src/build/android/pylib/io_stats_parser.py b/src/build/android/pylib/io_stats_parser.py
new file mode 100644
index 0000000..89097ab
--- /dev/null
+++ b/src/build/android/pylib/io_stats_parser.py
@@ -0,0 +1,32 @@
+# 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 communicate with the device via the adb command.
+
+Assumes adb binary is currently on system path.
+"""
+
+
+import collections
+
+
+def ParseIoStatsLine(line):
+ """Parses a line of io stats into a IoStats named tuple."""
+ # Field definitions: http://www.kernel.org/doc/Documentation/iostats.txt
+ IoStats = collections.namedtuple('IoStats',
+ ['device',
+ 'num_reads_issued',
+ 'num_reads_merged',
+ 'num_sectors_read',
+ 'ms_spent_reading',
+ 'num_writes_completed',
+ 'num_writes_merged',
+ 'num_sectors_written',
+ 'ms_spent_writing',
+ 'num_ios_in_progress',
+ 'ms_spent_doing_io',
+ 'ms_spent_doing_io_weighted',
+ ])
+ fields = line.split()
+ return IoStats._make([fields[2]] + [int(f) for f in fields[3:]])
diff --git a/src/build/android/pylib/java_unittest_utils.py b/src/build/android/pylib/java_unittest_utils.py
new file mode 100644
index 0000000..b5446dc
--- /dev/null
+++ b/src/build/android/pylib/java_unittest_utils.py
@@ -0,0 +1,27 @@
+# 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.
+
+"""This file is imported by python tests ran by run_python_tests.py."""
+
+import os
+
+import android_commands
+from run_java_tests import TestRunner
+
+
+def _GetPackageName(fname):
+ """Extracts the package name from the test file path."""
+ base_root = os.path.join('com', 'google', 'android')
+ dirname = os.path.dirname(fname)
+ package = dirname[dirname.rfind(base_root):]
+ return package.replace(os.sep, '.')
+
+
+def RunJavaTest(fname, suite, test, ports_to_forward):
+ device = android_commands.GetAttachedDevices()[0]
+ package_name = _GetPackageName(fname)
+ test = package_name + '.' + suite + '#' + test
+ java_test_runner = TestRunner(False, device, [test], False, False, False,
+ False, 0, ports_to_forward)
+ return java_test_runner.Run()
diff --git a/src/build/android/pylib/json_perf_parser.py b/src/build/android/pylib/json_perf_parser.py
new file mode 100644
index 0000000..1a8e617
--- /dev/null
+++ b/src/build/android/pylib/json_perf_parser.py
@@ -0,0 +1,160 @@
+# 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.
+
+
+"""A helper module for parsing JSON objects from perf tests results."""
+
+import json
+
+
+def GetAverageRunInfo(json_data, name):
+ """Summarizes TraceEvent JSON data for performance metrics.
+
+ Example JSON Inputs (More tags can be added but these are required):
+ Measuring Duration:
+ [
+ { "cat": "Java",
+ "ts": 10000000000,
+ "ph": "S",
+ "name": "TestTrace"
+ },
+ { "cat": "Java",
+ "ts": 10000004000,
+ "ph": "F",
+ "name": "TestTrace"
+ },
+ ...
+ ]
+
+ Measuring Call Frequency (FPS):
+ [
+ { "cat": "Java",
+ "ts": 10000000000,
+ "ph": "I",
+ "name": "TestTraceFPS"
+ },
+ { "cat": "Java",
+ "ts": 10000004000,
+ "ph": "I",
+ "name": "TestTraceFPS"
+ },
+ ...
+ ]
+
+ Args:
+ json_data: A list of dictonaries each representing a JSON object.
+ name: The 'name' tag to filter on in the JSON file.
+
+ Returns:
+ A dictionary of result data with the following tags:
+ min: The minimum value tracked.
+ max: The maximum value tracked.
+ average: The average of all the values tracked.
+ count: The number of times the category/name pair was tracked.
+ type: The type of tracking ('Instant' for instant tags and 'Span' for
+ begin/end tags.
+ category: The passed in category filter.
+ name: The passed in name filter.
+ data_points: A list of all of the times used to generate this data.
+ units: The units for the values being reported.
+
+ Raises:
+ Exception: if entry contains invalid data.
+ """
+
+ def EntryFilter(entry):
+ return entry['cat'] == 'Java' and entry['name'] == name
+ filtered_entries = filter(EntryFilter, json_data)
+
+ result = {}
+
+ result['min'] = -1
+ result['max'] = -1
+ result['average'] = 0
+ result['count'] = 0
+ result['type'] = 'Unknown'
+ result['category'] = 'Java'
+ result['name'] = name
+ result['data_points'] = []
+ result['units'] = ''
+
+ total_sum = 0
+
+ last_val = 0
+ val_type = None
+ for entry in filtered_entries:
+ if not val_type:
+ if 'mem' in entry:
+ val_type = 'mem'
+
+ def GetVal(entry):
+ return entry['mem']
+
+ result['units'] = 'kb'
+ elif 'ts' in entry:
+ val_type = 'ts'
+
+ def GetVal(entry):
+ return float(entry['ts']) / 1000.0
+
+ result['units'] = 'ms'
+ else:
+ raise Exception('Entry did not contain valid value info: %s' % entry)
+
+ if not val_type in entry:
+ raise Exception('Entry did not contain expected value type "%s" '
+ 'information: %s' % (val_type, entry))
+ val = GetVal(entry)
+ if (entry['ph'] == 'S' and
+ (result['type'] == 'Unknown' or result['type'] == 'Span')):
+ result['type'] = 'Span'
+ last_val = val
+ elif ((entry['ph'] == 'F' and result['type'] == 'Span') or
+ (entry['ph'] == 'I' and (result['type'] == 'Unknown' or
+ result['type'] == 'Instant'))):
+ if last_val > 0:
+ delta = val - last_val
+ if result['min'] == -1 or result['min'] > delta:
+ result['min'] = delta
+ if result['max'] == -1 or result['max'] < delta:
+ result['max'] = delta
+ total_sum += delta
+ result['count'] += 1
+ result['data_points'].append(delta)
+ if entry['ph'] == 'I':
+ result['type'] = 'Instant'
+ last_val = val
+ if result['count'] > 0: result['average'] = total_sum / result['count']
+
+ return result
+
+
+def GetAverageRunInfoFromJSONString(json_string, name):
+ """Returns the results from GetAverageRunInfo using a JSON string.
+
+ Args:
+ json_string: The string containing JSON.
+ name: The 'name' tag to filter on in the JSON file.
+
+ Returns:
+ See GetAverageRunInfo Returns section.
+ """
+ return GetAverageRunInfo(json.loads(json_string), name)
+
+
+def GetAverageRunInfoFromFile(json_file, name):
+ """Returns the results from GetAverageRunInfo using a JSON file.
+
+ Args:
+ json_file: The path to a JSON file.
+ name: The 'name' tag to filter on in the JSON file.
+
+ Returns:
+ See GetAverageRunInfo Returns section.
+ """
+ with open(json_file, 'r') as f:
+ data = f.read()
+ perf = json.loads(data)
+
+ return GetAverageRunInfo(perf, name)
diff --git a/src/build/android/pylib/perf_tests_helper.py b/src/build/android/pylib/perf_tests_helper.py
new file mode 100644
index 0000000..ca9023b
--- /dev/null
+++ b/src/build/android/pylib/perf_tests_helper.py
@@ -0,0 +1,165 @@
+# 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 re
+
+import android_commands
+import json
+import math
+
+# Valid values of result type.
+RESULT_TYPES = {'unimportant': 'RESULT ',
+ 'default': '*RESULT ',
+ 'informational': '',
+ 'unimportant-histogram': 'HISTOGRAM ',
+ 'histogram': '*HISTOGRAM '}
+
+
+def _EscapePerfResult(s):
+ """Escapes |s| for use in a perf result."""
+ # Colons (:), equal signs (=) and slashes (/) are not allowed.
+ return re.sub('[\:|=/]', '_', s)
+
+
+def GeomMeanAndStdDevFromHistogram(histogram_json):
+ histogram = json.loads(histogram_json)
+ count = 0
+ sum_of_logs = 0
+ for bucket in histogram['buckets']:
+ if 'high' in bucket:
+ bucket['mean'] = (bucket['low'] + bucket['high']) / 2.0
+ else:
+ bucket['mean'] = bucket['low']
+ if bucket['mean'] > 0:
+ sum_of_logs += math.log(bucket['mean']) * bucket['count']
+ count += bucket['count']
+
+ if count == 0:
+ return 0.0, 0.0
+
+ sum_of_squares = 0
+ geom_mean = math.exp(sum_of_logs / count)
+ for bucket in histogram['buckets']:
+ if bucket['mean'] > 0:
+ sum_of_squares += (bucket['mean'] - geom_mean) ** 2 * bucket['count']
+ return geom_mean, math.sqrt(sum_of_squares / count)
+
+
+def _MeanAndStdDevFromList(values):
+ avg = None
+ sd = None
+ if len(values) > 1:
+ try:
+ value = '[%s]' % ','.join([str(v) for v in values])
+ avg = sum([float(v) for v in values]) / len(values)
+ sqdiffs = [(float(v) - avg) ** 2 for v in values]
+ variance = sum(sqdiffs) / (len(values) - 1)
+ sd = math.sqrt(variance)
+ except ValueError:
+ value = ", ".join(values)
+ else:
+ value = values[0]
+ return value, avg, sd
+
+
+def PrintPerfResult(measurement, trace, values, units, result_type='default',
+ print_to_stdout=True):
+ """Prints numerical data to stdout in the format required by perf tests.
+
+ The string args may be empty but they must not contain any colons (:) or
+ equals signs (=).
+
+ Args:
+ measurement: A description of the quantity being measured, e.g. "vm_peak".
+ trace: A description of the particular data point, e.g. "reference".
+ values: A list of numeric measured values.
+ units: A description of the units of measure, e.g. "bytes".
+ result_type: Accepts values of RESULT_TYPES.
+ print_to_stdout: If True, prints the output in stdout instead of returning
+ the output to caller.
+
+ Returns:
+ String of the formated perf result.
+ """
+ assert result_type in RESULT_TYPES, 'result type: %s is invalid' % result_type
+
+ trace_name = _EscapePerfResult(trace)
+
+ if result_type in ['unimportant', 'default', 'informational']:
+ assert isinstance(values, list)
+ assert len(values)
+ assert '/' not in measurement
+ value, avg, sd = _MeanAndStdDevFromList(values)
+ output = '%s%s: %s%s%s %s' % (
+ RESULT_TYPES[result_type],
+ _EscapePerfResult(measurement),
+ trace_name,
+ # Do not show equal sign if the trace is empty. Usually it happens when
+ # measurement is enough clear to describe the result.
+ '= ' if trace_name else '',
+ value,
+ units)
+ else:
+ assert(result_type in ['histogram', 'unimportant-histogram'])
+ assert isinstance(values, list)
+ # The histograms can only be printed individually, there's no computation
+ # across different histograms.
+ assert len(values) == 1
+ value = values[0]
+ measurement += '.' + trace_name
+ output = '%s%s: %s= %s' % (
+ RESULT_TYPES[result_type],
+ _EscapePerfResult(measurement),
+ _EscapePerfResult(measurement),
+ value)
+ avg, sd = GeomMeanAndStdDevFromHistogram(value)
+
+ if avg:
+ output += '\nAvg %s: %f%s' % (measurement, avg, units)
+ if sd:
+ output += '\nSd %s: %f%s' % (measurement, sd, units)
+ if print_to_stdout:
+ print output
+ return output
+
+
+class PerfTestSetup(object):
+ """Provides methods for setting up a device for perf testing."""
+ _DROP_CACHES = '/proc/sys/vm/drop_caches'
+ _SCALING_GOVERNOR = '/sys/devices/system/cpu/cpu%d/cpufreq/scaling_governor'
+
+ def __init__(self, adb):
+ self._adb = adb
+ num_cpus = self._adb.GetFileContents('/sys/devices/system/cpu/online',
+ log_result=False)
+ assert num_cpus, 'Unable to find /sys/devices/system/cpu/online'
+ self._num_cpus = int(num_cpus[0].split('-')[-1])
+ self._original_scaling_governor = None
+
+ def DropRamCaches(self):
+ """Drops the filesystem ram caches for performance testing."""
+ if not self._adb.IsRootEnabled():
+ self._adb.EnableAdbRoot()
+ self._adb.RunShellCommand('sync')
+ self._adb.RunShellCommand('echo 3 > ' + PerfTestSetup._DROP_CACHES)
+
+ def SetUp(self):
+ """Sets up performance tests."""
+ if not self._original_scaling_governor:
+ self._original_scaling_governor = self._adb.GetFileContents(
+ PerfTestSetup._SCALING_GOVERNOR % 0,
+ log_result=False)[0]
+ self._SetScalingGovernorInternal('performance')
+ self.DropRamCaches()
+
+ def TearDown(self):
+ """Tears down performance tests."""
+ if self._original_scaling_governor:
+ self._SetScalingGovernorInternal(self._original_scaling_governor)
+ self._original_scaling_governor = None
+
+ def _SetScalingGovernorInternal(self, value):
+ for cpu in range(self._num_cpus):
+ self._adb.RunShellCommand(
+ ('echo %s > ' + PerfTestSetup._SCALING_GOVERNOR) % (value, cpu))
diff --git a/src/build/android/pylib/pexpect.py b/src/build/android/pylib/pexpect.py
new file mode 100644
index 0000000..f566f1c
--- /dev/null
+++ b/src/build/android/pylib/pexpect.py
@@ -0,0 +1,21 @@
+# 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.
+from __future__ import absolute_import
+
+import os
+import sys
+
+_CHROME_SRC = os.path.join(
+ os.path.abspath(os.path.dirname(__file__)), '..', '..', '..')
+
+_PEXPECT_PATH = os.path.join(_CHROME_SRC, 'third_party', 'pexpect')
+if _PEXPECT_PATH not in sys.path:
+ sys.path.append(_PEXPECT_PATH)
+
+# pexpect is not available on all platforms. We allow this file to be imported
+# on platforms without pexpect and only fail when pexpect is actually used.
+try:
+ from pexpect import *
+except:
+ pass
diff --git a/src/build/android/pylib/ports.py b/src/build/android/pylib/ports.py
new file mode 100644
index 0000000..74c84c1
--- /dev/null
+++ b/src/build/android/pylib/ports.py
@@ -0,0 +1,176 @@
+# 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.
+
+"""Functions that deal with local and device ports."""
+
+import contextlib
+import fcntl
+import httplib
+import logging
+import os
+import re
+import socket
+import traceback
+
+import cmd_helper
+import constants
+
+
+# The following two methods are used to allocate the port source for various
+# types of test servers. Because some net-related tests can be run on shards at
+# same time, it's important to have a mechanism to allocate the port
+# process-safe. In here, we implement the safe port allocation by leveraging
+# flock.
+def ResetTestServerPortAllocation():
+ """Resets the port allocation to start from TEST_SERVER_PORT_FIRST.
+
+ Returns:
+ Returns True if reset successes. Otherwise returns False.
+ """
+ try:
+ with open(constants.TEST_SERVER_PORT_FILE, 'w') as fp:
+ fp.write('%d' % constants.TEST_SERVER_PORT_FIRST)
+ if os.path.exists(constants.TEST_SERVER_PORT_LOCKFILE):
+ os.unlink(constants.TEST_SERVER_PORT_LOCKFILE)
+ return True
+ except Exception as e:
+ logging.error(e)
+ return False
+
+
+def AllocateTestServerPort():
+ """Allocates a port incrementally.
+
+ Returns:
+ Returns a valid port which should be in between TEST_SERVER_PORT_FIRST and
+ TEST_SERVER_PORT_LAST. Returning 0 means no more valid port can be used.
+ """
+ port = 0
+ ports_tried = []
+ try:
+ fp_lock = open(constants.TEST_SERVER_PORT_LOCKFILE, 'w')
+ fcntl.flock(fp_lock, fcntl.LOCK_EX)
+ # Get current valid port and calculate next valid port.
+ if not os.path.exists(constants.TEST_SERVER_PORT_FILE):
+ ResetTestServerPortAllocation()
+ with open(constants.TEST_SERVER_PORT_FILE, 'r+') as fp:
+ port = int(fp.read())
+ ports_tried.append(port)
+ while IsHostPortUsed(port):
+ port += 1
+ ports_tried.append(port)
+ if (port > constants.TEST_SERVER_PORT_LAST or
+ port < constants.TEST_SERVER_PORT_FIRST):
+ port = 0
+ else:
+ fp.seek(0, os.SEEK_SET)
+ fp.write('%d' % (port + 1))
+ except Exception as e:
+ logging.info(e)
+ finally:
+ if fp_lock:
+ fcntl.flock(fp_lock, fcntl.LOCK_UN)
+ fp_lock.close()
+ if port:
+ logging.info('Allocate port %d for test server.', port)
+ else:
+ logging.error('Could not allocate port for test server. '
+ 'List of ports tried: %s', str(ports_tried))
+ return port
+
+
+def IsHostPortUsed(host_port):
+ """Checks whether the specified host port is used or not.
+
+ Uses -n -P to inhibit the conversion of host/port numbers to host/port names.
+
+ Args:
+ host_port: Port on host we want to check.
+
+ Returns:
+ True if the port on host is already used, otherwise returns False.
+ """
+ port_info = '(\*)|(127\.0\.0\.1)|(localhost):%d' % host_port
+ # TODO(jnd): Find a better way to filter the port. Note that connecting to the
+ # socket and closing it would leave it in the TIME_WAIT state. Setting
+ # SO_LINGER on it and then closing it makes the Python HTTP server crash.
+ re_port = re.compile(port_info, re.MULTILINE)
+ if re_port.search(cmd_helper.GetCmdOutput(['lsof', '-nPi:%d' % host_port])):
+ return True
+ return False
+
+
+def IsDevicePortUsed(adb, device_port, state=''):
+ """Checks whether the specified device port is used or not.
+
+ Args:
+ adb: Instance of AndroidCommands for talking to the device.
+ device_port: Port on device we want to check.
+ state: String of the specified state. Default is empty string, which
+ means any state.
+
+ Returns:
+ True if the port on device is already used, otherwise returns False.
+ """
+ base_url = '127.0.0.1:%d' % device_port
+ netstat_results = adb.RunShellCommand('netstat', log_result=False)
+ for single_connect in netstat_results:
+ # Column 3 is the local address which we want to check with.
+ connect_results = single_connect.split()
+ if connect_results[0] != 'tcp':
+ continue
+ if len(connect_results) < 6:
+ raise Exception('Unexpected format while parsing netstat line: ' +
+ single_connect)
+ is_state_match = connect_results[5] == state if state else True
+ if connect_results[3] == base_url and is_state_match:
+ return True
+ return False
+
+
+def IsHttpServerConnectable(host, port, tries=3, command='GET', path='/',
+ expected_read='', timeout=2):
+ """Checks whether the specified http server is ready to serve request or not.
+
+ Args:
+ host: Host name of the HTTP server.
+ port: Port number of the HTTP server.
+ tries: How many times we want to test the connection. The default value is
+ 3.
+ command: The http command we use to connect to HTTP server. The default
+ command is 'GET'.
+ path: The path we use when connecting to HTTP server. The default path is
+ '/'.
+ expected_read: The content we expect to read from the response. The default
+ value is ''.
+ timeout: Timeout (in seconds) for each http connection. The default is 2s.
+
+ Returns:
+ Tuple of (connect status, client error). connect status is a boolean value
+ to indicate whether the server is connectable. client_error is the error
+ message the server returns when connect status is false.
+ """
+ assert tries >= 1
+ for i in xrange(0, tries):
+ client_error = None
+ try:
+ with contextlib.closing(httplib.HTTPConnection(
+ host, port, timeout=timeout)) as http:
+ # Output some debug information when we have tried more than 2 times.
+ http.set_debuglevel(i >= 2)
+ http.request(command, path)
+ r = http.getresponse()
+ content = r.read()
+ if r.status == 200 and r.reason == 'OK' and content == expected_read:
+ return (True, '')
+ client_error = ('Bad response: %s %s version %s\n ' %
+ (r.status, r.reason, r.version) +
+ '\n '.join([': '.join(h) for h in r.getheaders()]))
+ except (httplib.HTTPException, socket.error) as e:
+ # Probably too quick connecting: try again.
+ exception_error_msgs = traceback.format_exception_only(type(e), e)
+ if exception_error_msgs:
+ client_error = ''.join(exception_error_msgs)
+ # Only returns last client_error.
+ return (False, client_error or 'Timeout')
diff --git a/src/build/android/pylib/python_test_base.py b/src/build/android/pylib/python_test_base.py
new file mode 100644
index 0000000..d2cdfb0
--- /dev/null
+++ b/src/build/android/pylib/python_test_base.py
@@ -0,0 +1,172 @@
+# 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.
+
+"""Base class for Android Python-driven tests.
+
+This test case is intended to serve as the base class for any Python-driven
+tests. It is similar to the Python unitttest module in that the user's tests
+inherit from this case and add their tests in that case.
+
+When a PythonTestBase object is instantiated, its purpose is to run only one of
+its tests. The test runner gives it the name of the test the instance will
+run. The test runner calls SetUp with the Android device ID which the test will
+run against. The runner runs the test method itself, collecting the result,
+and calls TearDown.
+
+Tests can basically do whatever they want in the test methods, such as call
+Java tests using _RunJavaTests. Those methods have the advantage of massaging
+the Java test results into Python test results.
+"""
+
+import logging
+import os
+import time
+
+import android_commands
+import apk_info
+from run_java_tests import TestRunner
+from test_result import SingleTestResult, TestResults
+
+
+# aka the parent of com.google.android
+BASE_ROOT = 'src' + os.sep
+
+
+class PythonTestBase(object):
+ """Base class for Python-driven tests."""
+
+ def __init__(self, test_name):
+ # test_name must match one of the test methods defined on a subclass which
+ # inherits from this class.
+ # It's stored so we can do the attr lookup on demand, allowing this class
+ # to be pickled, a requirement for the multiprocessing module.
+ self.test_name = test_name
+ class_name = self.__class__.__name__
+ self.qualified_name = class_name + '.' + self.test_name
+
+ def SetUp(self, options):
+ self.options = options
+ self.shard_index = self.options.shard_index
+ self.device_id = self.options.device_id
+ self.adb = android_commands.AndroidCommands(self.device_id)
+ self.ports_to_forward = []
+
+ def TearDown(self):
+ pass
+
+ def GetOutDir(self):
+ return os.path.join(os.environ['CHROME_SRC'], 'out',
+ self.options.build_type)
+
+ def Run(self):
+ logging.warning('Running Python-driven test: %s', self.test_name)
+ return getattr(self, self.test_name)()
+
+ def _RunJavaTest(self, fname, suite, test):
+ """Runs a single Java test with a Java TestRunner.
+
+ Args:
+ fname: filename for the test (e.g. foo/bar/baz/tests/FooTest.py)
+ suite: name of the Java test suite (e.g. FooTest)
+ test: name of the test method to run (e.g. testFooBar)
+
+ Returns:
+ TestResults object with a single test result.
+ """
+ test = self._ComposeFullTestName(fname, suite, test)
+ apks = [apk_info.ApkInfo(self.options.test_apk_path,
+ self.options.test_apk_jar_path)]
+ java_test_runner = TestRunner(self.options, self.device_id, [test], False,
+ self.shard_index,
+ apks,
+ self.ports_to_forward)
+ return java_test_runner.Run()
+
+ def _RunJavaTests(self, fname, tests):
+ """Calls a list of tests and stops at the first test failure.
+
+ This method iterates until either it encounters a non-passing test or it
+ exhausts the list of tests. Then it returns the appropriate Python result.
+
+ Args:
+ fname: filename for the Python test
+ tests: a list of Java test names which will be run
+
+ Returns:
+ A TestResults object containing a result for this Python test.
+ """
+ start_ms = int(time.time()) * 1000
+
+ result = None
+ for test in tests:
+ # We're only running one test at a time, so this TestResults object will
+ # hold only one result.
+ suite, test_name = test.split('.')
+ result = self._RunJavaTest(fname, suite, test_name)
+ # A non-empty list means the test did not pass.
+ if result.GetAllBroken():
+ break
+
+ duration_ms = int(time.time()) * 1000 - start_ms
+
+ # Do something with result.
+ return self._ProcessResults(result, start_ms, duration_ms)
+
+ def _ProcessResults(self, result, start_ms, duration_ms):
+ """Translates a Java test result into a Python result for this test.
+
+ The TestRunner class that we use under the covers will return a test result
+ for that specific Java test. However, to make reporting clearer, we have
+ this method to abstract that detail and instead report that as a failure of
+ this particular test case while still including the Java stack trace.
+
+ Args:
+ result: TestResults with a single Java test result
+ start_ms: the time the test started
+ duration_ms: the length of the test
+
+ Returns:
+ A TestResults object containing a result for this Python test.
+ """
+ test_results = TestResults()
+
+ # If our test is in broken, then it crashed/failed.
+ broken = result.GetAllBroken()
+ if broken:
+ # Since we have run only one test, take the first and only item.
+ single_result = broken[0]
+
+ log = single_result.log
+ if not log:
+ log = 'No logging information.'
+
+ python_result = SingleTestResult(self.qualified_name, start_ms,
+ duration_ms,
+ log)
+
+ # Figure out where the test belonged. There's probably a cleaner way of
+ # doing this.
+ if single_result in result.crashed:
+ test_results.crashed = [python_result]
+ elif single_result in result.failed:
+ test_results.failed = [python_result]
+ elif single_result in result.unknown:
+ test_results.unknown = [python_result]
+
+ else:
+ python_result = SingleTestResult(self.qualified_name, start_ms,
+ duration_ms)
+ test_results.ok = [python_result]
+
+ return test_results
+
+ def _ComposeFullTestName(self, fname, suite, test):
+ package_name = self._GetPackageName(fname)
+ return package_name + '.' + suite + '#' + test
+
+ def _GetPackageName(self, fname):
+ """Extracts the package name from the test file path."""
+ dirname = os.path.dirname(fname)
+ package = dirname[dirname.rfind(BASE_ROOT) + len(BASE_ROOT):]
+ return package.replace(os.sep, '.')
diff --git a/src/build/android/pylib/python_test_caller.py b/src/build/android/pylib/python_test_caller.py
new file mode 100644
index 0000000..882b892
--- /dev/null
+++ b/src/build/android/pylib/python_test_caller.py
@@ -0,0 +1,84 @@
+# 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.
+
+"""Helper module for calling python-based tests."""
+
+
+import logging
+import sys
+import time
+
+from test_result import TestResults
+
+
+def CallPythonTest(test, options):
+ """Invokes a test function and translates Python exceptions into test results.
+
+ This method invokes SetUp()/TearDown() on the test. It is intended to be
+ resilient to exceptions in SetUp(), the test itself, and TearDown(). Any
+ Python exception means the test is marked as failed, and the test result will
+ contain information about the exception.
+
+ If SetUp() raises an exception, the test is not run.
+
+ If TearDown() raises an exception, the test is treated as a failure. However,
+ if the test itself raised an exception beforehand, that stack trace will take
+ precedence whether or not TearDown() also raised an exception.
+
+ shard_index is not applicable in single-device scenarios, when test execution
+ is serial rather than parallel. Tests can use this to bring up servers with
+ unique port numbers, for example. See also python_test_sharder.
+
+ Args:
+ test: an object which is ostensibly a subclass of PythonTestBase.
+ options: Options to use for setting up tests.
+
+ Returns:
+ A TestResults object which contains any results produced by the test or, in
+ the case of a Python exception, the Python exception info.
+ """
+
+ start_date_ms = int(time.time()) * 1000
+ failed = False
+
+ try:
+ test.SetUp(options)
+ except Exception:
+ failed = True
+ logging.exception(
+ 'Caught exception while trying to run SetUp() for test: ' +
+ test.qualified_name)
+ # Tests whose SetUp() method has failed are likely to fail, or at least
+ # yield invalid results.
+ exc_info = sys.exc_info()
+ return TestResults.FromPythonException(test.qualified_name, start_date_ms,
+ exc_info)
+
+ try:
+ result = test.Run()
+ except Exception:
+ # Setting this lets TearDown() avoid stomping on our stack trace from Run()
+ # should TearDown() also raise an exception.
+ failed = True
+ logging.exception('Caught exception while trying to run test: ' +
+ test.qualified_name)
+ exc_info = sys.exc_info()
+ result = TestResults.FromPythonException(test.qualified_name, start_date_ms,
+ exc_info)
+
+ try:
+ test.TearDown()
+ except Exception:
+ logging.exception(
+ 'Caught exception while trying run TearDown() for test: ' +
+ test.qualified_name)
+ if not failed:
+ # Don't stomp the error during the test if TearDown blows up. This is a
+ # trade-off: if the test fails, this will mask any problem with TearDown
+ # until the test is fixed.
+ exc_info = sys.exc_info()
+ result = TestResults.FromPythonException(test.qualified_name,
+ start_date_ms, exc_info)
+
+ return result
diff --git a/src/build/android/pylib/python_test_sharder.py b/src/build/android/pylib/python_test_sharder.py
new file mode 100644
index 0000000..e27096d
--- /dev/null
+++ b/src/build/android/pylib/python_test_sharder.py
@@ -0,0 +1,203 @@
+# 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.
+
+"""Takes care of sharding the python-drive tests in multiple devices."""
+
+import copy
+import logging
+import multiprocessing
+
+from python_test_caller import CallPythonTest
+from run_java_tests import FatalTestException
+import sharded_tests_queue
+from test_result import TestResults
+
+
+def SetTestsContainer(tests_container):
+ """Sets PythonTestSharder as a top-level field.
+
+ PythonTestSharder uses multiprocessing.Pool, which creates a pool of
+ processes. This is used to initialize each worker in the pool, ensuring that
+ each worker has access to this shared pool of tests.
+
+ The multiprocessing module requires that this be a top-level method.
+
+ Args:
+ tests_container: the container for all the tests.
+ """
+ PythonTestSharder.tests_container = tests_container
+
+
+def _DefaultRunnable(test_runner):
+ """A default runnable for a PythonTestRunner.
+
+ Args:
+ test_runner: A PythonTestRunner which will run tests.
+
+ Returns:
+ The test results.
+ """
+ return test_runner.RunTests()
+
+
+class PythonTestRunner(object):
+ """Thin wrapper around a list of PythonTestBase instances.
+
+ This is meant to be a long-lived object which can run multiple Python tests
+ within its lifetime. Tests will receive the device_id and shard_index.
+
+ The shard index affords the ability to create unique port numbers (e.g.
+ DEFAULT_PORT + shard_index) if the test so wishes.
+ """
+
+ def __init__(self, options):
+ """Constructor.
+
+ Args:
+ options: Options to use for setting up tests.
+ """
+ self.options = options
+
+ def RunTests(self):
+ """Runs tests from the shared pool of tests, aggregating results.
+
+ Returns:
+ A list of test results for all of the tests which this runner executed.
+ """
+ tests = PythonTestSharder.tests_container
+
+ results = []
+ for t in tests:
+ res = CallPythonTest(t, self.options)
+ results.append(res)
+
+ return TestResults.FromTestResults(results)
+
+
+class PythonTestSharder(object):
+ """Runs Python tests in parallel on multiple devices.
+
+ This is lifted more or less wholesale from BaseTestRunner.
+
+ Under the covers, it creates a pool of long-lived PythonTestRunners, which
+ execute tests from the pool of tests.
+
+ Args:
+ attached_devices: a list of device IDs attached to the host.
+ available_tests: a list of tests to run which subclass PythonTestBase.
+ options: Options to use for setting up tests.
+
+ Returns:
+ An aggregated list of test results.
+ """
+ tests_container = None
+
+ def __init__(self, attached_devices, available_tests, options):
+ self.options = options
+ self.attached_devices = attached_devices
+ self.retries = options.shard_retries
+ self.tests = available_tests
+
+ def _SetupSharding(self, tests):
+ """Creates the shared pool of tests and makes it available to test runners.
+
+ Args:
+ tests: the list of tests which will be consumed by workers.
+ """
+ SetTestsContainer(sharded_tests_queue.ShardedTestsQueue(
+ len(self.attached_devices), tests))
+
+ def RunShardedTests(self):
+ """Runs tests in parallel using a pool of workers.
+
+ Returns:
+ A list of test results aggregated from all test runs.
+ """
+ logging.warning('*' * 80)
+ logging.warning('Sharding in ' + str(len(self.attached_devices)) +
+ ' devices.')
+ logging.warning('Note that the output is not synchronized.')
+ logging.warning('Look for the "Final result" banner in the end.')
+ logging.warning('*' * 80)
+ all_passed = []
+ test_results = TestResults()
+ tests_to_run = self.tests
+ for retry in xrange(self.retries):
+ logging.warning('Try %d of %d', retry + 1, self.retries)
+ self._SetupSharding(self.tests)
+ test_runners = self._MakeTestRunners(self.attached_devices)
+ logging.warning('Starting...')
+ pool = multiprocessing.Pool(len(self.attached_devices),
+ SetTestsContainer,
+ [PythonTestSharder.tests_container])
+
+ # List of TestResults objects from each test execution.
+ try:
+ results_lists = pool.map(_DefaultRunnable, test_runners)
+ except Exception:
+ logging.exception('Unable to run tests. Something with the '
+ 'PythonTestRunners has gone wrong.')
+ raise FatalTestException('PythonTestRunners were unable to run tests.')
+
+ test_results = TestResults.FromTestResults(results_lists)
+ # Accumulate passing results.
+ all_passed += test_results.ok
+ # If we have failed tests, map them to tests to retry.
+ failed_tests = test_results.GetAllBroken()
+ tests_to_run = self._GetTestsToRetry(self.tests,
+ failed_tests)
+
+ # Bail out early if we have no more tests. This can happen if all tests
+ # pass before we're out of retries, for example.
+ if not tests_to_run:
+ break
+
+ final_results = TestResults()
+ # all_passed has accumulated all passing test results.
+ # test_results will have the results from the most recent run, which could
+ # include a variety of failure modes (unknown, crashed, failed, etc).
+ final_results = test_results
+ final_results.ok = all_passed
+
+ return final_results
+
+ def _MakeTestRunners(self, attached_devices):
+ """Initialize and return a list of PythonTestRunners.
+
+ Args:
+ attached_devices: list of device IDs attached to host.
+
+ Returns:
+ A list of PythonTestRunners, one for each device.
+ """
+ test_runners = []
+ for index, device in enumerate(attached_devices):
+ logging.warning('*' * 80)
+ logging.warning('Creating shard %d for %s', index, device)
+ logging.warning('*' * 80)
+ # Bind the PythonTestRunner to a device & shard index. Give it the
+ # runnable which it will use to actually execute the tests.
+ test_options = copy.deepcopy(self.options)
+ test_options.ensure_value('device_id', device)
+ test_options.ensure_value('shard_index', index)
+ test_runner = PythonTestRunner(test_options)
+ test_runners.append(test_runner)
+
+ return test_runners
+
+ def _GetTestsToRetry(self, available_tests, failed_tests):
+ """Infers a list of tests to retry from failed tests and available tests.
+
+ Args:
+ available_tests: a list of tests which subclass PythonTestBase.
+ failed_tests: a list of SingleTestResults representing failed tests.
+
+ Returns:
+ A list of test objects which correspond to test names found in
+ failed_tests, or an empty list if there is no correspondence.
+ """
+ failed_test_names = map(lambda t: t.test_name, failed_tests)
+ tests_to_retry = [t for t in available_tests
+ if t.qualified_name in failed_test_names]
+ return tests_to_retry
diff --git a/src/build/android/pylib/run_java_tests.py b/src/build/android/pylib/run_java_tests.py
new file mode 100644
index 0000000..07b45e0
--- /dev/null
+++ b/src/build/android/pylib/run_java_tests.py
@@ -0,0 +1,593 @@
+# 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.
+
+"""Runs the Java tests. See more information on run_instrumentation_tests.py."""
+
+import fnmatch
+import logging
+import os
+import re
+import shutil
+import sys
+import time
+
+import android_commands
+import apk_info
+from base_test_runner import BaseTestRunner
+from base_test_sharder import BaseTestSharder, SetTestsContainer
+import cmd_helper
+import constants
+import errors
+from forwarder import Forwarder
+from json_perf_parser import GetAverageRunInfoFromJSONString
+from perf_tests_helper import PrintPerfResult
+import sharded_tests_queue
+from test_result import SingleTestResult, TestResults
+import valgrind_tools
+
+_PERF_TEST_ANNOTATION = 'PerfTest'
+
+
+class FatalTestException(Exception):
+ """A fatal test exception."""
+ pass
+
+
+def _TestNameToExpectation(test_name):
+ # A test name is a Package.Path.Class#testName; convert to what we use in
+ # the expectation file.
+ return '.'.join(test_name.replace('#', '.').split('.')[-2:])
+
+
+def FilterTests(test_names, pattern_list, inclusive):
+ """Filters |test_names| using a list of patterns.
+
+ Args:
+ test_names: A list of test names.
+ pattern_list: A list of patterns.
+ inclusive: If True, returns the tests that match any pattern. if False,
+ returns the tests that do not match any pattern.
+ Returns:
+ A list of test names.
+ """
+ ret = []
+ for t in test_names:
+ has_match = False
+ for pattern in pattern_list:
+ has_match = has_match or fnmatch.fnmatch(_TestNameToExpectation(t),
+ pattern)
+ if has_match == inclusive:
+ ret += [t]
+ return ret
+
+
+class TestRunner(BaseTestRunner):
+ """Responsible for running a series of tests connected to a single device."""
+
+ _DEVICE_DATA_DIR = 'chrome/test/data'
+ _EMMA_JAR = os.path.join(os.environ.get('ANDROID_BUILD_TOP', ''),
+ 'external/emma/lib/emma.jar')
+ _COVERAGE_MERGED_FILENAME = 'unittest_coverage.es'
+ _COVERAGE_WEB_ROOT_DIR = os.environ.get('EMMA_WEB_ROOTDIR')
+ _COVERAGE_FILENAME = 'coverage.ec'
+ _COVERAGE_RESULT_PATH = ('/data/data/com.google.android.apps.chrome/files/' +
+ _COVERAGE_FILENAME)
+ _COVERAGE_META_INFO_PATH = os.path.join(os.environ.get('ANDROID_BUILD_TOP',
+ ''),
+ 'out/target/common/obj/APPS',
+ 'Chrome_intermediates/coverage.em')
+ _HOSTMACHINE_PERF_OUTPUT_FILE = '/tmp/chrome-profile'
+ _DEVICE_PERF_OUTPUT_SEARCH_PREFIX = (constants.DEVICE_PERF_OUTPUT_DIR +
+ '/chrome-profile*')
+ _DEVICE_HAS_TEST_FILES = {}
+
+ def __init__(self, options, device, tests_iter, coverage, shard_index, apks,
+ ports_to_forward):
+ """Create a new TestRunner.
+
+ Args:
+ options: An options object with the following required attributes:
+ - build_type: 'Release' or 'Debug'.
+ - install_apk: Re-installs the apk if opted.
+ - save_perf_json: Whether or not to save the JSON file from UI perf
+ tests.
+ - screenshot_failures: Take a screenshot for a test failure
+ - tool: Name of the Valgrind tool.
+ - wait_for_debugger: blocks until the debugger is connected.
+ - disable_assertions: Whether to disable java assertions on the device.
+ device: Attached android device.
+ tests_iter: A list of tests to be run.
+ coverage: Collects coverage information if opted.
+ shard_index: shard # for this TestRunner, used to create unique port
+ numbers.
+ apks: A list of ApkInfo objects need to be installed. The first element
+ should be the tests apk, the rests could be the apks used in test.
+ The default is ChromeTest.apk.
+ ports_to_forward: A list of port numbers for which to set up forwarders.
+ Can be optionally requested by a test case.
+ Raises:
+ FatalTestException: if coverage metadata is not available.
+ """
+ BaseTestRunner.__init__(
+ self, device, options.tool, shard_index, options.build_type)
+
+ if not apks:
+ apks = [apk_info.ApkInfo(options.test_apk_path,
+ options.test_apk_jar_path)]
+
+ self.build_type = options.build_type
+ self.install_apk = options.install_apk
+ self.test_data = options.test_data
+ self.save_perf_json = options.save_perf_json
+ self.screenshot_failures = options.screenshot_failures
+ self.wait_for_debugger = options.wait_for_debugger
+ self.disable_assertions = options.disable_assertions
+
+ self.tests_iter = tests_iter
+ self.coverage = coverage
+ self.apks = apks
+ self.test_apk = apks[0]
+ self.instrumentation_class_path = self.test_apk.GetPackageName()
+ self.ports_to_forward = ports_to_forward
+
+ self.test_results = TestResults()
+ self.forwarder = None
+
+ if self.coverage:
+ if os.path.exists(TestRunner._COVERAGE_MERGED_FILENAME):
+ os.remove(TestRunner._COVERAGE_MERGED_FILENAME)
+ if not os.path.exists(TestRunner._COVERAGE_META_INFO_PATH):
+ raise FatalTestException('FATAL ERROR in ' + sys.argv[0] +
+ ' : Coverage meta info [' +
+ TestRunner._COVERAGE_META_INFO_PATH +
+ '] does not exist.')
+ if (not TestRunner._COVERAGE_WEB_ROOT_DIR or
+ not os.path.exists(TestRunner._COVERAGE_WEB_ROOT_DIR)):
+ raise FatalTestException('FATAL ERROR in ' + sys.argv[0] +
+ ' : Path specified in $EMMA_WEB_ROOTDIR [' +
+ TestRunner._COVERAGE_WEB_ROOT_DIR +
+ '] does not exist.')
+
+ def _GetTestsIter(self):
+ if not self.tests_iter:
+ # multiprocessing.Queue can't be pickled across processes if we have it as
+ # a member set during constructor. Grab one here instead.
+ self.tests_iter = (BaseTestSharder.tests_container)
+ assert self.tests_iter
+ return self.tests_iter
+
+ def CopyTestFilesOnce(self):
+ """Pushes the test data files to the device. Installs the apk if opted."""
+ if TestRunner._DEVICE_HAS_TEST_FILES.get(self.device, False):
+ logging.warning('Already copied test files to device %s, skipping.',
+ self.device)
+ return
+ for dest_host_pair in self.test_data:
+ dst_src = dest_host_pair.split(':',1)
+ dst_layer = dst_src[0]
+ host_src = dst_src[1]
+ host_test_files_path = constants.CHROME_DIR + '/' + host_src
+ if os.path.exists(host_test_files_path):
+ self.adb.PushIfNeeded(host_test_files_path,
+ self.adb.GetExternalStorage() + '/' +
+ TestRunner._DEVICE_DATA_DIR + '/' + dst_layer)
+ if self.install_apk:
+ for apk in self.apks:
+ self.adb.ManagedInstall(apk.GetApkPath(),
+ package_name=apk.GetPackageName())
+ self.tool.CopyFiles()
+ TestRunner._DEVICE_HAS_TEST_FILES[self.device] = True
+
+ def SaveCoverageData(self, test):
+ """Saves the Emma coverage data before it's overwritten by the next test.
+
+ Args:
+ test: the test whose coverage data is collected.
+ """
+ if not self.coverage:
+ return
+ if not self.adb.Adb().Pull(TestRunner._COVERAGE_RESULT_PATH,
+ constants.CHROME_DIR):
+ logging.error('ERROR: Unable to find file ' +
+ TestRunner._COVERAGE_RESULT_PATH +
+ ' on the device for test ' + test)
+ pulled_coverage_file = os.path.join(constants.CHROME_DIR,
+ TestRunner._COVERAGE_FILENAME)
+ if os.path.exists(TestRunner._COVERAGE_MERGED_FILENAME):
+ cmd = ['java', '-classpath', TestRunner._EMMA_JAR, 'emma', 'merge',
+ '-in', pulled_coverage_file,
+ '-in', TestRunner._COVERAGE_MERGED_FILENAME,
+ '-out', TestRunner._COVERAGE_MERGED_FILENAME]
+ cmd_helper.RunCmd(cmd)
+ else:
+ shutil.copy(pulled_coverage_file,
+ TestRunner._COVERAGE_MERGED_FILENAME)
+ os.remove(pulled_coverage_file)
+
+ def GenerateCoverageReportIfNeeded(self):
+ """Uses the Emma to generate a coverage report and a html page."""
+ if not self.coverage:
+ return
+ cmd = ['java', '-classpath', TestRunner._EMMA_JAR,
+ 'emma', 'report', '-r', 'html',
+ '-in', TestRunner._COVERAGE_MERGED_FILENAME,
+ '-in', TestRunner._COVERAGE_META_INFO_PATH]
+ cmd_helper.RunCmd(cmd)
+ new_dir = os.path.join(TestRunner._COVERAGE_WEB_ROOT_DIR,
+ time.strftime('Coverage_for_%Y_%m_%d_%a_%H:%M'))
+ shutil.copytree('coverage', new_dir)
+
+ latest_dir = os.path.join(TestRunner._COVERAGE_WEB_ROOT_DIR,
+ 'Latest_Coverage_Run')
+ if os.path.exists(latest_dir):
+ shutil.rmtree(latest_dir)
+ os.mkdir(latest_dir)
+ webserver_new_index = os.path.join(new_dir, 'index.html')
+ webserver_new_files = os.path.join(new_dir, '_files')
+ webserver_latest_index = os.path.join(latest_dir, 'index.html')
+ webserver_latest_files = os.path.join(latest_dir, '_files')
+ # Setup new softlinks to last result.
+ os.symlink(webserver_new_index, webserver_latest_index)
+ os.symlink(webserver_new_files, webserver_latest_files)
+ cmd_helper.RunCmd(['chmod', '755', '-R', latest_dir, new_dir])
+
+ def _GetInstrumentationArgs(self):
+ ret = {}
+ if self.coverage:
+ ret['coverage'] = 'true'
+ if self.wait_for_debugger:
+ ret['debug'] = 'true'
+ return ret
+
+ def _TakeScreenshot(self, test):
+ """Takes a screenshot from the device."""
+ screenshot_name = os.path.join(constants.SCREENSHOTS_DIR, test + '.png')
+ logging.info('Taking screenshot named %s', screenshot_name)
+ self.adb.TakeScreenshot(screenshot_name)
+
+ def SetUp(self):
+ """Sets up the test harness and device before all tests are run."""
+ super(TestRunner, self).SetUp()
+ if not self.adb.IsRootEnabled():
+ logging.warning('Unable to enable java asserts for %s, non rooted device',
+ self.device)
+ else:
+ if self.adb.SetJavaAssertsEnabled(enable=not self.disable_assertions):
+ self.adb.Reboot(full_reboot=False)
+
+ # We give different default value to launch HTTP server based on shard index
+ # because it may have race condition when multiple processes are trying to
+ # launch lighttpd with same port at same time.
+ http_server_ports = self.LaunchTestHttpServer(
+ os.path.join(constants.CHROME_DIR),
+ (constants.LIGHTTPD_RANDOM_PORT_FIRST + self.shard_index))
+ if self.ports_to_forward:
+ port_pairs = [(port, port) for port in self.ports_to_forward]
+ # We need to remember which ports the HTTP server is using, since the
+ # forwarder will stomp on them otherwise.
+ port_pairs.append(http_server_ports)
+ self.forwarder = Forwarder(self.adb, self.build_type)
+ self.forwarder.Run(port_pairs, self.tool, '127.0.0.1')
+ self.CopyTestFilesOnce()
+ self.flags.AddFlags(['--enable-test-intents'])
+
+ def TearDown(self):
+ """Cleans up the test harness and saves outstanding data from test run."""
+ if self.forwarder:
+ self.forwarder.Close()
+ self.GenerateCoverageReportIfNeeded()
+ super(TestRunner, self).TearDown()
+
+ def TestSetup(self, test):
+ """Sets up the test harness for running a particular test.
+
+ Args:
+ test: The name of the test that will be run.
+ """
+ self.SetupPerfMonitoringIfNeeded(test)
+ self._SetupIndividualTestTimeoutScale(test)
+ self.tool.SetupEnvironment()
+
+ # Make sure the forwarder is still running.
+ self.RestartHttpServerForwarderIfNecessary()
+
+ def _IsPerfTest(self, test):
+ """Determines whether a test is a performance test.
+
+ Args:
+ test: The name of the test to be checked.
+
+ Returns:
+ Whether the test is annotated as a performance test.
+ """
+ return _PERF_TEST_ANNOTATION in self.test_apk.GetTestAnnotations(test)
+
+ def SetupPerfMonitoringIfNeeded(self, test):
+ """Sets up performance monitoring if the specified test requires it.
+
+ Args:
+ test: The name of the test to be run.
+ """
+ if not self._IsPerfTest(test):
+ return
+ self.adb.Adb().SendCommand('shell rm ' +
+ TestRunner._DEVICE_PERF_OUTPUT_SEARCH_PREFIX)
+ self.adb.StartMonitoringLogcat()
+
+ def TestTeardown(self, test, test_result):
+ """Cleans up the test harness after running a particular test.
+
+ Depending on the options of this TestRunner this might handle coverage
+ tracking or performance tracking. This method will only be called if the
+ test passed.
+
+ Args:
+ test: The name of the test that was just run.
+ test_result: result for this test.
+ """
+
+ self.tool.CleanUpEnvironment()
+
+ # The logic below relies on the test passing.
+ if not test_result or test_result.GetStatusCode():
+ return
+
+ self.TearDownPerfMonitoring(test)
+ self.SaveCoverageData(test)
+
+ def TearDownPerfMonitoring(self, test):
+ """Cleans up performance monitoring if the specified test required it.
+
+ Args:
+ test: The name of the test that was just run.
+ Raises:
+ FatalTestException: if there's anything wrong with the perf data.
+ """
+ if not self._IsPerfTest(test):
+ return
+ raw_test_name = test.split('#')[1]
+
+ # Wait and grab annotation data so we can figure out which traces to parse
+ regex = self.adb.WaitForLogMatch(re.compile('\*\*PERFANNOTATION\(' +
+ raw_test_name +
+ '\)\:(.*)'), None)
+
+ # If the test is set to run on a specific device type only (IE: only
+ # tablet or phone) and it is being run on the wrong device, the test
+ # just quits and does not do anything. The java test harness will still
+ # print the appropriate annotation for us, but will add --NORUN-- for
+ # us so we know to ignore the results.
+ # The --NORUN-- tag is managed by MainActivityTestBase.java
+ if regex.group(1) != '--NORUN--':
+
+ # Obtain the relevant perf data. The data is dumped to a
+ # JSON formatted file.
+ json_string = self.adb.GetFileContents(
+ '/data/data/com.google.android.apps.chrome/files/PerfTestData.txt')
+
+ if json_string:
+ json_string = '\n'.join(json_string)
+ else:
+ raise FatalTestException('Perf file does not exist or is empty')
+
+ if self.save_perf_json:
+ json_local_file = '/tmp/chromium-android-perf-json-' + raw_test_name
+ with open(json_local_file, 'w') as f:
+ f.write(json_string)
+ logging.info('Saving Perf UI JSON from test ' +
+ test + ' to ' + json_local_file)
+
+ raw_perf_data = regex.group(1).split(';')
+
+ for raw_perf_set in raw_perf_data:
+ if raw_perf_set:
+ perf_set = raw_perf_set.split(',')
+ if len(perf_set) != 3:
+ raise FatalTestException('Unexpected number of tokens in '
+ 'perf annotation string: ' + raw_perf_set)
+
+ # Process the performance data
+ result = GetAverageRunInfoFromJSONString(json_string, perf_set[0])
+
+ PrintPerfResult(perf_set[1], perf_set[2],
+ [result['average']], result['units'])
+
+ def _SetupIndividualTestTimeoutScale(self, test):
+ timeout_scale = self._GetIndividualTestTimeoutScale(test)
+ valgrind_tools.SetChromeTimeoutScale(self.adb, timeout_scale)
+
+ def _GetIndividualTestTimeoutScale(self, test):
+ """Returns the timeout scale for the given |test|."""
+ annotations = self.apks[0].GetTestAnnotations(test)
+ timeout_scale = 1
+ if 'TimeoutScale' in annotations:
+ for annotation in annotations:
+ scale_match = re.match('TimeoutScale:([0-9]+)', annotation)
+ if scale_match:
+ timeout_scale = int(scale_match.group(1))
+ if self.wait_for_debugger:
+ timeout_scale *= 100
+ return timeout_scale
+
+ def _GetIndividualTestTimeoutSecs(self, test):
+ """Returns the timeout in seconds for the given |test|."""
+ annotations = self.apks[0].GetTestAnnotations(test)
+ if 'Manual' in annotations:
+ return 600 * 60
+ if 'External' in annotations:
+ return 10 * 60
+ if 'LargeTest' in annotations or _PERF_TEST_ANNOTATION in annotations:
+ return 5 * 60
+ if 'MediumTest' in annotations:
+ return 3 * 60
+ return 1 * 60
+
+ def RunTests(self):
+ """Runs the tests, generating the coverage if needed.
+
+ Returns:
+ A TestResults object.
+ """
+ instrumentation_path = (self.instrumentation_class_path +
+ '/android.test.InstrumentationTestRunner')
+ instrumentation_args = self._GetInstrumentationArgs()
+ for test in self._GetTestsIter():
+ test_result = None
+ start_date_ms = None
+ try:
+ self.TestSetup(test)
+ start_date_ms = int(time.time()) * 1000
+ args_with_filter = dict(instrumentation_args)
+ args_with_filter['class'] = test
+ # |test_results| is a list that should contain
+ # a single TestResult object.
+ logging.warn(args_with_filter)
+ (test_results, _) = self.adb.Adb().StartInstrumentation(
+ instrumentation_path=instrumentation_path,
+ instrumentation_args=args_with_filter,
+ timeout_time=(self._GetIndividualTestTimeoutSecs(test) *
+ self._GetIndividualTestTimeoutScale(test) *
+ self.tool.GetTimeoutScale()))
+ duration_ms = int(time.time()) * 1000 - start_date_ms
+ assert len(test_results) == 1
+ test_result = test_results[0]
+ status_code = test_result.GetStatusCode()
+ if status_code:
+ log = test_result.GetFailureReason()
+ if not log:
+ log = 'No information.'
+ if self.screenshot_failures or log.find('INJECT_EVENTS perm') >= 0:
+ self._TakeScreenshot(test)
+ self.test_results.failed += [SingleTestResult(test, start_date_ms,
+ duration_ms, log)]
+ else:
+ result = [SingleTestResult(test, start_date_ms, duration_ms)]
+ self.test_results.ok += result
+ # Catch exceptions thrown by StartInstrumentation().
+ # See ../../third_party/android/testrunner/adb_interface.py
+ except (errors.WaitForResponseTimedOutError,
+ errors.DeviceUnresponsiveError,
+ errors.InstrumentationError), e:
+ if start_date_ms:
+ duration_ms = int(time.time()) * 1000 - start_date_ms
+ else:
+ start_date_ms = int(time.time()) * 1000
+ duration_ms = 0
+ message = str(e)
+ if not message:
+ message = 'No information.'
+ self.test_results.crashed += [SingleTestResult(test, start_date_ms,
+ duration_ms,
+ message)]
+ test_result = None
+ self.TestTeardown(test, test_result)
+ return self.test_results
+
+
+class TestSharder(BaseTestSharder):
+ """Responsible for sharding the tests on the connected devices."""
+
+ def __init__(self, attached_devices, options, tests, apks):
+ BaseTestSharder.__init__(self, attached_devices, options.build_type)
+ self.options = options
+ self.tests = tests
+ self.apks = apks
+
+ def SetupSharding(self, tests):
+ """Called before starting the shards."""
+ SetTestsContainer(sharded_tests_queue.ShardedTestsQueue(
+ len(self.attached_devices), tests))
+
+ def CreateShardedTestRunner(self, device, index):
+ """Creates a sharded test runner.
+
+ Args:
+ device: Device serial where this shard will run.
+ index: Index of this device in the pool.
+
+ Returns:
+ A TestRunner object.
+ """
+ return TestRunner(self.options, device, None, False, index, self.apks, [])
+
+
+def DispatchJavaTests(options, apks):
+ """Dispatches Java tests onto connected device(s).
+
+ If possible, this method will attempt to shard the tests to
+ all connected devices. Otherwise, dispatch and run tests on one device.
+
+ Args:
+ options: Command line options.
+ apks: list of APKs to use.
+
+ Returns:
+ A TestResults object holding the results of the Java tests.
+
+ Raises:
+ FatalTestException: when there's no attached the devices.
+ """
+ test_apk = apks[0]
+ # The default annotation for tests which do not have any sizes annotation.
+ default_size_annotation = 'SmallTest'
+
+ def _GetTestsMissingAnnotation(test_apk):
+ test_size_annotations = frozenset(['Smoke', 'SmallTest', 'MediumTest',
+ 'LargeTest', 'EnormousTest', 'FlakyTest',
+ 'DisabledTest', 'Manual', 'PerfTest'])
+ tests_missing_annotations = []
+ for test_method in test_apk.GetTestMethods():
+ annotations = frozenset(test_apk.GetTestAnnotations(test_method))
+ if (annotations.isdisjoint(test_size_annotations) and
+ not apk_info.ApkInfo.IsPythonDrivenTest(test_method)):
+ tests_missing_annotations.append(test_method)
+ return sorted(tests_missing_annotations)
+
+ if options.annotation:
+ available_tests = test_apk.GetAnnotatedTests(options.annotation)
+ if options.annotation.count(default_size_annotation) > 0:
+ tests_missing_annotations = _GetTestsMissingAnnotation(test_apk)
+ if tests_missing_annotations:
+ logging.warning('The following tests do not contain any annotation. '
+ 'Assuming "%s":\n%s',
+ default_size_annotation,
+ '\n'.join(tests_missing_annotations))
+ available_tests += tests_missing_annotations
+ else:
+ available_tests = [m for m in test_apk.GetTestMethods()
+ if not apk_info.ApkInfo.IsPythonDrivenTest(m)]
+ coverage = os.environ.get('EMMA_INSTRUMENT') == 'true'
+
+ tests = []
+ if options.test_filter:
+ # |available_tests| are in adb instrument format: package.path.class#test.
+ filter_without_hash = options.test_filter.replace('#', '.')
+ tests = [t for t in available_tests
+ if filter_without_hash in t.replace('#', '.')]
+ else:
+ tests = available_tests
+
+ if not tests:
+ logging.warning('No Java tests to run with current args.')
+ return TestResults()
+
+ tests *= options.number_of_runs
+
+ attached_devices = android_commands.GetAttachedDevices()
+ test_results = TestResults()
+
+ if not attached_devices:
+ raise FatalTestException('You have no devices attached or visible!')
+ if options.device:
+ attached_devices = [options.device]
+
+ logging.info('Will run: %s', str(tests))
+
+ if len(attached_devices) > 1 and (coverage or options.wait_for_debugger):
+ logging.warning('Coverage / debugger can not be sharded, '
+ 'using first available device')
+ attached_devices = attached_devices[:1]
+ sharder = TestSharder(attached_devices, options, tests, apks)
+ test_results = sharder.RunShardedTests()
+ return test_results
diff --git a/src/build/android/pylib/run_python_tests.py b/src/build/android/pylib/run_python_tests.py
new file mode 100644
index 0000000..7d39f48
--- /dev/null
+++ b/src/build/android/pylib/run_python_tests.py
@@ -0,0 +1,207 @@
+# 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.
+
+"""Runs the Python tests (relies on using the Java test runner)."""
+
+import logging
+import os
+import sys
+import types
+
+import android_commands
+import apk_info
+import constants
+import python_test_base
+from python_test_caller import CallPythonTest
+from python_test_sharder import PythonTestSharder
+import run_java_tests
+from run_java_tests import FatalTestException
+from test_info_collection import TestInfoCollection
+from test_result import TestResults
+
+
+def _GetPythonFiles(root, files):
+ """Returns all files from |files| that end in 'Test.py'.
+
+ Args:
+ root: A directory name with python files.
+ files: A list of file names.
+
+ Returns:
+ A list with all Python driven test file paths.
+ """
+ return [os.path.join(root, f) for f in files if f.endswith('Test.py')]
+
+
+def _InferImportNameFromFile(python_file):
+ """Given a file, infer the import name for that file.
+
+ Example: /usr/foo/bar/baz.py -> baz.
+
+ Args:
+ python_file: path to the Python file, ostensibly to import later.
+
+ Returns:
+ The module name for the given file.
+ """
+ return os.path.splitext(os.path.basename(python_file))[0]
+
+
+def DispatchPythonTests(options):
+ """Dispatches the Python tests. If there are multiple devices, use sharding.
+
+ Args:
+ options: command line options.
+
+ Returns:
+ A list of test results.
+ """
+
+ attached_devices = android_commands.GetAttachedDevices()
+ if not attached_devices:
+ raise FatalTestException('You have no devices attached or visible!')
+ if options.device:
+ attached_devices = [options.device]
+
+ test_collection = TestInfoCollection()
+ all_tests = _GetAllTests(options.python_test_root, options.official_build)
+ test_collection.AddTests(all_tests)
+ test_names = [t.qualified_name for t in all_tests]
+ logging.debug('All available tests: ' + str(test_names))
+
+ available_tests = test_collection.GetAvailableTests(
+ options.annotation, options.test_filter)
+
+ if not available_tests:
+ logging.warning('No Python tests to run with current args.')
+ return TestResults()
+
+ available_tests *= options.number_of_runs
+ test_names = [t.qualified_name for t in available_tests]
+ logging.debug('Final list of tests to run: ' + str(test_names))
+
+ # Copy files to each device before running any tests.
+ for device_id in attached_devices:
+ logging.debug('Pushing files to device %s', device_id)
+ apks = [apk_info.ApkInfo(options.test_apk_path, options.test_apk_jar_path)]
+ test_files_copier = run_java_tests.TestRunner(options, device_id,
+ None, False, 0, apks, [])
+ test_files_copier.CopyTestFilesOnce()
+
+ # Actually run the tests.
+ if len(attached_devices) > 1 and options.wait_for_debugger:
+ logging.warning('Debugger can not be sharded, '
+ 'using first available device')
+ attached_devices = attached_devices[:1]
+ logging.debug('Running Python tests')
+ sharder = PythonTestSharder(attached_devices, available_tests, options)
+ test_results = sharder.RunShardedTests()
+
+ return test_results
+
+
+def _GetTestModules(python_test_root, is_official_build):
+ """Retrieve a sorted list of pythonDrivenTests.
+
+ Walks the location of pythonDrivenTests, imports them, and provides the list
+ of imported modules to the caller.
+
+ Args:
+ python_test_root: the path to walk, looking for pythonDrivenTests
+ is_official_build: whether to run only those tests marked 'official'
+
+ Returns:
+ A list of Python modules which may have zero or more tests.
+ """
+ # By default run all python tests under pythonDrivenTests.
+ python_test_file_list = []
+ for root, _, files in os.walk(python_test_root):
+ if (root.endswith('pythonDrivenTests')
+ or (is_official_build
+ and root.endswith('pythonDrivenTests/official'))):
+ python_test_file_list += _GetPythonFiles(root, files)
+ python_test_file_list.sort()
+
+ test_module_list = [_GetModuleFromFile(test_file)
+ for test_file in python_test_file_list]
+ return test_module_list
+
+
+def _GetModuleFromFile(python_file):
+ """Gets the module associated with a file by importing it.
+
+ Args:
+ python_file: file to import
+
+ Returns:
+ The module object.
+ """
+ sys.path.append(os.path.dirname(python_file))
+ import_name = _InferImportNameFromFile(python_file)
+ return __import__(import_name)
+
+
+def _GetTestsFromClass(test_class):
+ """Create a list of test objects for each test method on this class.
+
+ Test methods are methods on the class which begin with 'test'.
+
+ Args:
+ test_class: class object which contains zero or more test methods.
+
+ Returns:
+ A list of test objects, each of which is bound to one test.
+ """
+ test_names = [m for m in dir(test_class)
+ if _IsTestMethod(m, test_class)]
+ return map(test_class, test_names)
+
+
+def _GetTestClassesFromModule(test_module):
+ tests = []
+ for name in dir(test_module):
+ attr = getattr(test_module, name)
+ if _IsTestClass(attr):
+ tests.extend(_GetTestsFromClass(attr))
+ return tests
+
+
+def _IsTestClass(test_class):
+ return (type(test_class) is types.TypeType and
+ issubclass(test_class, python_test_base.PythonTestBase) and
+ test_class is not python_test_base.PythonTestBase)
+
+
+def _IsTestMethod(attrname, test_case_class):
+ """Checks whether this is a valid test method.
+
+ Args:
+ attrname: the method name.
+ test_case_class: the test case class.
+
+ Returns:
+ True if test_case_class.'attrname' is callable and it starts with 'test';
+ False otherwise.
+ """
+ attr = getattr(test_case_class, attrname)
+ return callable(attr) and attrname.startswith('test')
+
+
+def _GetAllTests(test_root, is_official_build):
+ """Retrieve a list of Python test modules and their respective methods.
+
+ Args:
+ test_root: path which contains Python-driven test files
+ is_official_build: whether this is an official build
+
+ Returns:
+ List of test case objects for all available test methods.
+ """
+ if not test_root:
+ return []
+ all_tests = []
+ test_module_list = _GetTestModules(test_root, is_official_build)
+ for module in test_module_list:
+ all_tests.extend(_GetTestClassesFromModule(module))
+ return all_tests
diff --git a/src/build/android/pylib/run_tests_helper.py b/src/build/android/pylib/run_tests_helper.py
new file mode 100644
index 0000000..15e5d53
--- /dev/null
+++ b/src/build/android/pylib/run_tests_helper.py
@@ -0,0 +1,26 @@
+# 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.
+
+"""Helper functions common to native, java and python test runners."""
+
+import logging
+import os
+
+
+def GetExpectations(file_name):
+ """Returns a list of test names in the |file_name| test expectations file."""
+ if not file_name or not os.path.exists(file_name):
+ return []
+ return [x for x in [x.strip() for x in file(file_name).readlines()]
+ if x and x[0] != '#']
+
+
+def SetLogLevel(verbose_count):
+ """Sets log level as |verbose_count|."""
+ log_level = logging.WARNING # Default.
+ if verbose_count == 1:
+ log_level = logging.INFO
+ elif verbose_count >= 2:
+ log_level = logging.DEBUG
+ logging.getLogger().setLevel(log_level)
diff --git a/src/build/android/pylib/sharded_tests_queue.py b/src/build/android/pylib/sharded_tests_queue.py
new file mode 100644
index 0000000..9e28e2c
--- /dev/null
+++ b/src/build/android/pylib/sharded_tests_queue.py
@@ -0,0 +1,35 @@
+# 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.
+
+
+"""A module that contains a queue for running sharded tests."""
+
+import multiprocessing
+
+
+class ShardedTestsQueue(object):
+ """A queue for managing pending tests across different runners.
+
+ This class should only be used when sharding.
+
+ Attributes:
+ num_devices: an integer; the number of attached Android devices.
+ tests: a list of tests to be run.
+ tests_queue: if sharding, a JoinableQueue object that holds tests from
+ |tests|. Otherwise, a list holding tests.
+ results_queue: a Queue object to hold TestResults objects.
+ """
+ _STOP_SENTINEL = 'STOP' # sentinel value for iter()
+
+ def __init__(self, num_devices, tests):
+ self.num_devices = num_devices
+ self.tests_queue = multiprocessing.Queue()
+ for test in tests:
+ self.tests_queue.put(test)
+ for _ in xrange(self.num_devices):
+ self.tests_queue.put(ShardedTestsQueue._STOP_SENTINEL)
+
+ def __iter__(self):
+ """Returns an iterator with the test cases."""
+ return iter(self.tests_queue.get, ShardedTestsQueue._STOP_SENTINEL)
diff --git a/src/build/android/pylib/single_test_runner.py b/src/build/android/pylib/single_test_runner.py
new file mode 100644
index 0000000..dee6cd6
--- /dev/null
+++ b/src/build/android/pylib/single_test_runner.py
@@ -0,0 +1,293 @@
+# 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 glob
+import logging
+import os
+import sys
+
+import android_commands
+from android_commands import errors
+from base_test_runner import BaseTestRunner
+import constants
+import debug_info
+import perf_tests_helper
+import run_tests_helper
+from test_package_apk import TestPackageApk
+from test_package_executable import TestPackageExecutable
+from test_result import BaseTestResult, TestResults
+
+
+class SingleTestRunner(BaseTestRunner):
+ """Single test suite attached to a single device.
+
+ Args:
+ device: Device to run the tests.
+ test_suite: A specific test suite to run, empty to run all.
+ gtest_filter: A gtest_filter flag.
+ test_arguments: Additional arguments to pass to the test binary.
+ timeout: Timeout for each test.
+ cleanup_test_files: Whether or not to cleanup test files on device.
+ tool: Name of the Valgrind tool.
+ shard_index: index number of the shard on which the test suite will run.
+ dump_debug_info: Whether or not to dump debug information.
+ build_type: 'Release' or 'Debug'.
+ in_webkit_checkout: Whether the suite is being run from a WebKit checkout.
+ """
+
+ def __init__(self, device, test_suite, gtest_filter, test_arguments, timeout,
+ cleanup_test_files, tool_name, shard_index, dump_debug_info,
+ fast_and_loose, build_type, in_webkit_checkout):
+ BaseTestRunner.__init__(self, device, tool_name, shard_index, build_type)
+ self._running_on_emulator = self.device.startswith('emulator')
+ self._gtest_filter = gtest_filter
+ self._test_arguments = test_arguments
+ self.test_results = TestResults()
+ if dump_debug_info:
+ self.dump_debug_info = debug_info.GTestDebugInfo(
+ self.adb, device,
+ os.path.basename(test_suite), gtest_filter)
+ else:
+ self.dump_debug_info = None
+ self.fast_and_loose = fast_and_loose
+ self.in_webkit_checkout = in_webkit_checkout
+
+ logging.warning('Test suite: ' + test_suite)
+ if os.path.splitext(test_suite)[1] == '.apk':
+ self.test_package = TestPackageApk(
+ self.adb,
+ device,
+ test_suite,
+ timeout,
+ cleanup_test_files,
+ self.tool,
+ self.dump_debug_info)
+ else:
+ # Put a copy into the android out/target directory, to allow stack trace
+ # generation.
+ symbols_dir = os.path.join(constants.CHROME_DIR, 'out', build_type,
+ 'lib.target')
+ self.test_package = TestPackageExecutable(
+ self.adb,
+ device,
+ test_suite, timeout,
+ cleanup_test_files,
+ self.tool,
+ self.dump_debug_info,
+ symbols_dir)
+
+ def _TestSuiteRequiresMockTestServer(self):
+ """Returns True if the test suite requires mock test server."""
+ tests_require_net_test_server = ['unit_tests', 'net_unittests',
+ 'content_unittests']
+ return (self.test_package.test_suite_basename in
+ tests_require_net_test_server)
+
+ def _GetFilterFileName(self):
+ """Returns the filename of gtest filter."""
+ return os.path.join(
+ sys.path[0], 'gtest_filter',
+ self.test_package.test_suite_basename + '_disabled')
+
+ def _GetAdditionalEmulatorFilterName(self):
+ """Returns the filename of additional gtest filter for emulator."""
+ return os.path.join(
+ sys.path[0], 'gtest_filter',
+ self.test_package.test_suite_basename +
+ '_emulator_additional_disabled')
+
+ def GetDisabledTests(self):
+ """Returns a list of disabled tests.
+
+ Returns:
+ A list of disabled tests obtained from gtest_filter/test_suite_disabled.
+ """
+ disabled_tests = run_tests_helper.GetExpectations(self._GetFilterFileName())
+ if self._running_on_emulator:
+ # Append emulator's filter file.
+ disabled_tests.extend(run_tests_helper.GetExpectations(
+ self._GetAdditionalEmulatorFilterName()))
+ return disabled_tests
+
+ def GetDataFilesForTestSuite(self):
+ """Returns a list of data files/dirs needed by the test suite."""
+ # Ideally, we'd just push all test data. However, it has >100MB, and a lot
+ # of the files are not relevant (some are used for browser_tests, others for
+ # features not supported, etc..).
+ if self.test_package.test_suite_basename in ['base_unittests',
+ 'sql_unittests',
+ 'unit_tests']:
+ test_files = [
+ 'base/data/file_util_unittest',
+ 'base/data/json/bom_feff.json',
+ 'base/prefs/test/data/pref_service',
+ 'chrome/test/data/download-test1.lib',
+ 'chrome/test/data/extensions/bad_magic.crx',
+ 'chrome/test/data/extensions/good.crx',
+ 'chrome/test/data/extensions/icon1.png',
+ 'chrome/test/data/extensions/icon2.png',
+ 'chrome/test/data/extensions/icon3.png',
+ 'chrome/test/data/extensions/allow_silent_upgrade/',
+ 'chrome/test/data/extensions/app/',
+ 'chrome/test/data/extensions/bad/',
+ 'chrome/test/data/extensions/effective_host_permissions/',
+ 'chrome/test/data/extensions/empty_manifest/',
+ 'chrome/test/data/extensions/good/Extensions/',
+ 'chrome/test/data/extensions/manifest_tests/',
+ 'chrome/test/data/extensions/page_action/',
+ 'chrome/test/data/extensions/permissions/',
+ 'chrome/test/data/extensions/script_and_capture/',
+ 'chrome/test/data/extensions/unpacker/',
+ 'chrome/test/data/bookmarks/',
+ 'chrome/test/data/components/',
+ 'chrome/test/data/extensions/json_schema_test.js',
+ 'chrome/test/data/History/',
+ 'chrome/test/data/json_schema_validator/',
+ 'chrome/test/data/pref_service/',
+ 'chrome/test/data/serializer_nested_test.js',
+ 'chrome/test/data/serializer_test.js',
+ 'chrome/test/data/serializer_test_nowhitespace.js',
+ 'chrome/test/data/top_sites/',
+ 'chrome/test/data/web_app_info/',
+ 'chrome/test/data/web_database',
+ 'chrome/test/data/webui/',
+ 'chrome/test/data/zip',
+ 'chrome/third_party/mock4js/',
+ 'content/browser/gpu/software_rendering_list.json',
+ 'net/data/cache_tests/insert_load1',
+ 'net/data/cache_tests/dirty_entry5',
+ 'net/data/ssl/certificates/',
+ 'ui/base/test/data/data_pack_unittest',
+ ]
+ if self.test_package.test_suite_basename == 'unit_tests':
+ test_files += ['chrome/test/data/simple_open_search.xml']
+ # The following are spell check data. Now only list the data under
+ # third_party/hunspell_dictionaries which are used by unit tests.
+ old_cwd = os.getcwd()
+ os.chdir(constants.CHROME_DIR)
+ test_files += glob.glob('third_party/hunspell_dictionaries/*.bdic')
+ os.chdir(old_cwd)
+ return test_files
+ elif self.test_package.test_suite_basename == 'media_unittests':
+ return [
+ 'media/test/data',
+ ]
+ elif self.test_package.test_suite_basename == 'net_unittests':
+ return [
+ 'chrome/test/data/animate1.gif',
+ 'chrome/test/data/simple.html',
+ 'net/data/cache_tests',
+ 'net/data/filter_unittests',
+ 'net/data/ftp',
+ 'net/data/proxy_resolver_v8_unittest',
+ 'net/data/ssl/certificates',
+ 'net/data/url_request_unittest/',
+ 'net/data/proxy_script_fetcher_unittest'
+ ]
+ elif self.test_package.test_suite_basename == 'ui_tests':
+ return [
+ 'chrome/test/data/dromaeo',
+ 'chrome/test/data/json2.js',
+ 'chrome/test/data/sunspider',
+ 'chrome/test/data/v8_benchmark',
+ 'chrome/test/perf/v8_benchmark_uitest.js',
+ ]
+ elif self.test_package.test_suite_basename == 'content_unittests':
+ return [
+ 'content/test/data/gpu/webgl_conformance_test_expectations.txt',
+ 'net/data/ssl/certificates/',
+ 'webkit/data/dom_storage/webcore_test_database.localstorage',
+ 'third_party/hyphen/hyph_en_US.dic',
+ ]
+ elif self.test_package.test_suite_basename == 'media_unittests':
+ return [
+ 'media/test/data',
+ ]
+ return []
+
+ def LaunchHelperToolsForTestSuite(self):
+ """Launches helper tools for the test suite.
+
+ Sometimes one test may need to run some helper tools first in order to
+ successfully complete the test.
+ """
+ if self._TestSuiteRequiresMockTestServer():
+ self.LaunchChromeTestServerSpawner()
+
+ def StripAndCopyFiles(self):
+ """Strips and copies the required data files for the test suite."""
+ self.test_package.StripAndCopyExecutable()
+ self.test_package.PushDataAndPakFiles()
+ self.tool.CopyFiles()
+ test_data = self.GetDataFilesForTestSuite()
+ if test_data and not self.fast_and_loose:
+ # Make sure SD card is ready.
+ self.adb.WaitForSdCardReady(20)
+ for data in test_data:
+ self.CopyTestData([data], self.adb.GetExternalStorage())
+ if self.test_package.test_suite_basename == 'webkit_unit_tests':
+ self.PushWebKitUnitTestsData()
+
+ def PushWebKitUnitTestsData(self):
+ """Pushes the webkit_unit_tests data files to the device.
+
+ The path of this directory is different when the suite is being run as
+ part of a WebKit check-out.
+ """
+ webkit_src = os.path.join(constants.CHROME_DIR, 'third_party', 'WebKit')
+ if self.in_webkit_checkout:
+ webkit_src = os.path.join(constants.CHROME_DIR, '..', '..', '..')
+
+ self.adb.PushIfNeeded(
+ os.path.join(webkit_src, 'Source/WebKit/chromium/tests/data'),
+ os.path.join(
+ self.adb.GetExternalStorage(),
+ 'third_party/WebKit/Source/WebKit/chromium/tests/data'))
+
+ def RunTests(self):
+ """Runs tests on a single device.
+
+ Returns:
+ A TestResults object.
+ """
+ try:
+ self.test_package.CreateTestRunnerScript(self._gtest_filter,
+ self._test_arguments)
+ self.test_results = self.test_package.RunTestsAndListResults()
+ except errors.DeviceUnresponsiveError as e:
+ # Make sure this device is not attached
+ if android_commands.IsDeviceAttached(self.device):
+ raise e
+
+ # TODO(frankf): We should report these as "skipped" not "failures".
+ # Wrap the results
+ logging.warning(e)
+ failed_tests = []
+ for t in self._gtest_filter.split(':'):
+ failed_tests += [BaseTestResult(t, '')]
+ self.test_results = TestResults.FromRun(
+ failed=failed_tests, device_exception=self.device)
+
+ return self.test_results
+
+ def SetUp(self):
+ """Sets up necessary test enviroment for the test suite."""
+ super(SingleTestRunner, self).SetUp()
+ self.adb.ClearApplicationState(constants.CHROME_PACKAGE)
+ if self.dump_debug_info:
+ self.dump_debug_info.StartRecordingLog(True)
+ self.StripAndCopyFiles()
+ self.LaunchHelperToolsForTestSuite()
+ self.tool.SetupEnvironment()
+
+ def TearDown(self):
+ """Cleans up the test enviroment for the test suite."""
+ self.tool.CleanUpEnvironment()
+ if self.test_package.cleanup_test_files:
+ self.adb.RemovePushedFiles()
+ if self.dump_debug_info:
+ self.dump_debug_info.StopRecordingLog()
+ if self.dump_debug_info:
+ self.dump_debug_info.ArchiveNewCrashFiles()
+ super(SingleTestRunner, self).TearDown()
diff --git a/src/build/android/pylib/surface_stats_collector.py b/src/build/android/pylib/surface_stats_collector.py
new file mode 100644
index 0000000..9c0cb7e
--- /dev/null
+++ b/src/build/android/pylib/surface_stats_collector.py
@@ -0,0 +1,229 @@
+# 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 Queue
+import datetime
+import logging
+import re
+import threading
+
+from pylib import perf_tests_helper
+
+
+# Log marker containing SurfaceTexture timestamps.
+_SURFACE_TEXTURE_TIMESTAMPS_MESSAGE = 'SurfaceTexture update timestamps'
+_SURFACE_TEXTURE_TIMESTAMP_RE = '\d+'
+
+
+class SurfaceStatsCollector(object):
+ """Collects surface stats for a window from the output of SurfaceFlinger.
+
+ Args:
+ adb: the adb coonection to use.
+ window_package: Package name of the window.
+ window_activity: Activity name of the window.
+ """
+ def __init__(self, adb, window_package, window_activity, trace_tag):
+ self._adb = adb
+ self._window_package = window_package
+ self._window_activity = window_activity
+ self._trace_tag = trace_tag
+ self._collector_thread = None
+ self._use_legacy_method = False
+ self._surface_before = None
+ self._get_data_event = None
+ self._data_queue = None
+ self._stop_event = None
+
+ def __enter__(self):
+ assert not self._collector_thread
+
+ if self._ClearSurfaceFlingerLatencyData():
+ self._get_data_event = threading.Event()
+ self._stop_event = threading.Event()
+ self._data_queue = Queue.Queue()
+ self._collector_thread = threading.Thread(target=self._CollectorThread)
+ self._collector_thread.start()
+ else:
+ self._use_legacy_method = True
+ self._surface_before = self._GetSurfaceStatsLegacy()
+
+ def __exit__(self, *args):
+ self._PrintPerfResults()
+ if self._collector_thread:
+ self._stop_event.set()
+ self._collector_thread.join()
+ self._collector_thread = None
+
+ def _PrintPerfResults(self):
+ if self._use_legacy_method:
+ surface_after = self._GetSurfaceStatsLegacy()
+ td = surface_after['timestamp'] - self._surface_before['timestamp']
+ seconds = td.seconds + td.microseconds / 1e6
+ frame_count = (surface_after['page_flip_count'] -
+ self._surface_before['page_flip_count'])
+ else:
+ assert self._collector_thread
+ (seconds, latencies) = self._GetDataFromThread()
+ if not seconds or not len(latencies):
+ logging.warning('Surface stat data is empty')
+ return
+
+ frame_count = len(latencies)
+ jitter_count = 0
+ last_latency = latencies[0]
+ for latency in latencies[1:]:
+ if latency > last_latency:
+ jitter_count = jitter_count + 1
+ last_latency = latency
+
+ perf_tests_helper.PrintPerfResult(
+ 'surface_latencies', 'surface_latencies' + self._trace_tag,
+ latencies, '')
+ perf_tests_helper.PrintPerfResult(
+ 'peak_jitter', 'peak_jitter' + self._trace_tag, [max(latencies)], '')
+ perf_tests_helper.PrintPerfResult(
+ 'jitter_percent', 'jitter_percent' + self._trace_tag,
+ [jitter_count * 100.0 / frame_count], 'percent')
+
+ print 'SurfaceMonitorTime: %fsecs' % seconds
+ perf_tests_helper.PrintPerfResult(
+ 'avg_surface_fps', 'avg_surface_fps' + self._trace_tag,
+ [int(round(frame_count / seconds))], 'fps')
+
+ def _CollectorThread(self):
+ last_timestamp = 0
+ first_timestamp = 0
+ latencies = []
+
+ while not self._stop_event.is_set():
+ self._get_data_event.wait(1)
+ try:
+ (t, last_timestamp) = self._GetSurfaceFlingerLatencyData(last_timestamp,
+ latencies)
+ if not first_timestamp:
+ first_timestamp = t
+
+ if self._get_data_event.is_set():
+ self._get_data_event.clear()
+ self._data_queue.put(((last_timestamp - first_timestamp) / 1e9,
+ latencies))
+ latencies = []
+ first_timestamp = 0
+ except Exception as e:
+ # On any error, before aborting, put the exception into _data_queue to
+ # prevent the main thread from waiting at _data_queue.get() infinitely.
+ self._data_queue.put(e)
+ raise
+
+ def _GetDataFromThread(self):
+ self._get_data_event.set()
+ ret = self._data_queue.get()
+ if isinstance(ret, Exception):
+ raise ret
+ return ret
+
+ def _ClearSurfaceFlingerLatencyData(self):
+ """Clears the SurfaceFlinger latency data.
+
+ Returns:
+ True if SurfaceFlinger latency is supported by the device, otherwise
+ False.
+ """
+ # The command returns nothing if it is supported, otherwise returns many
+ # lines of result just like 'dumpsys SurfaceFlinger'.
+ results = self._adb.RunShellCommand(
+ 'dumpsys SurfaceFlinger --latency-clear %s/%s' %
+ (self._window_package, self._window_activity))
+ return not len(results)
+
+ def _GetSurfaceFlingerLatencyData(self, previous_timestamp, latencies):
+ """Returns collected SurfaceFlinger latency data.
+
+ Args:
+ previous_timestamp: The timestamp returned from the previous call or 0.
+ Only data after this timestamp will be returned.
+ latencies: A list to receive latency data. The latencies are integers
+ each of which is the number of refresh periods of each frame.
+
+ Returns:
+ A tuple containing:
+ - The timestamp of the beginning of the first frame (ns),
+ - The timestamp of the end of the last frame (ns).
+
+ Raises:
+ Exception if failed to run the SurfaceFlinger command or SurfaceFlinger
+ returned invalid result.
+ """
+ # adb shell dumpsys SurfaceFlinger --latency <window name>
+ # prints some information about the last 128 frames displayed in
+ # that window.
+ # The data returned looks like this:
+ # 16954612
+ # 7657467895508 7657482691352 7657493499756
+ # 7657484466553 7657499645964 7657511077881
+ # 7657500793457 7657516600576 7657527404785
+ # (...)
+ #
+ # The first line is the refresh period (here 16.95 ms), it is followed
+ # by 128 lines w/ 3 timestamps in nanosecond each:
+ # A) when the app started to draw
+ # B) the vsync immediately preceding SF submitting the frame to the h/w
+ # C) timestamp immediately after SF submitted that frame to the h/w
+ #
+ # The difference between the 1st and 3rd timestamp is the frame-latency.
+ # An interesting data is when the frame latency crosses a refresh period
+ # boundary, this can be calculated this way:
+ #
+ # ceil((C - A) / refresh-period)
+ #
+ # (each time the number above changes, we have a "jank").
+ # If this happens a lot during an animation, the animation appears
+ # janky, even if it runs at 60 fps in average.
+ results = self._adb.RunShellCommand(
+ 'dumpsys SurfaceFlinger --latency %s/%s' %
+ (self._window_package, self._window_activity), log_result=True)
+ assert len(results)
+
+ refresh_period = int(results[0])
+ last_timestamp = previous_timestamp
+ first_timestamp = 0
+ for line in results[1:]:
+ fields = line.split()
+ if len(fields) == 3:
+ timestamp = long(fields[0])
+ last_timestamp = long(fields[2])
+ if (timestamp > previous_timestamp):
+ if not first_timestamp:
+ first_timestamp = timestamp
+ # This is integral equivalent of ceil((C-A) / refresh-period)
+ latency_ns = int(last_timestamp - timestamp)
+ latencies.append((latency_ns + refresh_period - 1) / refresh_period)
+ return (first_timestamp, last_timestamp)
+
+ def _GetSurfaceStatsLegacy(self):
+ """Legacy method (before JellyBean), returns the current Surface index
+ and timestamp.
+
+ Calculate FPS by measuring the difference of Surface index returned by
+ SurfaceFlinger in a period of time.
+
+ Returns:
+ Dict of {page_flip_count (or 0 if there was an error), timestamp}.
+ """
+ results = self._adb.RunShellCommand('service call SurfaceFlinger 1013')
+ assert len(results) == 1
+ match = re.search('^Result: Parcel\((\w+)', results[0])
+ cur_surface = 0
+ if match:
+ try:
+ cur_surface = int(match.group(1), 16)
+ except Exception:
+ logging.error('Failed to parse current surface from ' + match.group(1))
+ else:
+ logging.warning('Failed to call SurfaceFlinger surface ' + results[0])
+ return {
+ 'page_flip_count': cur_surface,
+ 'timestamp': datetime.datetime.now(),
+ }
diff --git a/src/build/android/pylib/test_info_collection.py b/src/build/android/pylib/test_info_collection.py
new file mode 100644
index 0000000..fc4e806
--- /dev/null
+++ b/src/build/android/pylib/test_info_collection.py
@@ -0,0 +1,137 @@
+# 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.
+
+"""Module containing information about the python-driven tests."""
+
+import logging
+import os
+
+import tests_annotations
+
+
+class TestInfo(object):
+ """An object containing and representing a test function, plus metadata."""
+
+ def __init__(self, runnable, set_up=None, tear_down=None):
+ # The actual test function/method.
+ self.runnable = runnable
+ # Qualified name of test function/method (e.g. FooModule.testBar).
+ self.qualified_name = self._GetQualifiedName(runnable)
+ # setUp and teardown functions, if any.
+ self.set_up = set_up
+ self.tear_down = tear_down
+
+ def _GetQualifiedName(self, runnable):
+ """Helper method to infer a runnable's name and module name.
+
+ Many filters and lists presuppose a format of module_name.testMethodName.
+ To make this easy on everyone, we use some reflection magic to infer this
+ name automatically.
+
+ Args:
+ runnable: the test method to get the qualified name for
+
+ Returns:
+ qualified name for this runnable, incl. module name and method name.
+ """
+ runnable_name = runnable.__name__
+ # See also tests_annotations.
+ module_name = os.path.splitext(
+ os.path.basename(runnable.__globals__['__file__']))[0]
+ return '.'.join([module_name, runnable_name])
+
+ def __str__(self):
+ return self.qualified_name
+
+
+class TestInfoCollection(object):
+ """A collection of TestInfo objects which facilitates filtering."""
+
+ def __init__(self):
+ """Initialize a new TestInfoCollection."""
+ # Master list of all valid tests.
+ self.all_tests = []
+
+ def AddTests(self, test_infos):
+ """Adds a set of tests to this collection.
+
+ The user may then retrieve them, optionally according to criteria, via
+ GetAvailableTests().
+
+ Args:
+ test_infos: a list of TestInfos representing test functions/methods.
+ """
+ self.all_tests = test_infos
+
+ def GetAvailableTests(self, annotation, name_filter):
+ """Get a collection of TestInfos which match the supplied criteria.
+
+ Args:
+ annotation: annotation which tests must match, if any
+ name_filter: name filter which tests must match, if any
+
+ Returns:
+ List of available tests.
+ """
+ available_tests = self.all_tests
+
+ # Filter out tests which match neither the requested annotation, nor the
+ # requested name filter, if any.
+ available_tests = [t for t in available_tests if
+ self._AnnotationIncludesTest(t, annotation)]
+ if annotation and len(annotation) == 1 and annotation[0] == 'SmallTest':
+ tests_without_annotation = [
+ t for t in self.all_tests if
+ not tests_annotations.AnnotatedFunctions.GetTestAnnotations(
+ t.qualified_name)]
+ test_names = [t.qualified_name for t in tests_without_annotation]
+ logging.warning('The following tests do not contain any annotation. '
+ 'Assuming "SmallTest":\n%s',
+ '\n'.join(test_names))
+ available_tests += tests_without_annotation
+ available_tests = [t for t in available_tests if
+ self._NameFilterIncludesTest(t, name_filter)]
+
+ return available_tests
+
+ def _AnnotationIncludesTest(self, test_info, annotation_filter_list):
+ """Checks whether a given test represented by test_info matches annotation.
+
+ Args:
+ test_info: TestInfo object representing the test
+ annotation_filter_list: list of annotation filters to match (e.g. Smoke)
+
+ Returns:
+ True if no annotation was supplied or the test matches; false otherwise.
+ """
+ if not annotation_filter_list:
+ return True
+ for annotation_filter in annotation_filter_list:
+ filters = annotation_filter.split('=')
+ if len(filters) == 2:
+ key = filters[0]
+ value_list = filters[1].split(',')
+ for value in value_list:
+ if tests_annotations.AnnotatedFunctions.IsAnnotated(
+ key + ':' + value, test_info.qualified_name):
+ return True
+ elif tests_annotations.AnnotatedFunctions.IsAnnotated(
+ annotation_filter, test_info.qualified_name):
+ return True
+ return False
+
+ def _NameFilterIncludesTest(self, test_info, name_filter):
+ """Checks whether a name filter matches a given test_info's method name.
+
+ This is a case-sensitive, substring comparison: 'Foo' will match methods
+ Foo.testBar and Bar.testFoo. 'foo' would not match either.
+
+ Args:
+ test_info: TestInfo object representing the test
+ name_filter: substring to check for in the qualified name of the test
+
+ Returns:
+ True if no name filter supplied or it matches; False otherwise.
+ """
+ return not name_filter or name_filter in test_info.qualified_name
diff --git a/src/build/android/pylib/test_options_parser.py b/src/build/android/pylib/test_options_parser.py
new file mode 100644
index 0000000..d88e997
--- /dev/null
+++ b/src/build/android/pylib/test_options_parser.py
@@ -0,0 +1,184 @@
+# 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.
+
+"""Parses options for the instrumentation tests."""
+
+import constants
+import optparse
+import os
+import sys
+
+_SDK_OUT_DIR = os.path.join(constants.CHROME_DIR, 'out')
+
+
+def AddBuildTypeOption(option_parser):
+ """Decorates OptionParser with build type option."""
+ default_build_type = 'Debug'
+ if 'BUILDTYPE' in os.environ:
+ default_build_type = os.environ['BUILDTYPE']
+ option_parser.add_option('--debug', action='store_const', const='Debug',
+ dest='build_type', default=default_build_type,
+ help='If set, run test suites under out/Debug. '
+ 'Default is env var BUILDTYPE or Debug')
+ option_parser.add_option('--release', action='store_const', const='Release',
+ dest='build_type',
+ help='If set, run test suites under out/Release. '
+ 'Default is env var BUILDTYPE or Debug.')
+
+def AddInstallAPKOption(option_parser):
+ """Decorates OptionParser with apk option used to install the APK."""
+ AddBuildTypeOption(option_parser)
+ option_parser.add_option('--apk',
+ help=('The name of the apk containing the '
+ ' application (with the .apk extension).'))
+ option_parser.add_option('--apk_package',
+ help=('The package name used by the apk containing '
+ 'the application.'))
+
+
+def ValidateInstallAPKOption(option_parser, options):
+ if not options.apk:
+ option_parser.error('--apk is mandatory.')
+ if not os.path.exists(options.apk):
+ options.apk = os.path.join(os.environ['CHROME_SRC'],
+ 'out', options.build_type,
+ 'apks', options.apk)
+
+
+def AddTestRunnerOptions(option_parser, default_timeout=60):
+ """Decorates OptionParser with options applicable to all tests."""
+
+ option_parser.add_option('-t', dest='timeout',
+ help='Timeout to wait for each test',
+ type='int',
+ default=default_timeout)
+ option_parser.add_option('-c', dest='cleanup_test_files',
+ help='Cleanup test files on the device after run',
+ action='store_true')
+ option_parser.add_option('-v',
+ '--verbose',
+ dest='verbose_count',
+ default=0,
+ action='count',
+ help='Verbose level (multiple times for more)')
+ profilers = ['devicestatsmonitor', 'chrometrace', 'dumpheap', 'smaps',
+ 'traceview']
+ option_parser.add_option('--profiler', dest='profilers', action='append',
+ choices=profilers,
+ help='Profiling tool to run during test. '
+ 'Pass multiple times to run multiple profilers. '
+ 'Available profilers: %s' % profilers)
+ option_parser.add_option('--tool',
+ dest='tool',
+ help='Run the test under a tool '
+ '(use --tool help to list them)')
+ AddBuildTypeOption(option_parser)
+
+
+def AddInstrumentationOptions(option_parser):
+ """Decorates OptionParser with instrumentation tests options."""
+
+ AddTestRunnerOptions(option_parser)
+ option_parser.add_option('-w', '--wait_debugger', dest='wait_for_debugger',
+ action='store_true', help='Wait for debugger.')
+ option_parser.add_option('-I', dest='install_apk', help='Install APK.',
+ action='store_true')
+ option_parser.add_option('-f', '--test_filter',
+ help='Test filter (if not fully qualified, '
+ 'will run all matches).')
+ option_parser.add_option('-A', '--annotation', dest='annotation_str',
+ help=('Run only tests with any of the given '
+ 'annotations. '
+ 'An annotation can be either a key or a '
+ 'key-values pair. '
+ 'A test that has no annotation is '
+ 'considered "SmallTest".'))
+ option_parser.add_option('-j', '--java_only', action='store_true',
+ help='Run only the Java tests.')
+ option_parser.add_option('-p', '--python_only', action='store_true',
+ help='Run only the Python tests.')
+ option_parser.add_option('-n', '--run_count', type='int',
+ dest='number_of_runs', default=1,
+ help=('How many times to run each test, regardless '
+ 'of the result. (Default is 1)'))
+ option_parser.add_option('--test-apk', dest='test_apk',
+ help=('The name of the apk containing the tests '
+ '(without the .apk extension). For SDK '
+ 'builds, the apk name without the debug '
+ 'suffix(for example, ContentShellTest).'))
+ option_parser.add_option('--screenshot', dest='screenshot_failures',
+ action='store_true',
+ help='Capture screenshots of test failures')
+ option_parser.add_option('--save-perf-json', action='store_true',
+ help='Saves the JSON file for each UI Perf test.')
+ option_parser.add_option('--shard_retries', type=int, default=1,
+ help=('Number of times to retry each failure when '
+ 'sharding.'))
+ option_parser.add_option('--official-build', help='Run official build tests.')
+ option_parser.add_option('--device',
+ help='Serial number of device we should use.')
+ option_parser.add_option('--python_test_root',
+ help='Root of the python-driven tests.')
+ option_parser.add_option('--keep_test_server_ports',
+ action='store_true',
+ help='Indicates the test server ports must be '
+ 'kept. When this is run via a sharder '
+ 'the test server ports should be kept and '
+ 'should not be reset.')
+ option_parser.add_option('--flakiness-dashboard-server',
+ dest='flakiness_dashboard_server',
+ help=('Address of the server that is hosting the '
+ 'Chrome for Android flakiness dashboard.'))
+ option_parser.add_option('--buildbot-step-failure',
+ action='store_true',
+ help=('If present, will set the buildbot status '
+ 'as STEP_FAILURE, otherwise as STEP_WARNINGS '
+ 'when test(s) fail.'))
+ option_parser.add_option('--disable_assertions', action='store_true',
+ help='Run with java assertions disabled.')
+ option_parser.add_option('--test_data', action='append', default=[],
+ help=('Each instance defines a directory of test '
+ 'data that should be copied to the target(s) '
+ 'before running the tests. The argument '
+ 'should be of the form <target>:<source>, '
+ '<target> is relative to the device data'
+ 'directory, and <source> is relative to the '
+ 'chromium build directory.'))
+
+def ValidateInstrumentationOptions(option_parser, options, args):
+ """Validate options/arguments and populate options with defaults."""
+ if len(args) > 1:
+ option_parser.print_help(sys.stderr)
+ option_parser.error('Unknown arguments: %s' % args[1:])
+ if options.java_only and options.python_only:
+ option_parser.error('Options java_only (-j) and python_only (-p) '
+ 'are mutually exclusive.')
+ if not options.test_apk:
+ option_parser.error('--test-apk must be specified.')
+
+ options.run_java_tests = True
+ options.run_python_tests = True
+ if options.java_only:
+ options.run_python_tests = False
+ elif options.python_only:
+ options.run_java_tests = False
+
+ if os.path.exists(options.test_apk):
+ # The APK is fully qualified, assume the JAR lives along side.
+ options.test_apk_path = options.test_apk
+ options.test_apk_jar_path = os.path.splitext(options.test_apk_path) + '.jar'
+ else:
+ options.test_apk_path = os.path.join(_SDK_OUT_DIR,
+ options.build_type,
+ constants.SDK_BUILD_APKS_DIR,
+ '%s.apk' % options.test_apk)
+ options.test_apk_jar_path = os.path.join(
+ _SDK_OUT_DIR, options.build_type, constants.SDK_BUILD_TEST_JAVALIB_DIR,
+ '%s.jar' % options.test_apk)
+ if options.annotation_str:
+ options.annotation = options.annotation_str.split()
+ elif options.test_filter:
+ options.annotation = []
+ else:
+ options.annotation = ['Smoke', 'SmallTest', 'MediumTest', 'LargeTest']
diff --git a/src/build/android/pylib/test_package.py b/src/build/android/pylib/test_package.py
new file mode 100644
index 0000000..356268d
--- /dev/null
+++ b/src/build/android/pylib/test_package.py
@@ -0,0 +1,184 @@
+# 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 re
+import os
+
+import constants
+from perf_tests_helper import PrintPerfResult
+from pylib import pexpect
+from test_result import BaseTestResult, TestResults
+
+from android_commands import errors
+
+
+class TestPackage(object):
+ """A helper base class for both APK and stand-alone executables.
+
+ Args:
+ adb: ADB interface the tests are using.
+ device: Device to run the tests.
+ test_suite: A specific test suite to run, empty to run all.
+ timeout: Timeout for each test.
+ cleanup_test_files: Whether or not to cleanup test files on device.
+ tool: Name of the Valgrind tool.
+ dump_debug_info: A debug_info object.
+ """
+
+ def __init__(self, adb, device, test_suite, timeout,
+ cleanup_test_files, tool, dump_debug_info):
+ self.adb = adb
+ self.device = device
+ self.test_suite_full = test_suite
+ self.test_suite = os.path.splitext(test_suite)[0]
+ self.test_suite_basename = self._GetTestSuiteBaseName()
+ self.test_suite_dirname = os.path.dirname(
+ self.test_suite.split(self.test_suite_basename)[0])
+ self.cleanup_test_files = cleanup_test_files
+ self.tool = tool
+ if timeout == 0:
+ timeout = 60
+ # On a VM (e.g. chromium buildbots), this timeout is way too small.
+ if os.environ.get('BUILDBOT_SLAVENAME'):
+ timeout = timeout * 2
+ self.timeout = timeout * self.tool.GetTimeoutScale()
+ self.dump_debug_info = dump_debug_info
+
+ def GetDisabledPrefixes(self):
+ return ['DISABLED_', 'FLAKY_', 'FAILS_']
+
+ def _ParseGTestListTests(self, all_tests):
+ """Parses and filters the raw test lists.
+
+ Args:
+ all_tests: The raw test listing with the following format:
+
+ IPCChannelTest.
+ SendMessageInChannelConnected
+ IPCSyncChannelTest.
+ Simple
+ DISABLED_SendWithTimeoutMixedOKAndTimeout
+
+ Returns:
+ A list of non-disabled tests. For the above raw listing:
+
+ [IPCChannelTest.SendMessageInChannelConnected, IPCSyncChannelTest.Simple]
+ """
+ ret = []
+ current = ''
+ disabled_prefixes = self.GetDisabledPrefixes()
+ for test in all_tests:
+ if not test:
+ continue
+ if test[0] != ' ' and not test.endswith('.'):
+ # Ignore any lines with unexpected format.
+ continue
+ if test[0] != ' ' and test.endswith('.'):
+ current = test
+ continue
+ if 'YOU HAVE' in test:
+ break
+ test_name = test[2:]
+ if not any([test_name.startswith(x) for x in disabled_prefixes]):
+ ret += [current + test_name]
+ return ret
+
+ def PushDataAndPakFiles(self):
+ external_storage = self.adb.GetExternalStorage()
+ if (self.test_suite_basename == 'ui_unittests' or
+ self.test_suite_basename == 'unit_tests'):
+ self.adb.PushIfNeeded(
+ self.test_suite_dirname + '/chrome.pak',
+ external_storage + '/paks/chrome.pak')
+ self.adb.PushIfNeeded(
+ self.test_suite_dirname + '/locales/en-US.pak',
+ external_storage + '/paks/en-US.pak')
+ if self.test_suite_basename == 'unit_tests':
+ self.adb.PushIfNeeded(
+ self.test_suite_dirname + '/resources.pak',
+ external_storage + '/paks/resources.pak')
+ self.adb.PushIfNeeded(
+ self.test_suite_dirname + '/chrome_100_percent.pak',
+ external_storage + '/paks/chrome_100_percent.pak')
+ self.adb.PushIfNeeded(self.test_suite_dirname + '/test_data',
+ external_storage + '/test_data')
+ if self.test_suite_basename == 'content_unittests':
+ self.adb.PushIfNeeded(
+ self.test_suite_dirname + '/content_resources.pak',
+ external_storage + '/paks/content_resources.pak')
+ if self.test_suite_basename == 'breakpad_unittests':
+ self.adb.PushIfNeeded(
+ self.test_suite_dirname + '/linux_dumper_unittest_helper',
+ constants.TEST_EXECUTABLE_DIR + '/linux_dumper_unittest_helper')
+
+ def _WatchTestOutput(self, p):
+ """Watches the test output.
+ Args:
+ p: the process generating output as created by pexpect.spawn.
+ """
+ ok_tests = []
+ failed_tests = []
+ crashed_tests = []
+ timed_out = False
+ overall_fail = False
+
+ # Test case statuses.
+ re_run = re.compile('\[ RUN \] ?(.*)\r\n')
+ re_fail = re.compile('\[ FAILED \] ?(.*)\r\n')
+ re_ok = re.compile('\[ OK \] ?(.*?) .*\r\n')
+
+ # Test run statuses.
+ re_passed = re.compile('\[ PASSED \] ?(.*)\r\n')
+ re_runner_fail = re.compile('\[ RUNNER_FAILED \] ?(.*)\r\n')
+ # Signal handlers are installed before starting tests
+ # to output the CRASHED marker when a crash happens.
+ re_crash = re.compile('\[ CRASHED \](.*)\r\n')
+
+ try:
+ while True:
+ found = p.expect([re_run, re_passed, re_runner_fail],
+ timeout=self.timeout)
+ if found == 1: # re_passed
+ break
+ elif found == 2: # re_runner_fail
+ overall_fail = True
+ break
+ else: # re_run
+ if self.dump_debug_info:
+ self.dump_debug_info.TakeScreenshot('_Test_Start_Run_')
+
+ full_test_name = p.match.group(1).replace('\r', '')
+ found = p.expect([re_ok, re_fail, re_crash], timeout=self.timeout)
+ if found == 0: # re_ok
+ if full_test_name == p.match.group(1).replace('\r', ''):
+ ok_tests += [BaseTestResult(full_test_name, p.before)]
+ elif found == 2: # re_crash
+ crashed_tests += [BaseTestResult(full_test_name, p.before)]
+ overall_fail = True
+ break
+ else: # re_fail
+ failed_tests += [BaseTestResult(full_test_name, p.before)]
+ except pexpect.EOF:
+ logging.error('Test terminated - EOF')
+ raise errors.DeviceUnresponsiveError('Device may be offline')
+ except pexpect.TIMEOUT:
+ logging.error('Test terminated after %d second timeout.',
+ self.timeout)
+ timed_out = True
+ finally:
+ p.close()
+
+ ret_code = self._GetGTestReturnCode()
+ if ret_code:
+ logging.critical(
+ 'gtest exit code: %d\npexpect.before: %s\npexpect.after: %s',
+ ret_code, p.before, p.after)
+ overall_fail = True
+
+ # Create TestResults and return
+ return TestResults.FromRun(ok=ok_tests, failed=failed_tests,
+ crashed=crashed_tests, timed_out=timed_out,
+ overall_fail=overall_fail)
diff --git a/src/build/android/pylib/test_package_apk.py b/src/build/android/pylib/test_package_apk.py
new file mode 100644
index 0000000..e43ef75
--- /dev/null
+++ b/src/build/android/pylib/test_package_apk.py
@@ -0,0 +1,120 @@
+# 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 os
+import shlex
+import sys
+import tempfile
+import time
+
+import android_commands
+import constants
+from android_commands import errors
+from test_package import TestPackage
+from pylib import pexpect
+
+class TestPackageApk(TestPackage):
+ """A helper class for running APK-based native tests.
+
+ Args:
+ adb: ADB interface the tests are using.
+ device: Device to run the tests.
+ test_suite: A specific test suite to run, empty to run all.
+ timeout: Timeout for each test.
+ cleanup_test_files: Whether or not to cleanup test files on device.
+ tool: Name of the Valgrind tool.
+ dump_debug_info: A debug_info object.
+ """
+
+ def __init__(self, adb, device, test_suite, timeout,
+ cleanup_test_files, tool, dump_debug_info):
+ TestPackage.__init__(self, adb, device, test_suite, timeout,
+ cleanup_test_files, tool, dump_debug_info)
+
+ def _CreateTestRunnerScript(self, options):
+ command_line_file = tempfile.NamedTemporaryFile()
+ # GTest expects argv[0] to be the executable path.
+ command_line_file.write(self.test_suite_basename + ' ' + options)
+ command_line_file.flush()
+ self.adb.PushIfNeeded(command_line_file.name,
+ constants.TEST_EXECUTABLE_DIR +
+ '/chrome-native-tests-command-line')
+
+ def _GetGTestReturnCode(self):
+ return None
+
+ def _GetFifo(self):
+ # The test.fifo path is determined by:
+ # testing/android/java/src/org/chromium/native_test/
+ # ChromeNativeTestActivity.java and
+ # testing/android/native_test_launcher.cc
+ return '/data/data/org.chromium.native_test/files/test.fifo'
+
+ def _ClearFifo(self):
+ self.adb.RunShellCommand('rm -f ' + self._GetFifo())
+
+ def _WatchFifo(self, timeout, logfile=None):
+ for i in range(10):
+ if self.adb.FileExistsOnDevice(self._GetFifo()):
+ print 'Fifo created...'
+ break
+ time.sleep(i)
+ else:
+ raise errors.DeviceUnresponsiveError(
+ 'Unable to find fifo on device %s ' % self._GetFifo())
+ args = shlex.split(self.adb.Adb()._target_arg)
+ args += ['shell', 'cat', self._GetFifo()]
+ return pexpect.spawn('adb', args, timeout=timeout, logfile=logfile)
+
+ def GetAllTests(self):
+ """Returns a list of all tests available in the test suite."""
+ self._CreateTestRunnerScript('--gtest_list_tests')
+ try:
+ self.tool.SetupEnvironment()
+ # Clear and start monitoring logcat.
+ self._ClearFifo()
+ self.adb.RunShellCommand(
+ 'am start -n '
+ 'org.chromium.native_test/'
+ 'org.chromium.native_test.ChromeNativeTestActivity')
+ # Wait for native test to complete.
+ p = self._WatchFifo(timeout=30 * self.tool.GetTimeoutScale())
+ p.expect("<<ScopedMainEntryLogger")
+ p.close()
+ finally:
+ self.tool.CleanUpEnvironment()
+ # We need to strip the trailing newline.
+ content = [line.rstrip() for line in p.before.splitlines()]
+ ret = self._ParseGTestListTests(content)
+ return ret
+
+ def CreateTestRunnerScript(self, gtest_filter, test_arguments):
+ self._CreateTestRunnerScript('--gtest_filter=%s %s' % (gtest_filter,
+ test_arguments))
+
+ def RunTestsAndListResults(self):
+ try:
+ self.tool.SetupEnvironment()
+ self._ClearFifo()
+ self.adb.RunShellCommand(
+ 'am start -n '
+ 'org.chromium.native_test/'
+ 'org.chromium.native_test.ChromeNativeTestActivity')
+ finally:
+ self.tool.CleanUpEnvironment()
+ logfile = android_commands.NewLineNormalizer(sys.stdout)
+ return self._WatchTestOutput(self._WatchFifo(timeout=10, logfile=logfile))
+
+ def StripAndCopyExecutable(self):
+ self.tool.CopyFiles()
+ # Always uninstall the previous one (by activity name); we don't
+ # know what was embedded in it.
+ self.adb.ManagedInstall(self.test_suite_full, False,
+ package_name='org.chromium.native_test')
+
+ def _GetTestSuiteBaseName(self):
+ """Returns the base name of the test suite."""
+ # APK test suite names end with '-debug.apk'
+ return os.path.basename(self.test_suite).rsplit('-debug', 1)[0]
diff --git a/src/build/android/pylib/test_package_executable.py b/src/build/android/pylib/test_package_executable.py
new file mode 100644
index 0000000..cafe99c
--- /dev/null
+++ b/src/build/android/pylib/test_package_executable.py
@@ -0,0 +1,163 @@
+# 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 shutil
+import sys
+import tempfile
+
+import cmd_helper
+import constants
+from test_package import TestPackage
+from pylib import pexpect
+
+
+class TestPackageExecutable(TestPackage):
+ """A helper class for running stand-alone executables."""
+
+ _TEST_RUNNER_RET_VAL_FILE = 'gtest_retval'
+
+ def __init__(self, adb, device, test_suite, timeout,
+ cleanup_test_files, tool, dump_debug_info,
+ symbols_dir=None):
+ """
+ Args:
+ adb: ADB interface the tests are using.
+ device: Device to run the tests.
+ test_suite: A specific test suite to run, empty to run all.
+ timeout: Timeout for each test.
+ cleanup_test_files: Whether or not to cleanup test files on device.
+ tool: Name of the Valgrind tool.
+ dump_debug_info: A debug_info object.
+ symbols_dir: Directory to put the stripped binaries.
+ """
+ TestPackage.__init__(self, adb, device, test_suite, timeout,
+ cleanup_test_files, tool, dump_debug_info)
+ self.symbols_dir = symbols_dir
+
+ def _GetGTestReturnCode(self):
+ ret = None
+ ret_code = 1 # Assume failure if we can't find it
+ ret_code_file = tempfile.NamedTemporaryFile()
+ try:
+ if not self.adb.Adb().Pull(
+ constants.TEST_EXECUTABLE_DIR + '/' +
+ TestPackageExecutable._TEST_RUNNER_RET_VAL_FILE,
+ ret_code_file.name):
+ logging.critical('Unable to pull gtest ret val file %s',
+ ret_code_file.name)
+ raise ValueError
+ ret_code = file(ret_code_file.name).read()
+ ret = int(ret_code)
+ except ValueError:
+ logging.critical('Error reading gtest ret val file %s [%s]',
+ ret_code_file.name, ret_code)
+ ret = 1
+ return ret
+
+ def _AddNativeCoverageExports(self):
+ # export GCOV_PREFIX set the path for native coverage results
+ # export GCOV_PREFIX_STRIP indicates how many initial directory
+ # names to strip off the hardwired absolute paths.
+ # This value is calculated in buildbot.sh and
+ # depends on where the tree is built.
+ # Ex: /usr/local/google/code/chrome will become
+ # /code/chrome if GCOV_PREFIX_STRIP=3
+ try:
+ depth = os.environ['NATIVE_COVERAGE_DEPTH_STRIP']
+ except KeyError:
+ logging.info('NATIVE_COVERAGE_DEPTH_STRIP is not defined: '
+ 'No native coverage.')
+ return ''
+ export_string = ('export GCOV_PREFIX="%s/gcov"\n' %
+ self.adb.GetExternalStorage())
+ export_string += 'export GCOV_PREFIX_STRIP=%s\n' % depth
+ return export_string
+
+ def GetAllTests(self):
+ """Returns a list of all tests available in the test suite."""
+ all_tests = self.adb.RunShellCommand(
+ '%s %s/%s --gtest_list_tests' %
+ (self.tool.GetTestWrapper(),
+ constants.TEST_EXECUTABLE_DIR,
+ self.test_suite_basename))
+ return self._ParseGTestListTests(all_tests)
+
+ def CreateTestRunnerScript(self, gtest_filter, test_arguments):
+ """Creates a test runner script and pushes to the device.
+
+ Args:
+ gtest_filter: A gtest_filter flag.
+ test_arguments: Additional arguments to pass to the test binary.
+ """
+ tool_wrapper = self.tool.GetTestWrapper()
+ sh_script_file = tempfile.NamedTemporaryFile()
+ # We need to capture the exit status from the script since adb shell won't
+ # propagate to us.
+ sh_script_file.write('cd %s\n'
+ '%s'
+ '%s %s/%s --gtest_filter=%s %s\n'
+ 'echo $? > %s' %
+ (constants.TEST_EXECUTABLE_DIR,
+ self._AddNativeCoverageExports(),
+ tool_wrapper, constants.TEST_EXECUTABLE_DIR,
+ self.test_suite_basename,
+ gtest_filter, test_arguments,
+ TestPackageExecutable._TEST_RUNNER_RET_VAL_FILE))
+ sh_script_file.flush()
+ cmd_helper.RunCmd(['chmod', '+x', sh_script_file.name])
+ self.adb.PushIfNeeded(
+ sh_script_file.name,
+ constants.TEST_EXECUTABLE_DIR + '/chrome_test_runner.sh')
+ logging.info('Conents of the test runner script: ')
+ for line in open(sh_script_file.name).readlines():
+ logging.info(' ' + line.rstrip())
+
+ def RunTestsAndListResults(self):
+ """Runs all the tests and checks for failures.
+
+ Returns:
+ A TestResults object.
+ """
+ args = ['adb', '-s', self.device, 'shell', 'sh',
+ constants.TEST_EXECUTABLE_DIR + '/chrome_test_runner.sh']
+ logging.info(args)
+ p = pexpect.spawn(args[0], args[1:], logfile=sys.stdout)
+ return self._WatchTestOutput(p)
+
+ def StripAndCopyExecutable(self):
+ """Strips and copies the executable to the device."""
+ if self.tool.NeedsDebugInfo():
+ target_name = self.test_suite
+ else:
+ target_name = self.test_suite + '_' + self.device + '_stripped'
+ should_strip = True
+ if os.path.isfile(target_name):
+ logging.info('Found target file %s' % target_name)
+ target_mtime = os.stat(target_name).st_mtime
+ source_mtime = os.stat(self.test_suite).st_mtime
+ if target_mtime > source_mtime:
+ logging.info('Target mtime (%d) is newer than source (%d), assuming '
+ 'no change.' % (target_mtime, source_mtime))
+ should_strip = False
+
+ if should_strip:
+ logging.info('Did not find up-to-date stripped binary. Generating a '
+ 'new one (%s).' % target_name)
+ # Whenever we generate a stripped binary, copy to the symbols dir. If we
+ # aren't stripping a new binary, assume it's there.
+ if self.symbols_dir:
+ if not os.path.exists(self.symbols_dir):
+ os.makedirs(self.symbols_dir)
+ shutil.copy(self.test_suite, self.symbols_dir)
+ strip = os.environ['STRIP']
+ cmd_helper.RunCmd([strip, self.test_suite, '-o', target_name])
+ test_binary = constants.TEST_EXECUTABLE_DIR + '/' + self.test_suite_basename
+ self.adb.PushIfNeeded(target_name, test_binary)
+
+ def _GetTestSuiteBaseName(self):
+ """Returns the base name of the test suite."""
+ return os.path.basename(self.test_suite)
diff --git a/src/build/android/pylib/test_result.py b/src/build/android/pylib/test_result.py
new file mode 100644
index 0000000..2eb1c66
--- /dev/null
+++ b/src/build/android/pylib/test_result.py
@@ -0,0 +1,209 @@
+# 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 json
+import logging
+import os
+import time
+import traceback
+
+import buildbot_report
+import constants
+
+
+class BaseTestResult(object):
+ """A single result from a unit test."""
+
+ def __init__(self, name, log):
+ self.name = name
+ self.log = log.replace('\r', '')
+
+
+class SingleTestResult(BaseTestResult):
+ """Result information for a single test.
+
+ Args:
+ full_name: Full name of the test.
+ start_date: Date in milliseconds when the test began running.
+ dur: Duration of the test run in milliseconds.
+ log: An optional string listing any errors.
+ """
+
+ def __init__(self, full_name, start_date, dur, log=''):
+ BaseTestResult.__init__(self, full_name, log)
+ name_pieces = full_name.rsplit('#')
+ if len(name_pieces) > 1:
+ self.test_name = name_pieces[1]
+ self.class_name = name_pieces[0]
+ else:
+ self.class_name = full_name
+ self.test_name = full_name
+ self.start_date = start_date
+ self.dur = dur
+
+
+class TestResults(object):
+ """Results of a test run."""
+
+ def __init__(self):
+ self.ok = []
+ self.failed = []
+ self.crashed = []
+ self.unknown = []
+ self.timed_out = False
+ self.overall_fail = False
+ self.device_exception = None
+
+ @staticmethod
+ def FromRun(ok=None, failed=None, crashed=None, timed_out=False,
+ overall_fail=False, device_exception=None):
+ ret = TestResults()
+ ret.ok = ok or []
+ ret.failed = failed or []
+ ret.crashed = crashed or []
+ ret.timed_out = timed_out
+ ret.overall_fail = overall_fail
+ ret.device_exception = device_exception
+ return ret
+
+ @staticmethod
+ def FromTestResults(results):
+ """Combines a list of results in a single TestResults object."""
+ ret = TestResults()
+ for t in results:
+ ret.ok += t.ok
+ ret.failed += t.failed
+ ret.crashed += t.crashed
+ ret.unknown += t.unknown
+ if t.timed_out:
+ ret.timed_out = True
+ if t.overall_fail:
+ ret.overall_fail = True
+ return ret
+
+ @staticmethod
+ def FromPythonException(test_name, start_date_ms, exc_info):
+ """Constructs a TestResults with exception information for the given test.
+
+ Args:
+ test_name: name of the test which raised an exception.
+ start_date_ms: the starting time for the test.
+ exc_info: exception info, ostensibly from sys.exc_info().
+
+ Returns:
+ A TestResults object with a SingleTestResult in the failed list.
+ """
+ exc_type, exc_value, exc_traceback = exc_info
+ trace_info = ''.join(traceback.format_exception(exc_type, exc_value,
+ exc_traceback))
+ log_msg = 'Exception:\n' + trace_info
+ duration_ms = (int(time.time()) * 1000) - start_date_ms
+
+ exc_result = SingleTestResult(
+ full_name='PythonWrapper#' + test_name,
+ start_date=start_date_ms,
+ dur=duration_ms,
+ log=(str(exc_type) + ' ' + log_msg))
+
+ results = TestResults()
+ results.failed.append(exc_result)
+ return results
+
+ @staticmethod
+ def DeviceExceptions(results):
+ return set(filter(lambda t: t.device_exception, results))
+
+ def _Log(self, sorted_list):
+ for t in sorted_list:
+ logging.critical(t.name)
+ if t.log:
+ logging.critical(t.log)
+
+ def GetAllBroken(self):
+ """Returns the all broken tests including failed, crashed, unknown."""
+ return self.failed + self.crashed + self.unknown
+
+ def LogFull(self, test_group, test_suite, build_type, tests_to_run):
+ """Output broken test logs, summarize in a log file and the test output."""
+ # Output all broken tests or 'passed' if none broken.
+ logging.critical('*' * 80)
+ logging.critical('Final result')
+ if self.failed:
+ logging.critical('Failed:')
+ self._Log(sorted(self.failed))
+ if self.crashed:
+ logging.critical('Crashed:')
+ self._Log(sorted(self.crashed))
+ if self.unknown:
+ logging.critical('Unknown:')
+ self._Log(sorted(self.unknown))
+ if not self.GetAllBroken():
+ logging.critical('Passed')
+ logging.critical('*' * 80)
+
+ # Summarize in a log file, if tests are running on bots.
+ if test_group and test_suite and os.environ.get('BUILDBOT_BUILDERNAME'):
+ log_file_path = os.path.join(constants.CHROME_DIR, 'out',
+ build_type, 'test_logs')
+ if not os.path.exists(log_file_path):
+ os.mkdir(log_file_path)
+ full_file_name = os.path.join(log_file_path, test_group)
+ if not os.path.exists(full_file_name):
+ with open(full_file_name, 'w') as log_file:
+ print >> log_file, '\n%s results for %s build %s:' % (
+ test_group, os.environ.get('BUILDBOT_BUILDERNAME'),
+ os.environ.get('BUILDBOT_BUILDNUMBER'))
+ log_contents = [' %s result : %d tests ran' % (test_suite,
+ len(self.ok) +
+ len(self.failed) +
+ len(self.crashed) +
+ len(self.unknown))]
+ content_pairs = [('passed', len(self.ok)), ('failed', len(self.failed)),
+ ('crashed', len(self.crashed))]
+ for (result, count) in content_pairs:
+ if count:
+ log_contents.append(', %d tests %s' % (count, result))
+ with open(full_file_name, 'a') as log_file:
+ print >> log_file, ''.join(log_contents)
+ content = {'test_group': test_group,
+ 'ok': [t.name for t in self.ok],
+ 'failed': [t.name for t in self.failed],
+ 'crashed': [t.name for t in self.failed],
+ 'unknown': [t.name for t in self.unknown],}
+ with open(os.path.join(log_file_path, 'results.json'), 'a') as json_file:
+ print >> json_file, json.dumps(content)
+
+ # Summarize in the test output.
+ summary = ['Summary:\n']
+ if tests_to_run:
+ summary += ['TESTS_TO_RUN=%d\n' % (len(tests_to_run))]
+ num_tests_ran = (len(self.ok) + len(self.failed) +
+ len(self.crashed) + len(self.unknown))
+ tests_passed = [t.name for t in self.ok]
+ tests_failed = [t.name for t in self.failed]
+ tests_crashed = [t.name for t in self.crashed]
+ tests_unknown = [t.name for t in self.unknown]
+ summary += ['RAN=%d\n' % (num_tests_ran),
+ 'PASSED=%d\n' % len(tests_passed),
+ 'FAILED=%d %s\n' % (len(tests_failed), tests_failed),
+ 'CRASHED=%d %s\n' % (len(tests_crashed), tests_crashed),
+ 'UNKNOWN=%d %s\n' % (len(tests_unknown), tests_unknown)]
+ if tests_to_run and num_tests_ran != len(tests_to_run):
+ # Add the list of tests we failed to run.
+ tests_failed_to_run = list(set(tests_to_run) - set(tests_passed) -
+ set(tests_failed) - set(tests_crashed) -
+ set(tests_unknown))
+ summary += ['FAILED_TO_RUN=%d %s\n' % (len(tests_failed_to_run),
+ tests_failed_to_run)]
+ summary_string = ''.join(summary)
+ logging.critical(summary_string)
+ return summary_string
+
+ def PrintAnnotation(self):
+ """Print buildbot annotations for test results."""
+ if self.failed or self.crashed or self.overall_fail or self.timed_out:
+ buildbot_report.PrintError()
+ else:
+ print 'Step success!' # No annotation needed
diff --git a/src/build/android/pylib/tests_annotations.py b/src/build/android/pylib/tests_annotations.py
new file mode 100644
index 0000000..f2a1834
--- /dev/null
+++ b/src/build/android/pylib/tests_annotations.py
@@ -0,0 +1,89 @@
+# 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.
+
+"""Annotations for python-driven tests."""
+
+import os
+
+
+class AnnotatedFunctions(object):
+ """A container for annotated methods."""
+ _ANNOTATED = {}
+
+ @staticmethod
+ def _AddFunction(annotation, function):
+ """Adds an annotated to function to our container.
+
+ Args:
+ annotation: the annotation string.
+ function: the function.
+ Returns:
+ The function passed in.
+ """
+ module_name = os.path.splitext(os.path.basename(
+ function.__globals__['__file__']))[0]
+ qualified_function_name = '.'.join([module_name, function.func_name])
+ function_list = AnnotatedFunctions._ANNOTATED.get(annotation, [])
+ function_list.append(qualified_function_name)
+ AnnotatedFunctions._ANNOTATED[annotation] = function_list
+ return function
+
+ @staticmethod
+ def IsAnnotated(annotation, qualified_function_name):
+ """True if function name (module.function) contains the annotation.
+
+ Args:
+ annotation: the annotation string.
+ qualified_function_name: the qualified function name.
+ Returns:
+ True if module.function contains the annotation.
+ """
+ return qualified_function_name in AnnotatedFunctions._ANNOTATED.get(
+ annotation, [])
+
+ @staticmethod
+ def GetTestAnnotations(qualified_function_name):
+ """Returns a list containing all annotations for the given function.
+
+ Args:
+ qualified_function_name: the qualified function name.
+ Returns:
+ List of all annotations for this function.
+ """
+ return [annotation
+ for annotation, tests in AnnotatedFunctions._ANNOTATED.iteritems()
+ if qualified_function_name in tests]
+
+
+# The following functions are annotations used for the python driven tests.
+def Smoke(function):
+ return AnnotatedFunctions._AddFunction('Smoke', function)
+
+
+def SmallTest(function):
+ return AnnotatedFunctions._AddFunction('SmallTest', function)
+
+
+def MediumTest(function):
+ return AnnotatedFunctions._AddFunction('MediumTest', function)
+
+
+def LargeTest(function):
+ return AnnotatedFunctions._AddFunction('LargeTest', function)
+
+
+def FlakyTest(function):
+ return AnnotatedFunctions._AddFunction('FlakyTest', function)
+
+
+def DisabledTest(function):
+ return AnnotatedFunctions._AddFunction('DisabledTest', function)
+
+
+def Feature(feature_list):
+ def _AddFeatures(function):
+ for feature in feature_list:
+ AnnotatedFunctions._AddFunction('Feature' + feature, function)
+ return AnnotatedFunctions._AddFunction('Feature', function)
+ return _AddFeatures
diff --git a/src/build/android/pylib/thermal_throttle.py b/src/build/android/pylib/thermal_throttle.py
new file mode 100644
index 0000000..ebd61d6
--- /dev/null
+++ b/src/build/android/pylib/thermal_throttle.py
@@ -0,0 +1,70 @@
+# 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
+
+class ThermalThrottle(object):
+ """Class to detect and track thermal throttling
+
+ Usage:
+ Wait for IsThrottled() to be False before running test
+ After running test call HasBeenThrottled() to find out if the
+ test run was affected by thermal throttling.
+
+ Currently assumes an OMap device.
+ """
+ def __init__(self, adb):
+ self._adb = adb
+ self._throttled = False
+
+
+ def HasBeenThrottled(self):
+ """ True if there has been any throttling since the last call to
+ HasBeenThrottled or IsThrottled
+ """
+ return self._ReadLog()
+
+ def IsThrottled(self):
+ """True if currently throttled"""
+ self._ReadLog()
+ return self._throttled
+
+ def _ReadLog(self):
+ has_been_throttled = False
+ serial_number = self._adb.Adb().GetSerialNumber()
+ log = self._adb.RunShellCommand('dmesg -c')
+ degree_symbol = unichr(0x00B0)
+ for line in log:
+ if 'omap_thermal_throttle' in line:
+ if not self._throttled:
+ logging.warning('>>> Device %s Thermally Throttled', serial_number)
+ self._throttled = True
+ has_been_throttled = True
+ if 'omap_thermal_unthrottle' in line:
+ if self._throttled:
+ logging.warning('>>> Device %s Thermally Unthrottled', serial_number)
+ self._throttled = False
+ has_been_throttled = True
+ if 'throttle_delayed_work_fn' in line:
+ temp = float([s for s in line.split() if s.isdigit()][0]) / 1000.0
+ logging.info(u' Device %s Thermally Thottled at %3.1f%sC',
+ serial_number, temp, degree_symbol)
+
+ # Print temperature of CPU SoC.
+ omap_temp_file = '/sys/devices/platform/omap/omap_temp_sensor.0/temperature'
+ if self._adb.FileExistsOnDevice(omap_temp_file):
+ tempdata = self._adb.GetFileContents(omap_temp_file)
+ temp = float(tempdata[0]) / 1000.0
+ logging.info(u'Current OMAP Temperature of %s = %3.1f%sC',
+ serial_number, temp, degree_symbol)
+
+ # Print temperature of battery, to give a system temperature
+ dumpsys_log = self._adb.RunShellCommand('dumpsys battery')
+ for line in dumpsys_log:
+ if 'temperature' in line:
+ btemp = float([s for s in line.split() if s.isdigit()][0]) / 10.0
+ logging.info(u'Current battery temperature of %s = %3.1f%sC',
+ serial_number, btemp, degree_symbol)
+
+ return has_been_throttled
diff --git a/src/build/android/pylib/valgrind_tools.py b/src/build/android/pylib/valgrind_tools.py
new file mode 100644
index 0000000..fdc6ebf
--- /dev/null
+++ b/src/build/android/pylib/valgrind_tools.py
@@ -0,0 +1,256 @@
+# 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.
+
+"""
+Classes in this file define additional actions that need to be taken to run a
+test under some kind of runtime error detection tool.
+
+The interface is intended to be used as follows.
+
+1. For tests that simply run a native process (i.e. no activity is spawned):
+
+Call tool.CopyFiles().
+Prepend test command line with tool.GetTestWrapper().
+
+2. For tests that spawn an activity:
+
+Call tool.CopyFiles().
+Call tool.SetupEnvironment().
+Run the test as usual.
+Call tool.CleanUpEnvironment().
+"""
+
+import os.path
+import sys
+
+from constants import CHROME_DIR
+
+
+def SetChromeTimeoutScale(adb, scale):
+ """Sets the timeout scale in /data/local/tmp/chrome_timeout_scale to scale."""
+ path = '/data/local/tmp/chrome_timeout_scale'
+ if not scale or scale == 1.0:
+ # Delete if scale is None/0.0/1.0 since the default timeout scale is 1.0
+ adb.RunShellCommand('rm %s' % path)
+ else:
+ adb.SetFileContents(path, '%f' % scale)
+
+
+class BaseTool(object):
+ """A tool that does nothing."""
+
+ def GetTestWrapper(self):
+ """Returns a string that is to be prepended to the test command line."""
+ return ''
+
+ def GetUtilWrapper(self):
+ """Returns the wrapper name for the utilities.
+
+ Returns:
+ A string that is to be prepended to the command line of utility
+ processes (forwarder, etc.).
+ """
+ return ''
+
+ def CopyFiles(self):
+ """Copies tool-specific files to the device, create directories, etc."""
+ pass
+
+ def SetupEnvironment(self):
+ """Sets up the system environment for a test.
+
+ This is a good place to set system properties.
+ """
+ pass
+
+ def CleanUpEnvironment(self):
+ """Cleans up environment."""
+ pass
+
+ def GetTimeoutScale(self):
+ """Returns a multiplier that should be applied to timeout values."""
+ return 1.0
+
+ def NeedsDebugInfo(self):
+ """Whether this tool requires debug info.
+
+ Returns:
+ True if this tool can not work with stripped binaries.
+ """
+ return False
+
+
+class AddressSanitizerTool(BaseTool):
+ """AddressSanitizer tool."""
+
+ TMP_DIR = '/data/local/tmp/asan'
+ WRAPPER_NAME = 'asanwrapper.sh'
+
+ def __init__(self, adb):
+ self._adb = adb
+ self._wrap_properties = ['wrap.com.google.android.apps.ch',
+ 'wrap.org.chromium.native_test']
+
+ def CopyFiles(self):
+ """Copies ASan tools to the device."""
+ files = ['tools/android/asan/asanwrapper.sh',
+ 'third_party/llvm-build/Release+Asserts/lib/clang/3.2/lib/linux/' +
+ 'libclang_rt.asan-arm-android.so']
+ for f in files:
+ self._adb.PushIfNeeded(os.path.join(CHROME_DIR, f),
+ os.path.join(AddressSanitizerTool.TMP_DIR,
+ os.path.basename(f)))
+
+ def GetTestWrapper(self):
+ return os.path.join(AddressSanitizerTool.TMP_DIR,
+ AddressSanitizerTool.WRAPPER_NAME)
+
+ def GetUtilWrapper(self):
+ """Returns the wrapper for utilities, such as forwarder.
+
+ AddressSanitizer wrapper must be added to all instrumented binaries,
+ including forwarder and the like. This can be removed if such binaries
+ were built without instrumentation. """
+ return self.GetTestWrapper()
+
+ def SetupEnvironment(self):
+ self._adb.EnableAdbRoot()
+ for prop in self._wrap_properties:
+ self._adb.RunShellCommand('setprop %s "logwrapper %s"' % (
+ prop, self.GetTestWrapper()))
+ SetChromeTimeoutScale(self._adb, self.GetTimeoutScale())
+
+ def CleanUpEnvironment(self):
+ for prop in self._wrap_properties:
+ self._adb.RunShellCommand('setprop %s ""' % (prop,))
+ SetChromeTimeoutScale(self._adb, None)
+
+ def GetTimeoutScale(self):
+ # Very slow startup.
+ return 20.0
+
+
+class ValgrindTool(BaseTool):
+ """Base abstract class for Valgrind tools."""
+
+ VG_DIR = '/data/local/tmp/valgrind'
+ VGLOGS_DIR = '/data/local/tmp/vglogs'
+
+ def __init__(self, adb):
+ self._adb = adb
+ # exactly 31 chars, SystemProperties::PROP_NAME_MAX
+ self._wrap_properties = ['wrap.com.google.android.apps.ch',
+ 'wrap.org.chromium.native_test']
+
+ def CopyFiles(self):
+ """Copies Valgrind tools to the device."""
+ self._adb.RunShellCommand('rm -r %s; mkdir %s' %
+ (ValgrindTool.VG_DIR, ValgrindTool.VG_DIR))
+ self._adb.RunShellCommand('rm -r %s; mkdir %s' %
+ (ValgrindTool.VGLOGS_DIR,
+ ValgrindTool.VGLOGS_DIR))
+ files = self.GetFilesForTool()
+ for f in files:
+ self._adb.PushIfNeeded(os.path.join(CHROME_DIR, f),
+ os.path.join(ValgrindTool.VG_DIR,
+ os.path.basename(f)))
+
+ def SetupEnvironment(self):
+ """Sets up device environment."""
+ self._adb.RunShellCommand('chmod 777 /data/local/tmp')
+ for prop in self._wrap_properties:
+ self._adb.RunShellCommand('setprop %s "logwrapper %s"' % (
+ prop, self.GetTestWrapper()))
+ SetChromeTimeoutScale(self._adb, self.GetTimeoutScale())
+
+ def CleanUpEnvironment(self):
+ """Cleans up device environment."""
+ for prop in self._wrap_properties:
+ self._adb.RunShellCommand('setprop %s ""' % (prop,))
+ SetChromeTimeoutScale(self._adb, None)
+
+ def GetFilesForTool(self):
+ """Returns a list of file names for the tool."""
+ raise NotImplementedError()
+
+ def NeedsDebugInfo(self):
+ """Whether this tool requires debug info.
+
+ Returns:
+ True if this tool can not work with stripped binaries.
+ """
+ return True
+
+
+class MemcheckTool(ValgrindTool):
+ """Memcheck tool."""
+
+ def __init__(self, adb):
+ super(MemcheckTool, self).__init__(adb)
+
+ def GetFilesForTool(self):
+ """Returns a list of file names for the tool."""
+ return ['tools/valgrind/android/vg-chrome-wrapper.sh',
+ 'tools/valgrind/memcheck/suppressions.txt',
+ 'tools/valgrind/memcheck/suppressions_android.txt']
+
+ def GetTestWrapper(self):
+ """Returns a string that is to be prepended to the test command line."""
+ return ValgrindTool.VG_DIR + '/' + 'vg-chrome-wrapper.sh'
+
+ def GetTimeoutScale(self):
+ """Returns a multiplier that should be applied to timeout values."""
+ return 30
+
+
+class TSanTool(ValgrindTool):
+ """ThreadSanitizer tool. See http://code.google.com/p/data-race-test ."""
+
+ def __init__(self, adb):
+ super(TSanTool, self).__init__(adb)
+
+ def GetFilesForTool(self):
+ """Returns a list of file names for the tool."""
+ return ['tools/valgrind/android/vg-chrome-wrapper-tsan.sh',
+ 'tools/valgrind/tsan/suppressions.txt',
+ 'tools/valgrind/tsan/suppressions_android.txt',
+ 'tools/valgrind/tsan/ignores.txt']
+
+ def GetTestWrapper(self):
+ """Returns a string that is to be prepended to the test command line."""
+ return ValgrindTool.VG_DIR + '/' + 'vg-chrome-wrapper-tsan.sh'
+
+ def GetTimeoutScale(self):
+ """Returns a multiplier that should be applied to timeout values."""
+ return 30.0
+
+
+TOOL_REGISTRY = {
+ 'memcheck': lambda x: MemcheckTool(x),
+ 'memcheck-renderer': lambda x: MemcheckTool(x),
+ 'tsan': lambda x: TSanTool(x),
+ 'tsan-renderer': lambda x: TSanTool(x),
+ 'asan': lambda x: AddressSanitizerTool(x),
+}
+
+
+def CreateTool(tool_name, adb):
+ """Creates a tool with the specified tool name.
+
+ Args:
+ tool_name: Name of the tool to create.
+ adb: ADB interface the tool will use.
+ Returns:
+ A tool for the specified tool_name.
+ """
+ if not tool_name:
+ return BaseTool()
+
+ ctor = TOOL_REGISTRY.get(tool_name)
+ if ctor:
+ return ctor(adb)
+ else:
+ print 'Unknown tool %s, available tools: %s' % (
+ tool_name, ', '.join(sorted(TOOL_REGISTRY.keys())))
+ sys.exit(1)
diff --git a/src/build/android/run_instrumentation_tests.py b/src/build/android/run_instrumentation_tests.py
new file mode 100755
index 0000000..23e613c
--- /dev/null
+++ b/src/build/android/run_instrumentation_tests.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""Runs both the Python and Java tests."""
+
+import optparse
+import os
+import sys
+import time
+
+from pylib import apk_info
+from pylib import buildbot_report
+from pylib import constants
+from pylib import flakiness_dashboard_results_uploader
+from pylib import ports
+from pylib import run_java_tests
+from pylib import run_python_tests
+from pylib import run_tests_helper
+from pylib import test_options_parser
+from pylib.test_result import TestResults
+
+
+def SummarizeResults(java_results, python_results, annotation, build_type):
+ """Summarize the results from the various test types.
+
+ Args:
+ java_results: a TestResults object with java test case results.
+ python_results: a TestResults object with python test case results.
+ annotation: the annotation used for these results.
+ build_type: 'Release' or 'Debug'.
+
+ Returns:
+ A tuple (all_results, summary_string, num_failing)
+ """
+ all_results = TestResults.FromTestResults([java_results, python_results])
+ summary_string = all_results.LogFull('Instrumentation', annotation,
+ build_type, [])
+ num_failing = (len(all_results.failed) + len(all_results.crashed) +
+ len(all_results.unknown))
+ return all_results, summary_string, num_failing
+
+
+def DispatchInstrumentationTests(options):
+ """Dispatches the Java and Python instrumentation tests, sharding if possible.
+
+ Uses the logging module to print the combined final results and
+ summary of the Java and Python tests. If the java_only option is set, only
+ the Java tests run. If the python_only option is set, only the python tests
+ run. If neither are set, run both Java and Python tests.
+
+ Args:
+ options: command-line options for running the Java and Python tests.
+
+ Returns:
+ An integer representing the number of failing tests.
+ """
+ if not options.keep_test_server_ports:
+ # Reset the test port allocation. It's important to do it before starting
+ # to dispatch any tests.
+ if not ports.ResetTestServerPortAllocation():
+ raise Exception('Failed to reset test server port.')
+
+ start_date = int(time.time() * 1000)
+ java_results = TestResults()
+ python_results = TestResults()
+
+ if options.run_java_tests:
+ java_results = run_java_tests.DispatchJavaTests(
+ options,
+ [apk_info.ApkInfo(options.test_apk_path, options.test_apk_jar_path)])
+ if options.run_python_tests:
+ python_results = run_python_tests.DispatchPythonTests(options)
+
+ all_results, summary_string, num_failing = SummarizeResults(
+ java_results, python_results, options.annotation, options.build_type)
+
+ if options.flakiness_dashboard_server:
+ flakiness_dashboard_results_uploader.Upload(
+ options.flakiness_dashboard_server, 'Chromium_Android_Instrumentation',
+ TestResults.FromTestResults([java_results, python_results]))
+
+ return num_failing
+
+
+def main(argv):
+ option_parser = optparse.OptionParser()
+ test_options_parser.AddInstrumentationOptions(option_parser)
+ options, args = option_parser.parse_args(argv)
+ test_options_parser.ValidateInstrumentationOptions(option_parser, options,
+ args)
+
+ run_tests_helper.SetLogLevel(options.verbose_count)
+ buildbot_report.PrintNamedStep(
+ 'Instrumentation tests: %s - %s' % (', '.join(options.annotation),
+ options.test_apk))
+ ret = 1
+ try:
+ ret = DispatchInstrumentationTests(options)
+ finally:
+ buildbot_report.PrintStepResultIfNeeded(options, ret)
+ return ret
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/src/build/android/run_monkey_test.py b/src/build/android/run_monkey_test.py
new file mode 100755
index 0000000..433b2bd
--- /dev/null
+++ b/src/build/android/run_monkey_test.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python
+# 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.
+
+"""Runs the Monkey tests on one or more devices."""
+import logging
+import optparse
+import random
+import sys
+import time
+
+from pylib import android_commands
+from pylib import python_test_base
+from pylib import python_test_sharder
+from pylib import test_options_parser
+from pylib import test_result
+
+
+class MonkeyTest(python_test_base.PythonTestBase):
+ def testMonkey(self):
+ start_ms = int(time.time()) * 1000
+
+ # Launch and wait for Chrome to launch.
+ self.adb.StartActivity(self.options.package_name,
+ self.options.activity_name,
+ wait_for_completion=True,
+ action='android.intent.action.MAIN',
+ force_stop=True)
+
+ # Chrome crashes are not always caught by Monkey test runner.
+ # Verify Chrome has the same PID before and after the test.
+ before_pids = self.adb.ExtractPid(self.options.package_name)
+
+ # Run the test.
+ output = ''
+ duration_ms = 0
+ if before_pids:
+ output = '\n'.join(self._LaunchMonkeyTest())
+ duration_ms = int(time.time()) * 1000 - start_ms
+ after_pids = self.adb.ExtractPid(self.options.package_name)
+
+ crashed = (not before_pids or not after_pids
+ or after_pids[0] != before_pids[0])
+ result = test_result.SingleTestResult(self.qualified_name, start_ms,
+ duration_ms, log=output)
+ results = test_result.TestResults()
+
+ if 'Monkey finished' in output and not crashed:
+ results.ok = [result]
+ else:
+ results.crashed = [result]
+
+ return results
+
+ def _LaunchMonkeyTest(self):
+ """Runs monkey test for a given package.
+
+ Looks at the following parameters in the options object provided
+ in class initializer:
+ package_name: Allowed package.
+ category: A list of allowed categories.
+ throttle: Delay between events (ms).
+ seed: Seed value for pseduo-random generator. Same seed value
+ generates the same sequence of events. Seed is randomized by
+ default.
+ event_count: Number of events to generate.
+ verbosity: Verbosity level [0-3].
+ extra_args: A string of other args to pass to the command verbatim.
+ """
+
+ category = self.options.category or []
+ seed = self.options.seed or random.randint(1, 100)
+ throttle = self.options.throttle or 100
+ event_count = self.options.event_count or 10000
+ verbosity = self.options.verbosity or 1
+ extra_args = self.options.extra_args or ''
+
+ timeout_ms = event_count * throttle * 1.5
+
+ cmd = ['monkey',
+ '-p %s' % self.options.package_name,
+ ' '.join(['-c %s' % c for c in category]),
+ '--throttle %d' % throttle,
+ '-s %d' % seed,
+ '-v ' * verbosity,
+ '--monitor-native-crashes',
+ '--kill-process-after-error',
+ extra_args,
+ '%d' % event_count]
+ return self.adb.RunShellCommand(' '.join(cmd), timeout_time=timeout_ms)
+
+
+def DispatchPythonTests(options):
+ """Dispatches the Monkey tests, sharding it if there multiple devices."""
+ logger = logging.getLogger()
+ logger.setLevel(logging.DEBUG)
+
+ available_tests = [MonkeyTest('testMonkey')]
+ attached_devices = android_commands.GetAttachedDevices()
+ if not attached_devices:
+ raise Exception('You have no devices attached or visible!')
+
+ # Actually run the tests.
+ logging.debug('Running monkey tests.')
+ available_tests *= len(attached_devices)
+ options.ensure_value('shard_retries', 1)
+ sharder = python_test_sharder.PythonTestSharder(
+ attached_devices, available_tests, options)
+ result = sharder.RunShardedTests()
+ result.LogFull('Monkey', 'Monkey', options.build_type, available_tests)
+ result.PrintAnnotation()
+
+
+def main():
+ desc = 'Run the Monkey tests on 1 or more devices.'
+ parser = optparse.OptionParser(description=desc)
+ test_options_parser.AddBuildTypeOption(parser)
+ parser.add_option('--package-name', help='Allowed package.')
+ parser.add_option('--activity-name',
+ default='com.google.android.apps.chrome.Main',
+ help='Name of the activity to start [default: %default].')
+ parser.add_option('--category',
+ help='A list of allowed categories [default: ""].')
+ parser.add_option('--throttle', default=100, type='int',
+ help='Delay between events (ms) [default: %default]. ')
+ parser.add_option('--seed', type='int',
+ help=('Seed value for pseduo-random generator. Same seed '
+ 'value generates the same sequence of events. Seed '
+ 'is randomized by default.'))
+ parser.add_option('--event-count', default=10000, type='int',
+ help='Number of events to generate [default: %default].')
+ parser.add_option('--verbosity', default=1, type='int',
+ help='Verbosity level [0-3] [default: %default].')
+ parser.add_option('--extra-args', default='',
+ help=('String of other args to pass to the command verbatim'
+ ' [default: "%default"].'))
+ (options, args) = parser.parse_args()
+
+ if args:
+ parser.print_help(sys.stderr)
+ parser.error('Unknown arguments: %s' % args)
+
+ if not options.package_name:
+ parser.print_help(sys.stderr)
+ parser.error('Missing package name')
+
+ if options.category:
+ options.category = options.category.split(',')
+
+ DispatchPythonTests(options)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/build/android/run_tests.py b/src/build/android/run_tests.py
new file mode 100755
index 0000000..f7459dd
--- /dev/null
+++ b/src/build/android/run_tests.py
@@ -0,0 +1,479 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""Runs all the native unit tests.
+
+1. Copy over test binary to /data/local on device.
+2. Resources: chrome/unit_tests requires resources (chrome.pak and en-US.pak)
+ to be deployed to the device. We use the device's $EXTERNAL_STORAGE as the
+ base dir (which maps to Context.getExternalFilesDir()).
+3. Environment:
+3.1. chrome/unit_tests requires (via chrome_paths.cc) a directory named:
+ $EXTERNAL_STORAGE + /chrome/test/data
+4. Run the binary in the device and stream the log to the host.
+4.1. Optionally, filter specific tests.
+4.2. If we're running a single test suite and we have multiple devices
+ connected, we'll shard the tests.
+5. Clean up the device.
+
+Suppressions:
+
+Individual tests in a test binary can be suppressed by listing it in
+the gtest_filter directory in a file of the same name as the test binary,
+one test per line. Here is an example:
+
+ $ cat gtest_filter/base_unittests_disabled
+ DataPackTest.Load
+ ReadOnlyFileUtilTest.ContentsEqual
+
+This file is generated by the tests running on devices. If running on emulator,
+additonal filter file which lists the tests only failed in emulator will be
+loaded. We don't care about the rare testcases which succeeded on emuatlor, but
+failed on device.
+"""
+
+import copy
+import fnmatch
+import logging
+import optparse
+import os
+import signal
+import subprocess
+import sys
+import time
+
+import emulator
+from pylib import android_commands
+from pylib import buildbot_report
+from pylib import cmd_helper
+from pylib import debug_info
+from pylib import ports
+from pylib import run_tests_helper
+from pylib import test_options_parser
+from pylib.base_test_sharder import BaseTestSharder
+from pylib.single_test_runner import SingleTestRunner
+
+
+_TEST_SUITES = ['base_unittests',
+ 'cc_unittests',
+ 'content_unittests',
+ 'gpu_unittests',
+ 'ipc_tests',
+ 'media_unittests',
+ 'net_unittests',
+ 'sql_unittests',
+ 'sync_unit_tests',
+ 'ui_unittests',
+ 'unit_tests',
+ 'webkit_compositor_bindings_unittests',
+ ]
+
+
+def FullyQualifiedTestSuites(exe, option_test_suite, build_type):
+ """Get a list of absolute paths to test suite targets.
+
+ Args:
+ exe: if True, use the executable-based test runner.
+ option_test_suite: the test_suite specified as an option.
+ build_type: 'Release' or 'Debug'.
+ """
+ test_suite_dir = os.path.join(cmd_helper.OutDirectory.get(), build_type)
+ if option_test_suite:
+ all_test_suites = [option_test_suite]
+ else:
+ all_test_suites = _TEST_SUITES
+
+ if exe:
+ qualified_test_suites = [os.path.join(test_suite_dir, t)
+ for t in all_test_suites]
+ else:
+ # out/(Debug|Release)/$SUITE_apk/$SUITE-debug.apk
+ qualified_test_suites = [os.path.join(test_suite_dir,
+ t + '_apk',
+ t + '-debug.apk')
+ for t in all_test_suites]
+ for t, q in zip(all_test_suites, qualified_test_suites):
+ if not os.path.exists(q):
+ raise Exception('Test suite %s not found in %s.\n'
+ 'Supported test suites:\n %s\n'
+ 'Ensure it has been built.\n' %
+ (t, q, _TEST_SUITES))
+ return qualified_test_suites
+
+
+class TimeProfile(object):
+ """Class for simple profiling of action, with logging of cost."""
+
+ def __init__(self, description):
+ self._description = description
+ self.Start()
+
+ def Start(self):
+ self._starttime = time.time()
+
+ def Stop(self):
+ """Stop profiling and dump a log."""
+ if self._starttime:
+ stoptime = time.time()
+ logging.info('%fsec to perform %s',
+ stoptime - self._starttime, self._description)
+ self._starttime = None
+
+
+class Xvfb(object):
+ """Class to start and stop Xvfb if relevant. Nop if not Linux."""
+
+ def __init__(self):
+ self._pid = 0
+
+ def _IsLinux(self):
+ """Return True if on Linux; else False."""
+ return sys.platform.startswith('linux')
+
+ def Start(self):
+ """Start Xvfb and set an appropriate DISPLAY environment. Linux only.
+
+ Copied from tools/code_coverage/coverage_posix.py
+ """
+ if not self._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
+
+
+class TestSharder(BaseTestSharder):
+ """Responsible for sharding the tests on the connected devices."""
+
+ def __init__(self, attached_devices, test_suite, gtest_filter,
+ test_arguments, timeout, cleanup_test_files, tool,
+ log_dump_name, fast_and_loose, build_type, in_webkit_checkout):
+ BaseTestSharder.__init__(self, attached_devices, build_type)
+ self.test_suite = test_suite
+ self.test_suite_basename = os.path.basename(test_suite)
+ self.gtest_filter = gtest_filter or ''
+ self.test_arguments = test_arguments
+ self.timeout = timeout
+ self.cleanup_test_files = cleanup_test_files
+ self.tool = tool
+ self.log_dump_name = log_dump_name
+ self.fast_and_loose = fast_and_loose
+ self.in_webkit_checkout = in_webkit_checkout
+ self.all_tests = []
+ if not self.gtest_filter:
+ # No filter has been specified, let's add all tests then.
+ self.all_tests, self.attached_devices = self._GetAllEnabledTests()
+ self.tests = self.all_tests
+
+ def _GetAllEnabledTests(self):
+ """Get all enabled tests and available devices.
+
+ Obtains a list of enabled tests from the test package on the device,
+ then filters it again using the diabled list on the host.
+
+ Returns:
+ Tuple of (all enabled tests, available devices).
+
+ Raises Exception if all devices failed.
+ """
+ # TODO(frankf): This method is doing too much in a non-systematic way.
+ # If the intention is to drop flaky devices, why not go through all devices
+ # instead of breaking on the first succesfull run?
+ available_devices = list(self.attached_devices)
+ while available_devices:
+ try:
+ return (self._GetTestsFromDevice(available_devices[-1]),
+ available_devices)
+ except Exception as e:
+ logging.warning('Failed obtaining tests from %s %s',
+ available_devices[-1], e)
+ available_devices.pop()
+
+ raise Exception('No device available to get the list of tests.')
+
+ def _GetTestsFromDevice(self, device):
+ logging.info('Obtaining tests from %s', device)
+ test_runner = SingleTestRunner(
+ device,
+ self.test_suite,
+ self.gtest_filter,
+ self.test_arguments,
+ self.timeout,
+ self.cleanup_test_files,
+ self.tool,
+ 0,
+ not not self.log_dump_name,
+ self.fast_and_loose,
+ self.build_type,
+ self.in_webkit_checkout)
+ # The executable/apk needs to be copied before we can call GetAllTests.
+ test_runner.test_package.StripAndCopyExecutable()
+ all_tests = test_runner.test_package.GetAllTests()
+ disabled_list = test_runner.GetDisabledTests()
+ # Only includes tests that do not have any match in the disabled list.
+ all_tests = filter(lambda t:
+ not any([fnmatch.fnmatch(t, disabled_pattern)
+ for disabled_pattern in disabled_list]),
+ all_tests)
+ return all_tests
+
+ def CreateShardedTestRunner(self, device, index):
+ """Creates a suite-specific test runner.
+
+ Args:
+ device: Device serial where this shard will run.
+ index: Index of this device in the pool.
+
+ Returns:
+ A SingleTestRunner object.
+ """
+ device_num = len(self.attached_devices)
+ shard_size = (len(self.tests) + device_num - 1) / device_num
+ shard_test_list = self.tests[index * shard_size : (index + 1) * shard_size]
+ test_filter = ':'.join(shard_test_list) + self.gtest_filter
+ return SingleTestRunner(
+ device,
+ self.test_suite,
+ test_filter,
+ self.test_arguments,
+ self.timeout,
+ self.cleanup_test_files, self.tool, index,
+ not not self.log_dump_name,
+ self.fast_and_loose,
+ self.build_type,
+ self.in_webkit_checkout)
+
+ def OnTestsCompleted(self, test_runners, test_results):
+ """Notifies that we completed the tests."""
+ test_results.LogFull('Unit test', os.path.basename(self.test_suite),
+ self.build_type, self.all_tests)
+ test_results.PrintAnnotation()
+ if self.log_dump_name:
+ # Zip all debug info outputs into a file named by log_dump_name.
+ debug_info.GTestDebugInfo.ZipAndCleanResults(
+ os.path.join(
+ cmd_helper.OutDirectory.get(), self.build_type,
+ 'debug_info_dumps'),
+ self.log_dump_name)
+
+
+def _RunATestSuite(options):
+ """Run a single test suite.
+
+ Helper for Dispatch() to allow stop/restart of the emulator across
+ test bundles. If using the emulator, we start it on entry and stop
+ it on exit.
+
+ Args:
+ options: options for running the tests.
+
+ Returns:
+ 0 if successful, number of failing tests otherwise.
+ """
+ step_name = os.path.basename(options.test_suite).replace('-debug.apk', '')
+ buildbot_report.PrintNamedStep(step_name)
+ attached_devices = []
+ buildbot_emulators = []
+
+ if options.use_emulator:
+ for n in range(options.emulator_count):
+ t = TimeProfile('Emulator launch %d' % n)
+ avd_name = None
+ if n > 0:
+ # Creates a temporary AVD for the extra emulators.
+ avd_name = 'run_tests_avd_%d' % n
+ buildbot_emulator = emulator.Emulator(avd_name, options.fast_and_loose)
+ buildbot_emulator.Launch(kill_all_emulators=n == 0)
+ t.Stop()
+ buildbot_emulators.append(buildbot_emulator)
+ attached_devices.append(buildbot_emulator.device)
+ # Wait for all emulators to boot completed.
+ map(lambda buildbot_emulator: buildbot_emulator.ConfirmLaunch(True),
+ buildbot_emulators)
+ elif options.test_device:
+ attached_devices = [options.test_device]
+ else:
+ attached_devices = android_commands.GetAttachedDevices()
+
+ if not attached_devices:
+ logging.critical('A device must be attached and online.')
+ buildbot_report.PrintError()
+ return 1
+
+ # Reset the test port allocation. It's important to do it before starting
+ # to dispatch any tests.
+ if not ports.ResetTestServerPortAllocation():
+ raise Exception('Failed to reset test server port.')
+
+ if options.gtest_filter:
+ logging.warning('Sharding is not possible with these configurations.')
+ attached_devices = [attached_devices[0]]
+
+ sharder = TestSharder(
+ attached_devices,
+ options.test_suite,
+ options.gtest_filter,
+ options.test_arguments,
+ options.timeout,
+ options.cleanup_test_files,
+ options.tool,
+ options.log_dump,
+ options.fast_and_loose,
+ options.build_type,
+ options.webkit)
+ test_results = sharder.RunShardedTests()
+
+ for buildbot_emulator in buildbot_emulators:
+ buildbot_emulator.Shutdown()
+
+ return len(test_results.failed)
+
+
+def Dispatch(options):
+ """Dispatches the tests, sharding if possible.
+
+ If options.use_emulator is True, all tests will be run in new emulator
+ instance.
+
+ Args:
+ options: options for running the tests.
+
+ Returns:
+ 0 if successful, number of failing tests otherwise.
+ """
+ if options.test_suite == 'help':
+ ListTestSuites()
+ return 0
+
+ if options.use_xvfb:
+ xvfb = Xvfb()
+ xvfb.Start()
+
+ all_test_suites = FullyQualifiedTestSuites(options.exe, options.test_suite,
+ options.build_type)
+ failures = 0
+ for suite in all_test_suites:
+ # Give each test suite its own copy of options.
+ test_options = copy.deepcopy(options)
+ test_options.test_suite = suite
+ failures += _RunATestSuite(test_options)
+
+ if options.use_xvfb:
+ xvfb.Stop()
+ return failures
+
+
+def ListTestSuites():
+ """Display a list of available test suites."""
+ print 'Available test suites are:'
+ for test_suite in _TEST_SUITES:
+ print test_suite
+
+
+def main(argv):
+ option_parser = optparse.OptionParser()
+ test_options_parser.AddTestRunnerOptions(option_parser, default_timeout=0)
+ option_parser.add_option('-s', '--suite', dest='test_suite',
+ help='Executable name of the test suite to run '
+ '(use -s help to list them)')
+ option_parser.add_option('--out-directory', dest='out_directory',
+ help='Path to the out/ directory, irrespective of '
+ 'the build type. Only for non-Chromium uses.')
+ option_parser.add_option('-d', '--device', dest='test_device',
+ help='Target device the test suite to run ')
+ option_parser.add_option('-f', '--gtest_filter', dest='gtest_filter',
+ help='gtest filter')
+ option_parser.add_option('-a', '--test_arguments', dest='test_arguments',
+ help='Additional arguments to pass to the test')
+ option_parser.add_option('-L', dest='log_dump',
+ help='file name of log dump, which will be put in '
+ 'subfolder debug_info_dumps under the same '
+ 'directory in where the test_suite exists.')
+ option_parser.add_option('-e', '--emulator', dest='use_emulator',
+ action='store_true',
+ help='Run tests in a new instance of emulator')
+ option_parser.add_option('-n', '--emulator_count',
+ type='int', default=1,
+ help='Number of emulators to launch for running the '
+ 'tests.')
+ option_parser.add_option('-x', '--xvfb', dest='use_xvfb',
+ action='store_true',
+ help='Use Xvfb around tests (ignored if not Linux)')
+ option_parser.add_option('--webkit', action='store_true',
+ help='Run the tests from a WebKit checkout.')
+ option_parser.add_option('--fast', '--fast_and_loose', dest='fast_and_loose',
+ action='store_true',
+ help='Go faster (but be less stable), '
+ 'for quick testing. Example: when tracking down '
+ 'tests that hang to add to the disabled list, '
+ 'there is no need to redeploy the test binary '
+ 'or data to the device again. '
+ 'Don\'t use on bots by default!')
+ option_parser.add_option('--repeat', dest='repeat', type='int',
+ default=2,
+ help='Repeat count on test timeout')
+ option_parser.add_option('--exit_code', action='store_true',
+ help='If set, the exit code will be total number '
+ 'of failures.')
+ option_parser.add_option('--exe', action='store_true',
+ help='If set, use the exe test runner instead of '
+ 'the APK.')
+
+ options, args = option_parser.parse_args(argv)
+
+ if len(args) > 1:
+ print 'Unknown argument:', args[1:]
+ option_parser.print_usage()
+ sys.exit(1)
+
+ run_tests_helper.SetLogLevel(options.verbose_count)
+
+ if options.out_directory:
+ cmd_helper.OutDirectory.set(options.out_directory)
+
+ if options.use_emulator:
+ emulator.DeleteAllTempAVDs()
+
+ failed_tests_count = Dispatch(options)
+
+ # Failures of individual test suites are communicated by printing a
+ # STEP_FAILURE message.
+ # Returning a success exit status also prevents the buildbot from incorrectly
+ # marking the last suite as failed if there were failures in other suites in
+ # the batch (this happens because the exit status is a sum of all failures
+ # from all suites, but the buildbot associates the exit status only with the
+ # most recent step).
+ if options.exit_code:
+ return failed_tests_count
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/src/build/android/screenshot.py b/src/build/android/screenshot.py
new file mode 100755
index 0000000..86607cc
--- /dev/null
+++ b/src/build/android/screenshot.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+
+# 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.
+
+"""Takes and saves a screenshot from an Android device.
+
+Usage: screenshot.py [-s SERIAL] [[-f] FILE]
+
+Options:
+ -s SERIAL connect to device with specified SERIAL
+ -f FILE write screenshot to FILE (default: Screenshot.png)
+"""
+
+from optparse import OptionParser
+import os
+import sys
+
+from pylib import android_commands
+
+
+def main():
+ # Parse options.
+ parser = OptionParser(usage='screenshot.py [-s SERIAL] [[-f] FILE]')
+ parser.add_option('-s', '--serial', dest='serial',
+ help='connect to device with specified SERIAL',
+ metavar='SERIAL', default=None)
+ parser.add_option('-f', '--file', dest='filename',
+ help='write screenshot to FILE (default: %default)',
+ metavar='FILE', default='Screenshot.png')
+ (options, args) = parser.parse_args()
+
+ if not options.serial and len(android_commands.GetAttachedDevices()) > 1:
+ parser.error('Multiple devices are attached. '
+ 'Please specify SERIAL with -s.')
+
+ if len(args) > 1:
+ parser.error('Too many positional arguments.')
+ filename = os.path.abspath(args[0] if args else options.filename)
+
+ # Grab screenshot and write to disk.
+ ac = android_commands.AndroidCommands(options.serial)
+ ac.TakeScreenshot(filename)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())