blob: 6f606d9a735418dc38f99acaef77a2ba58668e43 [file] [log] [blame]
borenetd9fa7582016-02-18 08:05:48 -08001#!/usr/bin/env python
2#
3# Copyright 2016 Google Inc.
4#
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8
boreneta7a6f2e2016-02-29 05:57:31 -08009import contextlib
10import math
borenetd9fa7582016-02-18 08:05:48 -080011import os
boreneta7a6f2e2016-02-29 05:57:31 -080012import shutil
13import socket
borenetd9fa7582016-02-18 08:05:48 -080014import subprocess
15import sys
boreneta7a6f2e2016-02-29 05:57:31 -080016import time
17import urllib2
borenetd9fa7582016-02-18 08:05:48 -080018
19from flavor import android_flavor
20from flavor import chromeos_flavor
21from flavor import cmake_flavor
22from flavor import coverage_flavor
23from flavor import default_flavor
24from flavor import ios_flavor
25from flavor import valgrind_flavor
26from flavor import xsan_flavor
27
28
29CONFIG_COVERAGE = 'Coverage'
30CONFIG_DEBUG = 'Debug'
31CONFIG_RELEASE = 'Release'
32VALID_CONFIGS = (CONFIG_COVERAGE, CONFIG_DEBUG, CONFIG_RELEASE)
33
34GM_ACTUAL_FILENAME = 'actual-results.json'
35GM_EXPECTATIONS_FILENAME = 'expected-results.json'
36GM_IGNORE_TESTS_FILENAME = 'ignored-tests.txt'
37
boreneta7a6f2e2016-02-29 05:57:31 -080038GOLD_UNINTERESTING_HASHES_URL = 'https://gold.skia.org/_/hashes'
39
borenetd9fa7582016-02-18 08:05:48 -080040GS_GM_BUCKET = 'chromium-skia-gm'
41GS_SUMMARIES_BUCKET = 'chromium-skia-gm-summaries'
42
boreneta7a6f2e2016-02-29 05:57:31 -080043GS_SUBDIR_TMPL_SK_IMAGE = 'skimage/v%s'
44GS_SUBDIR_TMPL_SKP = 'playback_%s/skps'
45
borenetd9fa7582016-02-18 08:05:48 -080046SKIA_REPO = 'https://skia.googlesource.com/skia.git'
47INFRA_REPO = 'https://skia.googlesource.com/buildbot.git'
48
49SERVICE_ACCOUNT_FILE = 'service-account-skia.json'
50SERVICE_ACCOUNT_INTERNAL_FILE = 'service-account-skia-internal.json'
51
boreneta7a6f2e2016-02-29 05:57:31 -080052VERSION_FILE_SK_IMAGE = 'SK_IMAGE_VERSION'
53VERSION_FILE_SKP = 'SKP_VERSION'
54
borenetd9fa7582016-02-18 08:05:48 -080055
56def is_android(bot_cfg):
57 """Determine whether the given bot is an Android bot."""
58 return ('Android' in bot_cfg.get('extra_config', '') or
59 bot_cfg.get('os') == 'Android')
60
61def is_chromeos(bot_cfg):
62 return ('CrOS' in bot_cfg.get('extra_config', '') or
63 bot_cfg.get('os') == 'ChromeOS')
64
65def is_cmake(bot_cfg):
66 return 'CMake' in bot_cfg.get('extra_config', '')
67
68def is_ios(bot_cfg):
69 return ('iOS' in bot_cfg.get('extra_config', '') or
70 bot_cfg.get('os') == 'iOS')
71
72
73def is_valgrind(bot_cfg):
74 return 'Valgrind' in bot_cfg.get('extra_config', '')
75
76
77def is_xsan(bot_cfg):
78 return (bot_cfg.get('extra_config') == 'ASAN' or
79 bot_cfg.get('extra_config') == 'MSAN' or
80 bot_cfg.get('extra_config') == 'TSAN')
81
82
boreneta7a6f2e2016-02-29 05:57:31 -080083def download_dir(skia_dir, tmp_dir, version_file, gs_path_tmpl, dst_dir):
84 # Ensure that the tmp_dir exists.
85 if not os.path.isdir(tmp_dir):
86 os.makedirs(tmp_dir)
87
88 # Get the expected version.
89 with open(os.path.join(skia_dir, version_file)) as f:
90 expected_version = f.read().rstrip()
91
92 print 'Expected %s = %s' % (version_file, expected_version)
93
94 # Get the actually-downloaded version, if we have one.
95 actual_version_file = os.path.join(tmp_dir, version_file)
96 try:
97 with open(actual_version_file) as f:
98 actual_version = f.read().rstrip()
99 except IOError:
100 actual_version = -1
101
102 print 'Actual %s = %s' % (version_file, actual_version)
103
104 # If we don't have the desired version, download it.
105 if actual_version != expected_version:
106 if actual_version != -1:
107 os.remove(actual_version_file)
108 if os.path.isdir(dst_dir):
109 shutil.rmtree(dst_dir)
110 os.makedirs(dst_dir)
111 gs_path = 'gs://%s/%s/*' % (GS_GM_BUCKET, gs_path_tmpl % expected_version)
112 print 'Downloading from %s' % gs_path
113 subprocess.check_call(['gsutil', 'cp', '-R', gs_path, dst_dir])
114 with open(actual_version_file, 'w') as f:
115 f.write(expected_version)
116
117
118def get_uninteresting_hashes(hashes_file):
119 retries = 5
120 timeout = 60
121 wait_base = 15
122
123 socket.setdefaulttimeout(timeout)
124 for retry in range(retries):
125 try:
126 with contextlib.closing(
127 urllib2.urlopen(GOLD_UNINTERESTING_HASHES_URL, timeout=timeout)) as w:
128 hashes = w.read()
129 with open(hashes_file, 'w') as f:
130 f.write(hashes)
131 break
132 except Exception as e:
133 print >> sys.stderr, 'Failed to get uninteresting hashes from %s:\n%s' % (
134 GOLD_UNINTERESTING_HASHES_URL, e)
135 if retry == retries:
136 raise
137 waittime = wait_base * math.pow(2, retry)
138 print 'Retry in %d seconds.' % waittime
139 time.sleep(waittime)
140
141
borenetd9fa7582016-02-18 08:05:48 -0800142class BotInfo(object):
boreneta7a6f2e2016-02-29 05:57:31 -0800143 def __init__(self, bot_name, swarm_out_dir):
borenetd9fa7582016-02-18 08:05:48 -0800144 """Initialize the bot, given its name.
145
146 Assumes that CWD is the directory containing this file.
147 """
148 self.name = bot_name
borenetd9fa7582016-02-18 08:05:48 -0800149 self.skia_dir = os.path.abspath(os.path.join(
150 os.path.dirname(os.path.realpath(__file__)),
151 os.pardir, os.pardir))
boreneta7a6f2e2016-02-29 05:57:31 -0800152 self.swarm_out_dir = swarm_out_dir
borenetd9fa7582016-02-18 08:05:48 -0800153 os.chdir(self.skia_dir)
154 self.build_dir = os.path.abspath(os.path.join(self.skia_dir, os.pardir))
borenetd9fa7582016-02-18 08:05:48 -0800155 self.spec = self.get_bot_spec(bot_name)
boreneta7a6f2e2016-02-29 05:57:31 -0800156 self.bot_cfg = self.spec['builder_cfg']
157 if self.bot_cfg['role'] == 'Build':
158 self.out_dir = os.path.join(swarm_out_dir, 'out')
159 else:
160 self.out_dir = 'out'
borenetd9fa7582016-02-18 08:05:48 -0800161 self.configuration = self.spec['configuration']
162 self.default_env = {
163 'SKIA_OUT': self.out_dir,
164 'BUILDTYPE': self.configuration,
165 'PATH': os.environ['PATH'],
166 }
167 self.default_env.update(self.spec['env'])
168 self.build_targets = [str(t) for t in self.spec['build_targets']]
borenetd9fa7582016-02-18 08:05:48 -0800169 self.is_trybot = self.bot_cfg['is_trybot']
170 self.upload_dm_results = self.spec['upload_dm_results']
171 self.upload_perf_results = self.spec['upload_perf_results']
boreneta7a6f2e2016-02-29 05:57:31 -0800172 self.perf_data_dir = os.path.join(self.swarm_out_dir, 'perfdata',
173 self.name, 'data')
174 self.resource_dir = os.path.join(self.build_dir, 'resources')
175 self.images_dir = os.path.join(self.build_dir, 'images')
176 self.local_skp_dir = os.path.join(self.build_dir, 'playback', 'skps')
borenetd9fa7582016-02-18 08:05:48 -0800177 self.dm_flags = self.spec['dm_flags']
178 self.nanobench_flags = self.spec['nanobench_flags']
179 self._ccache = None
180 self._checked_for_ccache = False
boreneta7a6f2e2016-02-29 05:57:31 -0800181 self._already_ran = {}
182 self.tmp_dir = os.path.join(self.build_dir, 'tmp')
borenetd9fa7582016-02-18 08:05:48 -0800183 self.flavor = self.get_flavor(self.bot_cfg)
184
boreneta7a6f2e2016-02-29 05:57:31 -0800185 # These get filled in during subsequent steps.
186 self.device_dirs = None
187 self.build_number = None
188 self.got_revision = None
189 self.master_name = None
190 self.slave_name = None
191
borenetd9fa7582016-02-18 08:05:48 -0800192 @property
193 def ccache(self):
194 if not self._checked_for_ccache:
195 self._checked_for_ccache = True
196 if sys.platform != 'win32':
197 try:
198 result = subprocess.check_output(['which', 'ccache'])
199 self._ccache = result.rstrip()
200 except subprocess.CalledProcessError:
201 pass
202
203 return self._ccache
204
205 def get_bot_spec(self, bot_name):
206 """Retrieve the bot spec for this bot."""
207 sys.path.append(self.skia_dir)
208 from tools import buildbot_spec
209 return buildbot_spec.get_builder_spec(bot_name)
210
211 def get_flavor(self, bot_cfg):
212 """Return a flavor utils object specific to the given bot."""
213 if is_android(bot_cfg):
214 return android_flavor.AndroidFlavorUtils(self)
215 elif is_chromeos(bot_cfg):
216 return chromeos_flavor.ChromeOSFlavorUtils(self)
217 elif is_cmake(bot_cfg):
218 return cmake_flavor.CMakeFlavorUtils(self)
219 elif is_ios(bot_cfg):
220 return ios_flavor.iOSFlavorUtils(self)
221 elif is_valgrind(bot_cfg):
222 return valgrind_flavor.ValgrindFlavorUtils(self)
223 elif is_xsan(bot_cfg):
224 return xsan_flavor.XSanFlavorUtils(self)
225 elif bot_cfg.get('configuration') == CONFIG_COVERAGE:
226 return coverage_flavor.CoverageFlavorUtils(self)
227 else:
228 return default_flavor.DefaultFlavorUtils(self)
229
230 def run(self, cmd, env=None, cwd=None):
231 _env = {}
232 _env.update(self.default_env)
233 _env.update(env or {})
234 cwd = cwd or self.skia_dir
235 print '============'
236 print 'CMD: %s' % cmd
237 print 'CWD: %s' % cwd
238 print 'ENV: %s' % _env
239 print '============'
240 subprocess.check_call(cmd, env=_env, cwd=cwd)
boreneta7a6f2e2016-02-29 05:57:31 -0800241
242 def compile_steps(self):
243 for t in self.build_targets:
244 self.flavor.compile(t)
245
246 def _run_once(self, fn, *args, **kwargs):
247 if not fn.__name__ in self._already_ran:
248 self._already_ran[fn.__name__] = True
249 fn(*args, **kwargs)
250
251 def install(self):
252 """Copy the required executables and files to the device."""
253 self.device_dirs = self.flavor.get_device_dirs()
254
255 # Run any device-specific installation.
256 self.flavor.install()
257
258 # TODO(borenet): Only copy files which have changed.
259 # Resources
260 self.flavor.copy_directory_contents_to_device(self.resource_dir,
261 self.device_dirs.resource_dir)
262
263 def _key_params(self):
264 """Build a unique key from the builder name (as a list).
265
266 E.g. arch x86 gpu GeForce320M mode MacMini4.1 os Mac10.6
267 """
268 # Don't bother to include role, which is always Test.
269 # TryBots are uploaded elsewhere so they can use the same key.
270 blacklist = ['role', 'is_trybot']
271
272 flat = []
273 for k in sorted(self.bot_cfg.keys()):
274 if k not in blacklist:
275 flat.append(k)
276 flat.append(self.bot_cfg[k])
277 return flat
278
279 def test_steps(self, got_revision, master_name, slave_name, build_number):
280 """Run the DM test."""
281 self.build_number = build_number
282 self.got_revision = got_revision
283 self.master_name = master_name
284 self.slave_name = slave_name
285 self._run_once(self.install)
286
287 use_hash_file = False
288 if self.upload_dm_results:
289 # This must run before we write anything into self.device_dirs.dm_dir
290 # or we may end up deleting our output on machines where they're the same.
291 host_dm_dir = os.path.join(self.swarm_out_dir, 'dm')
292 print 'host dm dir: %s' % host_dm_dir
293 self.flavor.create_clean_host_dir(host_dm_dir)
294 if str(host_dm_dir) != str(self.device_dirs.dm_dir):
295 self.flavor.create_clean_device_dir(self.device_dirs.dm_dir)
296
297 # Obtain the list of already-generated hashes.
298 hash_filename = 'uninteresting_hashes.txt'
299 host_hashes_file = self.tmp_dir.join(hash_filename)
300 hashes_file = self.flavor.device_path_join(
301 self.device_dirs.tmp_dir, hash_filename)
302
303 try:
304 get_uninteresting_hashes(host_hashes_file)
305 except Exception:
306 pass
307
308 if os.path.exists(host_hashes_file):
309 self.flavor.copy_file_to_device(host_hashes_file, hashes_file)
310 use_hash_file = True
311
312 # Run DM.
313 properties = [
314 'gitHash', self.got_revision,
315 'master', self.master_name,
316 'builder', self.name,
317 'build_number', self.build_number,
318 ]
319 if self.is_trybot:
320 properties.extend([
321 'issue', self.m.properties['issue'],
322 'patchset', self.m.properties['patchset'],
323 ])
324
325 args = [
326 'dm',
327 '--undefok', # This helps branches that may not know new flags.
328 '--verbose',
329 '--resourcePath', self.device_dirs.resource_dir,
330 '--skps', self.device_dirs.skp_dir,
331 '--images', self.flavor.device_path_join(
332 self.device_dirs.images_dir, 'dm'),
333 '--nameByHash',
334 '--properties'
335 ] + properties
336
337 args.append('--key')
338 args.extend(self._key_params())
339 if use_hash_file:
340 args.extend(['--uninterestingHashesFile', hashes_file])
341 if self.upload_dm_results:
342 args.extend(['--writePath', self.device_dirs.dm_dir])
343
344 skip_flag = None
345 if self.bot_cfg.get('cpu_or_gpu') == 'CPU':
346 skip_flag = '--nogpu'
347 elif self.bot_cfg.get('cpu_or_gpu') == 'GPU':
348 skip_flag = '--nocpu'
349 if skip_flag:
350 args.append(skip_flag)
351 args.extend(self.dm_flags)
352
353 self.flavor.run(args, env=self.default_env)
354
355 if self.upload_dm_results:
356 # Copy images and JSON to host machine if needed.
357 self.flavor.copy_directory_contents_to_host(self.device_dirs.dm_dir,
358 host_dm_dir)
359
360 # See skia:2789.
361 if ('Valgrind' in self.name and
362 self.builder_cfg.get('cpu_or_gpu') == 'GPU'):
363 abandonGpuContext = list(args)
364 abandonGpuContext.append('--abandonGpuContext')
365 self.flavor.run(abandonGpuContext)
366 preAbandonGpuContext = list(args)
367 preAbandonGpuContext.append('--preAbandonGpuContext')
368 self.flavor.run(preAbandonGpuContext)