[chromedriver] Fix bug in release notes function, and remove Hotlist-GoodFirstBug...
[chromium-blink-merge.git] / chrome / test / chromedriver / run_buildbot_steps.py
blob276e85375231a3beb07b96f2c8e9a0366cc9a04c
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 'can=1&q=label%%3AChromeDriver-%s&colspec=ID%%20Summary' % version)
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].split(', ')
333 labels.remove('ChromeDriver-%s' % version)
334 labels.remove('Hotlist-GoodFirstBug')
335 fixed_issues += ['Resolved issue %s: %s [%s]' % (issue_id, desc, labels)]
337 old_notes = ''
338 temp_notes_fname = tempfile.mkstemp()[1]
339 if not slave_utils.GSUtilDownloadFile(prev_notes_url, temp_notes_fname):
340 with open(temp_notes_fname, 'rb') as f:
341 old_notes = f.read()
343 new_notes = '----------ChromeDriver v%s (%s)----------\n%s\n%s\n\n%s' % (
344 version, datetime.date.today().isoformat(),
345 'Supports Chrome v%s-%s' % _GetSupportedChromeVersions(),
346 '\n'.join(fixed_issues),
347 old_notes)
348 with open(temp_notes_fname, 'w') as f:
349 f.write(new_notes)
351 if slave_utils.GSUtilCopy(temp_notes_fname, notes_url, mimetype='text/plain'):
352 util.MarkBuildStepError()
355 def _MaybeUpdateLatestRelease(version):
356 """Update the file LATEST_RELEASE with the latest release version number."""
357 latest_release_fname = 'LATEST_RELEASE'
358 latest_release_url = '%s/%s' % (GS_CHROMEDRIVER_BUCKET, latest_release_fname)
360 # Check if LATEST_RELEASE is up-to-date.
361 latest_released_version = _GetWebPageContent(
362 '%s/%s' % (GS_CHROMEDRIVER_RELEASE_URL, latest_release_fname))
363 if version == latest_released_version:
364 return
366 # Check if chromedriver was released on all supported platforms.
367 supported_platforms = ['linux32', 'linux64', 'mac32', 'win32']
368 for platform in supported_platforms:
369 if not _WasReleased(version, platform):
370 return
372 util.MarkBuildStepStart('updating LATEST_RELEASE to %s' % version)
374 temp_latest_release_fname = tempfile.mkstemp()[1]
375 with open(temp_latest_release_fname, 'w') as f:
376 f.write(version)
377 if slave_utils.GSUtilCopy(temp_latest_release_fname, latest_release_url,
378 mimetype='text/plain'):
379 util.MarkBuildStepError()
382 def _CleanTmpDir():
383 tmp_dir = tempfile.gettempdir()
384 print 'cleaning temp directory:', tmp_dir
385 for file_name in os.listdir(tmp_dir):
386 file_path = os.path.join(tmp_dir, file_name)
387 if os.path.isdir(file_path):
388 print 'deleting sub-directory', file_path
389 shutil.rmtree(file_path, True)
390 if file_name.startswith('chromedriver_'):
391 print 'deleting file', file_path
392 os.remove(file_path)
395 def _GetCommitPositionFromGitHash(snapshot_hashcode):
396 json_url = GS_GIT_LOG_URL % snapshot_hashcode
397 try:
398 response = urllib2.urlopen(json_url)
399 except urllib2.HTTPError as error:
400 util.PrintAndFlush('HTTP Error %d' % error.getcode())
401 return None
402 except urllib2.URLError as error:
403 util.PrintAndFlush('URL Error %s' % error.message)
404 return None
405 data = json.loads(response.read()[4:])
406 if 'message' in data:
407 message = data['message'].split('\n')
408 message = [line for line in message if line.strip()]
409 search_pattern = re.compile(GS_SEARCH_PATTERN)
410 result = search_pattern.search(message[len(message)-1])
411 if result:
412 return result.group(1)
413 util.PrintAndFlush('Failed to get commit position number for %s' %
414 snapshot_hashcode)
415 return None
418 def _GetGitHashFromCommitPosition(commit_position):
419 json_url = CR_REV_URL % commit_position
420 try:
421 response = urllib2.urlopen(json_url)
422 except urllib2.HTTPError as error:
423 util.PrintAndFlush('HTTP Error %d' % error.getcode())
424 return None
425 except urllib2.URLError as error:
426 util.PrintAndFlush('URL Error %s' % error.message)
427 return None
428 data = json.loads(response.read())
429 if 'git_sha' in data:
430 return data['git_sha']
431 util.PrintAndFlush('Failed to get git hash for %s' % commit_position)
432 return None
435 def _WaitForLatestSnapshot(commit_position):
436 util.MarkBuildStepStart('wait_for_snapshot')
437 while True:
438 snapshot_position = archive.GetLatestSnapshotVersion()
439 if commit_position is not None and snapshot_position is not None:
440 if int(snapshot_position) >= int(commit_position):
441 break
442 util.PrintAndFlush('Waiting for snapshot >= %s, found %s' %
443 (commit_position, snapshot_position))
444 time.sleep(60)
445 util.PrintAndFlush('Got snapshot commit position %s' % snapshot_position)
448 def _AddToolsToPath(platform_name):
449 """Add some tools like Ant and Java to PATH for testing steps to use."""
450 paths = []
451 error_message = ''
452 if platform_name == 'win32':
453 paths = [
454 # Path to Ant and Java, required for the java acceptance tests.
455 'C:\\Program Files (x86)\\Java\\ant\\bin',
456 'C:\\Program Files (x86)\\Java\\jre\\bin',
458 error_message = ('Java test steps will fail as expected and '
459 'they can be ignored.\n'
460 'Ant, Java or others might not be installed on bot.\n'
461 'Please refer to page "WATERFALL" on site '
462 'go/chromedriver.')
463 if paths:
464 util.MarkBuildStepStart('Add tools to PATH')
465 path_missing = False
466 for path in paths:
467 if not os.path.isdir(path) or not os.listdir(path):
468 print 'Directory "%s" is not found or empty.' % path
469 path_missing = True
470 if path_missing:
471 print error_message
472 util.MarkBuildStepError()
473 return
474 os.environ['PATH'] += os.pathsep + os.pathsep.join(paths)
477 def main():
478 parser = optparse.OptionParser()
479 parser.add_option(
480 '', '--android-packages',
481 help=('Comma separated list of application package names, '
482 'if running tests on Android.'))
483 parser.add_option(
484 '-r', '--revision', help='Chromium git revision hash')
485 parser.add_option(
486 '', '--update-log', action='store_true',
487 help='Update the test results log (only applicable to Android)')
488 options, _ = parser.parse_args()
490 bitness = '32'
491 if util.IsLinux() and platform_module.architecture()[0] == '64bit':
492 bitness = '64'
493 platform = '%s%s' % (util.GetPlatformName(), bitness)
494 if options.android_packages:
495 platform = 'android'
497 _CleanTmpDir()
499 if not options.revision:
500 commit_position = None
501 else:
502 commit_position = _GetCommitPositionFromGitHash(options.revision)
504 if platform == 'android':
505 if not options.revision and options.update_log:
506 parser.error('Must supply a --revision with --update-log')
507 _DownloadPrebuilts()
508 else:
509 if not options.revision:
510 parser.error('Must supply a --revision')
511 if platform == 'linux64':
512 _ArchivePrebuilts(commit_position)
513 _WaitForLatestSnapshot(commit_position)
515 _AddToolsToPath(platform)
517 cmd = [
518 sys.executable,
519 os.path.join(_THIS_DIR, 'test', 'run_all_tests.py'),
521 if platform == 'android':
522 cmd.append('--android-packages=' + options.android_packages)
524 passed = (util.RunCommand(cmd) == 0)
526 _ArchiveServerLogs()
528 if platform == 'android':
529 if options.update_log:
530 util.MarkBuildStepStart('update test result log')
531 _UpdateTestResultsLog(platform, commit_position, passed)
532 elif passed:
533 _ArchiveGoodBuild(platform, commit_position)
534 _MaybeRelease(platform)
536 if not passed:
537 # Make sure the build is red if there is some uncaught exception during
538 # running run_all_tests.py.
539 util.MarkBuildStepStart('run_all_tests.py')
540 util.MarkBuildStepError()
542 # Add a "cleanup" step so that errors from runtest.py or bb_device_steps.py
543 # (which invoke this script) are kept in thier own build step.
544 util.MarkBuildStepStart('cleanup')
547 if __name__ == '__main__':
548 main()