blob: b3e59fe9f69aac6bc12c9993248791af047b2727 [file] [log] [blame]
#!/usr/bin/env python
# Copyright (C) 2017 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. Neither the name of Apple Inc. ("Apple") nor the names of
# its contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import argparse
import bisect
import json
import math
import os
import shutil
import subprocess
import sys
import tempfile
import urllib2
import urlparse
REST_API_URL = 'https://q1tzqfy48e.execute-api.us-west-2.amazonaws.com/v2/'
REST_API_ENDPOINT = 'archives/'
REST_API_MINIFIED_ENDPOINT = 'minified-archives/'
def bisect_builds(revision_list, start_index, end_index, options):
while True:
index_to_test = pick_next_build(revision_list, start_index, end_index)
if index_to_test == None:
print('No more builds to test...')
exit(1)
download_archive(options, revision_list[index_to_test])
extract_archive(options)
reproduces = test_archive(options, revision_list[index_to_test])
if reproduces: # bisect left
index_to_test -= 1 # We can remove this from the end of the list of builds to test
bisect_builds(revision_list, start_index, index_to_test, options)
if not reproduces: # bisect right
index_to_test += 1 # We can remove this from the start of the list of builds to test
bisect_builds(revision_list, index_to_test, end_index, options)
def download_archive(options, revision):
api_url = get_api_url(options)
s3_url = get_s3_location_for_revision(api_url, revision)
print('Archive URL: {}'.format(s3_url))
command = ['python', '../BuildSlaveSupport/download-built-product', '--{}'.format(options.configuration), '--platform', options.platform, s3_url]
print('Downloading revision: {}'.format(revision))
subprocess.check_call(command)
def extract_archive(options):
command = ['python', '../BuildSlaveSupport/built-product-archive', '--platform', options.platform, '--%s' % options.configuration, 'extract']
subprocess.check_call(command)
# ---- bisect helpers from https://docs.python.org/2/library/bisect.html ----
def find_le(a, x):
"""Find rightmost value less than or equal to x"""
i = bisect.bisect_right(a, x)
if i:
return i - 1
raise ValueError
def find_ge(a, x):
"""Find leftmost item greater than or equal to x"""
i = bisect.bisect_left(a, x)
if i != len(a):
return i
raise ValueError
# ---- end bisect helpers ----
def get_api_url(options):
if options.full:
base_url = urlparse.urljoin(REST_API_URL, REST_API_ENDPOINT)
else:
base_url = urlparse.urljoin(REST_API_URL, REST_API_MINIFIED_ENDPOINT)
api_url = urlparse.urljoin(base_url, '-'.join([options.platform, options.architecture, options.configuration]))
return api_url
def get_indices_from_revisions(revision_list, start_revision, end_revision):
if start_revision is None:
print('WARNING: No starting revision was given, defaulting to first available for this configuration')
start_index = 0
else:
start_index = find_ge(revision_list, start_revision)
if end_revision is None:
print('WARNING: No ending revision was given, defaulting to last avialable for this configuration')
end_index = len(revision_list) - 1
else:
end_index = find_le(revision_list, end_revision)
return start_index, end_index
def get_sorted_revisions(revisions_dict):
revisions = [int(item['revision']['N']) for item in revisions_dict['revisions']['Items']]
return sorted(revisions)
def get_s3_location_for_revision(url, revision):
url = '/'.join([url, str(revision)])
r = urllib2.urlopen(url)
data = json.load(r)
for archive in data['archive']:
s3_url = archive['s3_url']
return s3_url
def parse_args(args):
helptext = 'bisect-builds is designed to help pinpoint regressions to specific code changes. It does this by bisecting across archives produced by build.webkit.org. Full and "minified" archives are available. Minified archives are significantly smaller, as they have been stripped of dSYMs and other non-essential components.'
parser = argparse.ArgumentParser(description=helptext)
parser.add_argument('-c', '--configuration', default='release', help='The configuration to query [release | debug]')
parser.add_argument('-a', '--architecture', default='x86_64', help='The architecture to query [x86_64 | i386]')
parser.add_argument('-p', '--platform', default='None', required=True, help='The platform to query [mac-sierra | gtk | ios-simulator | win]')
parser.add_argument('-f', '--full', action='store_true', default=False, help='Use full archives containing debug symbols. These are significantly larger files!')
parser.add_argument('-s', '--start', default=None, type=int, help='The starting revision to bisect.')
parser.add_argument('-e', '--end', default=None, type=int, help='The ending revision to bisect')
parser.add_argument('-l', '--list', action='store_true', default=False, help='Display a list of platforms and revisions')
return parser.parse_args(args)
def pick_next_build(revision_list, start_index, end_index):
revisions_remaining = (end_index - start_index) + 1
print('Found {} revisions in this range to test...'.format(revisions_remaining))
if start_index >= end_index:
print('No archives available between {} and {}'.format(revision_list[end_index], revision_list[start_index]))
return None
middleIndex = (start_index + end_index) / 2
return int(math.ceil(middleIndex))
def prompt_did_reproduce():
var = raw_input('\nDid the error reproduce? [y/n]: ')
var = var.lower()
if 'y' in var:
return True
if 'n' in var:
return False
else:
prompt_did_reproduce()
def set_webkit_output_dir(temp_dir):
print('Setting environment variable WEBKIT_OUTPUTDIR to {}'.format(temp_dir))
os.environ['WEBKIT_OUTPUTDIR'] = temp_dir
def test_archive(options, revision):
print('Testing revision {}...'.format(revision))
command = []
if 'mac' in options.platform:
command = ['./run-safari']
elif 'ios' in options.platform:
command = ['./run-safari', '--simulator']
else:
print('Default test behavior for this platform is not implemented...'.format(options.platform))
if command:
subprocess.call(command)
return prompt_did_reproduce()
def minified_platforms():
# FIXME: query this dynamically from API
return ['mac-elcapitan', 'mac-sierra', 'mac-highsierra', 'ios-simulator-10', 'ios-simulator-11']
def unminified_platforms():
# FIXME: query this dynamically from API
return ['gtk', 'ios-simulator-10', 'ios-simulator-11', 'mac-elcapitan', 'mac-sierra', 'mac-highsierra', 'win', 'wpe']
def is_supported_platform(options):
if options.full:
return options.platform in unminified_platforms()
return options.platform in minified_platforms()
def validate_options(options):
if not is_supported_platform(options):
print('Unsupported platform: [{}], exiting...'.format(options.platform))
if options.full:
print('Available Unminified platforms: {}'.format(unminified_platforms()))
else:
print('Available Minified platforms: {}'.format(minified_platforms()))
print('INFO: pass --full to try against full archives')
exit(1)
def print_list_and_exit(revision_list, options):
print('Supported minified platforms: {}'.format(minified_platforms()))
print('Supported unminified platforms: {}'.format(unminified_platforms()))
print('{} revisions available for supplied platform: {}-{}-{}:'.format(len(revision_list), options.platform, options.architecture, options.configuration))
print(revision_list)
exit(0)
def main(options):
validate_options(options)
url = get_api_url(options)
r = urllib2.urlopen(url)
data = json.load(r)
revision_list = get_sorted_revisions(data)
if options.list:
print_list_and_exit(revision_list, options)
start_index, end_index = get_indices_from_revisions(revision_list, options.start, options.end)
print('Bisecting between {} and {}'.format(revision_list[start_index], revision_list[end_index]))
# from here forward, use indices instead of revisions
bisect_builds(revision_list, start_index, end_index, options)
if __name__ == '__main__':
options = parse_args(sys.argv[1:])
script_path = os.path.abspath(__file__)
script_directory = os.path.dirname(script_path)
os.chdir(script_directory)
webkit_output_dir = tempfile.mkdtemp()
set_webkit_output_dir(webkit_output_dir)
try:
main(options)
except KeyboardInterrupt:
exit("Aborting.")
finally:
shutil.rmtree(webkit_output_dir, ignore_errors=True)