[android] Upstream / sync most of build/android and build/android/pylib.
These files have diverged overtime. We need to get them in sync in preparation for the instrumentation tests. The patches downstream have been entangled, so this is a bit big and contains a series of otherwise unrelated patches. However, it's probably safer to do this way (as it's guaranteed to be similar to downstream), than trying to split it in multiple patches. BUG= TEST=try -b android_test Review URL: https://chromiumcodereview.appspot.com/10689132 git-svn-id: http://src.chromium.org/svn/trunk/src/build@145872 4ff67af0-8c30-449e-8e8b-ad334ec8d88c
This commit is contained in:
Родитель
6879b2ca14
Коммит
5d7670d4de
|
@ -8,7 +8,6 @@
|
|||
|
||||
from pylib import android_commands
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
|
@ -20,7 +19,7 @@ def main(argv):
|
|||
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)
|
||||
options, _ = option_parser.parse_args(argv)
|
||||
|
||||
commands = android_commands.AndroidCommands()
|
||||
if options.set_asserts != None:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env python
|
||||
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
@ -17,8 +18,12 @@ import pexpect
|
|||
import random
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from pylib import constants
|
||||
|
||||
|
||||
class LighttpdServer(object):
|
||||
|
@ -45,7 +50,7 @@ class LighttpdServer(object):
|
|||
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 9000
|
||||
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'
|
||||
|
@ -61,13 +66,15 @@ class LighttpdServer(object):
|
|||
return os.path.join(self.temp_dir, name)
|
||||
|
||||
def _GetRandomPort(self):
|
||||
# Ports 8001-8004 are reserved for other test servers. Ensure we don't
|
||||
# collide with them.
|
||||
return random.randint(8005, 8999)
|
||||
# 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."""
|
||||
# Currently we use lighttpd as http sever in test.
|
||||
# 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
|
||||
|
@ -139,6 +146,19 @@ class LighttpdServer(object):
|
|||
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",
|
||||
|
|
|
@ -5,30 +5,29 @@
|
|||
"""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 io_stats_parser
|
||||
import logging
|
||||
import optparse
|
||||
import os
|
||||
import pexpect
|
||||
import re
|
||||
import shlex
|
||||
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
|
||||
import errors # is under ../../../third_party/android_testrunner/errors.py
|
||||
|
||||
|
||||
# Pattern to search for the next whole line of pexpect output and capture it
|
||||
|
@ -51,14 +50,21 @@ LOCAL_PROPERTIES_PATH = '/data/local.prop'
|
|||
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'))
|
||||
'android.intent.action.MEDIA_MOUNTED path: /\w+/sdcard\d?'
|
||||
+ '|' + 'PowerManagerService(\(\s+\d+\))?: bootCompleted')
|
||||
|
||||
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
|
||||
KEYCODE_BACK = 4
|
||||
|
||||
|
||||
def GetEmulators():
|
||||
|
@ -189,10 +195,11 @@ def _GetFilesFromRecursiveLsOutput(path, ls_output, re_file, utc_offset=None):
|
|||
return files
|
||||
|
||||
|
||||
def GetLogTimestamp(log_line):
|
||||
"""Returns the timestamp of the given |log_line|."""
|
||||
def GetLogTimestamp(log_line, year):
|
||||
"""Returns the timestamp of the given |log_line| in the given year."""
|
||||
try:
|
||||
return datetime.datetime.strptime(log_line[:18], '%m-%d %H:%M:%S.%f')
|
||||
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
|
||||
|
@ -204,23 +211,28 @@ class AndroidCommands(object):
|
|||
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):
|
||||
def __init__(self, device=None):
|
||||
self._adb = adb_interface.AdbInterface()
|
||||
if device:
|
||||
self._adb.SetTargetSerial(device)
|
||||
if wait_for_pm:
|
||||
self.WaitForDevicePm()
|
||||
# So many users require root that we just always do it. This could
|
||||
# be made more fine grain if necessary.
|
||||
self._adb.EnableAdbRoot()
|
||||
self._logcat = None
|
||||
self._original_governor = None
|
||||
self._pushed_files = []
|
||||
self._device_utc_offset = self.RunShellCommand('date +%z')[0]
|
||||
|
||||
def Adb(self):
|
||||
"""Returns our AdbInterface to avoid us wrapping all its methods."""
|
||||
return self._adb
|
||||
|
||||
def GetDeviceYear(self):
|
||||
"""Returns the year information of the date on device"""
|
||||
return self.RunShellCommand('date +%Y')[0]
|
||||
|
||||
def WaitForDevicePm(self):
|
||||
"""Blocks until the device's package manager is available.
|
||||
|
||||
|
@ -269,33 +281,43 @@ class AndroidCommands(object):
|
|||
self.RestartShell()
|
||||
self.WaitForDevicePm()
|
||||
self.StartMonitoringLogcat(timeout=120)
|
||||
self.WaitForLogMatch(BOOT_COMPLETE_RE)
|
||||
self.UnlockDevice()
|
||||
self.WaitForLogMatch(BOOT_COMPLETE_RE, None)
|
||||
|
||||
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)
|
||||
self._adb.SendCommand(uninstall_command, timeout_time=60)
|
||||
return 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.
|
||||
"""
|
||||
|
||||
Returns:
|
||||
A status string returned by adb 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)
|
||||
return self._adb.SendCommand(install_command, timeout_time=2*60)
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
@ -337,44 +359,63 @@ class AndroidCommands(object):
|
|||
self.RunShellCommand('kill ' + ' '.join(pids))
|
||||
return len(pids)
|
||||
|
||||
def StartActivity(self, package, activity,
|
||||
action='android.intent.action.VIEW', data=None,
|
||||
def StartActivity(self, package, activity, wait_for_completion=False,
|
||||
action='android.intent.action.VIEW',
|
||||
category=None, 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').
|
||||
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.
|
||||
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.
|
||||
"""
|
||||
cmd = 'am start -a %s -n %s/%s' % (action, package, activity)
|
||||
cmd = 'am start -a %s' % action
|
||||
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:
|
||||
cmd += ' -e'
|
||||
for key in extras:
|
||||
cmd += ' %s %s' % (key, extras[key])
|
||||
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 += ' -S -P ' + trace_file_name
|
||||
cmd += ' --start-profiler ' + 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
|
||||
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)
|
||||
|
@ -423,15 +464,16 @@ class AndroidCommands(object):
|
|||
push_command = 'push %s %s' % (local_path, device_path)
|
||||
logging.info('>>> $' + push_command)
|
||||
output = self._adb.SendCommand(push_command, timeout_time=30*60)
|
||||
assert output
|
||||
# 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]', output):
|
||||
if not re.search('^[0-9]', output.splitlines()[-1]):
|
||||
logging.critical('PUSH FAILED: ' + output)
|
||||
|
||||
def GetFileContents(self, filename):
|
||||
def GetFileContents(self, filename, log_result=True):
|
||||
"""Gets contents from the file specified by |filename|."""
|
||||
return self.RunShellCommand('if [ -f "' + filename + '" ]; then cat "' +
|
||||
filename + '"; fi')
|
||||
filename + '"; fi', log_result=log_result)
|
||||
|
||||
def SetFileContents(self, filename, contents):
|
||||
"""Writes |contents| to the file specified by |filename|."""
|
||||
|
@ -466,13 +508,14 @@ class AndroidCommands(object):
|
|||
'(?P<filename>[^\s]+)$')
|
||||
return _GetFilesFromRecursiveLsOutput(
|
||||
path, self.RunShellCommand('ls -lR %s' % path), re_file,
|
||||
self.RunShellCommand('date +%z')[0])
|
||||
self._device_utc_offset)
|
||||
|
||||
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._original_governor = self.GetFileContents(
|
||||
SCALING_GOVERNOR, log_result=False)
|
||||
self.RunShellCommand('echo performance > ' + SCALING_GOVERNOR)
|
||||
self.DropRamCaches()
|
||||
|
||||
|
@ -535,7 +578,10 @@ class AndroidCommands(object):
|
|||
"""
|
||||
if clear:
|
||||
self.RunShellCommand('logcat -c')
|
||||
args = ['logcat', '-v', 'threadtime']
|
||||
args = []
|
||||
if self._adb._target_arg:
|
||||
args += shlex.split(self._adb._target_arg)
|
||||
args += ['logcat', '-v', 'threadtime']
|
||||
if filters:
|
||||
args.extend(filters)
|
||||
else:
|
||||
|
@ -560,18 +606,26 @@ class AndroidCommands(object):
|
|||
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.
|
||||
def WaitForLogMatch(self, success_re, error_re, clear=False):
|
||||
"""Blocks until a matching line is logged or a timeout occurs.
|
||||
|
||||
Args:
|
||||
search_re: The compiled re to search each line for.
|
||||
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.
|
||||
The re match object if |success_re| is matched first or None if |error_re|
|
||||
is matched first.
|
||||
"""
|
||||
if not self._logcat:
|
||||
self.StartMonitoringLogcat(clear=False)
|
||||
logging.info('<<< Waiting for logcat:' + str(search_re.pattern))
|
||||
self.StartMonitoringLogcat(clear)
|
||||
logging.info('<<< Waiting for logcat:' + str(success_re.pattern))
|
||||
t0 = time.time()
|
||||
try:
|
||||
while True:
|
||||
|
@ -581,15 +635,19 @@ class AndroidCommands(object):
|
|||
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
|
||||
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, search_re.pattern))
|
||||
(self._logcat.timeout, success_re.pattern))
|
||||
|
||||
def StartRecordingLogcat(self, clear=True, filters=['*:v']):
|
||||
"""Starts recording logcat output to eventually be saved as a string.
|
||||
|
@ -603,7 +661,8 @@ class AndroidCommands(object):
|
|||
"""
|
||||
if clear:
|
||||
self._adb.SendCommand('logcat -c')
|
||||
logcat_command = 'adb logcat -v threadtime %s' % ' '.join(filters)
|
||||
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)
|
||||
|
||||
|
@ -672,13 +731,18 @@ class AndroidCommands(object):
|
|||
|
||||
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'):
|
||||
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
|
||||
pids.append(data[1]) # PID is in the second 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
|
||||
|
@ -690,67 +754,88 @@ class AndroidCommands(object):
|
|||
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':
|
||||
for line in self.GetFileContents('/proc/diskstats', log_result=False):
|
||||
stats = io_stats_parser.ParseIoStatsLine(line)
|
||||
if stats.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]),
|
||||
'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 GetMemoryUsage(self, package):
|
||||
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:
|
||||
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.
|
||||
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)
|
||||
# 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
|
||||
smaps = collections.defaultdict(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)
|
||||
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
|
||||
|
|
|
@ -2,20 +2,26 @@
|
|||
# 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
|
||||
from chrome_test_server_spawner import SpawningServer
|
||||
import constants
|
||||
from flag_changer import FlagChanger
|
||||
from forwarder import Forwarder
|
||||
import lighttpd_server
|
||||
import run_tests_helper
|
||||
import ports
|
||||
from valgrind_tools import CreateTool
|
||||
|
||||
FORWARDER_PATH = '/data/local/tmp/forwarder'
|
||||
# These ports must match up with the constants in net/test/test_server.cc
|
||||
TEST_SERVER_SPAWNER_PORT = 8001
|
||||
TEST_SERVER_PORT = 8002
|
||||
TEST_SYNC_SERVER_PORT = 8003
|
||||
|
||||
# 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 = '/data/local/tmp/net-test-server-ports'
|
||||
|
||||
|
||||
class BaseTestRunner(object):
|
||||
|
@ -25,7 +31,7 @@ class BaseTestRunner(object):
|
|||
the Run() method will set up tests, run them and tear them down.
|
||||
"""
|
||||
|
||||
def __init__(self, device, shard_index):
|
||||
def __init__(self, device, tool, shard_index):
|
||||
"""
|
||||
Args:
|
||||
device: Tests will run on the device of this ID.
|
||||
|
@ -33,6 +39,7 @@ class BaseTestRunner(object):
|
|||
"""
|
||||
self.device = device
|
||||
self.adb = android_commands.AndroidCommands(device=device)
|
||||
self.tool = CreateTool(tool, self.adb)
|
||||
# Synchronize date/time between host and device. Otherwise same file on
|
||||
# host and device may have different timestamp which may cause
|
||||
# AndroidCommands.PushIfNeeded failed, or a test which may compare timestamp
|
||||
|
@ -40,13 +47,25 @@ class BaseTestRunner(object):
|
|||
self.adb.SynchronizeDateTime()
|
||||
self._http_server = None
|
||||
self._forwarder = None
|
||||
self._spawning_server = None
|
||||
self._spawner_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
|
||||
|
||||
def _PushTestServerPortInfoToDevice(self):
|
||||
"""Pushes the latest port information to device."""
|
||||
self.adb.SetFileContents(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.
|
||||
|
@ -54,6 +73,8 @@ class BaseTestRunner(object):
|
|||
Returns:
|
||||
Test results returned from RunTests().
|
||||
"""
|
||||
if not self.HasTests():
|
||||
return True
|
||||
self.SetUp()
|
||||
try:
|
||||
return self.RunTests()
|
||||
|
@ -64,6 +85,10 @@ class BaseTestRunner(object):
|
|||
"""Called before tests run."""
|
||||
pass
|
||||
|
||||
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
|
||||
|
@ -83,42 +108,71 @@ class BaseTestRunner(object):
|
|||
"""
|
||||
for p in test_data_paths:
|
||||
self.adb.PushIfNeeded(
|
||||
os.path.join(run_tests_helper.CHROME_DIR, p),
|
||||
os.path.join(constants.CHROME_DIR, p),
|
||||
os.path.join(dest_dir, p))
|
||||
|
||||
def LaunchTestHttpServer(self, document_root, extra_config_contents=None):
|
||||
def LinkSdCardPathsToTempDir(self, paths):
|
||||
"""Link |paths| which are under sdcard to /data/local/tmp.
|
||||
|
||||
For example, the test data '/sdcard/my_data' will be linked to
|
||||
'/data/local/tmp/my_data'.
|
||||
|
||||
Args:
|
||||
paths: A list of files and directories relative to /sdcard.
|
||||
"""
|
||||
links = set()
|
||||
for path in paths:
|
||||
link_name = os.path.dirname(path)
|
||||
assert link_name, 'Linked paths must be in a subdir of /sdcard/.'
|
||||
link_name = link_name.split('/')[0]
|
||||
if link_name not in links:
|
||||
mapped_device_path = '/data/local/tmp/' + link_name
|
||||
# Unlink the mapped_device_path at first in case it was mapped to
|
||||
# a wrong path. Add option '-r' becuase the old path could be a dir.
|
||||
self.adb.RunShellCommand('rm -r %s' % mapped_device_path)
|
||||
self.adb.RunShellCommand(
|
||||
'ln -s /sdcard/%s %s' % (link_name, mapped_device_path))
|
||||
links.add(link_name)
|
||||
|
||||
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, extra_config_contents=extra_config_contents)
|
||||
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')
|
||||
# Root access needed to make the forwarder executable work.
|
||||
self.adb.EnableAdbRoot()
|
||||
self.StartForwarderForHttpServer()
|
||||
|
||||
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.
|
||||
"""
|
||||
# Sometimes the forwarder device port may be already used. We have to kill
|
||||
# all forwarder processes to ensure that the forwarder can be started since
|
||||
# currently we can not associate the specified port to related pid.
|
||||
self.adb.KillAll('forwarder')
|
||||
if self._forwarder:
|
||||
self._forwarder.Close()
|
||||
self._forwarder = Forwarder(
|
||||
self.adb, port_pairs, self.tool, '127.0.0.1')
|
||||
|
||||
def StartForwarderForHttpServer(self):
|
||||
"""Starts a forwarder for the HTTP server.
|
||||
|
||||
The forwarder forwards HTTP requests and responses between host and device.
|
||||
"""
|
||||
# Sometimes the forwarder device port may be already used. We have to kill
|
||||
# all forwarder processes to ensure that the forwarder can be started since
|
||||
# currently we can not associate the specified port to related pid.
|
||||
# TODO(yfriedman/wangxianzhu): This doesn't work as most of the time the
|
||||
# port is in use but the forwarder is already dead. Killing all forwarders
|
||||
# is overly destructive and breaks other tests which make use of forwarders.
|
||||
# if IsDevicePortUsed(self.adb, self._forwarder_device_port):
|
||||
# self.adb.KillAll('forwarder')
|
||||
self._forwarder = run_tests_helper.ForwardDevicePorts(
|
||||
self.adb, [(self._forwarder_device_port, self._http_server.port)])
|
||||
self.StartForwarder([(self._forwarder_device_port, self._http_server.port)])
|
||||
|
||||
def RestartHttpServerForwarderIfNecessary(self):
|
||||
"""Restarts the forwarder if it's not open."""
|
||||
|
@ -126,7 +180,7 @@ class BaseTestRunner(object):
|
|||
# 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 run_tests_helper.IsDevicePortUsed(self.adb,
|
||||
if not ports.IsDevicePortUsed(self.adb,
|
||||
self._forwarder_device_port):
|
||||
self.StartForwarderForHttpServer()
|
||||
|
||||
|
@ -140,9 +194,9 @@ class BaseTestRunner(object):
|
|||
# (if it exists)
|
||||
self.adb.KillAll('forwarder')
|
||||
if self._forwarder:
|
||||
self._forwarder.kill()
|
||||
self._forwarder.Close()
|
||||
if self._spawner_forwarder:
|
||||
self._spawner_forwarder.kill()
|
||||
self._spawner_forwarder.Close()
|
||||
if self._http_server:
|
||||
self._http_server.ShutdownHttpServer()
|
||||
if self._spawning_server:
|
||||
|
@ -151,12 +205,32 @@ class BaseTestRunner(object):
|
|||
|
||||
def LaunchChromeTestServerSpawner(self):
|
||||
"""Launches test server spawner."""
|
||||
self._spawning_server = SpawningServer(TEST_SERVER_SPAWNER_PORT,
|
||||
TEST_SERVER_PORT)
|
||||
self._spawning_server.Start()
|
||||
# TODO(yfriedman): Ideally we'll only try to start up a port forwarder if
|
||||
# there isn't one already running but for now we just get an error message
|
||||
# and the existing forwarder still works.
|
||||
self._spawner_forwarder = run_tests_helper.ForwardDevicePorts(
|
||||
self.adb, [(TEST_SERVER_SPAWNER_PORT, TEST_SERVER_SPAWNER_PORT),
|
||||
(TEST_SERVER_PORT, TEST_SERVER_PORT)])
|
||||
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._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 = Forwarder(
|
||||
self.adb,
|
||||
[(self.test_server_spawner_port, self.test_server_spawner_port)],
|
||||
self.tool, '127.0.0.1')
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import logging
|
||||
import multiprocessing
|
||||
|
||||
from test_result import *
|
||||
from test_result import TestResults
|
||||
|
||||
|
||||
def _ShardedTestRunnable(test):
|
||||
|
@ -76,12 +76,12 @@ class BaseTestSharder(object):
|
|||
logging.warning('*' * 80)
|
||||
final_results = TestResults()
|
||||
for retry in xrange(self.retries):
|
||||
logging.warning('Try %d of %d' % (retry + 1, self.retries))
|
||||
logging.warning('Try %d of %d', retry + 1, self.retries)
|
||||
self.SetupSharding(self.tests)
|
||||
test_runners = []
|
||||
for index, device in enumerate(self.attached_devices):
|
||||
logging.warning('*' * 80)
|
||||
logging.warning('Creating shard %d for %s' % (index, device))
|
||||
logging.warning('Creating shard %d for %s', index, device)
|
||||
logging.warning('*' * 80)
|
||||
test_runner = self.CreateShardedTestRunner(device, index)
|
||||
test_runners += [test_runner]
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# 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 PrintError():
|
||||
"""Marks the current step as failed."""
|
||||
print '@@@STEP_FAILURE@@@'
|
|
@ -0,0 +1,36 @@
|
|||
# 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_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'..', '..', '..'))
|
||||
|
||||
# Ports arrangement for various test servers used in Clank.
|
||||
# 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'
|
|
@ -134,8 +134,8 @@ class GTestDebugInfo(object):
|
|||
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.
|
||||
Returns the file name on the host of the screenshot if successful,
|
||||
None otherwise.
|
||||
"""
|
||||
self.InitStorage()
|
||||
assert isinstance(identifier_mark, str)
|
||||
|
@ -148,9 +148,9 @@ class GTestDebugInfo(object):
|
|||
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
|
||||
return shot_path
|
||||
logging.error('Failed to take screen shot from device %s' % self.device)
|
||||
return False
|
||||
return None
|
||||
|
||||
def ListCrashFiles(self):
|
||||
"""Collects crash files from current specified device.
|
||||
|
@ -167,11 +167,19 @@ class GTestDebugInfo(object):
|
|||
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))
|
||||
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]:
|
||||
# Tomestones 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.GetStoragePath(), f))
|
||||
|
||||
@staticmethod
|
||||
def ZipAndCleanResults(dest_dir, dump_file_name, debug_info_list):
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
# 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 = '/data/local/tmp/fake_dns'
|
||||
|
||||
def __init__(self, adb):
|
||||
"""
|
||||
Args:
|
||||
adb: the AndroidCommands to use.
|
||||
"""
|
||||
self._adb = adb
|
||||
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', 'Release', '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')
|
|
@ -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 logging
|
||||
import os
|
||||
import pexpect
|
||||
import re
|
||||
import sys
|
||||
|
||||
import android_commands
|
||||
from constants import CHROME_DIR
|
||||
|
||||
class Forwarder(object):
|
||||
"""Class to manage port forwards from the device to the host."""
|
||||
|
||||
_FORWARDER_PATH = '/data/local/tmp/forwarder'
|
||||
_TIMEOUT_SECS = 30
|
||||
|
||||
def __init__(self, adb, port_pairs, tool, host_name):
|
||||
"""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.
|
||||
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: Optional. Address to forward to, must be addressable from the
|
||||
host machine. Usually this is omitted and loopback is used.
|
||||
|
||||
Raises:
|
||||
Exception on failure to forward the port.
|
||||
"""
|
||||
self._adb = adb
|
||||
self._host_to_device_port_map = dict()
|
||||
self._process = None
|
||||
adb.PushIfNeeded(
|
||||
os.path.join(CHROME_DIR, 'out', 'Release', 'forwarder'),
|
||||
Forwarder._FORWARDER_PATH)
|
||||
forward_string = ['%d:%d:%s' %
|
||||
(device, host, host_name) for device, host in port_pairs]
|
||||
|
||||
# Kill off any existing forwarders on conflicting non-dynamically allocated
|
||||
# ports.
|
||||
for device_port, _ in port_pairs:
|
||||
if device_port != 0:
|
||||
self._KillForwardersUsingDevicePort(device_port)
|
||||
|
||||
logging.info('Forwarding ports: %s' % (forward_string))
|
||||
process = pexpect.spawn(
|
||||
'adb', ['-s', adb._adb.GetSerialNumber(),
|
||||
'shell', '%s %s -D %s' % (
|
||||
tool.GetUtilWrapper(), Forwarder._FORWARDER_PATH,
|
||||
' '.join(forward_string))])
|
||||
|
||||
# Read the output of the command to determine which device ports where
|
||||
# forwarded to which host ports (necessary if
|
||||
success_re = re.compile('Forwarding device port (\d+) to host (\d+):')
|
||||
failure_re = re.compile('Couldn\'t start forwarder server for port spec: '
|
||||
'(\d+):(\d+)')
|
||||
for pair in port_pairs:
|
||||
index = process.expect([success_re, failure_re, pexpect.EOF,
|
||||
pexpect.TIMEOUT],
|
||||
Forwarder._TIMEOUT_SECS)
|
||||
if index == 0:
|
||||
# Success
|
||||
device_port = int(process.match.group(1))
|
||||
host_port = int(process.match.group(2))
|
||||
self._host_to_device_port_map[host_port] = device_port
|
||||
logging.info("Forwarding device port: %d to host port: %d." %
|
||||
(device_port, host_port))
|
||||
elif index == 1:
|
||||
# Failure
|
||||
device_port = int(process.match.group(1))
|
||||
host_port = int(process.match.group(2))
|
||||
process.close()
|
||||
raise Exception('Failed to forward port %d to %d' % (device_port,
|
||||
host_port))
|
||||
elif index == 2:
|
||||
logging.error(process.before)
|
||||
process.close()
|
||||
raise Exception('Unexpected EOF while trying to forward ports %s' %
|
||||
port_pairs)
|
||||
elif index == 3:
|
||||
logging.error(process.before)
|
||||
process.close()
|
||||
raise Exception('Timeout while trying to forward ports %s' % port_pairs)
|
||||
|
||||
self._process = process
|
||||
|
||||
def _KillForwardersUsingDevicePort(self, device_port):
|
||||
"""Check if the device port is in use and if it is try to terminate the
|
||||
forwarder process (if any) that may already be forwarding it"""
|
||||
processes = self._adb.ProcessesUsingDevicePort(device_port)
|
||||
for pid, name in processes:
|
||||
if name == 'forwarder':
|
||||
logging.warning(
|
||||
'Killing forwarder process with pid %d using device_port %d' % (
|
||||
pid, device_port))
|
||||
self._adb.RunShellCommand('kill %d' % pid)
|
||||
else:
|
||||
logging.error(
|
||||
'Not killing process with pid %d (%s) using device_port %d' % (
|
||||
pid, name, device_port))
|
||||
|
||||
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._process:
|
||||
self._process.close()
|
||||
self._process = None
|
|
@ -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:]])
|
|
@ -5,6 +5,12 @@
|
|||
import re
|
||||
|
||||
|
||||
# Valid values of result type.
|
||||
RESULT_TYPES = {'unimportant': 'RESULT ',
|
||||
'default': '*RESULT ',
|
||||
'informational': ''}
|
||||
|
||||
|
||||
def _EscapePerfResult(s):
|
||||
"""Escapes |s| for use in a perf result."""
|
||||
# Colons (:) and equal signs (=) are not allowed, and we chose an arbitrary
|
||||
|
@ -12,7 +18,7 @@ def _EscapePerfResult(s):
|
|||
return re.sub(':|=', '_', s[:40])
|
||||
|
||||
|
||||
def PrintPerfResult(measurement, trace, values, units, important=True,
|
||||
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.
|
||||
|
||||
|
@ -24,13 +30,16 @@ def PrintPerfResult(measurement, trace, values, units, important=True,
|
|||
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".
|
||||
important: If True, the output line will be specially marked, to notify the
|
||||
post-processor.
|
||||
result_type: A tri-state that accepts values of ['unimportant', 'default',
|
||||
'informational']. 'unimportant' prints RESULT, 'default' prints *RESULT
|
||||
and 'informational' prints nothing.
|
||||
print_to_stdout: If True, prints the output in stdout instead of returning
|
||||
the output to caller.
|
||||
|
||||
Returns:
|
||||
String of the formated perf result.
|
||||
"""
|
||||
important_marker = '*' if important else ''
|
||||
assert result_type in RESULT_TYPES, 'result type: %s is invalid' % result_type
|
||||
|
||||
assert isinstance(values, list)
|
||||
assert len(values)
|
||||
|
@ -45,12 +54,18 @@ def PrintPerfResult(measurement, trace, values, units, important=True,
|
|||
else:
|
||||
value = values[0]
|
||||
|
||||
output = '%sRESULT %s: %s= %s %s' % (important_marker,
|
||||
_EscapePerfResult(measurement),
|
||||
_EscapePerfResult(trace),
|
||||
value, units)
|
||||
trace_name = _EscapePerfResult(trace)
|
||||
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)
|
||||
if avg:
|
||||
output += '\nAvg %s: %d%s' % (measurement, avg, units)
|
||||
output += '\nAvg %s: %f%s' % (measurement, avg, units)
|
||||
if print_to_stdout:
|
||||
print output
|
||||
return output
|
||||
|
|
|
@ -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.
|
||||
|
||||
"""Functions that deals 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 relates 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():
|
||||
"""Reset 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():
|
||||
"""Allocate 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
|
||||
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.
|
||||
assert os.path.exists(constants.TEST_SERVER_PORT_FILE)
|
||||
with open(constants.TEST_SERVER_PORT_FILE, 'r+') as fp:
|
||||
port = int(fp.read())
|
||||
while IsHostPortUsed(port):
|
||||
port += 1
|
||||
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()
|
||||
logging.info('Allocate port %d for test server.', port)
|
||||
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.
|
||||
re_port = re.compile(port_info, re.MULTILINE)
|
||||
if re_port.findall(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()
|
||||
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')
|
|
@ -2,49 +2,21 @@
|
|||
# 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 test runners."""
|
||||
"""Helper functions common to native, java and python test runners."""
|
||||
|
||||
import contextlib
|
||||
import fcntl
|
||||
import httplib
|
||||
import logging
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# TODO(michaelbai): Move constant definitions like below to a common file.
|
||||
FORWARDER_PATH = '/data/local/tmp/forwarder'
|
||||
|
||||
CHROME_DIR = os.path.abspath(os.path.join(sys.path[0], '..', '..'))
|
||||
|
||||
|
||||
def IsRunningAsBuildbot():
|
||||
"""Returns True if we are currently running on buildbot; False otherwise."""
|
||||
return bool(os.getenv('BUILDBOT_BUILDERNAME'))
|
||||
|
||||
|
||||
def ReportBuildbotLink(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.
|
||||
"""
|
||||
if IsRunningAsBuildbot():
|
||||
print '@@@STEP_LINK@%s@%s@@@' % (label, url)
|
||||
|
||||
|
||||
def ReportBuildbotMsg(msg):
|
||||
"""Appends |msg| to the current buildbot step text.
|
||||
|
||||
Args:
|
||||
msg: String to be appended.
|
||||
"""
|
||||
if IsRunningAsBuildbot():
|
||||
print '@@@STEP_TEXT@%s@@@' % msg
|
||||
|
||||
def ReportBuildbotError():
|
||||
"""Marks the current step as failed."""
|
||||
if IsRunningAsBuildbot():
|
||||
print '@@@STEP_FAILURE@@@'
|
||||
import cmd_helper
|
||||
|
||||
|
||||
def GetExpectations(file_name):
|
||||
|
@ -63,71 +35,3 @@ def SetLogLevel(verbose_count):
|
|||
elif verbose_count >= 2:
|
||||
log_level = logging.DEBUG
|
||||
logging.getLogger().setLevel(log_level)
|
||||
|
||||
|
||||
def CreateTestRunnerOptionParser(usage=None, default_timeout=60):
|
||||
"""Returns a new OptionParser with arguments applicable to all tests."""
|
||||
option_parser = optparse.OptionParser(usage=usage)
|
||||
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',
|
||||
default=False)
|
||||
option_parser.add_option('-v',
|
||||
'--verbose',
|
||||
dest='verbose_count',
|
||||
default=0,
|
||||
action='count',
|
||||
help='Verbose level (multiple times for more)')
|
||||
option_parser.add_option('--tool',
|
||||
dest='tool',
|
||||
help='Run the test under a tool '
|
||||
'(use --tool help to list them)')
|
||||
return option_parser
|
||||
|
||||
|
||||
def ForwardDevicePorts(adb, ports, host_name='127.0.0.1'):
|
||||
"""Forwards a TCP port on the device back to the host.
|
||||
|
||||
Works like adb forward, but in reverse.
|
||||
|
||||
Args:
|
||||
adb: Instance of AndroidCommands for talking to the device.
|
||||
ports: A list of tuples (device_port, host_port) to forward.
|
||||
host_name: Optional. Address to forward to, must be addressable from the
|
||||
host machine. Usually this is omitted and loopback is used.
|
||||
|
||||
Returns:
|
||||
subprocess instance connected to the forwarder process on the device.
|
||||
"""
|
||||
adb.PushIfNeeded(
|
||||
os.path.join(CHROME_DIR, 'out', 'Release', 'forwarder'), FORWARDER_PATH)
|
||||
forward_string = ['%d:%d:%s' %
|
||||
(device, host, host_name) for device, host in ports]
|
||||
logging.info("Forwarding ports: %s" % (forward_string))
|
||||
|
||||
return subprocess.Popen(
|
||||
['adb', '-s', adb._adb.GetSerialNumber(),
|
||||
'shell', '%s -D %s' % (FORWARDER_PATH, ' '.join(forward_string))])
|
||||
|
||||
|
||||
def IsDevicePortUsed(adb, device_port):
|
||||
"""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.
|
||||
|
||||
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')
|
||||
for single_connect in netstat_results:
|
||||
# Column 3 is the local address which we want to check with.
|
||||
if single_connect.split()[3] == base_url:
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
# 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
|
||||
|
||||
from base_test_runner import BaseTestRunner
|
||||
import debug_info
|
||||
import constants
|
||||
import run_tests_helper
|
||||
from test_package_apk import TestPackageApk
|
||||
from test_package_executable import TestPackageExecutable
|
||||
|
@ -32,10 +34,10 @@ class SingleTestRunner(BaseTestRunner):
|
|||
"""
|
||||
|
||||
def __init__(self, device, test_suite, gtest_filter, test_arguments, timeout,
|
||||
rebaseline, performance_test, cleanup_test_files, tool,
|
||||
rebaseline, performance_test, cleanup_test_files, tool_name,
|
||||
shard_index, dump_debug_info=False,
|
||||
fast_and_loose=False):
|
||||
BaseTestRunner.__init__(self, device, shard_index)
|
||||
BaseTestRunner.__init__(self, device, tool_name, shard_index)
|
||||
self._running_on_emulator = self.device.startswith('emulator')
|
||||
self._gtest_filter = gtest_filter
|
||||
self._test_arguments = test_arguments
|
||||
|
@ -48,22 +50,14 @@ class SingleTestRunner(BaseTestRunner):
|
|||
self.fast_and_loose = fast_and_loose
|
||||
|
||||
if os.path.splitext(test_suite)[1] == '.apk':
|
||||
self.test_package = TestPackageApk(
|
||||
self.adb, device,
|
||||
self.test_package = TestPackageApk(self.adb, device,
|
||||
test_suite, timeout, rebaseline, performance_test, cleanup_test_files,
|
||||
tool, self.dump_debug_info)
|
||||
self.tool, self.dump_debug_info)
|
||||
else:
|
||||
self.test_package = TestPackageExecutable(
|
||||
self.adb, device,
|
||||
test_suite, timeout, rebaseline, performance_test, cleanup_test_files,
|
||||
tool, self.dump_debug_info)
|
||||
|
||||
def _GetHttpServerDocumentRootForTestSuite(self):
|
||||
"""Returns the document root needed by the test suite."""
|
||||
if self.test_package.test_suite_basename == 'page_cycler_tests':
|
||||
return os.path.join(run_tests_helper.CHROME_DIR, 'data', 'page_cycler')
|
||||
return None
|
||||
|
||||
self.tool, self.dump_debug_info)
|
||||
|
||||
def _TestSuiteRequiresMockTestServer(self):
|
||||
"""Returns True if the test suite requires mock test server."""
|
||||
|
@ -147,23 +141,55 @@ class SingleTestRunner(BaseTestRunner):
|
|||
if self.test_package.test_suite_basename in ['base_unittests',
|
||||
'sql_unittests',
|
||||
'unit_tests']:
|
||||
return [
|
||||
test_files = [
|
||||
'base/data/json/bom_feff.json',
|
||||
'base/data/file_util_unittest',
|
||||
'net/data/cache_tests/insert_load1',
|
||||
'net/data/cache_tests/dirty_entry5',
|
||||
'net/data/ssl/certificates/',
|
||||
'ui/base/test/data/data_pack_unittest',
|
||||
'chrome/test/data/bookmarks/History_with_empty_starred',
|
||||
'chrome/test/data/bookmarks/History_with_starred',
|
||||
'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/',
|
||||
]
|
||||
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 == 'net_unittests':
|
||||
return [
|
||||
'net/data/cache_tests',
|
||||
|
@ -171,6 +197,8 @@ class SingleTestRunner(BaseTestRunner):
|
|||
'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 [
|
||||
|
@ -178,8 +206,8 @@ class SingleTestRunner(BaseTestRunner):
|
|||
'chrome/test/data/json2.js',
|
||||
'chrome/test/data/sunspider',
|
||||
'chrome/test/data/v8_benchmark',
|
||||
'chrome/test/ui/sunspider_uitest.js',
|
||||
'chrome/test/ui/v8_benchmark_uitest.js',
|
||||
'chrome/test/perf/sunspider_uitest.js',
|
||||
'chrome/test/perf/v8_benchmark_uitest.js',
|
||||
]
|
||||
elif self.test_package.test_suite_basename == 'page_cycler_tests':
|
||||
data = [
|
||||
|
@ -196,6 +224,8 @@ class SingleTestRunner(BaseTestRunner):
|
|||
]
|
||||
elif self.test_package.test_suite_basename == 'content_unittests':
|
||||
return [
|
||||
'chrome/test/gpu/webgl_conformance_test_expectations.txt',
|
||||
'net/data/ssl/certificates/',
|
||||
'webkit/data/dom_storage/webcore_test_database.localstorage',
|
||||
]
|
||||
return []
|
||||
|
@ -206,9 +236,6 @@ class SingleTestRunner(BaseTestRunner):
|
|||
Sometimes one test may need to run some helper tools first in order to
|
||||
successfully complete the test.
|
||||
"""
|
||||
document_root = self._GetHttpServerDocumentRootForTestSuite()
|
||||
if document_root:
|
||||
self.LaunchTestHttpServer(document_root)
|
||||
if self._TestSuiteRequiresMockTestServer():
|
||||
self.LaunchChromeTestServerSpawner()
|
||||
|
||||
|
@ -216,28 +243,19 @@ class SingleTestRunner(BaseTestRunner):
|
|||
"""Strips and copies the required data files for the test suite."""
|
||||
self.test_package.StripAndCopyExecutable()
|
||||
self.test_package.PushDataAndPakFiles()
|
||||
self.test_package.tool.CopyFiles()
|
||||
self.tool.CopyFiles()
|
||||
test_data = self.GetDataFilesForTestSuite()
|
||||
if test_data and not self.fast_and_loose:
|
||||
if self.test_package.test_suite_basename == 'page_cycler_tests':
|
||||
# Since the test data for page cycler are huge (around 200M), we use
|
||||
# sdcard to store the data and create symbol links to map them to
|
||||
# data/local/tmp/ later.
|
||||
self.CopyTestData(test_data, '/sdcard/')
|
||||
for p in [os.path.dirname(d) for d in test_data if os.path.isdir(d)]:
|
||||
mapped_device_path = '/data/local/tmp/' + p
|
||||
# Unlink the mapped_device_path at first in case it was mapped to
|
||||
# a wrong path. Add option '-r' becuase the old path could be a dir.
|
||||
self.adb.RunShellCommand('rm -r %s' % mapped_device_path)
|
||||
self.adb.RunShellCommand(
|
||||
'ln -s /sdcard/%s %s' % (p, mapped_device_path))
|
||||
else:
|
||||
self.CopyTestData(test_data, '/data/local/tmp/')
|
||||
# Due to the large size of certain test data, we use sdcard to store the
|
||||
# test data and create symbolic links to map them to data/local/tmp/.
|
||||
for data in test_data:
|
||||
self.CopyTestData([data], '/sdcard/')
|
||||
self.LinkSdCardPathsToTempDir(test_data)
|
||||
|
||||
def RunTestsWithFilter(self):
|
||||
"""Runs a tests via a small, temporary shell script."""
|
||||
self.test_package.CreateTestRunnerScript(self._gtest_filter,
|
||||
self._test_arguments)
|
||||
self._test_arguments)
|
||||
self.test_results = self.test_package.RunTestsAndListResults()
|
||||
|
||||
def RebaselineTests(self):
|
||||
|
@ -292,20 +310,18 @@ class SingleTestRunner(BaseTestRunner):
|
|||
def SetUp(self):
|
||||
"""Sets up necessary test enviroment for the test suite."""
|
||||
super(SingleTestRunner, self).SetUp()
|
||||
self.adb.ClearApplicationState(constants.CHROME_PACKAGE)
|
||||
if self.test_package.performance_test:
|
||||
if run_tests_helper.IsRunningAsBuildbot():
|
||||
self.adb.SetJavaAssertsEnabled(enable=False)
|
||||
self.adb.Reboot(full_reboot=False)
|
||||
self.adb.SetupPerformanceTest()
|
||||
if self.dump_debug_info:
|
||||
self.dump_debug_info.StartRecordingLog(True)
|
||||
self.StripAndCopyFiles()
|
||||
self.LaunchHelperToolsForTestSuite()
|
||||
self.test_package.tool.SetupEnvironment()
|
||||
self.tool.SetupEnvironment()
|
||||
|
||||
def TearDown(self):
|
||||
"""Cleans up the test enviroment for the test suite."""
|
||||
self.test_package.tool.CleanUpEnvironment()
|
||||
self.tool.CleanUpEnvironment()
|
||||
if self.test_package.cleanup_test_files:
|
||||
self.adb.RemovePushedFiles()
|
||||
if self.dump_debug_info:
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# 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 os
|
||||
import optparse
|
||||
|
||||
|
||||
def CreateTestRunnerOptionParser(usage=None, default_timeout=60):
|
||||
"""Returns a new OptionParser with arguments applicable to all tests."""
|
||||
option_parser = optparse.OptionParser(usage=usage)
|
||||
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',
|
||||
default=False)
|
||||
option_parser.add_option('-v',
|
||||
'--verbose',
|
||||
dest='verbose_count',
|
||||
default=0,
|
||||
action='count',
|
||||
help='Verbose level (multiple times for more)')
|
||||
profilers = ['activitymonitor', '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)')
|
||||
return option_parser
|
|
@ -10,7 +10,6 @@ import pexpect
|
|||
|
||||
from perf_tests_helper import PrintPerfResult
|
||||
from test_result import BaseTestResult, TestResults
|
||||
from valgrind_tools import CreateTool
|
||||
|
||||
|
||||
# TODO(bulach): TestPackage, TestPackageExecutable and
|
||||
|
@ -43,12 +42,9 @@ class TestPackage(object):
|
|||
self.rebaseline = rebaseline
|
||||
self.performance_test = performance_test
|
||||
self.cleanup_test_files = cleanup_test_files
|
||||
self.tool = CreateTool(tool, self.adb)
|
||||
self.tool = tool
|
||||
if timeout == 0:
|
||||
if self.test_suite_basename == 'page_cycler_tests':
|
||||
timeout = 900
|
||||
else:
|
||||
timeout = 60
|
||||
timeout = 60
|
||||
# On a VM (e.g. chromium buildbots), this timeout is way too small.
|
||||
if os.environ.get('BUILDBOT_SLAVENAME'):
|
||||
timeout = timeout * 2
|
||||
|
@ -59,29 +55,14 @@ class TestPackage(object):
|
|||
"""Gets I/O statistics before running test.
|
||||
|
||||
Return:
|
||||
Tuple of (I/O stats object, flag of ready to continue). When encountering
|
||||
error, ready-to-continue flag is False, True otherwise. The I/O stats
|
||||
object may be None if the test is not performance test.
|
||||
I/O stats object.The I/O stats object may be None if the test is not
|
||||
performance test.
|
||||
"""
|
||||
initial_io_stats = None
|
||||
# Try to get the disk I/O statistics for all performance tests.
|
||||
if self.performance_test and not self.rebaseline:
|
||||
initial_io_stats = self.adb.GetIoStats()
|
||||
# Get rid of the noise introduced by launching Chrome for page cycler.
|
||||
if self.test_suite_basename == 'page_cycler_tests':
|
||||
try:
|
||||
chrome_launch_done_re = re.compile(
|
||||
re.escape('Finish waiting for browser launch!'))
|
||||
self.adb.WaitForLogMatch(chrome_launch_done_re)
|
||||
initial_io_stats = self.adb.GetIoStats()
|
||||
except pexpect.TIMEOUT:
|
||||
logging.error('Test terminated because Chrome launcher has no'
|
||||
'response after 120 second.')
|
||||
return (None, False)
|
||||
finally:
|
||||
if self.dump_debug_info:
|
||||
self.dump_debug_info.TakeScreenshot('_Launch_Chrome_')
|
||||
return (initial_io_stats, True)
|
||||
return initial_io_stats
|
||||
|
||||
def _EndGetIOStats(self, initial_io_stats):
|
||||
"""Gets I/O statistics after running test and calcuate the I/O delta.
|
||||
|
@ -99,7 +80,8 @@ class TestPackage(object):
|
|||
disk_io += '\n' + PrintPerfResult(stat, stat,
|
||||
[final_io_stats[stat] -
|
||||
initial_io_stats[stat]],
|
||||
stat.split('_')[1], True, False)
|
||||
stat.split('_')[1],
|
||||
print_to_stdout=False)
|
||||
logging.info(disk_io)
|
||||
return disk_io
|
||||
|
||||
|
@ -113,7 +95,7 @@ class TestPackage(object):
|
|||
for test in all_tests:
|
||||
if not test:
|
||||
continue
|
||||
if test[0] != ' ':
|
||||
if test[0] != ' ' and test.endswith('.'):
|
||||
current = test
|
||||
continue
|
||||
if 'YOU HAVE' in test:
|
||||
|
@ -149,8 +131,8 @@ class TestPackage(object):
|
|||
re_fail = re.compile('\[ FAILED \] ?(.*)\r\n')
|
||||
re_runner_fail = re.compile('\[ RUNNER_FAILED \] ?(.*)\r\n')
|
||||
re_ok = re.compile('\[ OK \] ?(.*)\r\n')
|
||||
(io_stats_before, ready_to_continue) = self._BeginGetIOStats()
|
||||
while ready_to_continue:
|
||||
io_stats_before = self._BeginGetIOStats()
|
||||
while True:
|
||||
found = p.expect([re_run, pexpect.EOF, re_end, re_runner_fail],
|
||||
timeout=self.timeout)
|
||||
if found == 1: # matched pexpect.EOF
|
||||
|
@ -186,7 +168,7 @@ class TestPackage(object):
|
|||
timed_out = True
|
||||
break
|
||||
p.close()
|
||||
if not self.rebaseline and ready_to_continue:
|
||||
if not self.rebaseline:
|
||||
ok_tests += self._EndGetIOStats(io_stats_before)
|
||||
ret_code = self._GetGTestReturnCode()
|
||||
if ret_code:
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import cmd_helper
|
||||
|
@ -38,9 +39,6 @@ class TestPackageApk(TestPackage):
|
|||
tool, dump_debug_info)
|
||||
|
||||
def _CreateTestRunnerScript(self, options):
|
||||
tool_wrapper = self.tool.GetTestWrapper()
|
||||
if tool_wrapper:
|
||||
raise RuntimeError("TestPackageApk does not support custom wrappers.")
|
||||
command_line_file = tempfile.NamedTemporaryFile()
|
||||
# GTest expects argv[0] to be the executable path.
|
||||
command_line_file.write(self.test_suite_basename + ' ' + options)
|
||||
|
@ -55,15 +53,27 @@ class TestPackageApk(TestPackage):
|
|||
def GetAllTests(self):
|
||||
"""Returns a list of all tests available in the test suite."""
|
||||
self._CreateTestRunnerScript('--gtest_list_tests')
|
||||
self.adb.RunShellCommand(
|
||||
'am start -n '
|
||||
'com.android.chrome.native_tests/'
|
||||
'android.app.NativeActivity')
|
||||
try:
|
||||
self.tool.SetupEnvironment()
|
||||
# Clear and start monitoring logcat.
|
||||
self.adb.StartMonitoringLogcat(clear=True,
|
||||
timeout=30 * self.tool.GetTimeoutScale())
|
||||
self.adb.RunShellCommand(
|
||||
'am start -n '
|
||||
'org.chromium.native_test/'
|
||||
'org.chromium.native_test.ChromeNativeTestActivity')
|
||||
# Wait for native test to complete.
|
||||
self.adb.WaitForLogMatch(re.compile('<<nativeRunTests'), None)
|
||||
finally:
|
||||
self.tool.CleanUpEnvironment()
|
||||
# Copy stdout.txt and read contents.
|
||||
stdout_file = tempfile.NamedTemporaryFile()
|
||||
ret = []
|
||||
self.adb.Adb().Pull(TestPackageApk.APK_DATA_DIR + 'stdout.txt',
|
||||
stdout_file.name)
|
||||
ret = self._ParseGTestListTests(stdout_file)
|
||||
# We need to strip the trailing newline.
|
||||
content = [line.rstrip() for line in open(stdout_file.name)]
|
||||
ret = self._ParseGTestListTests(content)
|
||||
return ret
|
||||
|
||||
def CreateTestRunnerScript(self, gtest_filter, test_arguments):
|
||||
|
@ -72,10 +82,14 @@ class TestPackageApk(TestPackage):
|
|||
|
||||
def RunTestsAndListResults(self):
|
||||
self.adb.StartMonitoringLogcat(clear=True, logfile=sys.stdout)
|
||||
self.adb.RunShellCommand(
|
||||
'am start -n '
|
||||
try:
|
||||
self.tool.SetupEnvironment()
|
||||
self.adb.RunShellCommand(
|
||||
'am start -n '
|
||||
'org.chromium.native_test/'
|
||||
'org.chromium.native_test.ChromeNativeTestActivity')
|
||||
finally:
|
||||
self.tool.CleanUpEnvironment()
|
||||
return self._WatchTestOutput(self.adb.GetMonitoredLogCat())
|
||||
|
||||
def StripAndCopyExecutable(self):
|
||||
|
|
|
@ -80,7 +80,8 @@ class TestPackageExecutable(TestPackage):
|
|||
def GetAllTests(self):
|
||||
"""Returns a list of all tests available in the test suite."""
|
||||
all_tests = self.adb.RunShellCommand(
|
||||
'/data/local/tmp/%s --gtest_list_tests' % self.test_suite_basename)
|
||||
'%s /data/local/tmp/%s --gtest_list_tests' %
|
||||
(self.tool.GetTestWrapper(), self.test_suite_basename))
|
||||
return self._ParseGTestListTests(all_tests)
|
||||
|
||||
def CreateTestRunnerScript(self, gtest_filter, test_arguments):
|
||||
|
|
|
@ -3,7 +3,13 @@
|
|||
# found in the LICENSE file.
|
||||
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import constants
|
||||
|
||||
|
||||
# Language values match constants in Sponge protocol buffer (sponge.proto).
|
||||
|
@ -35,7 +41,7 @@ class SingleTestResult(BaseTestResult):
|
|||
def __init__(self, full_name, start_date, dur, lang, log='', error=()):
|
||||
BaseTestResult.__init__(self, full_name, log)
|
||||
name_pieces = full_name.rsplit('#')
|
||||
if len(name_pieces) > 0:
|
||||
if len(name_pieces) > 1:
|
||||
self.test_name = name_pieces[1]
|
||||
self.class_name = name_pieces[0]
|
||||
else:
|
||||
|
@ -55,8 +61,6 @@ class TestResults(object):
|
|||
self.failed = []
|
||||
self.crashed = []
|
||||
self.unknown = []
|
||||
self.disabled = []
|
||||
self.unexpected_pass = []
|
||||
self.timed_out = False
|
||||
self.overall_fail = False
|
||||
|
||||
|
@ -80,14 +84,42 @@ class TestResults(object):
|
|||
ret.failed += t.failed
|
||||
ret.crashed += t.crashed
|
||||
ret.unknown += t.unknown
|
||||
ret.disabled += t.disabled
|
||||
ret.unexpected_pass += t.unexpected_pass
|
||||
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,
|
||||
lang=PYTHON,
|
||||
log=log_msg,
|
||||
error=(str(exc_type), log_msg))
|
||||
|
||||
results = TestResults()
|
||||
results.failed.append(exc_result)
|
||||
return results
|
||||
|
||||
def _Log(self, sorted_list):
|
||||
for t in sorted_list:
|
||||
logging.critical(t.name)
|
||||
|
@ -98,8 +130,9 @@ class TestResults(object):
|
|||
"""Returns the all broken tests including failed, crashed, unknown."""
|
||||
return self.failed + self.crashed + self.unknown
|
||||
|
||||
def LogFull(self):
|
||||
"""Output all broken tests or 'passed' if none broken"""
|
||||
def LogFull(self, test_group, test_suite):
|
||||
"""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:
|
||||
|
@ -115,6 +148,38 @@ class TestResults(object):
|
|||
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',
|
||||
'Release', '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_string = 'Summary:\n'
|
||||
summary_string += 'RAN=%d\n' % (len(self.ok) + len(self.failed) +
|
||||
|
|
|
@ -24,7 +24,7 @@ Call tool.CleanUpEnvironment().
|
|||
import os.path
|
||||
import sys
|
||||
|
||||
from run_tests_helper import CHROME_DIR
|
||||
from constants import CHROME_DIR
|
||||
|
||||
|
||||
class BaseTool(object):
|
||||
|
@ -37,6 +37,11 @@ class BaseTool(object):
|
|||
"""Returns a string that is to be prepended to the test command line."""
|
||||
return ''
|
||||
|
||||
def GetUtilWrapper(self):
|
||||
"""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
|
||||
|
@ -64,6 +69,54 @@ class BaseTool(object):
|
|||
return False
|
||||
|
||||
|
||||
class AddressSanitizerTool(BaseTool):
|
||||
"""AddressSanitizer tool."""
|
||||
|
||||
WRAPPER_PATH = "/system/bin/asanwrapper"
|
||||
|
||||
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 = ['system/lib/libasan_preload.so',
|
||||
'system/bin/asanwrapper',
|
||||
'system/bin/asan/app_process',
|
||||
'system/bin/linker']
|
||||
android_product_out = os.environ['ANDROID_PRODUCT_OUT']
|
||||
self.adb.MakeSystemFolderWritable()
|
||||
for f in files:
|
||||
self.adb.PushIfNeeded(os.path.join(android_product_out, f),
|
||||
os.path.join('/', f))
|
||||
|
||||
def GetTestWrapper(self):
|
||||
return AddressSanitizerTool.WRAPPER_PATH
|
||||
|
||||
def GetUtilWrapper(self):
|
||||
""" 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 AddressSanitizerTool.WRAPPER_PATH
|
||||
|
||||
def SetupEnvironment(self):
|
||||
for prop in self.wrap_properties:
|
||||
self.adb.RunShellCommand('setprop %s "logwrapper %s"' % (
|
||||
prop, self.GetTestWrapper()))
|
||||
self.adb.RunShellCommand('setprop chrome.timeout_scale %f' % (
|
||||
self.GetTimeoutScale()))
|
||||
|
||||
def CleanUpEnvironment(self):
|
||||
for prop in self.wrap_properties:
|
||||
self.adb.RunShellCommand('setprop %s ""' % (prop,))
|
||||
self.adb.RunShellCommand('setprop chrome.timeout_scale ""')
|
||||
|
||||
def GetTimeoutScale(self):
|
||||
# Very slow startup.
|
||||
return 20.0
|
||||
|
||||
|
||||
class ValgrindTool(BaseTool):
|
||||
"""Base abstract class for Valgrind tools."""
|
||||
|
||||
|
@ -72,11 +125,9 @@ class ValgrindTool(BaseTool):
|
|||
|
||||
def __init__(self, adb, renderer=False):
|
||||
self.adb = adb
|
||||
if renderer:
|
||||
# exactly 31 chars, SystemProperties::PROP_NAME_MAX
|
||||
self.wrap_property = 'wrap.com.android.chrome:sandbox'
|
||||
else:
|
||||
self.wrap_property = 'wrap.com.android.chrome'
|
||||
# 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."""
|
||||
|
@ -93,14 +144,16 @@ class ValgrindTool(BaseTool):
|
|||
def SetupEnvironment(self):
|
||||
"""Sets up device environment."""
|
||||
self.adb.RunShellCommand('chmod 777 /data/local/tmp')
|
||||
self.adb.RunShellCommand('setprop %s "logwrapper %s"' % (
|
||||
self.wrap_property, self.GetTestWrapper()))
|
||||
for prop in self.wrap_properties:
|
||||
self.adb.RunShellCommand('setprop %s "logwrapper %s"' % (
|
||||
prop, self.GetTestWrapper()))
|
||||
self.adb.RunShellCommand('setprop chrome.timeout_scale %f' % (
|
||||
self.GetTimeoutScale()))
|
||||
|
||||
def CleanUpEnvironment(self):
|
||||
"""Cleans up device environment."""
|
||||
self.adb.RunShellCommand('setprop %s ""' % (self.wrap_property,))
|
||||
for prop in self.wrap_properties:
|
||||
self.adb.RunShellCommand('setprop %s ""' % (prop,))
|
||||
self.adb.RunShellCommand('setprop chrome.timeout_scale ""')
|
||||
|
||||
def GetFilesForTool(self):
|
||||
|
@ -162,7 +215,8 @@ TOOL_REGISTRY = {
|
|||
'memcheck': lambda x: MemcheckTool(x, False),
|
||||
'memcheck-renderer': lambda x: MemcheckTool(x, True),
|
||||
'tsan': lambda x: TSanTool(x, False),
|
||||
'tsan-renderer': lambda x: TSanTool(x, True)
|
||||
'tsan-renderer': lambda x: TSanTool(x, True),
|
||||
'asan': lambda x: AddressSanitizerTool(x)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -50,21 +50,20 @@ failed on device.
|
|||
|
||||
import fnmatch
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from pylib import android_commands
|
||||
from pylib.base_test_sharder import BaseTestSharder
|
||||
from pylib import cmd_helper
|
||||
from pylib import constants
|
||||
from pylib import debug_info
|
||||
import emulator
|
||||
from pylib import ports
|
||||
from pylib import run_tests_helper
|
||||
from pylib import test_options_parser
|
||||
from pylib.single_test_runner import SingleTestRunner
|
||||
from pylib.test_package_executable import TestPackageExecutable
|
||||
from pylib.test_result import BaseTestResult, TestResults
|
||||
|
||||
_TEST_SUITES = ['base_unittests',
|
||||
|
@ -83,7 +82,7 @@ def FullyQualifiedTestSuites(apk):
|
|||
Args:
|
||||
apk: if True, use the apk-based test runner"""
|
||||
# If not specified, assume the test suites are in out/Release
|
||||
test_suite_dir = os.path.abspath(os.path.join(run_tests_helper.CHROME_DIR,
|
||||
test_suite_dir = os.path.abspath(os.path.join(constants.CHROME_DIR,
|
||||
'out', 'Release'))
|
||||
if apk:
|
||||
# out/Release/$SUITE_apk/$SUITE-debug.apk
|
||||
|
@ -194,17 +193,24 @@ def RunTests(device, test_suite, gtest_filter, test_arguments, rebaseline,
|
|||
A TestResults object.
|
||||
"""
|
||||
results = []
|
||||
global _TEST_SUITES
|
||||
|
||||
if test_suite:
|
||||
global _TEST_SUITES
|
||||
if (not os.path.exists(test_suite) and
|
||||
not os.path.splitext(test_suite)[1] == '.apk'):
|
||||
if (not os.path.exists(test_suite)):
|
||||
logging.critical('Unrecognized test suite %s, supported: %s' %
|
||||
(test_suite, _TEST_SUITES))
|
||||
if test_suite in _TEST_SUITES:
|
||||
logging.critical('(Remember to include the path: out/Release/%s)',
|
||||
test_suite)
|
||||
return TestResults.FromRun(failed=[BaseTestResult(test_suite, '')])
|
||||
test_suite_basename = os.path.basename(test_suite)
|
||||
if test_suite_basename in _TEST_SUITES:
|
||||
logging.critical('Try "make -j15 %s"' % test_suite_basename)
|
||||
else:
|
||||
logging.critical('Unrecognized test suite, supported: %s' %
|
||||
_TEST_SUITES)
|
||||
return TestResults.FromOkAndFailed([], [BaseTestResult(test_suite, '')],
|
||||
False, False)
|
||||
fully_qualified_test_suites = [test_suite]
|
||||
else:
|
||||
fully_qualified_test_suites = FullyQualifiedTestSuites(apk)
|
||||
|
@ -224,10 +230,10 @@ def RunTests(device, test_suite, gtest_filter, test_arguments, rebaseline,
|
|||
debug_info_list += [test.dump_debug_info]
|
||||
if rebaseline:
|
||||
test.UpdateFilter(test.test_results.failed)
|
||||
test.test_results.LogFull()
|
||||
test.test_results.LogFull('Unit test', os.path.basename(t))
|
||||
# Zip all debug info outputs into a file named by log_dump_name.
|
||||
debug_info.GTestDebugInfo.ZipAndCleanResults(
|
||||
os.path.join(run_tests_helper.CHROME_DIR, 'out', 'Release',
|
||||
os.path.join(constants.CHROME_DIR, 'out', 'Release',
|
||||
'debug_info_dumps'),
|
||||
log_dump_name, [d for d in debug_info_list if d])
|
||||
|
||||
|
@ -257,6 +263,8 @@ class TestSharder(BaseTestSharder):
|
|||
test = SingleTestRunner(self.attached_devices[0], test_suite, gtest_filter,
|
||||
test_arguments, timeout, rebaseline,
|
||||
performance_test, cleanup_test_files, tool, 0)
|
||||
# The executable/apk needs to be copied before we can call GetAllTests.
|
||||
test.test_package.StripAndCopyExecutable()
|
||||
all_tests = test.test_package.GetAllTests()
|
||||
if not rebaseline:
|
||||
disabled_list = test.GetDisabledTests()
|
||||
|
@ -277,7 +285,8 @@ class TestSharder(BaseTestSharder):
|
|||
Returns:
|
||||
A SingleTestRunner object.
|
||||
"""
|
||||
shard_size = len(self.tests) / len(self.attached_devices)
|
||||
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)
|
||||
return SingleTestRunner(device, self.test_suite,
|
||||
|
@ -287,7 +296,7 @@ class TestSharder(BaseTestSharder):
|
|||
|
||||
def OnTestsCompleted(self, test_runners, test_results):
|
||||
"""Notifies that we completed the tests."""
|
||||
test_results.LogFull()
|
||||
test_results.LogFull('Unit test', os.path.basename(self.test_suite))
|
||||
if self.annotate:
|
||||
PrintAnnotationForTestResults(test_results)
|
||||
if test_results.failed and self.rebaseline:
|
||||
|
@ -333,6 +342,11 @@ def _RunATestSuite(options):
|
|||
print '@@@STEP_FAILURE@@@'
|
||||
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 (len(attached_devices) > 1 and options.test_suite and
|
||||
not options.gtest_filter and not options.performance_test):
|
||||
sharder = TestSharder(attached_devices, options.test_suite,
|
||||
|
@ -410,7 +424,7 @@ def ListTestSuites():
|
|||
|
||||
|
||||
def main(argv):
|
||||
option_parser = run_tests_helper.CreateTestRunnerOptionParser(None,
|
||||
option_parser = test_options_parser.CreateTestRunnerOptionParser(None,
|
||||
default_timeout=0)
|
||||
option_parser.add_option('-s', '--suite', dest='test_suite',
|
||||
help='Executable name of the test suite to run '
|
||||
|
|
Загрузка…
Ссылка в новой задаче