Roll src/third_party/WebKit bf18a82:a9cee16 (svn 185297:185304)
[chromium-blink-merge.git] / chrome / test / chromedriver / run_buildbot_steps.py
blob8aa9fc6e6939f4e8bf18d621fe712c58caa2ef43
1 #!/usr/bin/env python
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Runs all the buildbot steps for ChromeDriver except for update/compile."""
8 import bisect
9 import csv
10 import datetime
11 import glob
12 import json
13 import optparse
14 import os
15 import platform as platform_module
16 import re
17 import shutil
18 import StringIO
19 import sys
20 import tempfile
21 import time
22 import urllib2
24 _THIS_DIR = os.path.abspath(os.path.dirname(__file__))
25 GS_CHROMEDRIVER_BUCKET = 'gs://chromedriver'
26 GS_CHROMEDRIVER_DATA_BUCKET = 'gs://chromedriver-data'
27 GS_CHROMEDRIVER_RELEASE_URL = 'http://chromedriver.storage.googleapis.com'
28 GS_CONTINUOUS_URL = GS_CHROMEDRIVER_DATA_BUCKET + '/continuous'
29 GS_PREBUILTS_URL = GS_CHROMEDRIVER_DATA_BUCKET + '/prebuilts'
30 GS_SERVER_LOGS_URL = GS_CHROMEDRIVER_DATA_BUCKET + '/server_logs'
31 SERVER_LOGS_LINK = (
32 'http://chromedriver-data.storage.googleapis.com/server_logs')
33 TEST_LOG_FORMAT = '%s_log.json'
34 GS_GIT_LOG_URL = (
35 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
36 GS_SEARCH_PATTERN = (
37 r'Cr-Commit-Position: refs/heads/master@{#(\d+)}')
38 CR_REV_URL = 'https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/%s'
40 SCRIPT_DIR = os.path.join(_THIS_DIR, os.pardir, os.pardir, os.pardir, os.pardir,
41 os.pardir, os.pardir, os.pardir, 'scripts')
42 SITE_CONFIG_DIR = os.path.join(_THIS_DIR, os.pardir, os.pardir, os.pardir,
43 os.pardir, os.pardir, os.pardir, os.pardir,
44 'site_config')
45 sys.path.append(SCRIPT_DIR)
46 sys.path.append(SITE_CONFIG_DIR)
48 import archive
49 import chrome_paths
50 from slave import gsutil_download
51 from slave import slave_utils
52 import util
55 def _ArchivePrebuilts(commit_position):
56 """Uploads the prebuilts to google storage."""
57 util.MarkBuildStepStart('archive prebuilts')
58 zip_path = util.Zip(os.path.join(chrome_paths.GetBuildDir(['chromedriver']),
59 'chromedriver'))
60 if slave_utils.GSUtilCopy(
61 zip_path,
62 '%s/%s' % (GS_PREBUILTS_URL, 'r%s.zip' % commit_position)):
63 util.MarkBuildStepError()
66 def _ArchiveServerLogs():
67 """Uploads chromedriver server logs to google storage."""
68 util.MarkBuildStepStart('archive chromedriver server logs')
69 for server_log in glob.glob(os.path.join(tempfile.gettempdir(),
70 'chromedriver_*')):
71 base_name = os.path.basename(server_log)
72 util.AddLink(base_name, '%s/%s' % (SERVER_LOGS_LINK, base_name))
73 slave_utils.GSUtilCopy(
74 server_log,
75 '%s/%s' % (GS_SERVER_LOGS_URL, base_name),
76 mimetype='text/plain')
79 def _DownloadPrebuilts():
80 """Downloads the most recent prebuilts from google storage."""
81 util.MarkBuildStepStart('Download latest chromedriver')
83 zip_path = os.path.join(util.MakeTempDir(), 'build.zip')
84 if gsutil_download.DownloadLatestFile(GS_PREBUILTS_URL,
85 GS_PREBUILTS_URL + '/r',
86 zip_path):
87 util.MarkBuildStepError()
89 util.Unzip(zip_path, chrome_paths.GetBuildDir(['host_forwarder']))
92 def _GetTestResultsLog(platform):
93 """Gets the test results log for the given platform.
95 Args:
96 platform: The platform that the test results log is for.
98 Returns:
99 A dictionary where the keys are commit positions and the values are booleans
100 indicating whether the tests passed.
102 temp_log = tempfile.mkstemp()[1]
103 log_name = TEST_LOG_FORMAT % platform
104 result = slave_utils.GSUtilDownloadFile(
105 '%s/%s' % (GS_CHROMEDRIVER_DATA_BUCKET, log_name), temp_log)
106 if result:
107 return {}
108 with open(temp_log, 'rb') as log_file:
109 json_dict = json.load(log_file)
110 # Workaround for json encoding dictionary keys as strings.
111 return dict([(int(v[0]), v[1]) for v in json_dict.items()])
114 def _PutTestResultsLog(platform, test_results_log):
115 """Pushes the given test results log to google storage."""
116 temp_dir = util.MakeTempDir()
117 log_name = TEST_LOG_FORMAT % platform
118 log_path = os.path.join(temp_dir, log_name)
119 with open(log_path, 'wb') as log_file:
120 json.dump(test_results_log, log_file)
121 if slave_utils.GSUtilCopyFile(log_path, GS_CHROMEDRIVER_DATA_BUCKET):
122 raise Exception('Failed to upload test results log to google storage')
125 def _UpdateTestResultsLog(platform, commit_position, passed):
126 """Updates the test results log for the given platform.
128 Args:
129 platform: The platform name.
130 commit_position: The commit position number.
131 passed: Boolean indicating whether the tests passed at this commit position.
134 assert commit_position.isdigit(), 'The commit position must be a number'
135 commit_position = int(commit_position)
136 log = _GetTestResultsLog(platform)
137 if len(log) > 500:
138 del log[min(log.keys())]
139 assert commit_position not in log, \
140 'Results already exist for commit position %s' % commit_position
141 log[commit_position] = bool(passed)
142 _PutTestResultsLog(platform, log)
145 def _GetVersion():
146 """Get the current chromedriver version."""
147 with open(os.path.join(_THIS_DIR, 'VERSION'), 'r') as f:
148 return f.read().strip()
151 def _GetSupportedChromeVersions():
152 """Get the minimum and maximum supported Chrome versions.
154 Returns:
155 A tuple of the form (min_version, max_version).
157 # Minimum supported Chrome version is embedded as:
158 # const int kMinimumSupportedChromeVersion[] = {27, 0, 1453, 0};
159 with open(os.path.join(_THIS_DIR, 'chrome', 'version.cc'), 'r') as f:
160 lines = f.readlines()
161 chrome_min_version_line = [
162 x for x in lines if 'kMinimumSupportedChromeVersion' in x]
163 chrome_min_version = chrome_min_version_line[0].split('{')[1].split(',')[0]
164 with open(os.path.join(chrome_paths.GetSrc(), 'chrome', 'VERSION'), 'r') as f:
165 chrome_max_version = f.readlines()[0].split('=')[1].strip()
166 return (chrome_min_version, chrome_max_version)
169 def _CommitPositionState(test_results_log, commit_position):
170 """Check the state of tests at a given commit position.
172 Considers tests as having passed at a commit position if they passed at
173 revisons both before and after.
175 Args:
176 test_results_log: A test results log dictionary from _GetTestResultsLog().
177 commit_position: The commit position to check at.
179 Returns:
180 'passed', 'failed', or 'unknown'
182 assert isinstance(commit_position, int), 'The commit position must be an int'
183 keys = sorted(test_results_log.keys())
184 # Return passed if the exact commit position passed on Android.
185 if commit_position in test_results_log:
186 return 'passed' if test_results_log[commit_position] else 'failed'
187 # Tests were not run on this exact commit position on Android.
188 index = bisect.bisect_right(keys, commit_position)
189 # Tests have not yet run on Android at or above this commit position.
190 if index == len(test_results_log):
191 return 'unknown'
192 # No log exists for any prior commit position, assume it failed.
193 if index == 0:
194 return 'failed'
195 # Return passed if the commit position on both sides passed.
196 if test_results_log[keys[index]] and test_results_log[keys[index - 1]]:
197 return 'passed'
198 return 'failed'
201 def _ArchiveGoodBuild(platform, commit_position):
202 """Archive chromedriver binary if the build is green."""
203 assert platform != 'android'
204 util.MarkBuildStepStart('archive build')
206 server_name = 'chromedriver'
207 if util.IsWindows():
208 server_name += '.exe'
209 zip_path = util.Zip(os.path.join(chrome_paths.GetBuildDir([server_name]),
210 server_name))
212 build_name = 'chromedriver_%s_%s.%s.zip' % (
213 platform, _GetVersion(), commit_position)
214 build_url = '%s/%s' % (GS_CONTINUOUS_URL, build_name)
215 if slave_utils.GSUtilCopy(zip_path, build_url):
216 util.MarkBuildStepError()
218 (latest_fd, latest_file) = tempfile.mkstemp()
219 os.write(latest_fd, build_name)
220 os.close(latest_fd)
221 latest_url = '%s/latest_%s' % (GS_CONTINUOUS_URL, platform)
222 if slave_utils.GSUtilCopy(latest_file, latest_url, mimetype='text/plain'):
223 util.MarkBuildStepError()
224 os.remove(latest_file)
227 def _WasReleased(version, platform):
228 """Check if the specified version is released for the given platform."""
229 result, _ = slave_utils.GSUtilListBucket(
230 '%s/%s/chromedriver_%s.zip' % (GS_CHROMEDRIVER_BUCKET, version, platform),
232 return result == 0
235 def _MaybeRelease(platform):
236 """Releases a release candidate if conditions are right."""
237 assert platform != 'android'
239 version = _GetVersion()
241 # Check if the current version has already been released.
242 if _WasReleased(version, platform):
243 return
245 # Fetch Android test results.
246 android_test_results = _GetTestResultsLog('android')
248 # Fetch release candidates.
249 result, output = slave_utils.GSUtilListBucket(
250 '%s/chromedriver_%s_%s*' % (
251 GS_CONTINUOUS_URL, platform, version),
253 assert result == 0 and output, 'No release candidates found'
254 candidate_pattern = re.compile(
255 r'.*/chromedriver_%s_%s\.(\d+)\.zip$' % (platform, version))
256 candidates = []
257 for line in output.strip().split('\n'):
258 result = candidate_pattern.match(line)
259 if not result:
260 print 'Ignored line "%s"' % line
261 continue
262 candidates.append(int(result.group(1)))
264 # Release the latest candidate build that passed Android, if any.
265 # In this way, if a hot fix is needed, we can delete the release from
266 # the chromedriver bucket instead of bumping up the release version number.
267 candidates.sort(reverse=True)
268 for commit_position in candidates:
269 android_result = _CommitPositionState(android_test_results, commit_position)
270 if android_result == 'failed':
271 print 'Android tests did not pass at commit position', commit_position
272 elif android_result == 'passed':
273 print 'Android tests passed at commit position', commit_position
274 candidate = 'chromedriver_%s_%s.%s.zip' % (
275 platform, version, commit_position)
276 _Release('%s/%s' % (GS_CONTINUOUS_URL, candidate), version, platform)
277 break
278 else:
279 print 'Android tests have not run at a commit position as recent as', \
280 commit_position
283 def _Release(build, version, platform):
284 """Releases the given candidate build."""
285 release_name = 'chromedriver_%s.zip' % platform
286 util.MarkBuildStepStart('releasing %s' % release_name)
287 temp_dir = util.MakeTempDir()
288 slave_utils.GSUtilCopy(build, temp_dir)
289 zip_path = os.path.join(temp_dir, os.path.basename(build))
291 if util.IsLinux():
292 util.Unzip(zip_path, temp_dir)
293 server_path = os.path.join(temp_dir, 'chromedriver')
294 util.RunCommand(['strip', server_path])
295 zip_path = util.Zip(server_path)
297 slave_utils.GSUtilCopy(
298 zip_path, '%s/%s/%s' % (GS_CHROMEDRIVER_BUCKET, version, release_name))
300 _MaybeUploadReleaseNotes(version)
301 _MaybeUpdateLatestRelease(version)
304 def _GetWebPageContent(url):
305 """Return the content of the web page specified by the given url."""
306 return urllib2.urlopen(url).read()
309 def _MaybeUploadReleaseNotes(version):
310 """Upload release notes if conditions are right."""
311 # Check if the current version has already been released.
312 notes_name = 'notes.txt'
313 notes_url = '%s/%s/%s' % (GS_CHROMEDRIVER_BUCKET, version, notes_name)
314 prev_version = '.'.join([version.split('.')[0],
315 str(int(version.split('.')[1]) - 1)])
316 prev_notes_url = '%s/%s/%s' % (
317 GS_CHROMEDRIVER_BUCKET, prev_version, notes_name)
319 result, _ = slave_utils.GSUtilListBucket(notes_url, [])
320 if result == 0:
321 return
323 fixed_issues = []
324 query = ('https://code.google.com/p/chromedriver/issues/csv?'
325 'q=status%3AToBeReleased&colspec=ID%20Summary')
326 issues = StringIO.StringIO(_GetWebPageContent(query).split('\n', 1)[1])
327 for issue in csv.reader(issues):
328 if not issue:
329 continue
330 issue_id = issue[0]
331 desc = issue[1]
332 labels = issue[2]
333 fixed_issues += ['Resolved issue %s: %s [%s]' % (issue_id, desc, labels)]
335 old_notes = ''
336 temp_notes_fname = tempfile.mkstemp()[1]
337 if not slave_utils.GSUtilDownloadFile(prev_notes_url, temp_notes_fname):
338 with open(temp_notes_fname, 'rb') as f:
339 old_notes = f.read()
341 new_notes = '----------ChromeDriver v%s (%s)----------\n%s\n%s\n\n%s' % (
342 version, datetime.date.today().isoformat(),
343 'Supports Chrome v%s-%s' % _GetSupportedChromeVersions(),
344 '\n'.join(fixed_issues),
345 old_notes)
346 with open(temp_notes_fname, 'w') as f:
347 f.write(new_notes)
349 if slave_utils.GSUtilCopy(temp_notes_fname, notes_url, mimetype='text/plain'):
350 util.MarkBuildStepError()
353 def _MaybeUpdateLatestRelease(version):
354 """Update the file LATEST_RELEASE with the latest release version number."""
355 latest_release_fname = 'LATEST_RELEASE'
356 latest_release_url = '%s/%s' % (GS_CHROMEDRIVER_BUCKET, latest_release_fname)
358 # Check if LATEST_RELEASE is up-to-date.
359 latest_released_version = _GetWebPageContent(
360 '%s/%s' % (GS_CHROMEDRIVER_RELEASE_URL, latest_release_fname))
361 if version == latest_released_version:
362 return
364 # Check if chromedriver was released on all supported platforms.
365 supported_platforms = ['linux32', 'linux64', 'mac32', 'win32']
366 for platform in supported_platforms:
367 if not _WasReleased(version, platform):
368 return
370 util.MarkBuildStepStart('updating LATEST_RELEASE to %s' % version)
372 temp_latest_release_fname = tempfile.mkstemp()[1]
373 with open(temp_latest_release_fname, 'w') as f:
374 f.write(version)
375 if slave_utils.GSUtilCopy(temp_latest_release_fname, latest_release_url,
376 mimetype='text/plain'):
377 util.MarkBuildStepError()
380 def _CleanTmpDir():
381 tmp_dir = tempfile.gettempdir()
382 print 'cleaning temp directory:', tmp_dir
383 for file_name in os.listdir(tmp_dir):
384 file_path = os.path.join(tmp_dir, file_name)
385 if os.path.isdir(file_path):
386 print 'deleting sub-directory', file_path
387 shutil.rmtree(file_path, True)
388 if file_name.startswith('chromedriver_'):
389 print 'deleting file', file_path
390 os.remove(file_path)
393 def _GetCommitPositionFromGitHash(snapshot_hashcode):
394 json_url = GS_GIT_LOG_URL % snapshot_hashcode
395 try:
396 response = urllib2.urlopen(json_url)
397 except urllib2.HTTPError as error:
398 util.PrintAndFlush('HTTP Error %d' % error.getcode())
399 return None
400 except urllib2.URLError as error:
401 util.PrintAndFlush('URL Error %s' % error.message)
402 return None
403 data = json.loads(response.read()[4:])
404 if 'message' in data:
405 message = data['message'].split('\n')
406 message = [line for line in message if line.strip()]
407 search_pattern = re.compile(GS_SEARCH_PATTERN)
408 result = search_pattern.search(message[len(message)-1])
409 if result:
410 return result.group(1)
411 util.PrintAndFlush('Failed to get commit position number for %s' %
412 snapshot_hashcode)
413 return None
416 def _GetGitHashFromCommitPosition(commit_position):
417 json_url = CR_REV_URL % commit_position
418 try:
419 response = urllib2.urlopen(json_url)
420 except urllib2.HTTPError as error:
421 util.PrintAndFlush('HTTP Error %d' % error.getcode())
422 return None
423 except urllib2.URLError as error:
424 util.PrintAndFlush('URL Error %s' % error.message)
425 return None
426 data = json.loads(response.read())
427 if 'git_sha' in data:
428 return data['git_sha']
429 util.PrintAndFlush('Failed to get git hash for %s' % commit_position)
430 return None
433 def _WaitForLatestSnapshot(commit_position):
434 util.MarkBuildStepStart('wait_for_snapshot')
435 while True:
436 snapshot_position = archive.GetLatestSnapshotVersion()
437 if commit_position is not None and snapshot_position is not None:
438 if int(snapshot_position) >= int(commit_position):
439 break
440 util.PrintAndFlush('Waiting for snapshot >= %s, found %s' %
441 (commit_position, snapshot_position))
442 time.sleep(60)
443 util.PrintAndFlush('Got snapshot commit position %s' % snapshot_position)
446 def _AddToolsToPath(platform_name):
447 """Add some tools like Ant and Java to PATH for testing steps to use."""
448 paths = []
449 error_message = ''
450 if platform_name == 'win32':
451 paths = [
452 # Path to Ant and Java, required for the java acceptance tests.
453 'C:\\Program Files (x86)\\Java\\ant\\bin',
454 'C:\\Program Files (x86)\\Java\\jre\\bin',
456 error_message = ('Java test steps will fail as expected and '
457 'they can be ignored.\n'
458 'Ant, Java or others might not be installed on bot.\n'
459 'Please refer to page "WATERFALL" on site '
460 'go/chromedriver.')
461 if paths:
462 util.MarkBuildStepStart('Add tools to PATH')
463 path_missing = False
464 for path in paths:
465 if not os.path.isdir(path) or not os.listdir(path):
466 print 'Directory "%s" is not found or empty.' % path
467 path_missing = True
468 if path_missing:
469 print error_message
470 util.MarkBuildStepError()
471 return
472 os.environ['PATH'] += os.pathsep + os.pathsep.join(paths)
475 def main():
476 parser = optparse.OptionParser()
477 parser.add_option(
478 '', '--android-packages',
479 help=('Comma separated list of application package names, '
480 'if running tests on Android.'))
481 parser.add_option(
482 '-r', '--revision', help='Chromium git revision hash')
483 parser.add_option(
484 '', '--update-log', action='store_true',
485 help='Update the test results log (only applicable to Android)')
486 options, _ = parser.parse_args()
488 bitness = '32'
489 if util.IsLinux() and platform_module.architecture()[0] == '64bit':
490 bitness = '64'
491 platform = '%s%s' % (util.GetPlatformName(), bitness)
492 if options.android_packages:
493 platform = 'android'
495 _CleanTmpDir()
497 if not options.revision:
498 commit_position = None
499 else:
500 commit_position = _GetCommitPositionFromGitHash(options.revision)
502 if platform == 'android':
503 if not options.revision and options.update_log:
504 parser.error('Must supply a --revision with --update-log')
505 _DownloadPrebuilts()
506 else:
507 if not options.revision:
508 parser.error('Must supply a --revision')
509 if platform == 'linux64':
510 _ArchivePrebuilts(commit_position)
511 _WaitForLatestSnapshot(commit_position)
513 _AddToolsToPath(platform)
515 cmd = [
516 sys.executable,
517 os.path.join(_THIS_DIR, 'test', 'run_all_tests.py'),
519 if platform == 'android':
520 cmd.append('--android-packages=' + options.android_packages)
522 passed = (util.RunCommand(cmd) == 0)
524 _ArchiveServerLogs()
526 if platform == 'android':
527 if options.update_log:
528 util.MarkBuildStepStart('update test result log')
529 _UpdateTestResultsLog(platform, commit_position, passed)
530 elif passed:
531 _ArchiveGoodBuild(platform, commit_position)
532 _MaybeRelease(platform)
534 if not passed:
535 # Make sure the build is red if there is some uncaught exception during
536 # running run_all_tests.py.
537 util.MarkBuildStepStart('run_all_tests.py')
538 util.MarkBuildStepError()
540 # Add a "cleanup" step so that errors from runtest.py or bb_device_steps.py
541 # (which invoke this script) are kept in thier own build step.
542 util.MarkBuildStepStart('cleanup')
545 if __name__ == '__main__':
546 main()