[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:
bulach@chromium.org 2012-07-10 13:21:43 +00:00
Родитель 6879b2ca14
Коммит 5d7670d4de
22 изменённых файлов: 1110 добавлений и 383 удалений

Просмотреть файл

@ -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):

61
android/pylib/fake_dns.py Normal file
Просмотреть файл

@ -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')

120
android/pylib/forwarder.py Normal file
Просмотреть файл

@ -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

160
android/pylib/ports.py Normal file
Просмотреть файл

@ -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 '