summaryrefslogtreecommitdiffstats
path: root/build
diff options
context:
space:
mode:
authormichaelbai@google.com <michaelbai@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2011-10-20 16:20:47 +0000
committermichaelbai@google.com <michaelbai@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2011-10-20 16:20:47 +0000
commit360ded8150e6868e6906e9c19dfcbf9823d2c115 (patch)
treea2d3f47b12ce72dcdcb7a133288b30304513b3b9 /build
parentcaf8deb860d5e3fa31a4620016303362a5edcb18 (diff)
downloadchromium_src-360ded8150e6868e6906e9c19dfcbf9823d2c115.zip
chromium_src-360ded8150e6868e6906e9c19dfcbf9823d2c115.tar.gz
chromium_src-360ded8150e6868e6906e9c19dfcbf9823d2c115.tar.bz2
Upstream: Test scripts for Android (phase 1)
These are the utilities BUG= TEST= Review URL: http://codereview.chromium.org/8356013 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@106507 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'build')
-rwxr-xr-xbuild/android/android_commands.py777
-rw-r--r--build/android/cmd_helper.py41
-rw-r--r--build/android/debug_info.py203
-rwxr-xr-xbuild/android/emulator.py84
4 files changed, 1105 insertions, 0 deletions
diff --git a/build/android/android_commands.py b/build/android/android_commands.py
new file mode 100755
index 0000000..622e5ef
--- /dev/null
+++ b/build/android/android_commands.py
@@ -0,0 +1,777 @@
+#!/usr/bin/python
+# Copyright (c) 2011 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.
+
+Usage:
+ python android_commands.py wait-for-pm
+"""
+
+import collections
+import datetime
+import logging
+import optparse
+import os
+import pexpect
+import re
+import subprocess
+import sys
+import tempfile
+import time
+
+# 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 cmd_helper
+import errors # is under ../../third_party/android/testrunner/errors.py
+from run_tests_helper import IsRunningAsBuildbot
+
+
+# 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~+~'
+
+# This only works for single core devices.
+SCALING_GOVERNOR = '/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor'
+DROP_CACHES = '/proc/sys/vm/drop_caches'
+
+# Java properties file
+LOCAL_PROPERTIES_PATH = '/data/local.prop'
+
+# Property in /data/local.prop that controls Java assertions.
+JAVA_ASSERT_PROPERTY = 'dalvik.vm.enableassertions'
+
+BOOT_COMPLETE_RE = re.compile(
+ re.escape('android.intent.action.MEDIA_MOUNTED path: /mnt/sdcard')
+ + '|' + re.escape('PowerManagerService: bootCompleted'))
+
+# Keycode "enum" suitable for passing to AndroidCommands.SendKey().
+KEYCODE_DPAD_RIGHT = 22
+KEYCODE_ENTER = 66
+KEYCODE_MENU = 82
+KEYCODE_BACK = 4
+
+
+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 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 _GetHostFileInfo(file_name):
+ """Returns a tuple containing size and modified UTC time for file_name."""
+ # The time accuracy on device is only to minute level, remove the second and
+ # microsecond from host results.
+ utc_time = datetime.datetime.utcfromtimestamp(os.path.getmtime(file_name))
+ time_delta = datetime.timedelta(seconds=utc_time.second,
+ microseconds=utc_time.microsecond)
+ return os.path.getsize(file_name), utc_time - time_delta
+
+
+def ListHostPathContents(path):
+ """Lists files in all subdirectories of |path|.
+
+ Args:
+ path: The path to list.
+
+ Returns:
+ A dict of {"name": (size, lastmod), ...}.
+ """
+ if os.path.isfile(path):
+ return {os.path.basename(path): _GetHostFileInfo(path)}
+ ret = {}
+ for root, dirs, files in os.walk(path):
+ for d in dirs:
+ if d.startswith('.'):
+ dirs.remove(d) # Prune the dir for subsequent iterations.
+ for f in files:
+ if f.startswith('.'):
+ continue
+ full_file_name = os.path.join(root, f)
+ file_name = os.path.relpath(full_file_name, path)
+ ret[file_name] = _GetHostFileInfo(full_file_name)
+ return ret
+
+
+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 GetLogTimestamp(log_line):
+ """Returns the timestamp of the given |log_line|."""
+ try:
+ return datetime.datetime.strptime(log_line[:18], '%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.
+ wait_for_pm: If true, issues an adb wait-for-device command.
+ """
+
+ def __init__(self, device=None, wait_for_pm=False):
+ self._adb = adb_interface.AdbInterface()
+ if device:
+ self._adb.SetTargetSerial(device)
+ if wait_for_pm:
+ self.WaitForDevicePm()
+ self._logcat = None
+ self._original_governor = None
+ self._pushed_files = []
+
+ def Adb(self):
+ """Returns our AdbInterface to avoid us wrapping all its methods."""
+ return self._adb
+
+ 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' % str(e))
+ retries -= 1
+ self.RestartShell()
+ raise last_err # Only reached after max retries, re-raise the last error.
+
+ def SynchronizeDateTime(self):
+ """Synchronize date/time between host and device."""
+ self._adb.SendShellCommand('date -u %f' % time.time())
+
+ 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:
+ self._adb.SendCommand('reboot')
+ else:
+ self.RestartShell()
+ self.WaitForDevicePm()
+ self.StartMonitoringLogcat(timeout=120)
+ self.WaitForLogMatch(BOOT_COMPLETE_RE)
+ self.UnlockDevice()
+
+ def Uninstall(self, package):
+ """Uninstalls the specified package from the device.
+
+ Args:
+ package: Name of the package to remove.
+ """
+ uninstall_command = 'uninstall %s' % package
+
+ logging.info('>>> $' + uninstall_command)
+ self._adb.SendCommand(uninstall_command, timeout_time=60)
+
+ def Install(self, package_file_path):
+ """Installs the specified package to the device.
+
+ Args:
+ package_file_path: Path to .apk file to install.
+ """
+
+ assert os.path.isfile(package_file_path)
+
+ install_command = 'install %s' % package_file_path
+
+ logging.info('>>> $' + install_command)
+ self._adb.SendCommand(install_command, timeout_time=2*60)
+
+ # 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=True):
+ """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 log_result:
+ logging.info('\n>>> '.join(result))
+ return result
+
+ def KillAll(self, process):
+ """Android version of killall, connected via adb.
+
+ Args:
+ process: name of the process to kill off
+
+ Returns:
+ the number of processess killed
+ """
+ pids = self.ExtractPid(process)
+ if pids:
+ self.RunShellCommand('kill ' + ' '.join(pids))
+ return len(pids)
+
+ def StartActivity(self, package, activity,
+ action='android.intent.action.VIEW', data=None,
+ extras=None, trace_file_name=None):
+ """Starts |package|'s activity on the device.
+
+ Args:
+ package: Name of package to start (e.g. 'com.android.chrome').
+ activity: Name of activity (e.g. '.Main' or 'com.android.chrome.Main').
+ data: Data string to pass to activity (e.g. 'http://www.example.com/').
+ extras: Dict of extras to pass to activity.
+ trace_file_name: If used, turns on and saves the trace to this file name.
+ """
+ cmd = 'am start -a %s -n %s/%s' % (action, package, activity)
+ if data:
+ cmd += ' -d "%s"' % data
+ if extras:
+ cmd += ' -e'
+ for key in extras:
+ cmd += ' %s %s' % (key, extras[key])
+ if trace_file_name:
+ cmd += ' -S -P ' + trace_file_name
+ self.RunShellCommand(cmd)
+
+ def EnableAdbRoot(self):
+ """Enable root on the device."""
+ self._adb.EnableAdbRoot()
+
+ 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.android.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/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 timestamp
+ and size.
+
+ All pushed files can be removed by calling RemovePushedFiles().
+ """
+ assert os.path.exists(local_path)
+ self._pushed_files.append(device_path)
+
+ # If the path contents are the same, there's nothing to do.
+ local_contents = ListHostPathContents(local_path)
+ device_contents = self.ListPathContents(device_path)
+ # Only compare the size and timestamp if only copying a file because
+ # the filename on device can be renamed.
+ if os.path.isfile(local_path):
+ assert len(local_contents) == 1
+ is_equal = local_contents.values() == device_contents.values()
+ else:
+ is_equal = local_contents == device_contents
+ if is_equal:
+ logging.info('%s is up-to-date. Skipping file push.' % device_path)
+ 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)
+ self._adb.SendCommand(push_command, timeout_time=30*60)
+
+ def GetFileContents(self, filename):
+ """Gets contents from the file specified by |filename|."""
+ return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' +
+ filename + '"; fi')
+
+ 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.RunShellCommand('date +%z')[0])
+
+ def SetupPerformanceTest(self):
+ """Sets up performance tests."""
+ # Disable CPU scaling to reduce noise in tests
+ if not self._original_governor:
+ self._original_governor = self.RunShellCommand('cat ' + SCALING_GOVERNOR)
+ self.RunShellCommand('echo performance > ' + SCALING_GOVERNOR)
+ self.DropRamCaches()
+
+ def TearDownPerformanceTest(self):
+ """Tears down performance tests."""
+ if self._original_governor:
+ self.RunShellCommand('echo %s > %s' % (self._original_governor[0],
+ SCALING_GOVERNOR))
+ self._original_governor = None
+
+ 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 DropRamCaches(self):
+ """Drops the filesystem ram caches for performance testing."""
+ self.RunShellCommand('echo 3 > ' + DROP_CACHES)
+
+ def StartMonitoringLogcat(self, clear=True, timeout=10, logfile=None,
+ filters=[]):
+ """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 = ['logcat', '-v', 'threadtime']
+ if filters:
+ args.extend(filters)
+ else:
+ args.append('*:v')
+
+ # 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, search_re):
+ """Blocks until a line containing |line_re| is logged or a timeout occurs.
+
+ Args:
+ search_re: The compiled re to search each line for.
+
+ Returns:
+ The re match object.
+ """
+ if not self._logcat:
+ self.StartMonitoringLogcat(clear=False)
+ logging.info('<<< Waiting for logcat:' + str(search_re.pattern))
+ t0 = time.time()
+ 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)
+ search_match = search_re.search(line)
+ if search_match:
+ return search_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, search_re.pattern))
+
+ 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 logcat -v threadtime %s' % ' '.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() == 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.
+ """
+ pids = []
+ for line in self.RunShellCommand('ps'):
+ data = line.split()
+ try:
+ if process_name in data[-1]: # name is in the last column
+ pids.append(data[1]) # PID is in the second column
+ 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.
+ """
+ # Field definitions.
+ # http://www.kernel.org/doc/Documentation/iostats.txt
+ device = 2
+ num_reads_issued_idx = 3
+ num_reads_merged_idx = 4
+ num_sectors_read_idx = 5
+ ms_spent_reading_idx = 6
+ num_writes_completed_idx = 7
+ num_writes_merged_idx = 8
+ num_sectors_written_idx = 9
+ ms_spent_writing_idx = 10
+ num_ios_in_progress_idx = 11
+ ms_spent_doing_io_idx = 12
+ ms_spent_doing_io_weighted_idx = 13
+
+ for line in self.RunShellCommand('cat /proc/diskstats'):
+ fields = line.split()
+ if fields[device] == 'mmcblk0':
+ return {
+ 'num_reads': int(fields[num_reads_issued_idx]),
+ 'num_writes': int(fields[num_writes_completed_idx]),
+ 'read_ms': int(fields[ms_spent_reading_idx]),
+ 'write_ms': int(fields[ms_spent_writing_idx]),
+ }
+ logging.warning('Could not find disk IO stats.')
+ return None
+
+ def GetMemoryUsage(self, package):
+ """Returns the memory usage for all processes whose name contains |pacakge|.
+
+ Args:
+ name: A string holding process name to lookup pid list for.
+
+ Returns:
+ Dict of {metric:usage_kb}, summed over all pids associated with |name|.
+ The metric keys retruned are: Size, Rss, Pss, Shared_Clean, Shared_Dirty,
+ Private_Clean, Private_Dirty, Referenced, Swap, KernelPageSize,
+ MMUPageSize.
+ """
+ usage_dict = collections.defaultdict(int)
+ pid_list = self.ExtractPid(package)
+ # We used to use the showmap command, but it is currently broken on
+ # stingray so it's easier to just parse /proc/<pid>/smaps directly.
+ memory_stat_re = re.compile('^(?P<key>\w+):\s+(?P<value>\d+) kB$')
+ for pid in pid_list:
+ for line in self.RunShellCommand('cat /proc/%s/smaps' % pid,
+ log_result=False):
+ match = re.match(memory_stat_re, line)
+ if match: usage_dict[match.group('key')] += int(match.group('value'))
+ if not usage_dict or not any(usage_dict.values()):
+ # Presumably the process died between ps and showmap.
+ logging.warning('Could not find memory usage for pid ' + str(pid))
+ return usage_dict
+
+ def UnlockDevice(self):
+ """Unlocks the screen of the device."""
+ # Make sure a menu button event will actually unlock the screen.
+ if IsRunningAsBuildbot():
+ assert self.RunShellCommand('getprop ro.test_harness')[0].strip() == '1'
+ # The following keyevent unlocks the screen if locked.
+ self.SendKeyEvent(KEYCODE_MENU)
+ # If the screen wasn't locked the previous command will bring up the menu,
+ # which this will dismiss. Otherwise this shouldn't change anything.
+ self.SendKeyEvent(KEYCODE_BACK)
+
+
+def main(argv):
+ option_parser = optparse.OptionParser()
+ option_parser.add_option('-w', '--wait_for_pm', action='store_true',
+ default=False, dest='wait_for_pm',
+ help='Waits for Device Package Manager to become available')
+ 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, args = option_parser.parse_args(argv)
+
+ commands = AndroidCommands(wait_for_pm=options.wait_for_pm)
+ if options.set_asserts != None:
+ if commands.SetJavaAssertsEnabled(options.set_asserts):
+ commands.Reboot(full_reboot=False)
+
+
+if __name__ == '__main__':
+ print os.path.abspath(os.path.dirname(__file__))
+ main(sys.argv)
diff --git a/build/android/cmd_helper.py b/build/android/cmd_helper.py
new file mode 100644
index 0000000..bedab94
--- /dev/null
+++ b/build/android/cmd_helper.py
@@ -0,0 +1,41 @@
+#!/usr/bin/python
+# Copyright (c) 2011 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 subprocess
+
+
+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.
+ """
+ logging.info(str(args) + ' ' + (cwd or ''))
+ p = subprocess.Popen(args=args, cwd=cwd)
+ return p.wait()
+
+
+def GetCmdOutput(args, cwd=None):
+ """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.
+ """
+ logging.info(str(args) + ' ' + (cwd or ''))
+ p = subprocess.Popen(args=args, cwd=cwd, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ if stderr:
+ logging.critical(stderr)
+ logging.info(stdout[:4096]) # Truncate output longer than 4k.
+ return stdout
diff --git a/build/android/debug_info.py b/build/android/debug_info.py
new file mode 100644
index 0000000..f4df79c
--- /dev/null
+++ b/build/android/debug_info.py
@@ -0,0 +1,203 @@
+#!/usr/bin/python
+# Copyright (c) 2011 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 get relate debug information for a gtest.
+
+ 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,
+ collect_new_crashes=True):
+ """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 = None
+ self.log_file_name = None
+ self.collect_new_crashes = collect_new_crashes
+ self.old_crash_files = self.ListCrashFiles()
+
+ def InitStorage(self):
+ """Initializes the storage in where we put the debug information."""
+ if self.has_storage:
+ return
+ self.has_storage = True
+ self.log_dir = tempfile.mkdtemp()
+ self.log_file_name = os.path.join(self.log_dir,
+ self._GeneratePrefixName() + '_log.txt')
+
+ def CleanupStorage(self):
+ """Cleans up the storage in where we put the debug information."""
+ if not self.has_storage:
+ return
+ self.has_storage = False
+ assert os.path.exists(self.log_dir)
+ shutil.rmtree(self.log_dir)
+ self.log_dir = None
+ self.log_file_name = None
+
+ def GetStoragePath(self):
+ """Returns the path in where we store the debug information."""
+ self.InitStorage()
+ return self.log_dir
+
+ 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)
+ return ''.join(c for c in self.gtest_filter if c in filename_chars)
+
+ 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) 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.InitStorage()
+ self.StopRecordingLog()
+ if clear:
+ cmd_helper.RunCmd(['adb', 'logcat', '-c'])
+ logging.info('Start dumping log to %s ...' % self.log_file_name)
+ command = 'adb logcat -v threadtime %s > %s' % (' '.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 True if successfully taking screen shot from device, otherwise
+ returns False.
+ """
+ self.InitStorage()
+ assert isinstance(identifier_mark, str)
+ shot_path = os.path.join(self.log_dir, ''.join([self._GeneratePrefixName(),
+ identifier_mark,
+ '_screenshot.png']))
+ screenshot_path = os.path.join(os.getenv('ANDROID_HOST_OUT'), 'bin',
+ 'screenshot2')
+ 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 True
+ logging.error('Failed to take screen shot from device %s' % self.device)
+ return False
+
+ def ListCrashFiles(self):
+ """Collects crash files from current specified device.
+
+ Returns:
+ A dict of crash files in format {"name": (size, lastmod), ...}.
+ """
+ if not self.collect_new_crashes:
+ return {}
+ return self.adb.ListPathContents(TOMBSTONE_DIR)
+
+ def ArchiveNewCrashFiles(self):
+ """Archives the crash files newly generated until calling this method."""
+ if not self.collect_new_crashes:
+ return
+ current_crash_files = self.ListCrashFiles()
+ files = [f for f in current_crash_files if f not in self.old_crash_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.GetStoragePath(), f))
+
+ @staticmethod
+ def ZipAndCleanResults(dest_dir, dump_file_name, debug_info_list):
+ """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.
+ debug_info_list: List of all debug info objects.
+ """
+ if not dest_dir or not dump_file_name or not debug_info_list:
+ return
+ cmd_helper.RunCmd(['mkdir', '-p', dest_dir])
+ log_basename = os.path.basename(dump_file_name)
+ log_file = os.path.join(dest_dir,
+ os.path.splitext(log_basename)[0] + '.zip')
+ logging.info('Zipping debug dumps into %s ...' % log_file)
+ for d in debug_info_list:
+ d.ArchiveNewCrashFiles()
+ # 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.
+ cmd_helper.RunCmd(['zip', '-q', '-r', log_file,
+ ' '.join([d.GetStoragePath() for d in debug_info_list])])
+ assert os.path.exists(log_file)
+ for debug_info in debug_info_list:
+ debug_info.CleanupStorage()
diff --git a/build/android/emulator.py b/build/android/emulator.py
new file mode 100755
index 0000000..a42564a
--- /dev/null
+++ b/build/android/emulator.py
@@ -0,0 +1,84 @@
+#!/usr/bin/python
+# Copyright (c) 2011 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 'buildbot' .
+"""
+
+import logging
+import os
+import subprocess
+import sys
+
+import android_commands
+
+
+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])
+ # The port must be an even number between 5554 and 5584.
+ for port in range(5554, 5585, 2):
+ 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 'buildbot'.
+
+ 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.
+ """
+
+ def __init__(self):
+ 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.popen = None
+ self.device = None
+
+ def Launch(self):
+ """Launches the emulator and waits for package manager to startup.
+
+ If fails, an exception will be raised.
+ """
+ port = _GetAvailablePort()
+ self.device = "emulator-%d" % port
+ self.popen = subprocess.Popen(args=[self.emulator, '-avd', 'buildbot',
+ '-port', str(port)])
+ # This will not return until device's package manager starts up or an
+ # exception is raised.
+ android_commands.AndroidCommands(self.device, True)
+
+ def Shutdown(self):
+ """Shuts down the process started by launch."""
+ self.popen.terminate()
+
+
+def main(argv):
+ Emulator().launch()
+
+
+if __name__ == '__main__':
+ main(sys.argv)