blob: 3ec38860c5a24e3db980b7164016ce6030d923e6 [file] [log] [blame]
# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import contextlib
import datetime
import json
import os
import pipes
import re
import sys
import urllib
from recipe_engine.types import freeze
from recipe_engine import recipe_api
def _TimestampToIsoFormat(timestamp):
return datetime.datetime.utcfromtimestamp(timestamp).strftime('%Y%m%dT%H%M%S')
class AndroidApi(recipe_api.RecipeApi):
def __init__(self, **kwargs):
super(AndroidApi, self).__init__(**kwargs)
self._devices = None
self._file_changes_path = None
def get_config_defaults(self):
return {
'REVISION': self.m.properties.get('revision', ''),
'CHECKOUT_PATH': self.m.path['checkout'],
}
@property
def devices(self):
assert self._devices is not None,\
'devices is only available after device_status()'
return self._devices
@property
def out_path(self):
return self.m.path['checkout'].join('out')
@property
def coverage_dir(self):
return self.out_path.join(self.c.BUILD_CONFIG, 'coverage')
@property
def known_devices_file(self):
return self.m.path.join(
self.m.path.expanduser('~'), '.android', 'known_devices.json')
@property
def file_changes_path(self):
"""Get or create the path to the file containing changes for this revision.
This file will contain a dict mapping file paths to lists of changed lines
for each file. This is used to generate incremental coverage reports.
"""
if not self._file_changes_path:
self._file_changes_path = (
self.m.path.mkdtemp('coverage').join('file_changes.json'))
return self._file_changes_path
def configure_from_properties(self, config_name, **kwargs):
self.set_config(config_name, **kwargs)
self.m.chromium.set_config(config_name, optional=True, **kwargs)
def make_zip_archive(self, step_name, archive_name, files=None,
preserve_paths=True, filters=None, **kwargs):
"""Creates and stores the archive file.
Args:
step_name: Name of the step.
archive_name: Name of the archive file.
files: If specified, only include files here instead of out/<target>.
filters: List of globs to be included in the archive.
preserve_paths: If True, files will be stored using the subdolders
in the archive.
"""
archive_args = ['--target', self.m.chromium.c.BUILD_CONFIG,
'--name', archive_name]
# TODO(luqui): Clean up when these are covered by the external builders.
if files: # pragma: no cover
archive_args.extend(['--files', ','.join(files)])
if filters:
archive_args.extend(['--filters', ','.join(filters)])
if not preserve_paths: # pragma: no cover
archive_args.append('--ignore-subfolder-names')
self.m.python(
step_name,
str(self.package_repo_resource(
'scripts', 'slave', 'android', 'archive_build.py')),
archive_args,
infra_step=True,
**kwargs
)
def init_and_sync(self, gclient_config='android_bare',
with_branch_heads=False, use_bot_update=True,
use_git_cache=True):
# TODO(sivachandra): Move the setting of the gclient spec below to an
# internal config extension when they are supported by the recipe system.
if use_git_cache:
spec = self.m.gclient.make_config(gclient_config)
else:
spec = self.m.gclient.make_config(gclient_config,
CACHE_DIR=None)
spec.target_os = ['android']
s = spec.solutions[0]
s.name = self.c.deps_dir
s.url = self.c.REPO_URL
s.deps_file = self.c.deps_file
s.custom_vars = self.c.gclient_custom_vars or {}
s.managed = self.c.managed
s.revision = self.c.revision
spec.revisions = self.c.revisions
self.m.gclient.break_locks()
refs = self.m.properties.get('event.patchSet.ref')
if refs:
refs = [refs]
if use_bot_update:
result = self.m.bot_update.ensure_checkout(
spec, refs=refs, with_branch_heads=with_branch_heads, force=True)
else:
result = self.m.gclient.checkout(
spec, with_branch_heads=with_branch_heads)
# TODO(sivachandra): Manufacture gclient spec such that it contains "src"
# solution + repo_name solution. Then checkout will be automatically
# correctly set by gclient.checkout
self.m.path['checkout'] = self.m.path['slave_build'].join('src')
self.clean_local_files()
return result
def clean_local_files(self):
target = self.c.BUILD_CONFIG
debug_info_dumps = self.m.path['checkout'].join('out',
target,
'debug_info_dumps')
test_logs = self.m.path['checkout'].join('out', target, 'test_logs')
build_product = self.m.path['checkout'].join('out', 'build_product.zip')
self.m.python.inline(
'clean local files',
"""
import shutil, sys, os
shutil.rmtree(sys.argv[1], True)
shutil.rmtree(sys.argv[2], True)
try:
os.remove(sys.argv[3])
except OSError:
pass
for base, _dirs, files in os.walk(sys.argv[4]):
for f in files:
if f.endswith('.pyc'):
os.remove(os.path.join(base, f))
""",
args=[debug_info_dumps, test_logs, build_product,
self.m.path['checkout']],
infra_step=True,
)
def run_tree_truth(self, additional_repos=None):
# TODO(sivachandra): The downstream ToT builder will require
# 'Show Revisions' step.
repos = ['src']
if additional_repos:
repos.extend(additional_repos)
if self.c.REPO_NAME not in repos and self.c.REPO_NAME:
repos.append(self.c.REPO_NAME)
# TODO(sivachandra): Disable subannottations after cleaning up
# tree_truth.sh.
self.m.step('tree truth steps',
[self.m.path['checkout'].join('build', 'tree_truth.sh'),
self.m.path['checkout']] + repos,
allow_subannotations=False)
def git_number(self, **kwargs):
return self.m.step(
'git_number',
[self.m.path['depot_tools'].join('git_number.py')],
stdout = self.m.raw_io.output(),
step_test_data=(
lambda:
self.m.raw_io.test_api.stream_output('3000\n')
),
cwd=self.m.path['checkout'],
infra_step=True,
**kwargs)
def java_method_count(self, dexfile, name='java_method_count', perf_id=None):
# TODO(agrieve): Remove once usages are elimintated.
self.resource_sizes(dexfile, perf_id=perf_id) # pragma: no cover
def resource_sizes(self, apk_path, chartjson_file=False,
upload_archives_to_bucket=None, perf_id=None):
cmd = ['build/android/resource_sizes.py', str(apk_path)]
if chartjson_file:
cmd.append('--chartjson')
config = {
'steps': {
'resource_sizes (%s)' % self.m.path.basename(apk_path): {
'cmd': ' '.join(pipes.quote(x) for x in cmd),
'device_affinity': None,
'archive_output_dir': True
}
},
'version': 1
}
self.run_sharded_perf_tests(
config=self.m.json.input(config),
flaky_config=None,
perf_id=perf_id or self.m.properties['buildername'],
chartjson_file=chartjson_file,
upload_archives_to_bucket=upload_archives_to_bucket)
def check_webview_licenses(self, name='check licenses'):
self.m.python(
name,
self.m.path['checkout'].join('android_webview',
'tools',
'webview_licenses.py'),
args=['scan'],
cwd=self.m.path['checkout'])
def upload_build(self, bucket, path):
archive_name = 'build_product.zip'
zipfile = self.m.path['checkout'].join('out', archive_name)
self.make_zip_archive(
'zip_build_product',
archive_name,
preserve_paths=True,
cwd=self.m.path['checkout']
)
self.m.gsutil.upload(
name='upload_build_product',
source=zipfile,
bucket=bucket,
dest=path,
version='4.7')
def download_build(self, bucket, path, extract_path=None):
zipfile = self.m.path['checkout'].join('out', 'build_product.zip')
self.m.gsutil.download(
name='download_build_product',
bucket=bucket,
source=path,
dest=zipfile,
version='4.7',
)
extract_path = extract_path or self.m.path['checkout']
self.m.step(
'unzip_build_product',
['unzip', '-o', zipfile],
cwd=extract_path,
infra_step=True,
)
def zip_and_upload_build(self, bucket):
# TODO(luqui): Unify make_zip_archive and upload_build with this
# (or at least make the difference clear).
self.m.archive.zip_and_upload_build(
'zip_build',
target=self.m.chromium.c.BUILD_CONFIG,
# We send None as the path so that zip_build.py gets it from factory
# properties.
build_url=None,
src_dir=self.m.path['slave_build'].join('src'),
exclude_files='lib.target,gen,android_webview,jingle_unittests')
def create_adb_symlink(self):
# Creates a sym link to the adb executable in the home dir
self.m.python(
'create adb symlink',
self.m.path['checkout'].join('build', 'symlink.py'),
['-f', self.m.adb.adb_path(), os.path.join('~', 'adb')],
infra_step=True)
def spawn_logcat_monitor(self):
self.m.step(
'spawn_logcat_monitor',
[self.package_repo_resource('scripts', 'slave', 'daemonizer.py'),
'--', self.c.cr_build_android.join('adb_logcat_monitor.py'),
self.m.chromium.c.build_dir.join('logcat'),
self.m.adb.adb_path()],
env=self.m.chromium.get_env(),
infra_step=True)
def spawn_device_monitor(self):
script = self.package_repo_resource('scripts', 'slave', 'daemonizer.py')
args = [
'--action', 'restart',
'--pid-file-path', '/tmp/device_monitor.pid',
'--', self.resource('spawn_device_monitor.py'),
self.m.adb.adb_path(),
json.dumps(self._devices),
'--blacklist-file', self.blacklist_file
]
self.m.python('spawn_device_monitor', script, args, infra_step=True)
def shutdown_device_monitor(self):
script = self.package_repo_resource('scripts', 'slave', 'daemonizer.py')
args = [
'--action', 'stop',
'--pid-file-path', '/tmp/device_monitor.pid',
]
self.m.python('shutdown_device_monitor', script, args, infra_step=True)
def authorize_adb_devices(self):
script = self.package_repo_resource(
'scripts', 'slave', 'android', 'authorize_adb_devices.py')
args = ['--verbose', '--adb-path', self.m.adb.adb_path()]
return self.m.python('authorize_adb_devices', script, args, infra_step=True,
env=self.m.chromium.get_env())
def detect_and_setup_devices(self, restart_usb=False, skip_wipe=False,
disable_location=False, min_battery_level=None,
disable_network=False, disable_java_debug=False,
reboot_timeout=None, max_battery_temp=None,
remove_system_webview=False):
self.authorize_adb_devices()
self.device_recovery()
self.provision_devices(
skip_wipe=skip_wipe, disable_location=disable_location,
min_battery_level=min_battery_level, disable_network=disable_network,
disable_java_debug=disable_java_debug, reboot_timeout=reboot_timeout,
max_battery_temp=max_battery_temp,
remove_system_webview=remove_system_webview)
self.device_status()
@property
def blacklist_file(self):
return self.out_path.join('bad_devices.json')
def revert_device_file_format(self):
# If current device file is jsonified, revert it back to original format.
if self.m.path.exists(self.known_devices_file):
with self.m.step.nest('fix_device_file_format'):
file_contents = self.m.file.read(
'read_device_file', self.known_devices_file,
test_data='device1\ndevice2\ndevice3')
try:
devices = json.loads(file_contents)
self.m.step.active_result.presentation.step_text += (
'file format is json, reverting')
old_format = '\n'.join(devices)
self.m.file.write(
'revert_device_file', self.known_devices_file, old_format)
except ValueError:
# File wasn't json, so no need to revert.
self.m.step.active_result.presentation.step_text += (
'file format is compatible')
def device_status_check(self, restart_usb=False, **kwargs):
# TODO(bpastene): Remove once chromium revisions prior to
# crrev.com/1faecde0c03013b6cd725da413339c60223f8948 are no longer tested.
# See crbug.com/619707 for context.
self.revert_device_file_format()
self.device_recovery()
return self.device_status()
def host_info(self, args=[], **kwargs):
results = None
try:
with self.handle_exit_codes():
if self.known_devices_file:
known_devices_arg = ['--known-devices-file', self.known_devices_file]
args.extend(['--args', self.m.json.input(known_devices_arg)])
args.extend(['run', '--output', self.m.json.output()])
results = self.m.step(
'Host Info',
[self.m.path['checkout'].join('testing', 'scripts',
'host_info.py')] + args,
env=self.m.chromium.get_env(),
infra_step=True,
step_test_data=lambda: self.m.json.test_api.output({
'valid': True,
'failures': [],
'_host_info': {
'os_system': 'os_system',
'os_release': 'os_release',
'processor': 'processor',
'num_cpus': 'num_cpus',
'free_disk_space': 'free_disk_space',
'python_version': 'python_version',
'python_path': 'python_path',
'devices': [{
"usb_status": True,
"blacklisted": None,
"ro.build.fingerprint": "fingerprint",
"battery": {
"status": "5",
"scale": "100",
"temperature": "240",
"level": "100",
"technology": "Li-ion",
"AC powered": "false",
"health": "2",
"voltage": "4302",
"Wireless powered": "false",
"USB powered": "true",
"Max charging current": "500000",
"present": "true"
},
"adb_status": "device",
"imei_slice": "",
"ro.build.product": "bullhead",
"ro.build.id": "MDB08Q",
"serial": "00d0d567893340f4",
"wifi_ip": ""
}]
}}),
**kwargs)
return results
except self.m.step.InfraFailure as f:
for failure in f.result.json.output.get('failures', []):
f.result.presentation.logs[failure] = [failure]
f.result.presentation.status = self.m.step.EXCEPTION
def device_recovery(self, restart_usb=False, **kwargs):
args = [
'--blacklist-file', self.blacklist_file,
'--known-devices-file', self.known_devices_file,
'--adb-path', self.m.adb.adb_path(),
'-v'
]
self.m.step(
'device_recovery',
[self.m.path['checkout'].join('third_party', 'catapult', 'devil',
'devil', 'android', 'tools',
'device_recovery.py')] + args,
env=self.m.chromium.get_env(),
infra_step=True,
**kwargs)
def device_status(self, **kwargs):
buildbot_file = '/home/chrome-bot/.adb_device_info'
args = [
'--json-output', self.m.json.output(),
'--blacklist-file', self.blacklist_file,
'--known-devices-file', self.known_devices_file,
'--buildbot-path', buildbot_file,
'--adb-path', self.m.adb.adb_path(),
'-v', '--overwrite-known-devices-files',
]
try:
result = self.m.step(
'device_status',
[self.m.path['checkout'].join('third_party', 'catapult', 'devil',
'devil', 'android', 'tools',
'device_status.py')] + args,
step_test_data=lambda: self.m.json.test_api.output([
{
"battery": {
"status": "5",
"scale": "100",
"temperature": "249",
"level": "100",
"AC powered": "false",
"health": "2",
"voltage": "4286",
"Wireless powered": "false",
"USB powered": "true",
"technology": "Li-ion",
"present": "true"
},
"wifi_ip": "",
"imei_slice": "Unknown",
"ro.build.id": "LRX21O",
"ro.build.product": "product_name",
"build_detail":
"google/razor/flo:5.0/LRX21O/1570415:userdebug/dev-keys",
"serial": "07a00ca4",
"adb_status": "device",
"blacklisted": False,
"usb_status": True,
},
{
"adb_status": "offline",
"blacklisted": True,
"serial": "03e0363a003c6ad4",
"usb_status": False,
},
{
"adb_status": "unauthorized",
"blacklisted": True,
"serial": "03e0363a003c6ad5",
"usb_status": True,
},
{
"adb_status": "device",
"blacklisted": True,
"serial": "03e0363a003c6ad6",
"usb_status": True,
}
]),
env=self.m.chromium.get_env(),
infra_step=True,
**kwargs)
self._devices = []
offline_device_index = 1
for d in result.json.output:
try:
if not d['usb_status']:
key = '%s: missing' % d['serial']
elif d['adb_status'] != 'device':
key = '%s: adb status %s' % (d['serial'], d['adb_status'])
elif d['blacklisted']:
key = '%s: blacklisted' % d['serial']
else:
key = '%s %s %s' % (d['ro.build.product'], d['ro.build.id'],
d['serial'])
self._devices.append(d['serial'])
except KeyError:
key = 'unknown device %d' % offline_device_index
offline_device_index += 1
result.presentation.logs[key] = self.m.json.dumps(
d, indent=2).splitlines()
result.presentation.step_text = 'Online devices: %s' % len(self._devices)
return result
except self.m.step.InfraFailure as f:
params = {
'summary': ('Device Offline on %s %s' %
(self.m.properties['mastername'], self.m.properties['slavename'])),
'comment': ('Buildbot: %s\n(Please do not change any labels)' %
self.m.properties['buildername']),
'labels': 'Restrict-View-Google,OS-Android,Infra,Infra-Labs',
}
link = ('https://code.google.com/p/chromium/issues/entry?%s' %
urllib.urlencode(params))
f.result.presentation.links.update({
'report a bug': link
})
raise
def provision_devices(self, skip_wipe=False, disable_location=False,
min_battery_level=None, disable_network=False,
disable_java_debug=False, max_battery_temp=None,
disable_system_chrome=False, reboot_timeout=None,
remove_system_webview=False, emulators=False,
**kwargs):
args = [
'--adb-path', self.m.adb.adb_path(),
'--blacklist-file', self.blacklist_file,
'--output-device-blacklist', self.m.json.output(add_json_log=False),
'-t', self.m.chromium.c.BUILD_CONFIG,
]
if skip_wipe:
args.append('--skip-wipe')
if disable_location:
args.append('--disable-location')
if reboot_timeout is not None:
assert isinstance(reboot_timeout, int)
assert reboot_timeout > 0
args.extend(['--reboot-timeout', reboot_timeout])
if min_battery_level is not None:
assert isinstance(min_battery_level, int)
assert min_battery_level >= 0
assert min_battery_level <= 100
args.extend(['--min-battery-level', min_battery_level])
if disable_network:
args.append('--disable-network')
if disable_java_debug:
args.append('--disable-java-debug')
if max_battery_temp:
assert isinstance(max_battery_temp, int)
assert max_battery_temp >= 0
assert max_battery_temp <= 500
args.extend(['--max-battery-temp', max_battery_temp])
if disable_system_chrome:
args.append('--disable-system-chrome')
if remove_system_webview:
args.append('--remove-system-webview')
if self.c and self.c.chrome_specific_wipe:
args.append('--chrome-specific-wipe')
if emulators:
args.append('--emulators')
result = self.m.python(
'provision_devices',
self.m.path['checkout'].join(
'build', 'android', 'provision_devices.py'),
args=args,
env=self.m.chromium.get_env(),
infra_step=True,
**kwargs)
def apk_path(self, apk):
return self.m.chromium.output_dir.join('apks', apk) if apk else None
def adb_install_apk(self, apk, allow_downgrade=False, devices=None):
install_cmd = [
self.m.path['checkout'].join('build',
'android',
'adb_install_apk.py'),
apk, '-v', '--blacklist-file', self.blacklist_file,
]
if int(self.m.chromium.get_version().get('MAJOR', 0)) > 50:
install_cmd += ['--adb-path', self.m.adb.adb_path()]
if devices and isinstance(devices, list):
for d in devices:
install_cmd += ['-d', d]
if allow_downgrade:
install_cmd.append('--downgrade')
if self.m.chromium.c.BUILD_CONFIG == 'Release':
install_cmd.append('--release')
return self.m.step('install ' + self.m.path.basename(apk), install_cmd,
infra_step=True,
env=self.m.chromium.get_env())
def _asan_device_setup(self, args):
script = self.m.path['checkout'].join(
'tools', 'android', 'asan', 'third_party', 'asan_device_setup.sh')
cmd = [script] + args
env = dict(self.m.chromium.get_env())
env['ADB'] = self.m.adb.adb_path()
for d in self.devices:
self.m.step(d,
cmd + ['--device', d],
infra_step=True,
env=env)
def asan_device_setup(self):
clang_version_cmd = [
self.m.path['checkout'].join('tools', 'clang', 'scripts', 'update.py'),
'--print-clang-version'
]
clang_version_step = self.m.step('get_clang_version',
clang_version_cmd,
stdout=self.m.raw_io.output(),
step_test_data=(
lambda: self.m.raw_io.test_api.stream_output('1.1.1')))
clang_version = clang_version_step.stdout.strip()
with self.m.step.nest('Set up ASAN on devices'):
self.m.adb.root_devices()
args = [
'--lib',
self.m.path['checkout'].join(
'third_party', 'llvm-build', 'Release+Asserts', 'lib', 'clang',
clang_version, 'lib', 'linux', 'libclang_rt.asan-arm-android.so')
]
self._asan_device_setup(args)
def asan_device_teardown(self):
with self.m.step.nest('Tear down ASAN on devices'):
self._asan_device_setup(['--revert'])
def monkey_test(self, **kwargs):
args = [
'monkey',
'-v',
'--package=%s' % self.c.channel,
'--event-count=50000',
'--blacklist-file', self.blacklist_file,
]
return self.test_runner(
'Monkey Test',
args,
env={'BUILDTYPE': self.c.BUILD_CONFIG},
**kwargs)
def _run_sharded_tests(self,
config='sharded_perf_tests.json',
flaky_config=None,
chartjson_output=False,
max_battery_temp=None,
known_devices_file=None,
enable_platform_mode=False,
write_buildbot_json=False,
**kwargs):
args = [
'perf',
'--release',
'--verbose',
'--steps', config,
'--blacklist-file', self.blacklist_file
]
if flaky_config:
args.extend(['--flaky-steps', flaky_config])
if chartjson_output:
args.append('--collect-chartjson-data')
if max_battery_temp:
args.extend(['--max-battery-temp', max_battery_temp])
if known_devices_file:
args.extend(['--known-devices-file', known_devices_file])
if enable_platform_mode:
args.extend(['--enable-platform-mode'])
if write_buildbot_json:
args.extend(['--write-buildbot-json'])
self.test_runner(
'Sharded Perf Tests',
args,
cwd=self.m.path['checkout'],
env=self.m.chromium.get_env(),
**kwargs)
def run_sharded_perf_tests(self, config, flaky_config=None, perf_id=None,
test_type_transform=lambda x: x,
chartjson_file=False, max_battery_temp=None,
upload_archives_to_bucket=None,
known_devices_file=None,
enable_platform_mode=False,
timestamp_as_point_id=False, **kwargs):
"""Run the perf tests from the given config file.
config: the path of the config file containing perf tests.
flaky_config: optional file of tests to avoid.
perf_id: the id of the builder running these tests
test_type_transform: a lambda transforming the test name to the
test_type to upload to.
known_devices_file: Path to file containing serial numbers of known devices.
enable_platform_mode: If set, will run using the android test runner's new
platform mode.
timestamp_as_point_id: If True, will use a unix timestamp as a point_id to
identify values in the perf dashboard; otherwise the default (commit
position) is used.
"""
# test_runner.py actually runs the tests and records the results
self._run_sharded_tests(config=config, flaky_config=flaky_config,
chartjson_output=chartjson_file,
max_battery_temp=max_battery_temp,
known_devices_file=known_devices_file,
enable_platform_mode=enable_platform_mode, **kwargs)
# now obtain the list of tests that were executed.
result = self.test_runner(
'get perf test list',
['perf', '--steps', config, '--output-json-list', self.m.json.output(),
'--blacklist-file', self.blacklist_file],
step_test_data=lambda: self.m.json.test_api.output([
{'test': 'perf_test.foo', 'device_affinity': 0,
'end_time': 1443438432.949711, 'has_archive': True},
{'test': 'page_cycler.foo', 'device_affinity': 0}]),
env=self.m.chromium.get_env()
)
perf_tests = result.json.output
if perf_tests and isinstance(perf_tests[0], dict):
perf_tests = sorted(perf_tests,
key=lambda x: (x['device_affinity'], x['test']))
else:
perf_tests = [{'test': v} for v in perf_tests]
failures = []
for test_data in perf_tests:
test_name = str(test_data['test']) # un-unicode
test_type = test_type_transform(test_name)
annotate = self.m.chromium.get_annotate_by_test_name(test_name)
test_end_time = int(test_data.get('end_time', 0))
if not test_end_time:
test_end_time = int(self.m.time.time())
point_id = test_end_time if timestamp_as_point_id else None
if upload_archives_to_bucket and test_data.get('has_archive'):
archive = self.m.path.mkdtemp('perf_archives').join('output_dir.zip')
else:
archive = None
print_step_cmd = ['perf', '--print-step', test_name, '--verbose',
'--adb-path', self.m.adb.adb_path(),
'--blacklist-file', self.blacklist_file]
if archive:
print_step_cmd.extend(['--get-output-dir-archive', archive])
if enable_platform_mode:
print_step_cmd.extend(['--enable-platform-mode'])
try:
with self.handle_exit_codes():
env = self.m.chromium.get_env()
env['CHROMIUM_OUTPUT_DIR'] = self.m.chromium.output_dir
self.m.chromium.runtest(
self.c.test_runner,
print_step_cmd,
name=test_name,
perf_dashboard_id=test_type,
point_id=point_id,
test_type=test_type,
annotate=annotate,
results_url='https://chromeperf.appspot.com',
perf_id=perf_id,
env=env,
chartjson_file=chartjson_file)
except self.m.step.StepFailure as f:
failures.append(f)
finally:
if 'device_affinity' in test_data:
step_result = self.m.step.active_result
affinity = test_data['device_affinity']
step_result.presentation.step_text += (
self.m.test_utils.format_step_text(
[['Device Affinity: %s' % affinity]]))
step_result.presentation.logs['device affinity'] = str(affinity)
if archive:
dest = '{builder}/{test}/{timestamp}_build_{buildno}.zip'.format(
builder=self.m.properties['buildername'],
test=test_name,
timestamp=_TimestampToIsoFormat(test_end_time),
buildno=self.m.properties['buildnumber'])
self.m.gsutil.upload(
name='upload %s output dir archive' % test_name,
source=archive,
bucket=upload_archives_to_bucket,
dest=dest,
link_name='output_dir.zip')
if failures:
raise self.m.step.StepFailure('sharded perf tests failed %s' % failures)
def run_telemetry_browser_test(self, test_name, browser='android-chromium'):
"""Run a telemetry browser test."""
try:
self.m.python(
name='Run telemetry browser_test %s' % test_name,
script=self.m.path['checkout'].join(
'chrome', 'test', 'android', 'telemetry_tests',
'run_chrome_browser_tests.py'),
args=['--browser=%s' % browser,
'--write-abbreviated-json-results-to', self.m.json.output(),
test_name],
step_test_data=lambda: self.m.json.test_api.output(
{'successes': ['passed_test1']}))
finally:
test_failures = self.m.step.active_result.json.output.get('failures', [])
self.m.step.active_result.presentation.step_text += (
self.m.test_utils.format_step_text(
[['failures:', test_failures]]))
def run_instrumentation_suite(self,
name,
test_apk=None,
apk_under_test=None,
additional_apks=None,
isolate_file_path=None,
flakiness_dashboard=None,
annotation=None, except_annotation=None,
screenshot=False, verbose=False, tool=None,
apk_package=None,
official_build=False,
json_results_file=None,
timeout_scale=None, strict_mode=None,
suffix=None, num_retries=None,
device_flags=None,
wrapper_script_suite_name=None,
result_details=False,
cs_base_url=None,
store_tombstones=False,
**kwargs):
args = [
'--blacklist-file', self.blacklist_file,
]
if tool:
args.append('--tool=%s' % tool)
if flakiness_dashboard:
args.extend(['--flakiness-dashboard-server', flakiness_dashboard])
if annotation:
args.extend(['-A', annotation])
if except_annotation:
args.extend(['-E', except_annotation])
if screenshot:
args.append('--screenshot')
if verbose:
args.append('--verbose')
if self.c.coverage or self.c.incremental_coverage:
args.extend(['--coverage-dir', self.coverage_dir])
if result_details:
details_dir = self.m.path.mkdtemp('temp_details')
if not json_results_file:
json_results_file = self.m.path.join(details_dir, 'json_results_file')
if json_results_file:
args.extend(['--json-results-file', json_results_file])
if store_tombstones:
args.append('--store-tombstones')
if timeout_scale:
args.extend(['--timeout-scale', timeout_scale])
if strict_mode:
args.extend(['--strict-mode', strict_mode])
if num_retries is not None:
args.extend(['--num-retries', str(num_retries)])
if device_flags:
args.extend(['--device-flags', device_flags])
if test_apk:
args.extend(['--test-apk', test_apk])
if apk_under_test:
args.extend(['--apk-under-test', apk_under_test])
for a in additional_apks or []:
args.extend(['--additional-apk', a])
if not wrapper_script_suite_name:
args.insert(0, 'instrumentation')
if isolate_file_path:
args.extend(['--isolate-file-path', isolate_file_path])
if self.m.chromium.c.BUILD_CONFIG == 'Release':
args.append('--release')
if official_build:
args.extend(['--official-build'])
step_name = '%s%s' % (
annotation or name, ' (%s)' % suffix if suffix else '')
try:
step_result = self.test_runner(
step_name,
args=args,
wrapper_script_suite_name=wrapper_script_suite_name,
**kwargs)
finally:
if result_details:
with self.m.step.nest('process results for %s' % step_name):
try:
details_html = details_dir.join('details.html')
presentation_args = ['--json-file',
json_results_file,
'--html-file',
details_html,
'--master-name',
self.m.properties.get('mastername')]
if cs_base_url:
presentation_args.extend(['--cs-base-url', cs_base_url])
self.m.python(
'Generate Result Details',
self.resource('test_results_presentation.py'),
args=presentation_args)
details_list = self.m.file.read(
'Read detail.html',
details_html,
test_data="<!DOCTYPE html><html></html>").splitlines()
self.m.step.active_result.presentation.logs['result_details'] = (
details_list)
finally:
self.m.file.rmtree('Remove details.html tmp files.', details_dir)
return step_result
def logcat_dump(self, gs_bucket=None):
if gs_bucket:
log_path = self.m.chromium.output_dir.join('full_log')
self.m.python(
'logcat_dump',
self.m.path['checkout'].join('build', 'android',
'adb_logcat_printer.py'),
[ '--output-path', log_path,
self.m.path['checkout'].join('out', 'logcat') ],
infra_step=True)
if self.m.tryserver.is_tryserver and not self.c.INTERNAL:
args = ['-a', 'public-read']
else:
args = []
self.m.gsutil.upload(
log_path,
gs_bucket,
'logcat_dumps/%s/%s' % (self.m.properties['buildername'],
self.m.properties['buildnumber']),
args=args,
link_name='logcat dump',
version='4.7',
parallel_upload=True)
else:
self.m.python(
'logcat_dump',
self.package_repo_resource('scripts', 'slave', 'tee.py'),
[self.m.chromium.output_dir.join('full_log'),
'--',
self.m.path['checkout'].join('build', 'android',
'adb_logcat_printer.py'),
self.m.path['checkout'].join('out', 'logcat')],
infra_step=True,
)
def stack_tool_steps(self, force_latest_version=False):
build_dir = self.m.path['checkout'].join('out',
self.m.chromium.c.BUILD_CONFIG)
log_file = build_dir.join('full_log')
target_arch = self.m.chromium.c.gyp_env.GYP_DEFINES['target_arch']
# gyp converts ia32 to x86, bot needs to do the same
target_arch = {'ia32': 'x86'}.get(target_arch) or target_arch
# --output-directory hasn't always exited on these scripts, so use the
# CHROMIUM_OUTPUT_DIR environment variable to avoid unrecognized flag
# failures on older script versions (e.g. when doing bisects).
# TODO(agrieve): Switch to --output-directory once we don't need bisects
# to be able to try revisions that happened before Feb 2016.
env = self.m.chromium.get_env()
env['CHROMIUM_OUTPUT_DIR'] = str(build_dir)
self.m.step(
'stack_tool_with_logcat_dump',
[self.m.path['checkout'].join('third_party', 'android_platform',
'development', 'scripts', 'stack'),
'--arch', target_arch, '--more-info', log_file],
env=env,
infra_step=True)
tombstones_cmd = [
self.m.path['checkout'].join('build', 'android', 'tombstones.py'),
'-a', '-s', '-w',
]
if (force_latest_version or
int(self.m.chromium.get_version().get('MAJOR', 0)) > 52):
tombstones_cmd += ['--adb-path', self.m.adb.adb_path()]
self.m.step(
'stack_tool_for_tombstones',
tombstones_cmd,
env=env,
infra_step=True)
if self.c.asan_symbolize:
self.m.step(
'stack_tool_for_asan',
[self.m.path['checkout'].join('build',
'android',
'asan_symbolize.py'),
'-l', log_file],
env=env,
infra_step=True)
def test_report(self):
self.m.python.inline(
'test_report',
"""
import glob, os, sys
for report in glob.glob(sys.argv[1]):
with open(report, 'r') as f:
for l in f.readlines():
print l
os.remove(report)
""",
args=[self.m.path['checkout'].join('out',
self.m.chromium.c.BUILD_CONFIG,
'test_logs',
'*.log')],
)
def common_tests_setup_steps(self, perf_setup=False,
remove_system_webview=False, skip_wipe=False):
self.create_adb_symlink()
self.spawn_logcat_monitor()
self.authorize_adb_devices()
self.device_recovery()
if perf_setup:
kwargs = {
'min_battery_level': 95,
'disable_network': True,
'disable_java_debug': True,
'max_battery_temp': 350}
else:
kwargs = {}
if skip_wipe:
kwargs['skip_wipe'] = True
self.provision_devices(remove_system_webview=remove_system_webview,
**kwargs)
self.device_status()
if self.m.chromium.c.gyp_env.GYP_DEFINES.get('asan', 0) == 1:
self.asan_device_setup()
self.spawn_device_monitor()
def common_tests_final_steps(self, logcat_gs_bucket='chromium-android'):
self.shutdown_device_monitor()
self.logcat_dump(gs_bucket=logcat_gs_bucket)
self.stack_tool_steps()
if self.m.chromium.c.gyp_env.GYP_DEFINES.get('asan', 0) == 1:
self.asan_device_teardown()
self.test_report()
def run_bisect_script(self, extra_src='', path_to_config='', **kwargs):
self.m.step('prepare bisect perf regression',
[self.m.path['checkout'].join('tools',
'prepare-bisect-perf-regression.py'),
'-w', self.m.path['slave_build']])
args = []
if extra_src:
args = args + ['--extra_src', extra_src]
if path_to_config:
args = args + ['--path_to_config', path_to_config]
self.m.step('run bisect perf regression',
[self.m.path['checkout'].join('tools',
'run-bisect-perf-regression.py'),
'-w', self.m.path['slave_build']] + args, **kwargs)
def run_test_suite(self, suite, verbose=True, isolate_file_path=None,
gtest_filter=None, tool=None,
name=None, json_results_file=None, shard_timeout=None,
args=None, **kwargs):
args = args or []
args.extend(['--blacklist-file', self.blacklist_file])
if verbose:
args.append('--verbose')
# TODO(agrieve): Remove once no more tests pass isolate_file_path (contained
# in wrapper scripts).
if isolate_file_path:
args.append('--isolate_file_path=%s' % isolate_file_path)
if gtest_filter:
args.append('--gtest_filter=%s' % gtest_filter)
if tool:
args.append('--tool=%s' % tool)
if json_results_file:
args.extend(['--json-results-file', json_results_file])
# TODO(agrieve): Remove once no more tests pass shard_timeout (contained in
# wrapper scripts).
if shard_timeout:
args.extend(['-t', str(shard_timeout)])
self.test_runner(
name or str(suite),
args=args,
wrapper_script_suite_name=suite,
env=self.m.chromium.get_env(),
**kwargs)
def run_java_unit_test_suite(self, suite, verbose=True,
json_results_file=None, suffix=None, **kwargs):
args = []
if verbose:
args.append('--verbose')
if self.c.BUILD_CONFIG == 'Release':
args.append('--release')
if json_results_file:
args.extend(['--json-results-file', json_results_file])
self.test_runner(
'%s%s' % (str(suite), ' (%s)' % suffix if suffix else ''),
['junit', '-s', suite] + args,
env=self.m.chromium.get_env(),
**kwargs)
def _set_webview_command_line(self, command_line_args):
"""Set the Android WebView command line.
Args:
command_line_args: A list of command line arguments you want set for
webview.
"""
_WEBVIEW_COMMAND_LINE = '/data/local/tmp/webview-command-line'
command_line_script_args = [
'--adb-path', self.m.adb.adb_path(),
'--device-path', _WEBVIEW_COMMAND_LINE,
'--executable', 'webview',
]
command_line_script_args.extend(command_line_args)
self.m.python('write webview command line file',
self.m.path['checkout'].join(
'build', 'android', 'adb_command_line.py'),
command_line_script_args)
def run_webview_cts(self, command_line_args=None, suffix=None,
android_platform='L', arch='arm_64'):
is_cts_v2 = (android_platform == 'N')
suffix = ' (%s)' % suffix if suffix else ''
if command_line_args:
self._set_webview_command_line(command_line_args)
_CTS_CONFIG_SRC_PATH = self.m.path['checkout'].join(
'android_webview', 'tools', 'cts_config')
cts_filenames_json = self.m.file.read(
'Fetch CTS filename data',
_CTS_CONFIG_SRC_PATH.join('webview_cts_gcs_path.json'),
test_data='''
{
"arm_64": {
"L": "cts_arm64_L.zip"
}
}''')
cts_filenames = self.m.json.loads(cts_filenames_json)
try:
cts_filename = cts_filenames[arch][android_platform]
except KeyError:
raise self.m.step.StepFailure(
'No CTS test found to use for arch:%s android:%s' % (
arch, android_platform))
expected_failure_json = self.m.file.read(
'Fetch expected failures data',
_CTS_CONFIG_SRC_PATH.join('expected_failure_on_bot.json'),
test_data = '''
{
"android.webkit.cts.ExampleBlacklistedTest":
[
{
"name": "testA",
"_bug_id": "crbug.com/123"
},
{"name": "testB"}
]
}''')
expected_failure = self.m.json.loads(expected_failure_json)
cts_base_dir = self.m.path['cache'].join('android_cts')
cts_zip_path = cts_base_dir.join(cts_filename)
cts_extract_dir = cts_base_dir.join('unzipped')
if not self.m.path.exists(cts_zip_path):
with self.m.step.nest('Update CTS'):
# Remove all old cts files before downloading new one.
self.m.file.rmtree('Delete old CTS', cts_base_dir)
self.m.file.makedirs('Create CTS dir', cts_base_dir)
self.m.gsutil.download(name='Download new CTS',
bucket='chromium-cts',
source=cts_filename,
dest=cts_zip_path)
self.m.zip.unzip(step_name='Extract new CTS',
zip_file=cts_zip_path,
output=cts_extract_dir)
cts_path = cts_extract_dir.join('android-cts', 'tools', 'cts-tradefed')
env = {'PATH': self.m.path.pathsep.join(
[self.m.adb.adb_dir(),
str(self.m.path['checkout'].join(
'third_party', 'android_tools', 'sdk', 'build-tools', '23.0.1')),
'%(PATH)s'])
}
try:
cts_v1_command = [cts_path, 'run', 'cts', '-p', 'android.webkit']
cts_v2_command = [cts_path, 'run', 'cts', '-m', 'CtsWebKitTestCases']
self.m.step('Run CTS%s' % suffix,
cts_v2_command if is_cts_v2 else cts_v1_command,
env=env, stdout=self.m.raw_io.output())
finally:
result = self.m.step.active_result
if result.stdout:
result.presentation.logs['stdout'] = result.stdout.splitlines()
result.presentation.logs['disabled_tests'] = (
expected_failure_json.split('\n'))
from xml.etree import ElementTree
def find_test_report_xml(test_output):
test_results_line = ('Test Result: ' if is_cts_v2 else
'Created xml report file at file://')
if test_output:
for line in test_output.splitlines():
split = line.split(test_results_line)
if (len(split) > 1):
return split[1]
raise self.m.step.StepFailure(
"Failed to parse the CTS output for the xml report file location")
report_xml = self.m.file.read('Read test result and report failures',
find_test_report_xml(result.stdout))
root = ElementTree.fromstring(report_xml)
not_executed_tests = []
unexpected_test_failures = []
_CTS_XML_TESTCASE_ELEMENTS = ('./TestPackage/TestSuite[@name="android"]/'
'TestSuite[@name="webkit"]/'
'TestSuite[@name="cts"]/TestCase')
test_classes = root.findall(_CTS_XML_TESTCASE_ELEMENTS)
for test_class in test_classes:
class_name = 'android.webkit.cts.%s' % test_class.get('name')
test_methods = test_class.findall('./Test')
for test_method in test_methods:
method_name = '%s#%s' % (class_name, test_method.get('name'))
if test_method.get('result') == 'notExecuted':
not_executed_tests.append(method_name)
elif (test_method.find('./FailedScene') is not None and
test_method.get('name') not in
[ t.get('name') for t in
expected_failure.get(class_name, []) ]):
unexpected_test_failures.append(method_name)
if unexpected_test_failures or not_executed_tests:
self.m.step.active_result.presentation.status = self.m.step.FAILURE
self.m.step.active_result.presentation.step_text += (
self.m.test_utils.format_step_text(
[['unexpected failures:', unexpected_test_failures],
['not executed:', not_executed_tests]]))
if unexpected_test_failures:
raise self.m.step.StepFailure("Unexpected Test Failures.")
if not_executed_tests:
raise self.m.step.StepFailure("Tests not executed.")
def coverage_report(self, upload=True, **kwargs):
"""Creates an EMMA HTML report and optionally uploads it to storage bucket.
Creates an EMMA HTML report using generate_emma_html.py, and uploads the
HTML report to the chrome-code-coverage storage bucket if |upload| is True.
Args:
upload: Uploads EMMA HTML report to storage bucket unless False is passed
in.
**kwargs: Kwargs for python and gsutil steps.
"""
assert self.c.coverage or self.c.incremental_coverage, (
'Trying to generate coverage report but coverage is not enabled')
self.m.python(
'Generate coverage report',
self.m.path['checkout'].join(
'build', 'android', 'generate_emma_html.py'),
args=['--coverage-dir', self.coverage_dir,
'--metadata-dir', self.out_path.join(self.c.BUILD_CONFIG),
'--cleanup',
'--output', self.coverage_dir.join('coverage_html',
'index.html')],
infra_step=True,
**kwargs)
if upload:
output_zip = self.coverage_dir.join('coverage_html.zip')
self.m.zip.directory(step_name='Zip generated coverage report files',
directory=self.coverage_dir.join('coverage_html'),
output=output_zip)
gs_dest = 'java/%s/%s/' % (
self.m.properties['buildername'], self.m.properties['revision'])
self.m.gsutil.upload(
source=output_zip,
bucket='chrome-code-coverage',
dest=gs_dest,
name='upload coverage report',
link_name='Coverage report',
version='4.7',
**kwargs)
def incremental_coverage_report(self):
"""Creates an incremental code coverage report.
Generates a JSON file containing incremental coverage stats. Requires
|file_changes_path| to contain a file with a valid JSON object.
"""
step_result = self.m.python(
'Incremental coverage report',
self.m.path.join('build', 'android', 'emma_coverage_stats.py'),
cwd=self.m.path['checkout'],
args=['-v',
'--out', self.m.json.output(),
'--emma-dir', self.coverage_dir.join('coverage_html'),
'--lines-for-coverage', self.file_changes_path],
step_test_data=lambda: self.m.json.test_api.output({
'files': {
'sample file 1': {
'absolute': {
'covered': 70,
'total': 100,
},
'incremental': {
'covered': 30,
'total': 50,
},
},
'sample file 2': {
'absolute': {
'covered': 50,
'total': 100,
},
'incremental': {
'covered': 50,
'total': 50,
},
},
},
'patch': {
'incremental': {
'covered': 80,
'total': 100,
},
},
})
)
if step_result.json.output:
covered_lines = step_result.json.output['patch']['incremental']['covered']
total_lines = step_result.json.output['patch']['incremental']['total']
percentage = covered_lines * 100.0 / total_lines if total_lines else 0
step_result.presentation.properties['summary'] = (
'Test coverage for this patch: %s/%s lines (%s%%).' % (
covered_lines,
total_lines,
int(percentage),
)
)
step_result.presentation.properties['moreInfoURL'] = self.m.url.join(
self.m.properties['buildbotURL'],
'builders',
self.m.properties['buildername'],
'builds',
self.m.properties['buildnumber'] or '0',
'steps',
'Incremental%20coverage%20report',
'logs',
'json.output',
)
def get_changed_lines_for_revision(self):
"""Saves a JSON file containing the files/lines requiring coverage analysis.
Saves a JSON object mapping file paths to lists of changed lines to the
coverage directory.
"""
# Git provides this default value for the commit hash for staged files when
# the -l option is used with git blame.
blame_cached_revision = '0000000000000000000000000000000000000000'
file_changes = {}
new_files = self.staged_files_matching_filter('A')
for new_file in new_files:
lines = self.m.file.read(
('Finding lines changed in added file %s' % new_file),
new_file,
test_data='int n = 0;\nn++;\nfor (int i = 0; i < n; i++) {'
)
file_changes[new_file] = range(1, len(lines.splitlines()) + 1)
changed_files = self.staged_files_matching_filter('M')
for changed_file in changed_files:
blame = self.m.git(
'blame', '-l', '-s', changed_file,
stdout=self.m.raw_io.output(),
name='Finding lines changed in modified file %s' % changed_file,
step_test_data=(
lambda: self.m.raw_io.test_api.stream_output(
'int n = 0;\nn++;\nfor (int i = 0; i < n; i++) {'))
)
blame_lines = blame.stdout.splitlines()
file_changes[changed_file] = [i + 1 for i, line in enumerate(blame_lines)
if line.startswith(blame_cached_revision)]
self.m.file.write(
'Saving changed lines for revision.',
self.file_changes_path,
self.m.json.dumps(file_changes)
)
def staged_files_matching_filter(self, diff_filter):
"""Returns list of files changed matching the provided diff-filter.
Args:
diff_filter: A string to be used as the diff-filter.
Returns:
A list of file paths (strings) matching the provided |diff-filter|.
"""
diff = self.m.git(
'diff', '--staged', '--name-only', '--diff-filter', diff_filter,
stdout=self.m.raw_io.output(),
name='Finding changed files matching diff filter: %s' % diff_filter,
step_test_data=(
lambda: self.m.raw_io.test_api.stream_output(
'fake/file1.java\nfake/file2.java;\nfake/file3.java'))
)
return diff.stdout.splitlines()
@contextlib.contextmanager
def handle_exit_codes(self):
"""Handles exit codes emitted by the test runner and other scripts."""
EXIT_CODES = {
'error': 1,
'infra': 87,
'warning': 88,
}
try:
yield
except self.m.step.StepFailure as f:
if (f.result.retcode == EXIT_CODES['infra']):
i = self.m.step.InfraFailure(f.name or f.reason, result=f.result)
i.result.presentation.status = self.m.step.EXCEPTION
raise i
elif (f.result.retcode == EXIT_CODES['warning']):
w = self.m.step.StepWarning(f.name or f.reason, result=f.result)
w.result.presentation.status = self.m.step.WARNING
raise w
elif (f.result.retcode == EXIT_CODES['error']):
f.result.presentation.status = self.m.step.FAILURE
raise
def test_runner(self, step_name, args=None, wrapper_script_suite_name=None,
**kwargs):
"""Wrapper for the python testrunner script.
Args:
step_name: Name of the step.
args: Testrunner arguments.
"""
if not args: # pragma: no cover
args = []
args.extend(['--adb-path', self.m.adb.adb_path()])
with self.handle_exit_codes():
script = self.c.test_runner
if wrapper_script_suite_name:
script = self.m.chromium.output_dir.join('bin', 'run_%s' %
wrapper_script_suite_name)
else:
env = kwargs.get('env', {})
env['CHROMIUM_OUTPUT_DIR'] = env.get('CHROMIUM_OUTPUT_DIR',
self.m.chromium.output_dir)
kwargs['env'] = env
return self.m.python(step_name, script, args, **kwargs)