OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # | 2 # |
3 # Copyright 2016 Google Inc. | 3 # Copyright 2016 Google Inc. |
4 # | 4 # |
5 # Use of this source code is governed by a BSD-style license that can be | 5 # Use of this source code is governed by a BSD-style license that can be |
6 # found in the LICENSE file. | 6 # found in the LICENSE file. |
7 | 7 |
8 | 8 |
| 9 import contextlib |
| 10 import math |
9 import os | 11 import os |
| 12 import shutil |
| 13 import socket |
10 import subprocess | 14 import subprocess |
11 import sys | 15 import sys |
| 16 import time |
| 17 import urllib2 |
12 | 18 |
13 from flavor import android_flavor | 19 from flavor import android_flavor |
14 from flavor import chromeos_flavor | 20 from flavor import chromeos_flavor |
15 from flavor import cmake_flavor | 21 from flavor import cmake_flavor |
16 from flavor import coverage_flavor | 22 from flavor import coverage_flavor |
17 from flavor import default_flavor | 23 from flavor import default_flavor |
18 from flavor import ios_flavor | 24 from flavor import ios_flavor |
19 from flavor import valgrind_flavor | 25 from flavor import valgrind_flavor |
20 from flavor import xsan_flavor | 26 from flavor import xsan_flavor |
21 | 27 |
22 | 28 |
23 CONFIG_COVERAGE = 'Coverage' | 29 CONFIG_COVERAGE = 'Coverage' |
24 CONFIG_DEBUG = 'Debug' | 30 CONFIG_DEBUG = 'Debug' |
25 CONFIG_RELEASE = 'Release' | 31 CONFIG_RELEASE = 'Release' |
26 VALID_CONFIGS = (CONFIG_COVERAGE, CONFIG_DEBUG, CONFIG_RELEASE) | 32 VALID_CONFIGS = (CONFIG_COVERAGE, CONFIG_DEBUG, CONFIG_RELEASE) |
27 | 33 |
28 GM_ACTUAL_FILENAME = 'actual-results.json' | 34 GM_ACTUAL_FILENAME = 'actual-results.json' |
29 GM_EXPECTATIONS_FILENAME = 'expected-results.json' | 35 GM_EXPECTATIONS_FILENAME = 'expected-results.json' |
30 GM_IGNORE_TESTS_FILENAME = 'ignored-tests.txt' | 36 GM_IGNORE_TESTS_FILENAME = 'ignored-tests.txt' |
31 | 37 |
| 38 GOLD_UNINTERESTING_HASHES_URL = 'https://gold.skia.org/_/hashes' |
| 39 |
32 GS_GM_BUCKET = 'chromium-skia-gm' | 40 GS_GM_BUCKET = 'chromium-skia-gm' |
33 GS_SUMMARIES_BUCKET = 'chromium-skia-gm-summaries' | 41 GS_SUMMARIES_BUCKET = 'chromium-skia-gm-summaries' |
34 | 42 |
| 43 GS_SUBDIR_TMPL_SK_IMAGE = 'skimage/v%s' |
| 44 GS_SUBDIR_TMPL_SKP = 'playback_%s/skps' |
| 45 |
35 SKIA_REPO = 'https://skia.googlesource.com/skia.git' | 46 SKIA_REPO = 'https://skia.googlesource.com/skia.git' |
36 INFRA_REPO = 'https://skia.googlesource.com/buildbot.git' | 47 INFRA_REPO = 'https://skia.googlesource.com/buildbot.git' |
37 | 48 |
38 SERVICE_ACCOUNT_FILE = 'service-account-skia.json' | 49 SERVICE_ACCOUNT_FILE = 'service-account-skia.json' |
39 SERVICE_ACCOUNT_INTERNAL_FILE = 'service-account-skia-internal.json' | 50 SERVICE_ACCOUNT_INTERNAL_FILE = 'service-account-skia-internal.json' |
40 | 51 |
| 52 VERSION_FILE_SK_IMAGE = 'SK_IMAGE_VERSION' |
| 53 VERSION_FILE_SKP = 'SKP_VERSION' |
| 54 |
41 | 55 |
42 def is_android(bot_cfg): | 56 def is_android(bot_cfg): |
43 """Determine whether the given bot is an Android bot.""" | 57 """Determine whether the given bot is an Android bot.""" |
44 return ('Android' in bot_cfg.get('extra_config', '') or | 58 return ('Android' in bot_cfg.get('extra_config', '') or |
45 bot_cfg.get('os') == 'Android') | 59 bot_cfg.get('os') == 'Android') |
46 | 60 |
47 def is_chromeos(bot_cfg): | 61 def is_chromeos(bot_cfg): |
48 return ('CrOS' in bot_cfg.get('extra_config', '') or | 62 return ('CrOS' in bot_cfg.get('extra_config', '') or |
49 bot_cfg.get('os') == 'ChromeOS') | 63 bot_cfg.get('os') == 'ChromeOS') |
50 | 64 |
51 def is_cmake(bot_cfg): | 65 def is_cmake(bot_cfg): |
52 return 'CMake' in bot_cfg.get('extra_config', '') | 66 return 'CMake' in bot_cfg.get('extra_config', '') |
53 | 67 |
54 def is_ios(bot_cfg): | 68 def is_ios(bot_cfg): |
55 return ('iOS' in bot_cfg.get('extra_config', '') or | 69 return ('iOS' in bot_cfg.get('extra_config', '') or |
56 bot_cfg.get('os') == 'iOS') | 70 bot_cfg.get('os') == 'iOS') |
57 | 71 |
58 | 72 |
59 def is_valgrind(bot_cfg): | 73 def is_valgrind(bot_cfg): |
60 return 'Valgrind' in bot_cfg.get('extra_config', '') | 74 return 'Valgrind' in bot_cfg.get('extra_config', '') |
61 | 75 |
62 | 76 |
63 def is_xsan(bot_cfg): | 77 def is_xsan(bot_cfg): |
64 return (bot_cfg.get('extra_config') == 'ASAN' or | 78 return (bot_cfg.get('extra_config') == 'ASAN' or |
65 bot_cfg.get('extra_config') == 'MSAN' or | 79 bot_cfg.get('extra_config') == 'MSAN' or |
66 bot_cfg.get('extra_config') == 'TSAN') | 80 bot_cfg.get('extra_config') == 'TSAN') |
67 | 81 |
68 | 82 |
| 83 def 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 |
| 118 def 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 |
69 class BotInfo(object): | 142 class BotInfo(object): |
70 def __init__(self, bot_name, slave_name, out_dir): | 143 def __init__(self, bot_name, swarm_out_dir): |
71 """Initialize the bot, given its name. | 144 """Initialize the bot, given its name. |
72 | 145 |
73 Assumes that CWD is the directory containing this file. | 146 Assumes that CWD is the directory containing this file. |
74 """ | 147 """ |
75 self.name = bot_name | 148 self.name = bot_name |
76 self.slave_name = slave_name | |
77 self.skia_dir = os.path.abspath(os.path.join( | 149 self.skia_dir = os.path.abspath(os.path.join( |
78 os.path.dirname(os.path.realpath(__file__)), | 150 os.path.dirname(os.path.realpath(__file__)), |
79 os.pardir, os.pardir)) | 151 os.pardir, os.pardir)) |
| 152 self.swarm_out_dir = swarm_out_dir |
80 os.chdir(self.skia_dir) | 153 os.chdir(self.skia_dir) |
81 self.build_dir = os.path.abspath(os.path.join(self.skia_dir, os.pardir)) | 154 self.build_dir = os.path.abspath(os.path.join(self.skia_dir, os.pardir)) |
82 self.out_dir = out_dir | |
83 self.spec = self.get_bot_spec(bot_name) | 155 self.spec = self.get_bot_spec(bot_name) |
| 156 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' |
84 self.configuration = self.spec['configuration'] | 161 self.configuration = self.spec['configuration'] |
85 self.default_env = { | 162 self.default_env = { |
86 'SKIA_OUT': self.out_dir, | 163 'SKIA_OUT': self.out_dir, |
87 'BUILDTYPE': self.configuration, | 164 'BUILDTYPE': self.configuration, |
88 'PATH': os.environ['PATH'], | 165 'PATH': os.environ['PATH'], |
89 } | 166 } |
90 self.default_env.update(self.spec['env']) | 167 self.default_env.update(self.spec['env']) |
91 self.build_targets = [str(t) for t in self.spec['build_targets']] | 168 self.build_targets = [str(t) for t in self.spec['build_targets']] |
92 self.bot_cfg = self.spec['builder_cfg'] | |
93 self.is_trybot = self.bot_cfg['is_trybot'] | 169 self.is_trybot = self.bot_cfg['is_trybot'] |
94 self.upload_dm_results = self.spec['upload_dm_results'] | 170 self.upload_dm_results = self.spec['upload_dm_results'] |
95 self.upload_perf_results = self.spec['upload_perf_results'] | 171 self.upload_perf_results = self.spec['upload_perf_results'] |
| 172 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') |
96 self.dm_flags = self.spec['dm_flags'] | 177 self.dm_flags = self.spec['dm_flags'] |
97 self.nanobench_flags = self.spec['nanobench_flags'] | 178 self.nanobench_flags = self.spec['nanobench_flags'] |
98 self._ccache = None | 179 self._ccache = None |
99 self._checked_for_ccache = False | 180 self._checked_for_ccache = False |
| 181 self._already_ran = {} |
| 182 self.tmp_dir = os.path.join(self.build_dir, 'tmp') |
100 self.flavor = self.get_flavor(self.bot_cfg) | 183 self.flavor = self.get_flavor(self.bot_cfg) |
101 | 184 |
| 185 # 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 |
102 @property | 192 @property |
103 def ccache(self): | 193 def ccache(self): |
104 if not self._checked_for_ccache: | 194 if not self._checked_for_ccache: |
105 self._checked_for_ccache = True | 195 self._checked_for_ccache = True |
106 if sys.platform != 'win32': | 196 if sys.platform != 'win32': |
107 try: | 197 try: |
108 result = subprocess.check_output(['which', 'ccache']) | 198 result = subprocess.check_output(['which', 'ccache']) |
109 self._ccache = result.rstrip() | 199 self._ccache = result.rstrip() |
110 except subprocess.CalledProcessError: | 200 except subprocess.CalledProcessError: |
111 pass | 201 pass |
(...skipping 29 matching lines...) Expand all Loading... |
141 _env = {} | 231 _env = {} |
142 _env.update(self.default_env) | 232 _env.update(self.default_env) |
143 _env.update(env or {}) | 233 _env.update(env or {}) |
144 cwd = cwd or self.skia_dir | 234 cwd = cwd or self.skia_dir |
145 print '============' | 235 print '============' |
146 print 'CMD: %s' % cmd | 236 print 'CMD: %s' % cmd |
147 print 'CWD: %s' % cwd | 237 print 'CWD: %s' % cwd |
148 print 'ENV: %s' % _env | 238 print 'ENV: %s' % _env |
149 print '============' | 239 print '============' |
150 subprocess.check_call(cmd, env=_env, cwd=cwd) | 240 subprocess.check_call(cmd, env=_env, cwd=cwd) |
| 241 |
| 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) |
OLD | NEW |