aboutsummaryrefslogtreecommitdiffhomepage
path: root/infra
diff options
context:
space:
mode:
authorGravatar borenet <borenet@google.com>2016-02-18 08:05:48 -0800
committerGravatar Commit bot <commit-bot@chromium.org>2016-02-18 08:05:48 -0800
commitd9fa758292462c4200231f49286aa97750f5e689 (patch)
treea8c8b7cd785c4254d522f3b2900af92fe1147e9c /infra
parente1fce93f36d7b73df9942135dc5a342b629e6b3a (diff)
Port Skia recipe to normal Python scripts, move to Skia repo
Diffstat (limited to 'infra')
-rw-r--r--infra/bots/README.md18
-rw-r--r--infra/bots/common.py150
-rw-r--r--infra/bots/compile_skia.isolate10
-rw-r--r--infra/bots/compile_skia.py23
-rw-r--r--infra/bots/flavor/__init__.py6
-rw-r--r--infra/bots/flavor/android_devices.py100
-rw-r--r--infra/bots/flavor/android_flavor.py241
-rw-r--r--infra/bots/flavor/chromeos_flavor.py59
-rw-r--r--infra/bots/flavor/cmake_flavor.py20
-rw-r--r--infra/bots/flavor/coverage_flavor.py114
-rw-r--r--infra/bots/flavor/default_flavor.py172
-rw-r--r--infra/bots/flavor/ios_flavor.py114
-rw-r--r--infra/bots/flavor/ssh_devices.py32
-rw-r--r--infra/bots/flavor/ssh_flavor.py123
-rw-r--r--infra/bots/flavor/valgrind_flavor.py31
-rw-r--r--infra/bots/flavor/xsan_flavor.py54
-rw-r--r--infra/bots/skia_repo.isolate7
17 files changed, 1274 insertions, 0 deletions
diff --git a/infra/bots/README.md b/infra/bots/README.md
new file mode 100644
index 0000000000..9f184aaadc
--- /dev/null
+++ b/infra/bots/README.md
@@ -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
diff --git a/infra/bots/common.py b/infra/bots/common.py
new file mode 100644
index 0000000000..9b96440c4f
--- /dev/null
+++ b/infra/bots/common.py
@@ -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)
diff --git a/infra/bots/compile_skia.isolate b/infra/bots/compile_skia.isolate
new file mode 100644
index 0000000000..51168e0119
--- /dev/null
+++ b/infra/bots/compile_skia.isolate
@@ -0,0 +1,10 @@
+{
+ 'includes': [
+ 'skia_repo.isolate',
+ ],
+ 'variables': {
+ 'command': [
+ 'python', 'compile_skia.py', '<(BUILDER_NAME)', '${ISOLATED_OUTDIR}/out',
+ ],
+ },
+}
diff --git a/infra/bots/compile_skia.py b/infra/bots/compile_skia.py
new file mode 100644
index 0000000000..b3b625121f
--- /dev/null
+++ b/infra/bots/compile_skia.py
@@ -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()
diff --git a/infra/bots/flavor/__init__.py b/infra/bots/flavor/__init__.py
new file mode 100644
index 0000000000..78953f5f51
--- /dev/null
+++ b/infra/bots/flavor/__init__.py
@@ -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.
diff --git a/infra/bots/flavor/android_devices.py b/infra/bots/flavor/android_devices.py
new file mode 100644
index 0000000000..37ceabef3a
--- /dev/null
+++ b/infra/bots/flavor/android_devices.py
@@ -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
+
diff --git a/infra/bots/flavor/android_flavor.py b/infra/bots/flavor/android_flavor.py
new file mode 100644
index 0000000000..5a30079951
--- /dev/null
+++ b/infra/bots/flavor/android_flavor.py
@@ -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')
+
diff --git a/infra/bots/flavor/chromeos_flavor.py b/infra/bots/flavor/chromeos_flavor.py
new file mode 100644
index 0000000000..8e8221489d
--- /dev/null
+++ b/infra/bots/flavor/chromeos_flavor.py
@@ -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'))
+
diff --git a/infra/bots/flavor/cmake_flavor.py b/infra/bots/flavor/cmake_flavor.py
new file mode 100644
index 0000000000..cfba2bbefd
--- /dev/null
+++ b/infra/bots/flavor/cmake_flavor.py
@@ -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])
diff --git a/infra/bots/flavor/coverage_flavor.py b/infra/bots/flavor/coverage_flavor.py
new file mode 100644
index 0000000000..0a11d08ee8
--- /dev/null
+++ b/infra/bots/flavor/coverage_flavor.py
@@ -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)))
+
diff --git a/infra/bots/flavor/default_flavor.py b/infra/bots/flavor/default_flavor.py
new file mode 100644
index 0000000000..5263073744
--- /dev/null
+++ b/infra/bots/flavor/default_flavor.py
@@ -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
diff --git a/infra/bots/flavor/ios_flavor.py b/infra/bots/flavor/ios_flavor.py
new file mode 100644
index 0000000000..c2d8737750
--- /dev/null
+++ b/infra/bots/flavor/ios_flavor.py
@@ -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')
diff --git a/infra/bots/flavor/ssh_devices.py b/infra/bots/flavor/ssh_devices.py
new file mode 100644
index 0000000000..f113fdb8a7
--- /dev/null
+++ b/infra/bots/flavor/ssh_devices.py
@@ -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
+
diff --git a/infra/bots/flavor/ssh_flavor.py b/infra/bots/flavor/ssh_flavor.py
new file mode 100644
index 0000000000..07c383f603
--- /dev/null
+++ b/infra/bots/flavor/ssh_flavor.py
@@ -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])
diff --git a/infra/bots/flavor/valgrind_flavor.py b/infra/bots/flavor/valgrind_flavor.py
new file mode 100644
index 0000000000..129a7c08ed
--- /dev/null
+++ b/infra/bots/flavor/valgrind_flavor.py
@@ -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)
+
diff --git a/infra/bots/flavor/xsan_flavor.py b/infra/bots/flavor/xsan_flavor.py
new file mode 100644
index 0000000000..5807be0180
--- /dev/null
+++ b/infra/bots/flavor/xsan_flavor.py
@@ -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)
diff --git a/infra/bots/skia_repo.isolate b/infra/bots/skia_repo.isolate
new file mode 100644
index 0000000000..7410388b3f
--- /dev/null
+++ b/infra/bots/skia_repo.isolate
@@ -0,0 +1,7 @@
+{
+ 'variables': {
+ 'files': [
+ '../../',
+ ],
+ },
+}