blob: 8aee805260d7205be934d41bcd5857deb01bccd1 [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 textwrap
import urllib
from recipe_engine.types import freeze
from recipe_engine import recipe_api
_RESULT_DETAILS_LINK = 'result_details (logcats, flakiness links)'
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, include_filters=None,
exclude_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>.
preserve_paths: If True, files will be stored using the subdirectories
in the archive.
include_filters: List of globs to be included in the archive.
exclude_filters: List of globs to be excluded from 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 include_filters:
for f in include_filters:
archive_args.extend(['--include-filter', f])
if exclude_filters:
for f in exclude_filters:
archive_args.extend(['--exclude-filter', f])
if not preserve_paths: # pragma: no cover
archive_args.append('--ignore-subfolder-names')
self.m.build.python(
step_name,
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, manifest_name=None):
# TODO(jbudorick): Rewrite this to use chromium_checkout.
# 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
# TODO(jbudorick): Remove this after resolving crbug.com/794764.
self.m.file.rmtree(
'remove .cipd',
self.m.chromium_checkout.get_checkout_dir(spec).join('.cipd'))
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,
manifest_name=manifest_name)
else:
result = self.m.gclient.checkout(spec)
# 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['start_dir'].join('src')
self.clean_local_files()
return result
def clean_local_files(self, clean_pyc_files=True):
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')
python_inline_script = textwrap.dedent("""
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
""")
if clean_pyc_files:
python_inline_script += textwrap.dedent("""\
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))
""")
self.m.python.inline(
'clean local files',
python_inline_script,
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):
with self.m.context(cwd=self.m.path['checkout']):
return self.m.step(
'git_number',
[self.m.depot_tools.package_repo_resource('git_number.py')],
stdout = self.m.raw_io.output_text(),
step_test_data=(
lambda:
self.m.raw_io.test_api.stream_output('3000\n')
),
infra_step=True,
**kwargs)
def resource_sizes(self, apk_path, chartjson_file=False, perf_id=None,
step_suffix=''):
test_name = 'resource_sizes ({})'.format(self.m.path.basename(apk_path))
resource_sizes_args = [str(apk_path)]
if chartjson_file:
resource_sizes_args.append('--chartjson')
with self.handle_exit_codes():
self.m.chromium.runtest(
self.c.resource_sizes,
resource_sizes_args,
name=test_name + step_suffix,
perf_dashboard_id=test_name,
point_id=None,
test_type=test_name,
annotate=self.m.chromium.get_annotate_by_test_name(test_name),
results_url='https://chromeperf.appspot.com',
perf_id=perf_id or self.m.properties['buildername'],
chartjson_file=chartjson_file)
def supersize_archive(self, apk_path, size_path, step_suffix=''):
"""Creates a .size file for the given .apk."""
step_name = 'supersize archive ({}){}'.format(
self.m.path.basename(apk_path), step_suffix)
download_objdump_path = self.m.path['checkout'].join(
'tools', 'clang', 'scripts', 'download_objdump.py')
supersize_path = self.m.path['checkout'].join(
'tools', 'binary_size', 'supersize')
with self.m.context(env=self.m.chromium.get_env()):
self.m.python('download objdump', download_objdump_path)
return self.m.step(
step_name,
[supersize_path, 'archive', size_path, '--apk-file', apk_path, '-v'])
def upload_apks_for_bisect(self, update_properties, bucket, path):
"""Uploads android apks for functional bisects."""
archive_name = 'build_product.zip'
zipfile = self.m.path['checkout'].join('out', archive_name)
with self.m.context(cwd=self.m.path['checkout']):
self.make_zip_archive(
'package_apks_for_bisect',
archive_name,
files=['apks'],
preserve_paths=False,
)
# Get the commit postion for the revision to be used in archive name,
# if not found use the git hash.
try:
branch, rev = self.m.commit_position.parse(
update_properties.get('got_revision_cp'))
except ValueError: # pragma: no cover
rev = update_properties.get('got_revision')
self.m.gsutil.upload(
name='upload_apks_for_bisect',
source=zipfile,
bucket=bucket,
dest=path % rev)
def upload_build(self, bucket, path):
archive_name = 'build_product.zip'
zipfile = self.m.path['checkout'].join('out', archive_name)
with self.m.context(cwd=self.m.path['checkout']):
self.make_zip_archive(
'zip_build_product',
archive_name,
preserve_paths=True,
exclude_filters=[
"obj/*", "gen/*", # Default toolchain's obj/ and gen/
"*/obj/*", "*/gen/*", # Secondary toolchains' obj/ and gen/
"*.stamp", "*.d", # Files used only for incremental builds
"*.ninja", ".ninja_*", # Build files, .ninja_log, .ninja_deps
]
)
self.m.gsutil.upload(
name='upload_build_product',
source=zipfile,
bucket=bucket,
dest=path)
def download_build(self, bucket, path, extract_path=None, globs=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)
extract_path = extract_path or self.m.path['checkout']
globs = globs or []
with self.m.context(cwd=extract_path):
self.m.step(
'unzip_build_product',
['unzip', '-o', zipfile] + globs,
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['start_dir'].join('src'),
exclude_files='lib.target,gen,android_webview,jingle_unittests')
def use_devil_adb(self):
# TODO(jbudorick): Remove this after resolving
# https://github.com/catapult-project/catapult/issues/2901
devil_path = self.m.path['checkout'].join('third_party', 'catapult', 'devil')
self.m.python.inline(
'initialize devil',
"""
import sys
sys.path.append(sys.argv[1])
from devil import devil_env
devil_env.config.Initialize()
devil_env.config.PrefetchPaths(dependencies=['adb'])
""",
args=[devil_path])
self.m.adb.set_adb_path(
devil_path.join('bin', 'deps', 'linux2', 'x86_64', 'bin', 'adb'))
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):
with self.m.context(env=self.m.chromium.get_env()):
self.m.build.python(
'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()],
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.m.path['checkout'].join('third_party', 'catapult', 'devil',
'devil', 'android', 'tools',
'device_monitor.py'),
'--adb-path', self.m.adb.adb_path(),
'--blacklist-file', self.blacklist_file
]
self.m.build.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.build.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()]
with self.m.context(env=self.m.chromium.get_env()):
return self.m.build.python(
'authorize_adb_devices', script, args, infra_step=True)
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): # pragma: no cover
# TODO(jbudorick): Remove this once internal clients no longer use it.
return self.common_tests_setup_steps(
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)
@property
def blacklist_file(self):
return self.out_path.join('bad_devices.json')
def non_blacklisted_devices(self):
if not self.m.path.exists(self.blacklist_file):
return self.devices
step_result = self.m.json.read('read_blacklist_file', self.blacklist_file)
blacklisted_devices = step_result.json.output
return [s for s in self.devices if s not in blacklisted_devices]
def device_status_check(self, restart_usb=False, **kwargs):
self.device_recovery()
return self.device_status()
def host_info(self, args=None, **kwargs):
args = args or []
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()])
with self.m.context(env=self.m.chromium.get_env()):
results = self.m.step(
'Host Info',
[self.m.path['checkout'].join('testing', 'scripts',
'host_info.py')] + args,
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
# TODO(jbudorick): Remove restart_usb once it's unused.
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'
]
if self.c.restart_usb or restart_usb:
args += ['--enable-usb-reset']
with self.m.context(env=self.m.chromium.get_env()):
self.m.step(
'device_recovery',
[self.m.path['checkout'].join('third_party', 'catapult', 'devil',
'devil', 'android', 'tools',
'device_recovery.py')] + args,
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:
with self.m.context(env=self.m.chromium.get_env()):
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,
}
]),
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['bot_id'])),
'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,
reboot_timeout=None, 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,
'-v',
]
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 self.c and self.c.remove_system_packages:
args.append('--remove-system-packages')
args.extend(self.c.remove_system_packages)
if self.c and self.c.chrome_specific_wipe:
args.append('--chrome-specific-wipe')
if emulators:
args.append('--emulators')
if self.c and self.c.use_devil_provision:
provision_path = self.m.path['checkout'].join(
'third_party', 'catapult', 'devil', 'devil', 'android', 'tools',
'provision_devices.py')
else:
provision_path = self.m.path['checkout'].join(
'build', 'android', 'provision_devices.py')
with self.m.context(env=self.m.chromium.get_env()):
with self.handle_exit_codes():
result = self.m.python(
'provision_devices',
provision_path,
args=args,
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, keep_data=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 keep_data:
install_cmd.append('--keep_data')
if self.m.chromium.c.BUILD_CONFIG == 'Release':
install_cmd.append('--release')
with self.m.context(env=self.m.chromium.get_env()):
return self.m.step('install ' + self.m.path.basename(apk), install_cmd,
infra_step=True)
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()
with self.m.context(env=env):
for d in self.devices:
self.m.step(d,
cmd + ['--device', d],
infra_step=True)
self.wait_for_devices(self.devices, timeout=150)
def wait_for_devices(self, devices, timeout=None):
script = self.m.path['checkout'].join(
'third_party', 'catapult', 'devil', 'devil', 'android', 'tools',
'wait_for_devices.py')
args = [
'--adb-path', self.m.adb.adb_path(),
'-v'
]
if timeout:
args.extend([
'--timeout', timeout
])
args += devices
self.m.python('wait_for_devices', script, args, infra_step=True)
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_text(),
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')
]
try:
self._asan_device_setup(args)
except self.m.step.StepFailure:
# Attempt to restore the devices to a non-ASAN state.
self._asan_device_setup(['--revert'])
raise
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',
'--browser=%s' % self.c.channel,
'--event-count=50000',
'--blacklist-file', self.blacklist_file,
]
with self.m.context(env={'BUILDTYPE': self.c.BUILD_CONFIG}):
return self.test_runner(
'Monkey Test',
args,
**kwargs)
def _run_sharded_tests(self, config, test_trace=None,
write_buildbot_json=False):
args = [
'perf',
'--release',
'--verbose',
'--steps', config,
'--blacklist-file', self.blacklist_file,
'--num-retries', '0',
'--collect-chartjson-data'
]
if write_buildbot_json:
args.extend(['--write-buildbot-json'])
if test_trace:
args.extend(['--trace-output', test_trace])
with self.m.context(cwd=self.m.path['checkout'],
env=self.m.chromium.get_env()):
self.test_runner('Sharded Perf Tests', args)
def _upload_trace_results(self, trace_json_path, test_name):
dest = '{builder}/trace_{buildnumber}_{name}.html'.format(
builder=self.m.properties['buildername'],
buildnumber=self.m.properties['buildnumber'],
name=test_name)
test_data = lambda: self.m.raw_io.test_api.output_text('test data',
name='trace_html')
step_result = self.m.python(
name='Convert trace to HTML for %s' % test_name,
script=self.m.path['checkout'].join('third_party', 'catapult',
'tracing', 'bin',
'trace2html'),
args=[trace_json_path, '--output',
self.m.raw_io.output_text(name='trace_html')],
step_test_data=test_data)
trace_html = step_result.raw_io.output_texts['trace_html']
trace_html_path = self.m.raw_io.input_text(trace_html)
self.m.gsutil.upload(
name='Upload test trace for %s' % test_name,
source=trace_html_path,
bucket='chromium-testrunner-trace',
dest=dest,
link_name='Test Trace')
def run_sharded_perf_tests(self, config, test_type_transform=None,
upload_archives_to_bucket=None,
timestamp_as_point_id=False):
"""Run the perf tests from the given config file.
config: the path of the config file containing perf tests.
test_type_transform: a lambda transforming the test name to the
test_type to upload to.
upload_archives_to_bucket: an optional string, if given will create an
archive of all output files per test and upload to the bucket specified.
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.
"""
if test_type_transform is None:
test_type_transform = lambda x: x
with self.m.tempfile.temp_dir('test_runner_trace') as trace_dir:
test_trace_path = self.m.path.join(trace_dir, 'test_trace.json')
# test_runner.py actually runs the tests and records the results
self._run_sharded_tests(
config,
test_trace=test_trace_path,
# Need some extra buildbot data in archives when going to upload.
write_buildbot_json=bool(upload_archives_to_bucket))
self._upload_trace_results(test_trace_path, 'perf')
# now obtain the list of tests that were executed.
with self.m.context(env=self.m.chromium.get_env()):
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': 'perf_test.foo.reference', 'device_affinity': 0},
{'test': 'page_cycler.foo', 'device_affinity': 0}]),
)
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:
# TODO(phajdan.jr): restore coverage after moving to chromium/src .
perf_tests = [{'test': v} for v in perf_tests] # pragma: no cover
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])
try:
with self.handle_exit_codes():
env = self.m.chromium.get_env()
env['CHROMIUM_OUTPUT_DIR'] = self.m.chromium.output_dir
with self.m.context(env=env):
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=self.m.properties['buildername'],
chartjson_file=True)
except self.m.step.StepFailure as f:
# Only warn for failures on reference builds.
if test_name.endswith('.reference'):
if f.result.presentation.status == self.m.step.FAILURE:
f.result.presentation.status = self.m.step.WARNING
else:
failures.append(f)
else:
failures.append(f)
finally:
if 'device_affinity' in test_data:
self.m.step.active_result.presentation.step_text += (
self.m.test_utils.format_step_text(
[['Device Affinity: %s' % test_data['device_affinity']]]))
if archive:
dest = '{builder}/{test}/{timestamp}_build_{buildnumber}.zip'.format(
builder=self.m.properties['buildername'],
test=test_name,
timestamp=_TimestampToIsoFormat(test_end_time),
buildnumber=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,
flakiness_dashboard=None,
annotation=None, except_annotation=None,
screenshot=False, verbose=False, tool=None,
apk_package=None,
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,
store_tombstones=False,
trace_output=False,
args=None,
**kwargs):
args = args or []
args.extend(['--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 and not json_results_file:
json_results_file = self.m.test_utils.gtest_results(add_json_log=False)
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-file', 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 self.m.chromium.c.BUILD_CONFIG == 'Release':
args.append('--release')
step_name = '%s%s' % (
annotation or name, ' (%s)' % suffix if suffix else '')
if trace_output:
args.extend([
'--trace-output', self.m.raw_io.output_text(name='trace_json'),
'--trace-all'])
test_data = lambda: self.m.raw_io.test_api.output_text('{"test data"}',
name='trace_json')
kwargs['step_test_data'] = test_data
try:
self.test_runner(
step_name,
args=args,
wrapper_script_suite_name=wrapper_script_suite_name,
**kwargs)
finally:
result_step = self.m.step.active_result
if result_details:
if (hasattr(result_step, 'test_utils') and
hasattr(result_step.test_utils, 'gtest_results')):
json_results = self.m.json.input(
result_step.test_utils.gtest_results.raw)
details_link = self.create_result_details(step_name,
json_results)
self.m.step.active_result.presentation.links[_RESULT_DETAILS_LINK] = (
details_link)
if trace_output:
trace_json = result_step.raw_io.output_texts['trace_json']
trace_json_path = self.m.raw_io.input_text(trace_json)
self._upload_trace_results(trace_json_path, name)
# Need to copy gtest results over. A few places call
# |run_instrumentation_suite| function and then look for results in
# the active_result.
self.copy_gtest_results(result_step, self.m.step.active_result)
return result_step
def copy_gtest_results(self, result_step, active_step):
if (hasattr(result_step, 'test_utils') and
hasattr(result_step.test_utils, 'gtest_results')):
active_step.test_utils = result_step.test_utils
def create_result_details(self, step_name, json_results_file):
presentation_args = ['--json-file', json_results_file,
'--test-name', step_name,
'--builder-name', self.m.properties['buildername'],
'--build-number', self.m.properties['buildnumber'],
'--cs-base-url', self.c.cs_base_url,
'--bucket', self.c.results_bucket]
try:
result_details = self.m.python(
'%s: generate result details' % step_name,
script=self.m.path['checkout'].join(
'build', 'android', 'pylib', 'results', 'presentation',
'test_results_presentation.py'),
args=presentation_args,
stdout=self.m.raw_io.output_text(),
step_test_data=(
lambda: self.m.raw_io.test_api.stream_output(
'https://storage.cloud.google.com/chromium-result-details')))
return result_details.stdout.strip()
except self.m.step.StepFailure as f:
return ('https://storage.googleapis.com/chromium-result-details/'
'UploadQuietFailure.txt')
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',
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 generate_breakpad_symbols(
self, symbols_dir, binary_path, root_chromium_dir):
"""Generate breakpad symbols.
This step requires dump_syms binary to exist in the build dir.
Args:
symbols_dir: The directory to dump the breakpad symbols to.
binary_path: Path to binary to generate symbols for.
root_chromium_dir: Root Chromium directory.
"""
build_dir = root_chromium_dir.join(
'out', self.m.chromium.c.BUILD_CONFIG)
generate_symbols_args = ['--symbols-dir', symbols_dir,
'--build-dir', build_dir,
'--binary', binary_path]
self.m.python(('generate breakpad symbols for %s'
% self.m.path.basename(binary_path)),
root_chromium_dir.join(
'components', 'crash', 'content',
'tools', 'generate_breakpad_symbols.py'),
generate_symbols_args)
def stackwalker(self, root_chromium_dir, binary_paths):
"""Runs stack walker tool to symbolize breakpad crashes.
This step requires logcat file. The logcat monitor must have
been run on the bot.
Args:
binary_paths: Paths to binaries to generate breakpad symbols.
root_chromium_dir: Root Chromium directory.
"""
build_dir = root_chromium_dir.join(
'out', self.m.chromium.c.BUILD_CONFIG)
logcat = build_dir.join('full_log')
dump_syms_path = build_dir.join('dump_syms')
microdump_stackwalk_path = build_dir.join('microdump_stackwalk')
required_binaries = binary_paths + [
microdump_stackwalk_path, dump_syms_path]
if not all(map(self.m.path.exists, required_binaries)):
result = self.m.step(
'skipping stackwalker step',
['echo', 'Missing: %s' % ' '.join(
[str(b) for b in required_binaries
if not self.m.path.exists(b)])])
result.presentation.logs['info'] = [
'This bot appears to not have some of the binaries required to run ',
'stackwalker. No action is needed at this time; contact infra-dev@ ',
'for any questions or issues'
]
return
with self.m.tempfile.temp_dir('symbols') as temp_symbols_dir:
# TODO(mikecase): Only generate breakpad symbols if we
# know there is at least one breakpad crash. This step takes
# several minutes and we should only run it if we need to.
for binary in binary_paths:
self.generate_breakpad_symbols(
temp_symbols_dir, binary, root_chromium_dir)
stackwalker_args = ['--stackwalker-binary-path',
microdump_stackwalk_path,
'--stack-trace-path', logcat,
'--symbols-path', temp_symbols_dir]
self.m.python('symbolized breakpad crashes',
root_chromium_dir.join(
'build', 'android', 'stacktrace', 'stackwalker.py'),
stackwalker_args)
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)
with self.m.context(env=env):
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],
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()]
with self.m.context(env=env):
self.m.step(
'stack_tool_for_tombstones',
tombstones_cmd,
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],
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, **provision_kwargs):
if self.c.use_devil_adb:
self.use_devil_adb()
self.create_adb_symlink()
self.spawn_logcat_monitor()
self.spawn_device_monitor()
self.authorize_adb_devices()
# TODO(jbudorick): Restart USB only on perf bots while we
# figure out the fate of the usb reset in general.
self.device_recovery(restart_usb=perf_setup)
if perf_setup:
kwargs = {
'min_battery_level': 95,
'disable_network': True,
'disable_java_debug': True,
'max_battery_temp': 350}
else:
kwargs = {}
kwargs.update(provision_kwargs)
self.provision_devices(**kwargs)
self.device_status()
if self.m.chromium.c.gyp_env.GYP_DEFINES.get('asan', 0) == 1:
self.asan_device_setup()
def common_tests_final_steps(self, logcat_gs_bucket='chromium-android',
force_latest_version=False, checkout_dir=None):
try:
self.shutdown_device_monitor()
self.logcat_dump(gs_bucket=logcat_gs_bucket)
self.stack_tool_steps(force_latest_version)
finally:
if self.m.chromium.c.gyp_env.GYP_DEFINES.get('asan', 0) == 1:
self.asan_device_teardown()
self.test_report()
if checkout_dir:
binary_dir = self.m.chromium.output_dir.join('lib.unstripped')
breakpad_binaries = [binary_dir.join('libchrome.so')]
if self.m.path.exists(
binary_dir.join('libwebviewchromium.so')):
breakpad_binaries.append(binary_dir.join('libwebviewchromium.so'))
self.stackwalker(
root_chromium_dir=checkout_dir,
binary_paths=breakpad_binaries)
def android_build_wrapper(self, logcat_gs_bucket='chromium-android'):
@contextlib.contextmanager
def wrapper(api):
"""A context manager for use as auto_bisect's build_context_mgr.
This wraps every overall bisect run.
"""
try:
self.common_tests_setup_steps(perf_setup=True)
with api.context(cwd=api.path['checkout']):
api.chromium.runhooks()
yield
finally:
self.common_tests_final_steps(logcat_gs_bucket=logcat_gs_bucket)
return wrapper
def android_test_wrapper(self, _logcat_gs_bucket='chromium-android'):
@contextlib.contextmanager
def wrapper(_api):
"""A context manager for running android test steps."""
yield
return wrapper
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['start_dir']])
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['start_dir']] + args, **kwargs)
def run_test_suite(self, suite, verbose=True, tool=None,
result_details=False, store_tombstones=False, 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')
if tool:
args.append('--tool=%s' % tool)
if result_details and not json_results_file:
json_results_file = self.m.test_utils.gtest_results(add_json_log=False)
if json_results_file:
args.extend(['--json-results-file', json_results_file])
if store_tombstones:
args.append('--store-tombstones')
# TODO(agrieve): Remove once no more tests pass shard_timeout (contained in
# wrapper scripts).
if shard_timeout:
args.extend(['-t', str(shard_timeout)])
step_name = name or str(suite)
try:
with self.m.context(env=self.m.chromium.get_env()):
self.test_runner(
step_name,
args=args,
wrapper_script_suite_name=suite,
**kwargs)
finally:
result_step = self.m.step.active_result
if result_details:
if (hasattr(result_step, 'test_utils') and
hasattr(result_step.test_utils, 'gtest_results')):
json_results = self.m.json.input(
result_step.test_utils.gtest_results.raw)
details_link = self.create_result_details(step_name,
json_results)
self.m.step.active_result.presentation.links[_RESULT_DETAILS_LINK] = (
details_link)
self.copy_gtest_results(result_step,
self.m.step.active_result)
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])
with self.m.context(env=self.m.chromium.get_env()):
return self.test_runner(
'%s%s' % (str(suite), ' (%s)' % suffix if suffix else ''),
args=args,
wrapper_script_suite_name=str(suite),
pass_adb_path=False,
**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.
"""
command_line_script_args = [
'--adb-path', self.m.adb.adb_path(),
'--name', 'webview-command-line',
]
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, android_platform, arch,
command_line_args=None, suffix=None,
json_results_file=None,
result_details=False):
suffix = ' (%s)' % suffix if suffix else ''
if command_line_args:
self._set_webview_command_line(command_line_args)
cts_runner_args = ['--arch', arch,
'--platform', android_platform,
'--skip-expected-failures',
'--apk-dir', self.m.path['cache']]
if result_details and not json_results_file:
json_results_file = self.m.test_utils.gtest_results(add_json_log=False)
if json_results_file:
cts_runner_args.extend(['--json-results-file', json_results_file])
cts_runner_args.append('--verbose')
try:
self.m.python(
'Run CTS%s' % suffix,
self.m.path['checkout'].join(
'android_webview', 'tools', 'run_cts.py'),
cts_runner_args)
finally:
step_result = self.m.step.active_result
if (hasattr(step_result, 'test_utils') and
hasattr(step_result.test_utils, 'gtest_results')):
gtest_results = step_result.test_utils.gtest_results
step_result.presentation.step_text += (
self.m.test_utils.format_step_text(
[['failures:', gtest_results.failures]]))
if result_details:
json_results = self.m.json.input(
step_result.test_utils.gtest_results.raw)
details_link = self.create_result_details('CTS',
json_results)
self.m.step.active_result.presentation.links[_RESULT_DETAILS_LINK] = (
details_link)
self.copy_gtest_results(step_result, self.m.step.active_result)
return step_result
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',
**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.
"""
with self.m.context(cwd=self.m.path['checkout']):
step_result = self.m.python(
'Incremental coverage report',
self.m.path.join('build', 'android', 'emma_coverage_stats.py'),
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',
str(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_text(
('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:
with self.m.context(cwd=self.m.path['checkout']):
blame = self.m.git(
'blame', '-l', '-s', changed_file,
stdout=self.m.raw_io.output_text(),
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_text(
'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|.
"""
with self.m.context(cwd=self.m.path['checkout']):
diff = self.m.git(
'diff', '--staged', '--name-only', '--diff-filter', diff_filter,
stdout=self.m.raw_io.output_text(),
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,
pass_adb_path=True, **kwargs):
"""Wrapper for the python testrunner script.
Args:
step_name: Name of the step.
args: Testrunner arguments.
"""
if not args: # pragma: no cover
args = []
if pass_adb_path:
args.extend(['--adb-path', self.m.adb.adb_path()])
with self.handle_exit_codes():
script = self.c.test_runner
env = {}
if wrapper_script_suite_name:
script = self.m.chromium.output_dir.join('bin', 'run_%s' %
wrapper_script_suite_name)
else:
env['CHROMIUM_OUTPUT_DIR'] = self.m.context.env.get(
'CHROMIUM_OUTPUT_DIR', self.m.chromium.output_dir)
with self.m.context(env=env):
return self.m.python(step_name, script, args, venv=True, **kwargs)