#!/usr/bin/env python
# Copyright (c) 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Runs all the buildbot steps for ChromeDriver except for update/compile."""
import bisect
import csv
import datetime
import glob
import json
import optparse
import os
import platform as platform_module
import re
import shutil
import StringIO
import sys
import tempfile
import time
import urllib2
_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
GS_CHROMEDRIVER_BUCKET = 'gs://chromedriver'
GS_CHROMEDRIVER_DATA_BUCKET = 'gs://chromedriver-data'
GS_CHROMEDRIVER_RELEASE_URL = 'http://chromedriver.storage.googleapis.com'
GS_CONTINUOUS_URL = GS_CHROMEDRIVER_DATA_BUCKET + '/continuous'
GS_PREBUILTS_URL = GS_CHROMEDRIVER_DATA_BUCKET + '/prebuilts'
GS_SERVER_LOGS_URL = GS_CHROMEDRIVER_DATA_BUCKET + '/server_logs'
SERVER_LOGS_LINK = (
'http://chromedriver-data.storage.googleapis.com/server_logs')
TEST_LOG_FORMAT = '%s_log.json'
SCRIPT_DIR = os.path.join(_THIS_DIR, os.pardir, os.pardir, os.pardir, os.pardir,
os.pardir, os.pardir, os.pardir, 'scripts')
SITE_CONFIG_DIR = os.path.join(_THIS_DIR, os.pardir, os.pardir, os.pardir,
os.pardir, os.pardir, os.pardir, os.pardir,
'site_config')
sys.path.append(SCRIPT_DIR)
sys.path.append(SITE_CONFIG_DIR)
import archive
import chrome_paths
from slave import gsutil_download
from slave import slave_utils
import util
def _ArchivePrebuilts(revision):
"""Uploads the prebuilts to google storage."""
util.MarkBuildStepStart('archive prebuilts')
zip_path = util.Zip(os.path.join(chrome_paths.GetBuildDir(['chromedriver']),
'chromedriver'))
if slave_utils.GSUtilCopy(
zip_path,
'%s/%s' % (GS_PREBUILTS_URL, 'r%s.zip' % revision)):
util.MarkBuildStepError()
def _ArchiveServerLogs():
"""Uploads chromedriver server logs to google storage."""
util.MarkBuildStepStart('archive chromedriver server logs')
for server_log in glob.glob(os.path.join(tempfile.gettempdir(),
'chromedriver_*')):
base_name = os.path.basename(server_log)
util.AddLink(base_name, '%s/%s' % (SERVER_LOGS_LINK, base_name))
slave_utils.GSUtilCopy(
server_log,
'%s/%s' % (GS_SERVER_LOGS_URL, base_name),
mimetype='text/plain')
def _DownloadPrebuilts():
"""Downloads the most recent prebuilts from google storage."""
util.MarkBuildStepStart('Download latest chromedriver')
zip_path = os.path.join(util.MakeTempDir(), 'build.zip')
if gsutil_download.DownloadLatestFile(GS_PREBUILTS_URL,
GS_PREBUILTS_URL + '/r',
zip_path):
util.MarkBuildStepError()
util.Unzip(zip_path, chrome_paths.GetBuildDir(['host_forwarder']))
def _GetTestResultsLog(platform):
"""Gets the test results log for the given platform.
Args:
platform: The platform that the test results log is for.
Returns:
A dictionary where the keys are SVN revisions and the values are booleans
indicating whether the tests passed.
"""
temp_log = tempfile.mkstemp()[1]
log_name = TEST_LOG_FORMAT % platform
result = slave_utils.GSUtilDownloadFile(
'%s/%s' % (GS_CHROMEDRIVER_DATA_BUCKET, log_name), temp_log)
if result:
return {}
with open(temp_log, 'rb') as log_file:
json_dict = json.load(log_file)
# Workaround for json encoding dictionary keys as strings.
return dict([(int(v[0]), v[1]) for v in json_dict.items()])
def _PutTestResultsLog(platform, test_results_log):
"""Pushes the given test results log to google storage."""
temp_dir = util.MakeTempDir()
log_name = TEST_LOG_FORMAT % platform
log_path = os.path.join(temp_dir, log_name)
with open(log_path, 'wb') as log_file:
json.dump(test_results_log, log_file)
if slave_utils.GSUtilCopyFile(log_path, GS_CHROMEDRIVER_DATA_BUCKET):
raise Exception('Failed to upload test results log to google storage')
def _UpdateTestResultsLog(platform, revision, passed):
"""Updates the test results log for the given platform.
Args:
platform: The platform name.
revision: The SVN revision number.
passed: Boolean indicating whether the tests passed at this revision.
"""
assert isinstance(revision, int), 'The revision must be an integer'
log = _GetTestResultsLog(platform)
if len(log) > 500:
del log[min(log.keys())]
assert revision not in log, 'Results already exist for revision %s' % revision
log[revision] = bool(passed)
_PutTestResultsLog(platform, log)
def _GetVersion():
"""Get the current chromedriver version."""
with open(os.path.join(_THIS_DIR, 'VERSION'), 'r') as f:
return f.read().strip()
def _GetSupportedChromeVersions():
"""Get the minimum and maximum supported Chrome versions.
Returns:
A tuple of the form (min_version, max_version).
"""
# Minimum supported Chrome version is embedded as:
# const int kMinimumSupportedChromeVersion[] = {27, 0, 1453, 0};
with open(os.path.join(_THIS_DIR, 'chrome', 'version.cc'), 'r') as f:
lines = f.readlines()
chrome_min_version_line = [
x for x in lines if 'kMinimumSupportedChromeVersion' in x]
chrome_min_version = chrome_min_version_line[0].split('{')[1].split(',')[0]
with open(os.path.join(chrome_paths.GetSrc(), 'chrome', 'VERSION'), 'r') as f:
chrome_max_version = f.readlines()[0].split('=')[1].strip()
return (chrome_min_version, chrome_max_version)
def _RevisionState(test_results_log, revision):
"""Check the state of tests at a given SVN revision.
Considers tests as having passed at a revision if they passed at revisons both
before and after.
Args:
test_results_log: A test results log dictionary from _GetTestResultsLog().
revision: The revision to check at.
Returns:
'passed', 'failed', or 'unknown'
"""
assert isinstance(revision, int), 'The revision must be an integer'
keys = sorted(test_results_log.keys())
# Return passed if the exact revision passed on Android.
if revision in test_results_log:
return 'passed' if test_results_log[revision] else 'failed'
# Tests were not run on this exact revision on Android.
index = bisect.bisect_right(keys, revision)
# Tests have not yet run on Android at or above this revision.
if index == len(test_results_log):
return 'unknown'
# No log exists for any prior revision, assume it failed.
if index == 0:
return 'failed'
# Return passed if the revisions on both sides passed.
if test_results_log[keys[index]] and test_results_log[keys[index - 1]]:
return 'passed'
return 'failed'
def _ArchiveGoodBuild(platform, revision):
"""Archive chromedriver binary if the build is green."""
assert platform != 'android'
util.MarkBuildStepStart('archive build')
server_name = 'chromedriver'
if util.IsWindows():
server_name += '.exe'
zip_path = util.Zip(os.path.join(chrome_paths.GetBuildDir([server_name]),
server_name))
build_name = 'chromedriver_%s_%s.%s.zip' % (
platform, _GetVersion(), revision)
build_url = '%s/%s' % (GS_CONTINUOUS_URL, build_name)
if slave_utils.GSUtilCopy(zip_path, build_url):
util.MarkBuildStepError()
(latest_fd, latest_file) = tempfile.mkstemp()
os.write(latest_fd, build_name)
os.close(latest_fd)
latest_url = '%s/latest_%s' % (GS_CONTINUOUS_URL, platform)
if slave_utils.GSUtilCopy(latest_file, latest_url, mimetype='text/plain'):
util.MarkBuildStepError()
os.remove(latest_file)
def _WasReleased(version, platform):
"""Check if the specified version is released for the given platform."""
result, _ = slave_utils.GSUtilListBucket(
'%s/%s/chromedriver_%s.zip' % (GS_CHROMEDRIVER_BUCKET, version, platform),
[])
return result == 0
def _MaybeRelease(platform):
"""Releases a release candidate if conditions are right."""
assert platform != 'android'
version = _GetVersion()
# Check if the current version has already been released.
if _WasReleased(version, platform):
return
# Fetch Android test results.
android_test_results = _GetTestResultsLog('android')
# Fetch release candidates.
result, output = slave_utils.GSUtilListBucket(
'%s/chromedriver_%s_%s*' % (
GS_CONTINUOUS_URL, platform, version),
[])
assert result == 0 and output, 'No release candidates found'
candidate_pattern = re.compile(
r'.*/chromedriver_%s_%s\.(\d+)\.zip$' % (platform, version))
candidates = []
for line in output.strip().split('\n'):
result = candidate_pattern.match(line)
if not result:
print 'Ignored line "%s"' % line
continue
candidates.append(int(result.group(1)))
# Release the latest candidate build that passed Android, if any.
# In this way, if a hot fix is needed, we can delete the release from
# the chromedriver bucket instead of bumping up the release version number.
candidates.sort(reverse=True)
for revision in candidates:
android_result = _RevisionState(android_test_results, revision)
if android_result == 'failed':
print 'Android tests did not pass at revision', revision
elif android_result == 'passed':
print 'Android tests passed at revision', revision
candidate = 'chromedriver_%s_%s.%s.zip' % (platform, version, revision)
_Release('%s/%s' % (GS_CONTINUOUS_URL, candidate), version, platform)
break
else:
print 'Android tests have not run at a revision as recent as', revision
def _Release(build, version, platform):
"""Releases the given candidate build."""
release_name = 'chromedriver_%s.zip' % platform
util.MarkBuildStepStart('releasing %s' % release_name)
temp_dir = util.MakeTempDir()
slave_utils.GSUtilCopy(build, temp_dir)
zip_path = os.path.join(temp_dir, os.path.basename(build))
if util.IsLinux():
util.Unzip(zip_path, temp_dir)
server_path = os.path.join(temp_dir, 'chromedriver')
util.RunCommand(['strip', server_path])
zip_path = util.Zip(server_path)
slave_utils.GSUtilCopy(
zip_path, '%s/%s/%s' % (GS_CHROMEDRIVER_BUCKET, version, release_name))
_MaybeUploadReleaseNotes(version)
_MaybeUpdateLatestRelease(version)
def _GetWebPageContent(url):
"""Return the content of the web page specified by the given url."""
return urllib2.urlopen(url).read()
def _MaybeUploadReleaseNotes(version):
"""Upload release notes if conditions are right."""
# Check if the current version has already been released.
notes_name = 'notes.txt'
notes_url = '%s/%s/%s' % (GS_CHROMEDRIVER_BUCKET, version, notes_name)
prev_version = '.'.join([version.split('.')[0],
str(int(version.split('.')[1]) - 1)])
prev_notes_url = '%s/%s/%s' % (
GS_CHROMEDRIVER_BUCKET, prev_version, notes_name)
result, _ = slave_utils.GSUtilListBucket(notes_url, [])
if result == 0:
return
fixed_issues = []
query = ('https://code.google.com/p/chromedriver/issues/csv?'
'q=status%3AToBeReleased&colspec=ID%20Summary')
issues = StringIO.StringIO(_GetWebPageContent(query).split('\n', 1)[1])
for issue in csv.reader(issues):
if not issue:
continue
issue_id = issue[0]
desc = issue[1]
labels = issue[2]
fixed_issues += ['Resolved issue %s: %s [%s]' % (issue_id, desc, labels)]
old_notes = ''
temp_notes_fname = tempfile.mkstemp()[1]
if not slave_utils.GSUtilDownloadFile(prev_notes_url, temp_notes_fname):
with open(temp_notes_fname, 'rb') as f:
old_notes = f.read()
new_notes = '----------ChromeDriver v%s (%s)----------\n%s\n%s\n\n%s' % (
version, datetime.date.today().isoformat(),
'Supports Chrome v%s-%s' % _GetSupportedChromeVersions(),
'\n'.join(fixed_issues),
old_notes)
with open(temp_notes_fname, 'w') as f:
f.write(new_notes)
if slave_utils.GSUtilCopy(temp_notes_fname, notes_url, mimetype='text/plain'):
util.MarkBuildStepError()
def _MaybeUpdateLatestRelease(version):
"""Update the file LATEST_RELEASE with the latest release version number."""
latest_release_fname = 'LATEST_RELEASE'
latest_release_url = '%s/%s' % (GS_CHROMEDRIVER_BUCKET, latest_release_fname)
# Check if LATEST_RELEASE is up-to-date.
latest_released_version = _GetWebPageContent(
'%s/%s' % (GS_CHROMEDRIVER_RELEASE_URL, latest_release_fname))
if version == latest_released_version:
return
# Check if chromedriver was released on all supported platforms.
supported_platforms = ['linux32', 'linux64', 'mac32', 'win32']
for platform in supported_platforms:
if not _WasReleased(version, platform):
return
util.MarkBuildStepStart('updating LATEST_RELEASE to %s' % version)
temp_latest_release_fname = tempfile.mkstemp()[1]
with open(temp_latest_release_fname, 'w') as f:
f.write(version)
if slave_utils.GSUtilCopy(temp_latest_release_fname, latest_release_url,
mimetype='text/plain'):
util.MarkBuildStepError()
def _CleanTmpDir():
tmp_dir = tempfile.gettempdir()
print 'cleaning temp directory:', tmp_dir
for file_name in os.listdir(tmp_dir):
file_path = os.path.join(tmp_dir, file_name)
if os.path.isdir(file_path):
print 'deleting sub-directory', file_path
shutil.rmtree(file_path, True)
if file_name.startswith('chromedriver_'):
print 'deleting file', file_path
os.remove(file_path)
def _WaitForLatestSnapshot(revision):
util.MarkBuildStepStart('wait_for_snapshot')
while True:
snapshot_revision = archive.GetLatestSnapshotVersion()
if int(snapshot_revision) >= int(revision):
break
util.PrintAndFlush('Waiting for snapshot >= %s, found %s' %
(revision, snapshot_revision))
time.sleep(60)
util.PrintAndFlush('Got snapshot revision %s' % snapshot_revision)
def _AddToolsToPath(platform_name):
"""Add some tools like Ant and Java to PATH for testing steps to use."""
paths = []
error_message = ''
if platform_name == 'win32':
paths = [
# Path to Ant and Java, required for the java acceptance tests.
'C:\\Program Files (x86)\\Java\\ant\\bin',
'C:\\Program Files (x86)\\Java\\jre\\bin',
]
error_message = ('Java test steps will fail as expected and '
'they can be ignored.\n'
'Ant, Java or others might not be installed on bot.\n'
'Please refer to page "WATERFALL" on site '
'go/chromedriver.')
if paths:
util.MarkBuildStepStart('Add tools to PATH')
path_missing = False
for path in paths:
if not os.path.isdir(path) or not os.listdir(path):
print 'Directory "%s" is not found or empty.' % path
path_missing = True
if path_missing:
print error_message
util.MarkBuildStepError()
return
os.environ['PATH'] += os.pathsep + os.pathsep.join(paths)
def main():
parser = optparse.OptionParser()
parser.add_option(
'', '--android-packages',
help=('Comma separated list of application package names, '
'if running tests on Android.'))
parser.add_option(
'-r', '--revision', type='int', help='Chromium revision')
parser.add_option(
'', '--update-log', action='store_true',
help='Update the test results log (only applicable to Android)')
options, _ = parser.parse_args()
bitness = '32'
if util.IsLinux() and platform_module.architecture()[0] == '64bit':
bitness = '64'
platform = '%s%s' % (util.GetPlatformName(), bitness)
if options.android_packages:
platform = 'android'
_CleanTmpDir()
if platform == 'android':
if not options.revision and options.update_log:
parser.error('Must supply a --revision with --update-log')
_DownloadPrebuilts()
else:
if not options.revision:
parser.error('Must supply a --revision')
if platform == 'linux64':
_ArchivePrebuilts(options.revision)
_WaitForLatestSnapshot(options.revision)
_AddToolsToPath(platform)
cmd = [
sys.executable,
os.path.join(_THIS_DIR, 'test', 'run_all_tests.py'),
]
if platform == 'android':
cmd.append('--android-packages=' + options.android_packages)
passed = (util.RunCommand(cmd) == 0)
_ArchiveServerLogs()
if platform == 'android':
if options.update_log:
util.MarkBuildStepStart('update test result log')
_UpdateTestResultsLog(platform, options.revision, passed)
elif passed:
_ArchiveGoodBuild(platform, options.revision)
_MaybeRelease(platform)
if not passed:
# Make sure the build is red if there is some uncaught exception during
# running run_all_tests.py.
util.MarkBuildStepStart('run_all_tests.py')
util.MarkBuildStepError()
# Add a "cleanup" step so that errors from runtest.py or bb_device_steps.py
# (which invoke this script) are kept in thier own build step.
util.MarkBuildStepStart('cleanup')
if __name__ == '__main__':
main()