Port Skia recipe to normal Python scripts, move to Skia repo

BUG=skia:4763
GOLD_TRYBOT_URL= https://gold.skia.org/search2?unt=true&query=source_type%3Dgm&master=false&issue=1703663002

Review URL: https://codereview.chromium.org/1703663002
This commit is contained in:
borenet 2016-02-18 08:05:48 -08:00 committed by Commit bot
parent e1fce93f36
commit d9fa758292
18 changed files with 1275 additions and 0 deletions

View File

@ -85,6 +85,7 @@ def _PythonChecks(input_api, output_api):
'R0201', # Method could be a function.
'E1003', # Using class name in super.
'W0613', # Unused argument.
'W0105', # String statement has no effect.
)
# Run Pylint on only the modified python files. Unfortunately it still runs
# Pylint on the whole file instead of just the modified lines.

18
infra/bots/README.md Normal file
View File

@ -0,0 +1,18 @@
Skia Buildbot Scripts
=====================
The scripts in this directory are ported from Skia's buildbot recipes and are
intended to run as standalone Python scripts either locally or via Swarming.
How to Run
----------
The scripts can be run by hand, eg:
$ cd infra/bots
$ python compile_skia.py Build-Ubuntu-GCC-x86_64-Debug ../../out
Or, you can run the scripts via Swarming:
$ isolate archive --isolate-server https://isolateserver.appspot.com/ -i infra/bots/compile_skia.isolate -s ../compile-skia.isolated --verbose --config-variable BUILDER_NAME=Build-Ubuntu-GCC-x86_64-Debug
$ swarming.py run --swarming https://chromium-swarm.appspot.com --isolate-server https://isolateserver.appspot.com --dimension os Ubuntu --dimension pool Skia --task-name compile-skia --io-timeout=3600 --hard-timeout=3600 ../compile-skia.isolated

150
infra/bots/common.py Normal file
View File

@ -0,0 +1,150 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import subprocess
import sys
from flavor import android_flavor
from flavor import chromeos_flavor
from flavor import cmake_flavor
from flavor import coverage_flavor
from flavor import default_flavor
from flavor import ios_flavor
from flavor import valgrind_flavor
from flavor import xsan_flavor
CONFIG_COVERAGE = 'Coverage'
CONFIG_DEBUG = 'Debug'
CONFIG_RELEASE = 'Release'
VALID_CONFIGS = (CONFIG_COVERAGE, CONFIG_DEBUG, CONFIG_RELEASE)
GM_ACTUAL_FILENAME = 'actual-results.json'
GM_EXPECTATIONS_FILENAME = 'expected-results.json'
GM_IGNORE_TESTS_FILENAME = 'ignored-tests.txt'
GS_GM_BUCKET = 'chromium-skia-gm'
GS_SUMMARIES_BUCKET = 'chromium-skia-gm-summaries'
SKIA_REPO = 'https://skia.googlesource.com/skia.git'
INFRA_REPO = 'https://skia.googlesource.com/buildbot.git'
SERVICE_ACCOUNT_FILE = 'service-account-skia.json'
SERVICE_ACCOUNT_INTERNAL_FILE = 'service-account-skia-internal.json'
def is_android(bot_cfg):
"""Determine whether the given bot is an Android bot."""
return ('Android' in bot_cfg.get('extra_config', '') or
bot_cfg.get('os') == 'Android')
def is_chromeos(bot_cfg):
return ('CrOS' in bot_cfg.get('extra_config', '') or
bot_cfg.get('os') == 'ChromeOS')
def is_cmake(bot_cfg):
return 'CMake' in bot_cfg.get('extra_config', '')
def is_ios(bot_cfg):
return ('iOS' in bot_cfg.get('extra_config', '') or
bot_cfg.get('os') == 'iOS')
def is_valgrind(bot_cfg):
return 'Valgrind' in bot_cfg.get('extra_config', '')
def is_xsan(bot_cfg):
return (bot_cfg.get('extra_config') == 'ASAN' or
bot_cfg.get('extra_config') == 'MSAN' or
bot_cfg.get('extra_config') == 'TSAN')
class BotInfo(object):
def __init__(self, bot_name, slave_name, out_dir):
"""Initialize the bot, given its name.
Assumes that CWD is the directory containing this file.
"""
self.name = bot_name
self.slave_name = slave_name
self.skia_dir = os.path.abspath(os.path.join(
os.path.dirname(os.path.realpath(__file__)),
os.pardir, os.pardir))
os.chdir(self.skia_dir)
self.build_dir = os.path.abspath(os.path.join(self.skia_dir, os.pardir))
self.out_dir = out_dir
self.spec = self.get_bot_spec(bot_name)
self.configuration = self.spec['configuration']
self.default_env = {
'SKIA_OUT': self.out_dir,
'BUILDTYPE': self.configuration,
'PATH': os.environ['PATH'],
}
self.default_env.update(self.spec['env'])
self.build_targets = [str(t) for t in self.spec['build_targets']]
self.bot_cfg = self.spec['builder_cfg']
self.is_trybot = self.bot_cfg['is_trybot']
self.upload_dm_results = self.spec['upload_dm_results']
self.upload_perf_results = self.spec['upload_perf_results']
self.dm_flags = self.spec['dm_flags']
self.nanobench_flags = self.spec['nanobench_flags']
self._ccache = None
self._checked_for_ccache = False
self.flavor = self.get_flavor(self.bot_cfg)
@property
def ccache(self):
if not self._checked_for_ccache:
self._checked_for_ccache = True
if sys.platform != 'win32':
try:
result = subprocess.check_output(['which', 'ccache'])
self._ccache = result.rstrip()
except subprocess.CalledProcessError:
pass
return self._ccache
def get_bot_spec(self, bot_name):
"""Retrieve the bot spec for this bot."""
sys.path.append(self.skia_dir)
from tools import buildbot_spec
return buildbot_spec.get_builder_spec(bot_name)
def get_flavor(self, bot_cfg):
"""Return a flavor utils object specific to the given bot."""
if is_android(bot_cfg):
return android_flavor.AndroidFlavorUtils(self)
elif is_chromeos(bot_cfg):
return chromeos_flavor.ChromeOSFlavorUtils(self)
elif is_cmake(bot_cfg):
return cmake_flavor.CMakeFlavorUtils(self)
elif is_ios(bot_cfg):
return ios_flavor.iOSFlavorUtils(self)
elif is_valgrind(bot_cfg):
return valgrind_flavor.ValgrindFlavorUtils(self)
elif is_xsan(bot_cfg):
return xsan_flavor.XSanFlavorUtils(self)
elif bot_cfg.get('configuration') == CONFIG_COVERAGE:
return coverage_flavor.CoverageFlavorUtils(self)
else:
return default_flavor.DefaultFlavorUtils(self)
def run(self, cmd, env=None, cwd=None):
_env = {}
_env.update(self.default_env)
_env.update(env or {})
cwd = cwd or self.skia_dir
print '============'
print 'CMD: %s' % cmd
print 'CWD: %s' % cwd
print 'ENV: %s' % _env
print '============'
subprocess.check_call(cmd, env=_env, cwd=cwd)

View File

@ -0,0 +1,10 @@
{
'includes': [
'skia_repo.isolate',
],
'variables': {
'command': [
'python', 'compile_skia.py', '<(BUILDER_NAME)', '${ISOLATED_OUTDIR}/out',
],
},
}

View File

@ -0,0 +1,23 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import common
import sys
def main():
if len(sys.argv) != 3:
print >> sys.stderr, 'Usage: compile_skia.py <builder name> <out-dir>'
sys.exit(1)
bot = common.BotInfo(sys.argv[1], 'fake-slave', sys.argv[2])
for t in bot.build_targets:
bot.flavor.compile(t)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,6 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

View File

@ -0,0 +1,100 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import json
DEFAULT_SDK_ROOT = '/home/chrome-bot/android-sdk-linux'
MAC_SDK_ROOT = '/Users/chrome-bot/adt-bundle-mac-x86_64-20140702/sdk'
MACMINI_SDK_ROOT = '/Users/chrome-bot/android-sdk-macosx'
SlaveInfo = collections.namedtuple('SlaveInfo',
'serial android_sdk_root has_root')
SLAVE_INFO = {
'skiabot-mac-10_8-compile-000':
SlaveInfo('noserial', MAC_SDK_ROOT, True),
'skiabot-mac-10_8-compile-001':
SlaveInfo('noserial', MAC_SDK_ROOT, True),
'skiabot-mac-10_8-compile-002':
SlaveInfo('noserial', MAC_SDK_ROOT, True),
'skiabot-mac-10_8-compile-003':
SlaveInfo('noserial', MAC_SDK_ROOT, True),
'skiabot-mac-10_8-compile-004':
SlaveInfo('noserial', MAC_SDK_ROOT, True),
'skiabot-mac-10_8-compile-005':
SlaveInfo('noserial', MAC_SDK_ROOT, True),
'skiabot-mac-10_8-compile-006':
SlaveInfo('noserial', MAC_SDK_ROOT, True),
'skiabot-mac-10_8-compile-007':
SlaveInfo('noserial', MAC_SDK_ROOT, True),
'skiabot-mac-10_8-compile-008':
SlaveInfo('noserial', MAC_SDK_ROOT, True),
'skiabot-mac-10_8-compile-009':
SlaveInfo('noserial', MAC_SDK_ROOT, True),
'skiabot-shuttle-ubuntu15-androidone-001':
SlaveInfo('AG86044202A04GC', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu15-androidone-002':
SlaveInfo('AG8404EC06G02GC', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu15-androidone-003':
SlaveInfo('AG8404EC0688EGC', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-galaxys3-001':
SlaveInfo('4df713b8244a21cf', DEFAULT_SDK_ROOT, False),
'skiabot-shuttle-ubuntu12-galaxys3-002':
SlaveInfo('32309a56e9b3a09f', DEFAULT_SDK_ROOT, False),
'skiabot-shuttle-ubuntu12-galaxys4-001':
SlaveInfo('4d0032a5d8cb6125', MACMINI_SDK_ROOT, False),
'skiabot-shuttle-ubuntu12-galaxys4-002':
SlaveInfo('4d00353cd8ed61c3', MACMINI_SDK_ROOT, False),
'skiabot-shuttle-ubuntu12-nexus5-001':
SlaveInfo('03f61449437cc47b', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexus5-002':
SlaveInfo('018dff3520c970f6', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu15-nexus6-001':
SlaveInfo('ZX1G22JJWS', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu15-nexus6-002':
SlaveInfo('ZX1G22JN35', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu15-nexus6-003':
SlaveInfo('ZX1G22JXXM', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexus7-001':
SlaveInfo('015d210a13480604', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexus7-002':
SlaveInfo('015d18848c280217', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexus7-003':
SlaveInfo('015d16897c401e17', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexus9-001':
SlaveInfo('HT43RJT00022', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexus9-002':
SlaveInfo('HT4AEJT03112', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexus9-003':
SlaveInfo('HT4ADJT03339', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexus10-001':
SlaveInfo('R32C801B5LH', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexus10-003':
SlaveInfo('R32CB017X2L', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexusplayer-001':
SlaveInfo('D76C708B', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu12-nexusplayer-002':
SlaveInfo('8AB5139A', DEFAULT_SDK_ROOT, True),
'skiabot-shuttle-ubuntu15-nvidia-shield-001':
SlaveInfo('04217150066510000078', MACMINI_SDK_ROOT, False),
'skiabot-linux-housekeeper-003':
SlaveInfo('noserial', DEFAULT_SDK_ROOT, False),
'vm690-m3': SlaveInfo('noserial', MACMINI_SDK_ROOT, False),
'vm691-m3': SlaveInfo('noserial', MACMINI_SDK_ROOT, False),
'vm692-m3': SlaveInfo('noserial', MACMINI_SDK_ROOT, False),
'vm693-m3': SlaveInfo('noserial', MACMINI_SDK_ROOT, False),
'default':
SlaveInfo('noserial', DEFAULT_SDK_ROOT, False),
}
if __name__ == '__main__':
print json.dumps(SLAVE_INFO) # pragma: no cover

View File

@ -0,0 +1,241 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import android_devices
import default_flavor
import os
"""Android flavor utils, used for building for and running tests on Android."""
class _ADBWrapper(object):
"""Wrapper for ADB."""
def __init__(self, path_to_adb, serial, android_flavor):
self._adb = path_to_adb
self._serial = serial
self._wait_count = 0
self._android_flavor = android_flavor
def wait_for_device(self):
"""Run 'adb wait-for-device'."""
self._wait_count += 1
cmd = [
os.path.join(self._android_flavor.android_bin, 'adb_wait_for_device'),
'-s', self._serial,
]
self._android_flavor._bot_info.run(
cmd, env=self._android_flavor._default_env)
def maybe_wait_for_device(self):
"""Run 'adb wait-for-device' if it hasn't already been run."""
if self._wait_count == 0:
self.wait_for_device()
def __call__(self, *args, **kwargs):
self.maybe_wait_for_device()
return self._android_flavor._bot_info.run(self._adb + args, **kwargs)
class AndroidFlavorUtils(default_flavor.DefaultFlavorUtils):
def __init__(self, skia_api):
super(AndroidFlavorUtils, self).__init__(skia_api)
self.device = self._bot_info.spec['device_cfg']
slave_info = android_devices.SLAVE_INFO.get(
self._bot_info.slave_name,
android_devices.SLAVE_INFO['default'])
self.serial = slave_info.serial
self.android_bin = os.path.join(
self._bot_info.skia_dir, 'platform_tools', 'android', 'bin')
self._android_sdk_root = slave_info.android_sdk_root
self._adb = _ADBWrapper(
os.path.join(self._android_sdk_root, 'platform-tools', 'adb'),
self.serial,
self)
self._has_root = slave_info.has_root
self._default_env = {'ANDROID_SDK_ROOT': self._android_sdk_root,
'ANDROID_HOME': self._android_sdk_root,
'SKIA_ANDROID_VERBOSE_SETUP': '1'}
def step(self, name, cmd, env=None, **kwargs):
self._adb.maybe_wait_for_device()
args = [self.android_bin.join('android_run_skia'),
'--verbose',
'--logcat',
'-d', self.device,
'-s', self.serial,
'-t', self._bot_info.configuration,
]
env = dict(env or {})
env.update(self._default_env)
return self._bot_info.run(self._bot_info.m.step, name=name, cmd=args + cmd,
env=env, **kwargs)
def compile(self, target):
"""Build the given target."""
env = dict(self._default_env)
ccache = self._bot_info.ccache
if ccache:
env['ANDROID_MAKE_CCACHE'] = ccache
cmd = [os.path.join(self.android_bin, 'android_ninja'), target,
'-d', self.device]
if 'Clang' in self._bot_info.name:
cmd.append('--clang')
self._bot_info.run(cmd, env=env)
def device_path_join(self, *args):
"""Like os.path.join(), but for paths on a connected Android device."""
return '/'.join(args)
def device_path_exists(self, path):
"""Like os.path.exists(), but for paths on a connected device."""
exists_str = 'FILE_EXISTS'
return exists_str in self._adb(
name='exists %s' % self._bot_info.m.path.basename(path),
serial=self.serial,
cmd=['shell', 'if', '[', '-e', path, '];',
'then', 'echo', exists_str + ';', 'fi'],
stdout=self._bot_info.m.raw_io.output(),
infra_step=True
).stdout
def _remove_device_dir(self, path):
"""Remove the directory on the device."""
self._adb(name='rmdir %s' % self._bot_info.m.path.basename(path),
serial=self.serial,
cmd=['shell', 'rm', '-r', path],
infra_step=True)
# Sometimes the removal fails silently. Verify that it worked.
if self.device_path_exists(path):
raise Exception('Failed to remove %s!' % path) # pragma: no cover
def _create_device_dir(self, path):
"""Create the directory on the device."""
self._adb(name='mkdir %s' % self._bot_info.m.path.basename(path),
serial=self.serial,
cmd=['shell', 'mkdir', '-p', path],
infra_step=True)
def copy_directory_contents_to_device(self, host_dir, device_dir):
"""Like shutil.copytree(), but for copying to a connected device."""
self._bot_info.run(
self._bot_info.m.step,
name='push %s' % self._bot_info.m.path.basename(host_dir),
cmd=[self.android_bin.join('adb_push_if_needed'), '--verbose',
'-s', self.serial, host_dir, device_dir],
env=self._default_env,
infra_step=True)
def copy_directory_contents_to_host(self, device_dir, host_dir):
"""Like shutil.copytree(), but for copying from a connected device."""
self._bot_info.run(
self._bot_info.m.step,
name='pull %s' % self._bot_info.m.path.basename(device_dir),
cmd=[self.android_bin.join('adb_pull_if_needed'), '--verbose',
'-s', self.serial, device_dir, host_dir],
env=self._default_env,
infra_step=True)
def copy_file_to_device(self, host_path, device_path):
"""Like shutil.copyfile, but for copying to a connected device."""
self._adb(name='push %s' % self._bot_info.m.path.basename(host_path),
serial=self.serial,
cmd=['push', host_path, device_path],
infra_step=True)
def create_clean_device_dir(self, path):
"""Like shutil.rmtree() + os.makedirs(), but on a connected device."""
self._remove_device_dir(path)
self._create_device_dir(path)
def install(self):
"""Run device-specific installation steps."""
if self._has_root:
self._adb(name='adb root',
serial=self.serial,
cmd=['root'],
infra_step=True)
# Wait for the device to reconnect.
self._bot_info.run(
self._bot_info.m.step,
name='wait',
cmd=['sleep', '10'],
infra_step=True)
self._adb.wait_for_device()
# TODO(borenet): Set CPU scaling mode to 'performance'.
self._bot_info.run(self._bot_info.m.step,
name='kill skia',
cmd=[self.android_bin.join('android_kill_skia'),
'--verbose', '-s', self.serial],
env=self._default_env,
infra_step=True)
if self._has_root:
self._adb(name='stop shell',
serial=self.serial,
cmd=['shell', 'stop'],
infra_step=True)
# Print out battery stats.
self._adb(name='starting battery stats',
serial=self.serial,
cmd=['shell', 'dumpsys', 'batteryproperties'],
infra_step=True)
def cleanup_steps(self):
"""Run any device-specific cleanup steps."""
self._adb(name='final battery stats',
serial=self.serial,
cmd=['shell', 'dumpsys', 'batteryproperties'],
infra_step=True)
self._adb(name='reboot',
serial=self.serial,
cmd=['reboot'],
infra_step=True)
self._bot_info.run(
self._bot_info.m.step,
name='wait for reboot',
cmd=['sleep', '10'],
infra_step=True)
self._adb.wait_for_device()
def read_file_on_device(self, path, *args, **kwargs):
"""Read the given file."""
return self._adb(name='read %s' % self._bot_info.m.path.basename(path),
serial=self.serial,
cmd=['shell', 'cat', path],
stdout=self._bot_info.m.raw_io.output(),
infra_step=True).stdout.rstrip()
def remove_file_on_device(self, path, *args, **kwargs):
"""Delete the given file."""
return self._adb(name='rm %s' % self._bot_info.m.path.basename(path),
serial=self.serial,
cmd=['shell', 'rm', '-f', path],
infra_step=True,
*args,
**kwargs)
def get_device_dirs(self):
""" Set the directories which will be used by the build steps."""
device_scratch_dir = self._adb(
name='get EXTERNAL_STORAGE dir',
serial=self.serial,
cmd=['shell', 'echo', '$EXTERNAL_STORAGE'],
)
prefix = self.device_path_join(device_scratch_dir, 'skiabot', 'skia_')
return default_flavor.DeviceDirs(
dm_dir=prefix + 'dm',
perf_data_dir=prefix + 'perf',
resource_dir=prefix + 'resources',
images_dir=prefix + 'images',
skp_dir=prefix + 'skp/skps',
tmp_dir=prefix + 'tmp_dir')

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import default_flavor
import os
import ssh_flavor
"""Utils for building for and running tests on ChromeOS."""
class ChromeOSFlavorUtils(ssh_flavor.SSHFlavorUtils):
def __init__(self, bot_info):
super(ChromeOSFlavorUtils, self).__init__(bot_info)
self.board = self._bot_info.spec['device_cfg']
self.device_root_dir = '/usr/local/skiabot'
self.device_bin_dir = self.device_path_join(self.device_root_dir, 'bin')
def step(self, name, cmd, **kwargs):
"""Wrapper for the Step API; runs a step as appropriate for this flavor."""
local_path = self._bot_info.out_dir.join(
'config', 'chromeos-%s' % self.board,
self._bot_info.configuration, cmd[0])
remote_path = self.device_path_join(self.device_bin_dir, cmd[0])
self.copy_file_to_device(local_path, remote_path)
super(ChromeOSFlavorUtils, self).step(name=name,
cmd=[remote_path]+cmd[1:],
**kwargs)
def compile(self, target):
"""Build the given target."""
cmd = [os.path.join(self._bot_info.skia_dir, 'platform_tools', 'chromeos',
'bin', 'chromeos_make'),
'-d', self.board,
target]
self._bot_info.run(cmd)
def install(self):
"""Run any device-specific installation steps."""
self.create_clean_device_dir(self.device_bin_dir)
def get_device_dirs(self):
""" Set the directories which will be used by the build steps."""
prefix = self.device_path_join(self.device_root_dir, 'skia_')
def join(suffix):
return ''.join((prefix, suffix))
return default_flavor.DeviceDirs(
dm_dir=join('dm_out'), # 'dm' conflicts with the binary
perf_data_dir=join('perf'),
resource_dir=join('resources'),
images_dir=join('images'),
skp_dir=self.device_path_join(join('skp'), 'skps'),
tmp_dir=join('tmp_dir'))

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import default_flavor
import os
"""CMake flavor utils, used for building Skia with CMake."""
class CMakeFlavorUtils(default_flavor.DefaultFlavorUtils):
def compile(self, target):
"""Build Skia with CMake. Ignores `target`."""
cmake_build = os.path.join(self._bot_info.skia_dir, 'cmake', 'cmake_build')
self._bot_info.run([cmake_build, target])

View File

@ -0,0 +1,114 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import default_flavor
import os
import subprocess
import time
"""Utils for running coverage tests."""
class CoverageFlavorUtils(default_flavor.DefaultFlavorUtils):
def compile(self, target):
"""Build the given target."""
cmd = [os.path.join(self._bot_info.skia_dir, 'tools',
'llvm_coverage_build'),
target]
self._bot_info.run(cmd)
def step(self, cmd, **kwargs):
"""Run the given step through coverage."""
# Slice out the 'key' and 'properties' arguments to be reused.
key = []
properties = []
current = None
for i in xrange(0, len(cmd)):
if isinstance(cmd[i], basestring) and cmd[i] == '--key':
current = key
elif isinstance(cmd[i], basestring) and cmd[i] == '--properties':
current = properties
elif isinstance(cmd[i], basestring) and cmd[i].startswith('--'):
current = None
if current is not None:
current.append(cmd[i])
results_dir = self._bot_info.out_dir.join('coverage_results')
self.create_clean_host_dir(results_dir)
# Run DM under coverage.
report_file_basename = '%s.cov' % self._bot_info.got_revision
report_file = os.path.join(results_dir, report_file_basename)
args = [
'python',
os.path.join(self._bot_info.skia_dir, 'tools', 'llvm_coverage_run.py'),
] + cmd + ['--outResultsFile', report_file]
self._bot_info.run(args, **kwargs)
# Generate nanobench-style JSON output from the coverage report.
git_timestamp = subprocess.check_output(['git', 'log', '-n1',
self._bot_info.got_revision, '--format=%%ci']).rstrip()
nanobench_json = results_dir.join('nanobench_%s_%s.json' % (
self._bot_info.got_revision, git_timestamp))
line_by_line_basename = ('coverage_by_line_%s_%s.json' % (
self._bot_info.got_revision, git_timestamp))
line_by_line = results_dir.join(line_by_line_basename)
args = [
'python',
os.path.join(self._bot_info.skia_dir, 'tools',
'parse_llvm_coverage.py'),
'--report', report_file, '--nanobench', nanobench_json,
'--linebyline', line_by_line]
args.extend(key)
args.extend(properties)
self._bot_info.run(args)
# Upload raw coverage data.
now = time.utcnow()
gs_json_path = '/'.join((
str(now.year).zfill(4), str(now.month).zfill(2),
str(now.day).zfill(2), str(now.hour).zfill(2),
self._bot_info.name,
str(self._bot_info.build_number)))
if self._bot_info.is_trybot:
gs_json_path = '/'.join(('trybot', gs_json_path,
str(self._bot_info.issue)))
self._bot_info.gsutil_upload(
'upload raw coverage data',
source=report_file,
bucket='skia-infra',
dest='/'.join(('coverage-raw-v1', gs_json_path, report_file_basename)))
# Upload nanobench JSON data.
gsutil_path = self._bot_info.m.path['depot_tools'].join(
'third_party', 'gsutil', 'gsutil')
upload_args = [self._bot_info.name,
self._bot_info.m.properties['buildnumber'],
results_dir,
self._bot_info.got_revision, gsutil_path]
if self._bot_info.is_trybot:
upload_args.append(self._bot_info.m.properties['issue'])
self._bot_info.run(
self._bot_info.m.python,
'upload nanobench coverage results',
script=self._bot_info.resource('upload_bench_results.py'),
args=upload_args,
cwd=self._bot_info.m.path['checkout'],
abort_on_failure=False,
infra_step=True)
# Upload line-by-line coverage data.
self._bot_info.gsutil_upload(
'upload line-by-line coverage data',
source=line_by_line,
bucket='skia-infra',
dest='/'.join(('coverage-json-v1', gs_json_path,
line_by_line_basename)))

View File

@ -0,0 +1,172 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Default flavor utils class, used for desktop bots."""
import os
import shutil
import sys
class DeviceDirs(object):
def __init__(self,
dm_dir,
perf_data_dir,
resource_dir,
images_dir,
skp_dir,
tmp_dir):
self._dm_dir = dm_dir
self._perf_data_dir = perf_data_dir
self._resource_dir = resource_dir
self._images_dir = images_dir
self._skp_dir = skp_dir
self._tmp_dir = tmp_dir
@property
def dm_dir(self):
"""Where DM writes."""
return self._dm_dir
@property
def perf_data_dir(self):
return self._perf_data_dir
@property
def resource_dir(self):
return self._resource_dir
@property
def images_dir(self):
return self._images_dir
@property
def skp_dir(self):
return self._skp_dir
@property
def tmp_dir(self):
return self._tmp_dir
class DefaultFlavorUtils(object):
"""Utilities to be used by build steps.
The methods in this class define how certain high-level functions should
work. Each build step flavor should correspond to a subclass of
DefaultFlavorUtils which may override any of these functions as appropriate
for that flavor.
For example, the AndroidFlavorUtils will override the functions for
copying files between the host and Android device, as well as the
'step' function, so that commands may be run through ADB.
"""
def __init__(self, bot_info, *args, **kwargs):
self._bot_info = bot_info
self.chrome_path = os.path.join(os.path.expanduser('~'), 'src')
def step(self, cmd, **kwargs):
"""Runs a step as appropriate for this flavor."""
path_to_app = self._bot_info.out_dir.join(
self._bot_info.configuration, cmd[0])
if (sys.platform == 'linux' and
'x86_64' in self._bot_info.bot_name and
not 'TSAN' in self._bot_info.bot_name):
new_cmd = ['catchsegv', path_to_app]
else:
new_cmd = [path_to_app]
new_cmd.extend(cmd[1:])
return self._bot_info.run(new_cmd, **kwargs)
def compile(self, target):
"""Build the given target."""
# The CHROME_PATH environment variable is needed for bots that use
# toolchains downloaded by Chrome.
env = {'CHROME_PATH': self.chrome_path}
if sys.platform == 'win32':
make_cmd = ['python', 'make.py']
else:
make_cmd = ['make']
cmd = make_cmd + [target]
self._bot_info.run(cmd, env=env)
def device_path_join(self, *args):
"""Like os.path.join(), but for paths on a connected device."""
return os.path.join(*args)
def device_path_exists(self, path):
"""Like os.path.exists(), but for paths on a connected device."""
return os.path.exists(path, infra_step=True) # pragma: no cover
def copy_directory_contents_to_device(self, host_dir, device_dir):
"""Like shutil.copytree(), but for copying to a connected device."""
# For "normal" bots who don't have an attached device, we expect
# host_dir and device_dir to be the same.
if str(host_dir) != str(device_dir):
raise ValueError('For bots who do not have attached devices, copying '
'from host to device is undefined and only allowed if '
'host_path and device_path are the same (%s vs %s).' % (
str(host_dir), str(device_dir))) # pragma: no cover
def copy_directory_contents_to_host(self, device_dir, host_dir):
"""Like shutil.copytree(), but for copying from a connected device."""
# For "normal" bots who don't have an attached device, we expect
# host_dir and device_dir to be the same.
if str(host_dir) != str(device_dir):
raise ValueError('For bots who do not have attached devices, copying '
'from device to host is undefined and only allowed if '
'host_path and device_path are the same (%s vs %s).' % (
str(host_dir), str(device_dir))) # pragma: no cover
def copy_file_to_device(self, host_path, device_path):
"""Like shutil.copyfile, but for copying to a connected device."""
# For "normal" bots who don't have an attached device, we expect
# host_dir and device_dir to be the same.
if str(host_path) != str(device_path): # pragma: no cover
raise ValueError('For bots who do not have attached devices, copying '
'from host to device is undefined and only allowed if '
'host_path and device_path are the same (%s vs %s).' % (
str(host_path), str(device_path)))
def create_clean_device_dir(self, path):
"""Like shutil.rmtree() + os.makedirs(), but on a connected device."""
self.create_clean_host_dir(path)
def create_clean_host_dir(self, path):
"""Convenience function for creating a clean directory."""
shutil.rmtree(path)
os.makedirs(path)
def install(self):
"""Run device-specific installation steps."""
pass
def cleanup_steps(self):
"""Run any device-specific cleanup steps."""
pass
def get_device_dirs(self):
""" Set the directories which will be used by the build steps.
These refer to paths on the same device where the test executables will
run, for example, for Android bots these are paths on the Android device
itself. For desktop bots, these are just local paths.
"""
join = lambda p: os.path.join(self._bot_info.build_dir, p)
return DeviceDirs(
dm_dir=join('dm'),
perf_data_dir=self._bot_info.perf_data_dir,
resource_dir=self._bot_info.resource_dir,
images_dir=join('images'),
skp_dir=self._bot_info.local_skp_dir,
tmp_dir=join('tmp'))
def __repr__(self):
return '<%s object>' % self.__class__.__name__ # pragma: no cover

View File

@ -0,0 +1,114 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import default_flavor
import os
import subprocess
"""iOS flavor utils, used for building for and running tests on iOS."""
class iOSFlavorUtils(default_flavor.DefaultFlavorUtils):
def __init__(self, bot_info):
super(iOSFlavorUtils, self).__init__(bot_info)
self.ios_bin = os.path.join(self._bot_info.skia_dir, 'platform_tools',
'ios', 'bin')
def step(self, cmd, **kwargs):
args = [os.path.join(self.ios_bin, 'ios_run_skia')]
# Convert 'dm' and 'nanobench' from positional arguments
# to flags, which is what iOSShell expects to select which
# one is being run.
cmd = ["--" + c if c in ['dm', 'nanobench'] else c
for c in cmd]
return self._bot_info.run(args + cmd, **kwargs)
def compile(self, target):
"""Build the given target."""
cmd = [os.path.join(self.ios_bin, 'ios_ninja')]
self._bot_info.run(cmd)
def device_path_join(self, *args):
"""Like os.path.join(), but for paths on a connected iOS device."""
return '/'.join(args)
def device_path_exists(self, path):
"""Like os.path.exists(), but for paths on a connected device."""
return self._bot_info.run(
[os.path.join(self.ios_bin, 'ios_path_exists'), path],
) # pragma: no cover
def _remove_device_dir(self, path):
"""Remove the directory on the device."""
return self._bot_info.run(
[os.path.join(self.ios_bin, 'ios_rm'), path],
)
def _create_device_dir(self, path):
"""Create the directory on the device."""
return self._bot_info.run(
[os.path.join(self.ios_bin, 'ios_mkdir'), path],
)
def copy_directory_contents_to_device(self, host_dir, device_dir):
"""Like shutil.copytree(), but for copying to a connected device."""
return self._bot_info.run([
os.path.join(self.ios_bin, 'ios_push_if_needed'),
host_dir, device_dir
])
def copy_directory_contents_to_host(self, device_dir, host_dir):
"""Like shutil.copytree(), but for copying from a connected device."""
return self._bot_info.run(
[os.path.join(self.ios_bin, 'ios_pull_if_needed'),
device_dir, host_dir],
)
def copy_file_to_device(self, host_path, device_path):
"""Like shutil.copyfile, but for copying to a connected device."""
self._bot_info.run(
[os.path.join(self.ios_bin, 'ios_push_file'), host_path, device_path],
) # pragma: no cover
def create_clean_device_dir(self, path):
"""Like shutil.rmtree() + os.makedirs(), but on a connected device."""
self._remove_device_dir(path)
self._create_device_dir(path)
def install(self):
"""Run device-specific installation steps."""
self._bot_info.run([os.path.join(self.ios_bin, 'ios_install')])
def cleanup_steps(self):
"""Run any device-specific cleanup steps."""
self._bot_info.run([os.path.join(self.ios_bin, 'ios_restart')])
self._bot_info.run(['sleep', '20'])
def read_file_on_device(self, path):
"""Read the given file."""
return subprocess.check_output(
[os.path.join(self.ios_bin, 'ios_cat_file'), path]).rstrip()
def remove_file_on_device(self, path):
"""Remove the file on the device."""
return self._bot_info.run(
[os.path.join(self.ios_bin, 'ios_rm'), path],
)
def get_device_dirs(self):
""" Set the directories which will be used by the build steps."""
prefix = self.device_path_join('skiabot', 'skia_')
return default_flavor.DeviceDirs(
dm_dir=prefix + 'dm',
perf_data_dir=prefix + 'perf',
resource_dir=prefix + 'resources',
images_dir=prefix + 'images',
skp_dir=prefix + 'skp/skps',
tmp_dir=prefix + 'tmp_dir')

View File

@ -0,0 +1,32 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import json
DEFAULT_PORT = '22'
DEFAULT_USER = 'chrome-bot'
SlaveInfo = collections.namedtuple('SlaveInfo',
'ssh_user ssh_host ssh_port')
SLAVE_INFO = {
'skiabot-shuttle-ubuntu12-003':
SlaveInfo('root', '192.168.1.123', DEFAULT_PORT),
'skiabot-shuttle-ubuntu12-004':
SlaveInfo('root', '192.168.1.134', DEFAULT_PORT),
'default':
SlaveInfo('nouser', 'noip', 'noport'),
}
if __name__ == '__main__':
print json.dumps(SLAVE_INFO) # pragma: no cover

View File

@ -0,0 +1,123 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import default_flavor
import os
import posixpath
import subprocess
import ssh_devices
"""Utils for running tests remotely over SSH."""
class SSHFlavorUtils(default_flavor.DefaultFlavorUtils):
def __init__(self, *args, **kwargs):
super(SSHFlavorUtils, self).__init__(*args, **kwargs)
slave_info = ssh_devices.SLAVE_INFO.get(self._bot_info.slave_name,
ssh_devices.SLAVE_INFO['default'])
self._host = slave_info.ssh_host
self._port = slave_info.ssh_port
self._user = slave_info.ssh_user
@property
def host(self):
return self._host
@property
def port(self):
return self._port
@property
def user(self):
return self._user
def ssh(self, cmd, **kwargs):
"""Run the given SSH command."""
ssh_cmd = ['ssh']
if self.port:
ssh_cmd.extend(['-p', self.port])
dest = self.host
if self.user:
dest = self.user + '@' + dest
ssh_cmd.append(dest)
ssh_cmd.extend(cmd)
return self._bot_info.run(ssh_cmd, **kwargs)
def step(self, *args, **kwargs):
"""Run the given step over SSH."""
self.ssh(*args, **kwargs)
def device_path_join(self, *args):
"""Like os.path.join(), but for paths on a remote machine."""
return posixpath.join(*args)
def device_path_exists(self, path): # pragma: no cover
"""Like os.path.exists(), but for paths on a remote device."""
try:
self.ssh(['test', '-e', path])
return True
except subprocess.CalledProcessError:
return False
def _remove_device_dir(self, path):
"""Remove the directory on the device."""
self.ssh(['rm', '-rf', path])
def _create_device_dir(self, path):
"""Create the directory on the device."""
self.ssh(['mkdir', '-p', path])
def create_clean_device_dir(self, path):
"""Like shutil.rmtree() + os.makedirs(), but on a remote device."""
self._remove_device_dir(path)
self._create_device_dir(path)
def _make_scp_cmd(self, remote_path, recurse=True):
"""Prepare an SCP command.
Returns a partial SCP command and an adjusted remote path.
"""
cmd = ['scp']
if recurse:
cmd.append('-r')
if self.port:
cmd.extend(['-P', self.port])
adj_remote_path = self.host + ':' + remote_path
if self.user:
adj_remote_path = self.user + '@' + adj_remote_path
return cmd, adj_remote_path
def copy_directory_contents_to_device(self, host_dir, device_dir):
"""Like shutil.copytree(), but for copying to a remote device."""
_, remote_path = self._make_scp_cmd(device_dir)
cmd = [os.path.join(self._bot_info.skia_dir, 'tools',
'scp_dir_contents.sh'),
host_dir, remote_path]
self._bot_info.run(cmd)
def copy_directory_contents_to_host(self, device_dir, host_dir):
"""Like shutil.copytree(), but for copying from a remote device."""
_, remote_path = self._make_scp_cmd(device_dir)
cmd = [os.path.join(self._bot_info.skia_dir, 'tools',
'scp_dir_contents.sh'),
remote_path, host_dir]
self._bot_info.run(cmd)
def copy_file_to_device(self, host_path, device_path):
"""Like shutil.copyfile, but for copying to a connected device."""
cmd, remote_path = self._make_scp_cmd(device_path, recurse=False)
cmd.extend([host_path, remote_path])
self._bot_info.run(cmd)
def read_file_on_device(self, path):
return self.ssh(['cat', path]).rstrip()
def remove_file_on_device(self, path):
"""Delete the given file."""
return self.ssh(['rm', '-f', path])

View File

@ -0,0 +1,31 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import default_flavor
import os
"""Utils for running under Valgrind."""
class ValgrindFlavorUtils(default_flavor.DefaultFlavorUtils):
def __init__(self, *args, **kwargs):
super(ValgrindFlavorUtils, self).__init__(*args, **kwargs)
self._suppressions_file = os.path.join(self._bot_info.skia_dir,
'tools', 'valgrind.supp')
def step(self, name, cmd, **kwargs):
new_cmd = ['valgrind', '--gen-suppressions=all', '--leak-check=full',
'--track-origins=yes', '--error-exitcode=1', '--num-callers=40',
'--suppressions=%s' % self._suppressions_file]
path_to_app = os.path.join(self._bot_info.out_dir,
self._bot_info.configuration, cmd[0])
new_cmd.append(path_to_app)
new_cmd.extend(cmd[1:])
return self._bot_info.run(new_cmd, **kwargs)

View File

@ -0,0 +1,54 @@
#!/usr/bin/env python
#
# Copyright 2016 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utils for running under *SAN"""
import default_flavor
import os
class XSanFlavorUtils(default_flavor.DefaultFlavorUtils):
def __init__(self, *args, **kwargs):
super(XSanFlavorUtils, self).__init__(*args, **kwargs)
self._sanitizer = {
# We'd love to just pass 'address,undefined' and get all the checks, but
# we're not anywhere close to being able to do that. Instead we start
# with a set of checks that we know pass or nearly pass. See here for
# more information:
# http://clang.llvm.org/docs/UsersManual.html#controlling-code-generation
'ASAN': ('address,bool,function,integer-divide-by-zero,nonnull-attribute,'
'null,object-size,return,returns-nonnull-attribute,shift,'
'signed-integer-overflow,unreachable,vla-bound,vptr'),
# MSAN and TSAN can't run together with ASAN, so they're their own bots.
'MSAN': 'memory',
'TSAN': 'thread',
}[self._bot_info.bot_cfg['extra_config']]
def compile(self, target):
cmd = [os.path.join(self._bot_info.skia_dir, 'tools', 'xsan_build'),
self._sanitizer, target]
self._bot_info.run(cmd)
def step(self, cmd, env=None, **kwargs):
"""Wrapper for the Step API; runs a step as appropriate for this flavor."""
lsan_suppressions = self._bot_info.skia_dir.join('tools', 'lsan.supp')
tsan_suppressions = self._bot_info.skia_dir.join('tools', 'tsan.supp')
ubsan_suppressions = self._bot_info.skia_dir.join('tools', 'ubsan.supp')
env = dict(env or {})
env['ASAN_OPTIONS'] = 'symbolize=1 detect_leaks=1'
env['LSAN_OPTIONS'] = ('symbolize=1 print_suppressions=1 suppressions=%s' %
lsan_suppressions)
env['TSAN_OPTIONS'] = 'suppressions=%s' % tsan_suppressions
env['UBSAN_OPTIONS'] = 'suppressions=%s' % ubsan_suppressions
path_to_app = os.path.join(self._bot_info.out_dir,
self._bot_info.configuration, cmd[0])
new_cmd = [path_to_app]
new_cmd.extend(cmd[1:])
return self._bot_info.run(new_cmd, env=env, **kwargs)

View File

@ -0,0 +1,7 @@
{
'variables': {
'files': [
'../../',
],
},
}