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."""
15 import platform
as platform_module
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'
32 'http://chromedriver-data.storage.googleapis.com/server_logs')
33 TEST_LOG_FORMAT
= '%s_log.json'
35 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
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
,
45 sys
.path
.append(SCRIPT_DIR
)
46 sys
.path
.append(SITE_CONFIG_DIR
)
50 from slave
import gsutil_download
51 from slave
import slave_utils
55 def _ArchivePrebuilts(revision
):
56 """Uploads the prebuilts to google storage."""
57 util
.MarkBuildStepStart('archive prebuilts')
58 zip_path
= util
.Zip(os
.path
.join(chrome_paths
.GetBuildDir(['chromedriver']),
60 if slave_utils
.GSUtilCopy(
62 '%s/%s' % (GS_PREBUILTS_URL
, 'r%s.zip' % revision
)):
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(),
71 base_name
= os
.path
.basename(server_log
)
72 util
.AddLink(base_name
, '%s/%s' % (SERVER_LOGS_LINK
, base_name
))
73 slave_utils
.GSUtilCopy(
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',
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.
96 platform: The platform that the test results log is for.
99 A dictionary where the keys are SVN revisions 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
)
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
, revision
, passed
):
126 """Updates the test results log for the given platform.
129 platform: The platform name.
130 revision: The SVN revision number.
131 passed: Boolean indicating whether the tests passed at this revision.
133 assert isinstance(revision
, int), 'The revision must be an integer'
134 log
= _GetTestResultsLog(platform
)
136 del log
[min(log
.keys())]
137 assert revision
not in log
, 'Results already exist for revision %s' % revision
138 log
[revision
] = bool(passed
)
139 _PutTestResultsLog(platform
, log
)
143 """Get the current chromedriver version."""
144 with
open(os
.path
.join(_THIS_DIR
, 'VERSION'), 'r') as f
:
145 return f
.read().strip()
148 def _GetSupportedChromeVersions():
149 """Get the minimum and maximum supported Chrome versions.
152 A tuple of the form (min_version, max_version).
154 # Minimum supported Chrome version is embedded as:
155 # const int kMinimumSupportedChromeVersion[] = {27, 0, 1453, 0};
156 with
open(os
.path
.join(_THIS_DIR
, 'chrome', 'version.cc'), 'r') as f
:
157 lines
= f
.readlines()
158 chrome_min_version_line
= [
159 x
for x
in lines
if 'kMinimumSupportedChromeVersion' in x
]
160 chrome_min_version
= chrome_min_version_line
[0].split('{')[1].split(',')[0]
161 with
open(os
.path
.join(chrome_paths
.GetSrc(), 'chrome', 'VERSION'), 'r') as f
:
162 chrome_max_version
= f
.readlines()[0].split('=')[1].strip()
163 return (chrome_min_version
, chrome_max_version
)
166 def _RevisionState(test_results_log
, revision
):
167 """Check the state of tests at a given SVN revision.
169 Considers tests as having passed at a revision if they passed at revisons both
173 test_results_log: A test results log dictionary from _GetTestResultsLog().
174 revision: The revision to check at.
177 'passed', 'failed', or 'unknown'
179 assert isinstance(revision
, int), 'The revision must be an integer'
180 keys
= sorted(test_results_log
.keys())
181 # Return passed if the exact revision passed on Android.
182 if revision
in test_results_log
:
183 return 'passed' if test_results_log
[revision
] else 'failed'
184 # Tests were not run on this exact revision on Android.
185 index
= bisect
.bisect_right(keys
, revision
)
186 # Tests have not yet run on Android at or above this revision.
187 if index
== len(test_results_log
):
189 # No log exists for any prior revision, assume it failed.
192 # Return passed if the revisions on both sides passed.
193 if test_results_log
[keys
[index
]] and test_results_log
[keys
[index
- 1]]:
198 def _ArchiveGoodBuild(platform
, revision
):
199 """Archive chromedriver binary if the build is green."""
200 assert platform
!= 'android'
201 util
.MarkBuildStepStart('archive build')
203 server_name
= 'chromedriver'
205 server_name
+= '.exe'
206 zip_path
= util
.Zip(os
.path
.join(chrome_paths
.GetBuildDir([server_name
]),
209 build_name
= 'chromedriver_%s_%s.%s.zip' % (
210 platform
, _GetVersion(), revision
)
211 build_url
= '%s/%s' % (GS_CONTINUOUS_URL
, build_name
)
212 if slave_utils
.GSUtilCopy(zip_path
, build_url
):
213 util
.MarkBuildStepError()
215 (latest_fd
, latest_file
) = tempfile
.mkstemp()
216 os
.write(latest_fd
, build_name
)
218 latest_url
= '%s/latest_%s' % (GS_CONTINUOUS_URL
, platform
)
219 if slave_utils
.GSUtilCopy(latest_file
, latest_url
, mimetype
='text/plain'):
220 util
.MarkBuildStepError()
221 os
.remove(latest_file
)
224 def _WasReleased(version
, platform
):
225 """Check if the specified version is released for the given platform."""
226 result
, _
= slave_utils
.GSUtilListBucket(
227 '%s/%s/chromedriver_%s.zip' % (GS_CHROMEDRIVER_BUCKET
, version
, platform
),
232 def _MaybeRelease(platform
):
233 """Releases a release candidate if conditions are right."""
234 assert platform
!= 'android'
236 version
= _GetVersion()
238 # Check if the current version has already been released.
239 if _WasReleased(version
, platform
):
242 # Fetch Android test results.
243 android_test_results
= _GetTestResultsLog('android')
245 # Fetch release candidates.
246 result
, output
= slave_utils
.GSUtilListBucket(
247 '%s/chromedriver_%s_%s*' % (
248 GS_CONTINUOUS_URL
, platform
, version
),
250 assert result
== 0 and output
, 'No release candidates found'
251 candidate_pattern
= re
.compile(
252 r
'.*/chromedriver_%s_%s\.(\d+)\.zip$' % (platform
, version
))
254 for line
in output
.strip().split('\n'):
255 result
= candidate_pattern
.match(line
)
257 print 'Ignored line "%s"' % line
259 candidates
.append(int(result
.group(1)))
261 # Release the latest candidate build that passed Android, if any.
262 # In this way, if a hot fix is needed, we can delete the release from
263 # the chromedriver bucket instead of bumping up the release version number.
264 candidates
.sort(reverse
=True)
265 for revision
in candidates
:
266 android_result
= _RevisionState(android_test_results
, revision
)
267 if android_result
== 'failed':
268 print 'Android tests did not pass at revision', revision
269 elif android_result
== 'passed':
270 print 'Android tests passed at revision', revision
271 candidate
= 'chromedriver_%s_%s.%s.zip' % (platform
, version
, revision
)
272 _Release('%s/%s' % (GS_CONTINUOUS_URL
, candidate
), version
, platform
)
275 print 'Android tests have not run at a revision as recent as', revision
278 def _Release(build
, version
, platform
):
279 """Releases the given candidate build."""
280 release_name
= 'chromedriver_%s.zip' % platform
281 util
.MarkBuildStepStart('releasing %s' % release_name
)
282 temp_dir
= util
.MakeTempDir()
283 slave_utils
.GSUtilCopy(build
, temp_dir
)
284 zip_path
= os
.path
.join(temp_dir
, os
.path
.basename(build
))
287 util
.Unzip(zip_path
, temp_dir
)
288 server_path
= os
.path
.join(temp_dir
, 'chromedriver')
289 util
.RunCommand(['strip', server_path
])
290 zip_path
= util
.Zip(server_path
)
292 slave_utils
.GSUtilCopy(
293 zip_path
, '%s/%s/%s' % (GS_CHROMEDRIVER_BUCKET
, version
, release_name
))
295 _MaybeUploadReleaseNotes(version
)
296 _MaybeUpdateLatestRelease(version
)
299 def _GetWebPageContent(url
):
300 """Return the content of the web page specified by the given url."""
301 return urllib2
.urlopen(url
).read()
304 def _MaybeUploadReleaseNotes(version
):
305 """Upload release notes if conditions are right."""
306 # Check if the current version has already been released.
307 notes_name
= 'notes.txt'
308 notes_url
= '%s/%s/%s' % (GS_CHROMEDRIVER_BUCKET
, version
, notes_name
)
309 prev_version
= '.'.join([version
.split('.')[0],
310 str(int(version
.split('.')[1]) - 1)])
311 prev_notes_url
= '%s/%s/%s' % (
312 GS_CHROMEDRIVER_BUCKET
, prev_version
, notes_name
)
314 result
, _
= slave_utils
.GSUtilListBucket(notes_url
, [])
319 query
= ('https://code.google.com/p/chromedriver/issues/csv?'
320 'q=status%3AToBeReleased&colspec=ID%20Summary')
321 issues
= StringIO
.StringIO(_GetWebPageContent(query
).split('\n', 1)[1])
322 for issue
in csv
.reader(issues
):
328 fixed_issues
+= ['Resolved issue %s: %s [%s]' % (issue_id
, desc
, labels
)]
331 temp_notes_fname
= tempfile
.mkstemp()[1]
332 if not slave_utils
.GSUtilDownloadFile(prev_notes_url
, temp_notes_fname
):
333 with
open(temp_notes_fname
, 'rb') as f
:
336 new_notes
= '----------ChromeDriver v%s (%s)----------\n%s\n%s\n\n%s' % (
337 version
, datetime
.date
.today().isoformat(),
338 'Supports Chrome v%s-%s' % _GetSupportedChromeVersions(),
339 '\n'.join(fixed_issues
),
341 with
open(temp_notes_fname
, 'w') as f
:
344 if slave_utils
.GSUtilCopy(temp_notes_fname
, notes_url
, mimetype
='text/plain'):
345 util
.MarkBuildStepError()
348 def _MaybeUpdateLatestRelease(version
):
349 """Update the file LATEST_RELEASE with the latest release version number."""
350 latest_release_fname
= 'LATEST_RELEASE'
351 latest_release_url
= '%s/%s' % (GS_CHROMEDRIVER_BUCKET
, latest_release_fname
)
353 # Check if LATEST_RELEASE is up-to-date.
354 latest_released_version
= _GetWebPageContent(
355 '%s/%s' % (GS_CHROMEDRIVER_RELEASE_URL
, latest_release_fname
))
356 if version
== latest_released_version
:
359 # Check if chromedriver was released on all supported platforms.
360 supported_platforms
= ['linux32', 'linux64', 'mac32', 'win32']
361 for platform
in supported_platforms
:
362 if not _WasReleased(version
, platform
):
365 util
.MarkBuildStepStart('updating LATEST_RELEASE to %s' % version
)
367 temp_latest_release_fname
= tempfile
.mkstemp()[1]
368 with
open(temp_latest_release_fname
, 'w') as f
:
370 if slave_utils
.GSUtilCopy(temp_latest_release_fname
, latest_release_url
,
371 mimetype
='text/plain'):
372 util
.MarkBuildStepError()
376 tmp_dir
= tempfile
.gettempdir()
377 print 'cleaning temp directory:', tmp_dir
378 for file_name
in os
.listdir(tmp_dir
):
379 file_path
= os
.path
.join(tmp_dir
, file_name
)
380 if os
.path
.isdir(file_path
):
381 print 'deleting sub-directory', file_path
382 shutil
.rmtree(file_path
, True)
383 if file_name
.startswith('chromedriver_'):
384 print 'deleting file', file_path
388 def _GetCommitPositionFromGitHash(snapshot_hashcode
):
389 json_url
= GS_GIT_LOG_URL
% snapshot_hashcode
391 response
= urllib2
.urlopen(json_url
)
392 except urllib2
.HTTPError
as error
:
393 util
.PrintAndFlush('HTTP Error %d' % error
.getcode())
395 except urllib2
.URLError
as error
:
396 util
.PrintAndFlush('URL Error %s' % error
.message
)
398 data
= json
.loads(response
.read()[4:])
399 if 'message' in data
:
400 message
= data
['message'].split('\n')
401 message
= [line
for line
in message
if line
.strip()]
402 search_pattern
= re
.compile(GS_SEARCH_PATTERN
)
403 result
= search_pattern
.search(message
[len(message
)-1])
405 return result
.group(1)
406 util
.PrintAndFlush('Failed to get svn revision number for %s' %
411 def _GetGitHashFromCommitPosition(commit_position
):
412 json_url
= CR_REV_URL
% commit_position
414 response
= urllib2
.urlopen(json_url
)
415 except urllib2
.HTTPError
as error
:
416 util
.PrintAndFlush('HTTP Error %d' % error
.getcode())
418 except urllib2
.URLError
as error
:
419 util
.PrintAndFlush('URL Error %s' % error
.message
)
421 data
= json
.loads(response
.read())
422 if 'git_sha' in data
:
423 return data
['git_sha']
424 util
.PrintAndFlush('Failed to get git hash for %s' % commit_position
)
428 def _WaitForLatestSnapshot(revision
):
429 util
.MarkBuildStepStart('wait_for_snapshot')
430 def _IsRevisionNumber(revision
):
431 if isinstance(revision
, int):
434 return revision
.isdigit()
436 snapshot_revision
= archive
.GetLatestSnapshotVersion()
437 if not _IsRevisionNumber(snapshot_revision
):
438 snapshot_revision
= _GetCommitPositionFromGitHash(snapshot_revision
)
439 if revision
is not None and snapshot_revision
is not None:
440 if int(snapshot_revision
) >= int(revision
):
442 util
.PrintAndFlush('Waiting for snapshot >= %s, found %s' %
443 (revision
, snapshot_revision
))
445 util
.PrintAndFlush('Got snapshot revision %s' % snapshot_revision
)
448 def _AddToolsToPath(platform_name
):
449 """Add some tools like Ant and Java to PATH for testing steps to use."""
452 if platform_name
== 'win32':
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 '
464 util
.MarkBuildStepStart('Add tools to PATH')
467 if not os
.path
.isdir(path
) or not os
.listdir(path
):
468 print 'Directory "%s" is not found or empty.' % path
472 util
.MarkBuildStepError()
474 os
.environ
['PATH'] += os
.pathsep
+ os
.pathsep
.join(paths
)
478 parser
= optparse
.OptionParser()
480 '', '--android-packages',
481 help=('Comma separated list of application package names, '
482 'if running tests on Android.'))
484 '-r', '--revision', help='Chromium revision')
486 '', '--update-log', action
='store_true',
487 help='Update the test results log (only applicable to Android)')
488 options
, _
= parser
.parse_args()
491 if util
.IsLinux() and platform_module
.architecture()[0] == '64bit':
493 platform
= '%s%s' % (util
.GetPlatformName(), bitness
)
494 if options
.android_packages
:
499 if not options
.revision
:
500 commit_position
= None
501 elif options
.revision
.isdigit():
502 commit_position
= options
.revision
504 commit_position
= _GetCommitPositionFromGitHash(options
.revision
)
506 if platform
== 'android':
507 if not options
.revision
and options
.update_log
:
508 parser
.error('Must supply a --revision with --update-log')
511 if not options
.revision
:
512 parser
.error('Must supply a --revision')
513 if platform
== 'linux64':
514 _ArchivePrebuilts(commit_position
)
515 _WaitForLatestSnapshot(commit_position
)
517 _AddToolsToPath(platform
)
521 os
.path
.join(_THIS_DIR
, 'test', 'run_all_tests.py'),
523 if platform
== 'android':
524 cmd
.append('--android-packages=' + options
.android_packages
)
526 passed
= (util
.RunCommand(cmd
) == 0)
530 if platform
== 'android':
531 if options
.update_log
:
532 util
.MarkBuildStepStart('update test result log')
533 _UpdateTestResultsLog(platform
, commit_position
, passed
)
535 _ArchiveGoodBuild(platform
, commit_position
)
536 _MaybeRelease(platform
)
539 # Make sure the build is red if there is some uncaught exception during
540 # running run_all_tests.py.
541 util
.MarkBuildStepStart('run_all_tests.py')
542 util
.MarkBuildStepError()
544 # Add a "cleanup" step so that errors from runtest.py or bb_device_steps.py
545 # (which invoke this script) are kept in thier own build step.
546 util
.MarkBuildStepStart('cleanup')
549 if __name__
== '__main__':