#!/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()