blob: 6db0047963a8969fce86b6a47d4c70e10ec9b6db [file] [log] [blame]
[email protected]cb155a82011-11-29 17:25:341#!/usr/bin/env python
[email protected]5e93cf162012-01-28 02:16:562# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]67e0bc62009-09-03 22:06:093# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Snapshot Build Bisect Tool
7
[email protected]7ad66a72009-09-04 17:52:338This script bisects a snapshot archive using binary search. It starts at
[email protected]67e0bc62009-09-03 22:06:099a bad revision (it will try to guess HEAD) and asks for a last known-good
10revision. It will then binary search across this revision range by downloading,
11unzipping, and opening Chromium for you. After testing the specific revision,
12it will ask you whether it is good or bad before continuing the search.
[email protected]67e0bc62009-09-03 22:06:0913"""
14
[email protected]4df583c2014-07-31 17:11:5515# The base URL for stored build archives.
16CHROMIUM_BASE_URL = ('http://commondatastorage.googleapis.com'
17 '/chromium-browser-snapshots')
18WEBKIT_BASE_URL = ('http://commondatastorage.googleapis.com'
19 '/chromium-webkit-snapshots')
[email protected]011886692014-08-01 21:00:2120ASAN_BASE_URL = ('http://commondatastorage.googleapis.com'
21 '/chromium-browser-asan')
[email protected]67e0bc62009-09-03 22:06:0922
[email protected]4df583c2014-07-31 17:11:5523# URL template for viewing changelogs between revisions.
pshenoy9ce271f2014-09-02 22:14:0524CHANGELOG_URL = ('https://chromium.googlesource.com/chromium/src/+log/%s..%s')
25
26# URL to convert SVN revision to git hash.
pshenoy13cb79e02014-09-05 01:42:5327CRREV_URL = ('https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/')
[email protected]f6a71a72009-10-08 19:55:3828
[email protected]b2fe7f22011-10-25 22:58:3129# DEPS file URL.
pshenoycd6bd682014-09-10 20:50:2230DEPS_FILE_OLD = ('http://src.chromium.org/viewvc/chrome/trunk/src/'
31 'DEPS?revision=%d')
32DEPS_FILE_NEW = ('https://chromium.googlesource.com/chromium/src/+/%s/DEPS')
[email protected]b2fe7f22011-10-25 22:58:3133
[email protected]4df583c2014-07-31 17:11:5534# Blink changelogs URL.
35BLINK_CHANGELOG_URL = ('http://build.chromium.org'
36 '/f/chromium/perf/dashboard/ui/changelog_blink.html'
37 '?url=/trunk&range=%d%%3A%d')
38
39DONE_MESSAGE_GOOD_MIN = ('You are probably looking for a change made after %s ('
40 'known good), but no later than %s (first known bad).')
41DONE_MESSAGE_GOOD_MAX = ('You are probably looking for a change made after %s ('
42 'known bad), but no later than %s (first known good).')
[email protected]05ff3fd2012-04-17 23:24:0643
[email protected]3e7c85322014-06-27 20:27:3644CHROMIUM_GITHASH_TO_SVN_URL = (
45 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5546
[email protected]3e7c85322014-06-27 20:27:3647BLINK_GITHASH_TO_SVN_URL = (
48 'https://chromium.googlesource.com/chromium/blink/+/%s?format=json')
[email protected]4df583c2014-07-31 17:11:5549
50GITHASH_TO_SVN_URL = {
51 'chromium': CHROMIUM_GITHASH_TO_SVN_URL,
52 'blink': BLINK_GITHASH_TO_SVN_URL,
53}
54
55# Search pattern to be matched in the JSON output from
[email protected]3e7c85322014-06-27 20:27:3656# CHROMIUM_GITHASH_TO_SVN_URL to get the chromium revision (svn revision).
pshenoyb23a1452014-09-05 22:52:0557CHROMIUM_SEARCH_PATTERN_OLD = (
[email protected]3e7c85322014-06-27 20:27:3658 r'.*git-svn-id: svn://svn.chromium.org/chrome/trunk/src@(\d+) ')
pshenoyb23a1452014-09-05 22:52:0559CHROMIUM_SEARCH_PATTERN = (
60 r'Cr-Commit-Position: refs/heads/master@{#(\d+)}')
[email protected]4df583c2014-07-31 17:11:5561
[email protected]3e7c85322014-06-27 20:27:3662# Search pattern to be matched in the json output from
63# BLINK_GITHASH_TO_SVN_URL to get the blink revision (svn revision).
64BLINK_SEARCH_PATTERN = (
65 r'.*git-svn-id: svn://svn.chromium.org/blink/trunk@(\d+) ')
[email protected]4df583c2014-07-31 17:11:5566
67SEARCH_PATTERN = {
68 'chromium': CHROMIUM_SEARCH_PATTERN,
69 'blink': BLINK_SEARCH_PATTERN,
70}
[email protected]3e7c85322014-06-27 20:27:3671
[email protected]480369782014-08-22 20:15:5872CREDENTIAL_ERROR_MESSAGE = ('You are attempting to access protected data with '
73 'no configured credentials')
74
[email protected]67e0bc62009-09-03 22:06:0975###############################################################################
76
[email protected]83048502014-08-21 16:48:4477import httplib
[email protected]4c6fec6b2013-09-17 17:44:0878import json
[email protected]7ad66a72009-09-04 17:52:3379import optparse
[email protected]67e0bc62009-09-03 22:06:0980import os
81import re
[email protected]61ea90a2013-09-26 10:17:3482import shlex
[email protected]67e0bc62009-09-03 22:06:0983import shutil
[email protected]afe30662011-07-30 01:05:5284import subprocess
[email protected]67e0bc62009-09-03 22:06:0985import sys
[email protected]7ad66a72009-09-04 17:52:3386import tempfile
[email protected]afe30662011-07-30 01:05:5287import threading
[email protected]67e0bc62009-09-03 22:06:0988import urllib
[email protected]d0149c5c2012-05-29 21:12:1189from distutils.version import LooseVersion
[email protected]183706d92011-06-10 13:06:2290from xml.etree import ElementTree
[email protected]bd8dcb92010-03-31 01:05:2491import zipfile
92
[email protected]cb155a82011-11-29 17:25:3493
[email protected]183706d92011-06-10 13:06:2294class PathContext(object):
95 """A PathContext is used to carry the information used to construct URLs and
96 paths when dealing with the storage server and archives."""
[email protected]4c6fec6b2013-09-17 17:44:0897 def __init__(self, base_url, platform, good_revision, bad_revision,
Jason Kersey97bb027a2016-05-11 20:10:4398 is_asan, use_local_cache, flash_path = None):
[email protected]183706d92011-06-10 13:06:2299 super(PathContext, self).__init__()
100 # Store off the input parameters.
[email protected]4c6fec6b2013-09-17 17:44:08101 self.base_url = base_url
[email protected]183706d92011-06-10 13:06:22102 self.platform = platform # What's passed in to the '-a/--archive' option.
103 self.good_revision = good_revision
104 self.bad_revision = bad_revision
[email protected]011886692014-08-01 21:00:21105 self.is_asan = is_asan
106 self.build_type = 'release'
[email protected]fc3702e2013-11-09 04:23:00107 self.flash_path = flash_path
[email protected]3e7c85322014-06-27 20:27:36108 # Dictionary which stores svn revision number as key and it's
109 # corresponding git hash as value. This data is populated in
110 # _FetchAndParse and used later in GetDownloadURL while downloading
111 # the build.
112 self.githash_svn_dict = {}
[email protected]183706d92011-06-10 13:06:22113 # The name of the ZIP file in a revision directory on the server.
114 self.archive_name = None
115
rob724c9062015-01-22 00:26:42116 # Whether to cache and use the list of known revisions in a local file to
117 # speed up the initialization of the script at the next run.
118 self.use_local_cache = use_local_cache
119
120 # Locate the local checkout to speed up the script by using locally stored
121 # metadata.
122 abs_file_path = os.path.abspath(os.path.realpath(__file__))
123 local_src_path = os.path.join(os.path.dirname(abs_file_path), '..')
124 if abs_file_path.endswith(os.path.join('tools', 'bisect-builds.py')) and\
125 os.path.exists(os.path.join(local_src_path, '.git')):
126 self.local_src_path = os.path.normpath(local_src_path)
127 else:
128 self.local_src_path = None
[email protected]6a7a5d62014-07-09 04:45:50129
[email protected]183706d92011-06-10 13:06:22130 # Set some internal members:
131 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
132 # _archive_extract_dir = Uncompressed directory in the archive_name file.
133 # _binary_name = The name of the executable to run.
dmazzoni76e907d2015-01-22 08:14:49134 if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'):
[email protected]183706d92011-06-10 13:06:22135 self._binary_name = 'chrome'
[email protected]480369782014-08-22 20:15:58136 elif self.platform in ('mac', 'mac64'):
[email protected]183706d92011-06-10 13:06:22137 self.archive_name = 'chrome-mac.zip'
138 self._archive_extract_dir = 'chrome-mac'
[email protected]480369782014-08-22 20:15:58139 elif self.platform in ('win', 'win64'):
[email protected]183706d92011-06-10 13:06:22140 self.archive_name = 'chrome-win32.zip'
141 self._archive_extract_dir = 'chrome-win32'
142 self._binary_name = 'chrome.exe'
143 else:
[email protected]afe30662011-07-30 01:05:52144 raise Exception('Invalid platform: %s' % self.platform)
[email protected]183706d92011-06-10 13:06:22145
Jason Kersey97bb027a2016-05-11 20:10:43146 if self.platform in ('linux', 'linux64', 'linux-arm', 'chromeos'):
147 self.archive_name = 'chrome-linux.zip'
148 self._archive_extract_dir = 'chrome-linux'
[email protected]d0149c5c2012-05-29 21:12:11149 if self.platform == 'linux':
Jason Kersey97bb027a2016-05-11 20:10:43150 self._listing_platform_dir = 'Linux/'
[email protected]d0149c5c2012-05-29 21:12:11151 elif self.platform == 'linux64':
Jason Kersey97bb027a2016-05-11 20:10:43152 self._listing_platform_dir = 'Linux_x64/'
153 elif self.platform == 'linux-arm':
154 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
155 elif self.platform == 'chromeos':
156 self._listing_platform_dir = 'Linux_ChromiumOS_Full/'
157 elif self.platform in ('mac', 'mac64'):
158 self._listing_platform_dir = 'Mac/'
159 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
160 elif self.platform == 'win':
161 self._listing_platform_dir = 'Win/'
[email protected]d0149c5c2012-05-29 21:12:11162
[email protected]011886692014-08-01 21:00:21163 def GetASANPlatformDir(self):
164 """ASAN builds are in directories like "linux-release", or have filenames
165 like "asan-win32-release-277079.zip". This aligns to our platform names
166 except in the case of Windows where they use "win32" instead of "win"."""
167 if self.platform == 'win':
168 return 'win32'
169 else:
170 return self.platform
171
[email protected]183706d92011-06-10 13:06:22172 def GetListingURL(self, marker=None):
173 """Returns the URL for a directory listing, with an optional marker."""
174 marker_param = ''
175 if marker:
176 marker_param = '&marker=' + str(marker)
[email protected]011886692014-08-01 21:00:21177 if self.is_asan:
178 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type)
179 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param
180 else:
181 return (self.base_url + '/?delimiter=/&prefix=' +
182 self._listing_platform_dir + marker_param)
[email protected]183706d92011-06-10 13:06:22183
184 def GetDownloadURL(self, revision):
185 """Gets the download URL for a build archive of a specific revision."""
[email protected]011886692014-08-01 21:00:21186 if self.is_asan:
187 return '%s/%s-%s/%s-%d.zip' % (
188 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type,
189 self.GetASANBaseName(), revision)
Jason Kersey97bb027a2016-05-11 20:10:43190 if str(revision) in self.githash_svn_dict:
191 revision = self.githash_svn_dict[str(revision)]
192 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
193 revision, self.archive_name)
[email protected]183706d92011-06-10 13:06:22194
195 def GetLastChangeURL(self):
196 """Returns a URL to the LAST_CHANGE file."""
[email protected]4c6fec6b2013-09-17 17:44:08197 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
[email protected]183706d92011-06-10 13:06:22198
[email protected]011886692014-08-01 21:00:21199 def GetASANBaseName(self):
200 """Returns the base name of the ASAN zip file."""
201 if 'linux' in self.platform:
202 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(),
203 self.build_type)
204 else:
205 return 'asan-%s-%s' % (self.GetASANPlatformDir(), self.build_type)
206
207 def GetLaunchPath(self, revision):
[email protected]183706d92011-06-10 13:06:22208 """Returns a relative path (presumably from the archive extraction location)
209 that is used to run the executable."""
[email protected]011886692014-08-01 21:00:21210 if self.is_asan:
211 extract_dir = '%s-%d' % (self.GetASANBaseName(), revision)
212 else:
213 extract_dir = self._archive_extract_dir
214 return os.path.join(extract_dir, self._binary_name)
[email protected]183706d92011-06-10 13:06:22215
rob724c9062015-01-22 00:26:42216 def ParseDirectoryIndex(self, last_known_rev):
[email protected]afe30662011-07-30 01:05:52217 """Parses the Google Storage directory listing into a list of revision
[email protected]eadd95d2012-11-02 22:42:09218 numbers."""
[email protected]afe30662011-07-30 01:05:52219
rob724c9062015-01-22 00:26:42220 def _GetMarkerForRev(revision):
221 if self.is_asan:
222 return '%s-%s/%s-%d.zip' % (
223 self.GetASANPlatformDir(), self.build_type,
224 self.GetASANBaseName(), revision)
225 return '%s%d' % (self._listing_platform_dir, revision)
226
[email protected]afe30662011-07-30 01:05:52227 def _FetchAndParse(url):
228 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
229 next-marker is not None, then the listing is a partial listing and another
230 fetch should be performed with next-marker being the marker= GET
231 parameter."""
232 handle = urllib.urlopen(url)
233 document = ElementTree.parse(handle)
234
235 # All nodes in the tree are namespaced. Get the root's tag name to extract
236 # the namespace. Etree does namespaces as |{namespace}tag|.
237 root_tag = document.getroot().tag
238 end_ns_pos = root_tag.find('}')
239 if end_ns_pos == -1:
[email protected]4df583c2014-07-31 17:11:55240 raise Exception('Could not locate end namespace for directory index')
[email protected]afe30662011-07-30 01:05:52241 namespace = root_tag[:end_ns_pos + 1]
242
243 # Find the prefix (_listing_platform_dir) and whether or not the list is
244 # truncated.
245 prefix_len = len(document.find(namespace + 'Prefix').text)
246 next_marker = None
247 is_truncated = document.find(namespace + 'IsTruncated')
248 if is_truncated is not None and is_truncated.text.lower() == 'true':
249 next_marker = document.find(namespace + 'NextMarker').text
[email protected]afe30662011-07-30 01:05:52250 # Get a list of all the revisions.
[email protected]afe30662011-07-30 01:05:52251 revisions = []
[email protected]3e7c85322014-06-27 20:27:36252 githash_svn_dict = {}
[email protected]011886692014-08-01 21:00:21253 if self.is_asan:
254 asan_regex = re.compile(r'.*%s-(\d+)\.zip$' % (self.GetASANBaseName()))
255 # Non ASAN builds are in a <revision> directory. The ASAN builds are
256 # flat
257 all_prefixes = document.findall(namespace + 'Contents/' +
258 namespace + 'Key')
259 for prefix in all_prefixes:
260 m = asan_regex.match(prefix.text)
261 if m:
262 try:
263 revisions.append(int(m.group(1)))
264 except ValueError:
265 pass
266 else:
267 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
268 namespace + 'Prefix')
269 # The <Prefix> nodes have content of the form of
270 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
271 # trailing slash to just have a number.
272 for prefix in all_prefixes:
273 revnum = prefix.text[prefix_len:-1]
274 try:
dimua1dfa0ce2016-03-31 01:08:45275 revnum = int(revnum)
276 revisions.append(revnum)
277 # Notes:
278 # Ignore hash in chromium-browser-snapshots as they are invalid
279 # Resulting in 404 error in fetching pages:
280 # https://chromium.googlesource.com/chromium/src/+/[rev_hash]
[email protected]011886692014-08-01 21:00:21281 except ValueError:
282 pass
[email protected]3e7c85322014-06-27 20:27:36283 return (revisions, next_marker, githash_svn_dict)
[email protected]9639b002013-08-30 14:45:52284
[email protected]afe30662011-07-30 01:05:52285 # Fetch the first list of revisions.
rob724c9062015-01-22 00:26:42286 if last_known_rev:
287 revisions = []
288 # Optimization: Start paging at the last known revision (local cache).
289 next_marker = _GetMarkerForRev(last_known_rev)
290 # Optimization: Stop paging at the last known revision (remote).
291 last_change_rev = GetChromiumRevision(self, self.GetLastChangeURL())
292 if last_known_rev == last_change_rev:
293 return []
294 else:
295 (revisions, next_marker, new_dict) = _FetchAndParse(self.GetListingURL())
296 self.githash_svn_dict.update(new_dict)
297 last_change_rev = None
298
[email protected]afe30662011-07-30 01:05:52299 # If the result list was truncated, refetch with the next marker. Do this
300 # until an entire directory listing is done.
301 while next_marker:
rob724c9062015-01-22 00:26:42302 sys.stdout.write('\rFetching revisions at marker %s' % next_marker)
303 sys.stdout.flush()
304
[email protected]afe30662011-07-30 01:05:52305 next_url = self.GetListingURL(next_marker)
[email protected]3e7c85322014-06-27 20:27:36306 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url)
[email protected]afe30662011-07-30 01:05:52307 revisions.extend(new_revisions)
[email protected]3e7c85322014-06-27 20:27:36308 self.githash_svn_dict.update(new_dict)
rob724c9062015-01-22 00:26:42309 if last_change_rev and last_change_rev in new_revisions:
310 break
311 sys.stdout.write('\r')
312 sys.stdout.flush()
[email protected]afe30662011-07-30 01:05:52313 return revisions
314
[email protected]6a7a5d62014-07-09 04:45:50315 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot):
[email protected]3e7c85322014-06-27 20:27:36316 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1
[email protected]2e0f2672014-08-13 20:32:58317 response = urllib.urlopen(json_url)
318 if response.getcode() == 200:
319 try:
320 data = json.loads(response.read()[4:])
321 except ValueError:
322 print 'ValueError for JSON URL: %s' % json_url
323 raise ValueError
324 else:
325 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36326 if 'message' in data:
327 message = data['message'].split('\n')
328 message = [line for line in message if line.strip()]
329 search_pattern = re.compile(SEARCH_PATTERN[depot])
330 result = search_pattern.search(message[len(message)-1])
331 if result:
332 return result.group(1)
pshenoyb23a1452014-09-05 22:52:05333 else:
334 if depot == 'chromium':
335 result = re.search(CHROMIUM_SEARCH_PATTERN_OLD,
336 message[len(message)-1])
337 if result:
338 return result.group(1)
[email protected]3e7c85322014-06-27 20:27:36339 print 'Failed to get svn revision number for %s' % git_sha1
[email protected]1f99f4d2014-07-23 16:44:14340 raise ValueError
[email protected]3e7c85322014-06-27 20:27:36341
[email protected]6a7a5d62014-07-09 04:45:50342 def _GetSVNRevisionFromGitHashFromGitCheckout(self, git_sha1, depot):
343 def _RunGit(command, path):
344 command = ['git'] + command
[email protected]6a7a5d62014-07-09 04:45:50345 shell = sys.platform.startswith('win')
346 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
rob724c9062015-01-22 00:26:42347 stderr=subprocess.PIPE, cwd=path)
[email protected]6a7a5d62014-07-09 04:45:50348 (output, _) = proc.communicate()
[email protected]6a7a5d62014-07-09 04:45:50349 return (output, proc.returncode)
350
rob724c9062015-01-22 00:26:42351 path = self.local_src_path
[email protected]6a7a5d62014-07-09 04:45:50352 if depot == 'blink':
rob724c9062015-01-22 00:26:42353 path = os.path.join(self.local_src_path, 'third_party', 'WebKit')
354 revision = None
355 try:
[email protected]6a7a5d62014-07-09 04:45:50356 command = ['svn', 'find-rev', git_sha1]
357 (git_output, return_code) = _RunGit(command, path)
358 if not return_code:
rob724c9062015-01-22 00:26:42359 revision = git_output.strip('\n')
360 except ValueError:
361 pass
362 if not revision:
363 command = ['log', '-n1', '--format=%s', git_sha1]
364 (git_output, return_code) = _RunGit(command, path)
365 if not return_code:
366 revision = re.match('SVN changes up to revision ([0-9]+)', git_output)
367 revision = revision.group(1) if revision else None
368 if revision:
369 return revision
370 raise ValueError
[email protected]6a7a5d62014-07-09 04:45:50371
372 def GetSVNRevisionFromGitHash(self, git_sha1, depot='chromium'):
rob724c9062015-01-22 00:26:42373 if not self.local_src_path:
[email protected]6a7a5d62014-07-09 04:45:50374 return self._GetSVNRevisionFromGitHashWithoutGitCheckout(git_sha1, depot)
375 else:
376 return self._GetSVNRevisionFromGitHashFromGitCheckout(git_sha1, depot)
377
[email protected]afe30662011-07-30 01:05:52378 def GetRevList(self):
379 """Gets the list of revision numbers between self.good_revision and
380 self.bad_revision."""
rob724c9062015-01-22 00:26:42381
382 cache = {}
383 # The cache is stored in the same directory as bisect-builds.py
384 cache_filename = os.path.join(
385 os.path.abspath(os.path.dirname(__file__)),
386 '.bisect-builds-cache.json')
387 cache_dict_key = self.GetListingURL()
388
389 def _LoadBucketFromCache():
390 if self.use_local_cache:
391 try:
392 with open(cache_filename) as cache_file:
rob1c836052015-05-18 16:34:02393 for (key, value) in json.load(cache_file).items():
394 cache[key] = value
rob724c9062015-01-22 00:26:42395 revisions = cache.get(cache_dict_key, [])
396 githash_svn_dict = cache.get('githash_svn_dict', {})
397 if revisions:
398 print 'Loaded revisions %d-%d from %s' % (revisions[0],
399 revisions[-1], cache_filename)
400 return (revisions, githash_svn_dict)
401 except (EnvironmentError, ValueError):
402 pass
403 return ([], {})
404
405 def _SaveBucketToCache():
406 """Save the list of revisions and the git-svn mappings to a file.
407 The list of revisions is assumed to be sorted."""
408 if self.use_local_cache:
409 cache[cache_dict_key] = revlist_all
410 cache['githash_svn_dict'] = self.githash_svn_dict
411 try:
412 with open(cache_filename, 'w') as cache_file:
413 json.dump(cache, cache_file)
414 print 'Saved revisions %d-%d to %s' % (
415 revlist_all[0], revlist_all[-1], cache_filename)
416 except EnvironmentError:
417 pass
418
[email protected]afe30662011-07-30 01:05:52419 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09420 minrev = min(self.good_revision, self.bad_revision)
421 maxrev = max(self.good_revision, self.bad_revision)
rob724c9062015-01-22 00:26:42422
423 (revlist_all, self.githash_svn_dict) = _LoadBucketFromCache()
424 last_known_rev = revlist_all[-1] if revlist_all else 0
425 if last_known_rev < maxrev:
426 revlist_all.extend(map(int, self.ParseDirectoryIndex(last_known_rev)))
427 revlist_all = list(set(revlist_all))
428 revlist_all.sort()
429 _SaveBucketToCache()
[email protected]37ed3172013-09-24 23:49:30430
431 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
[email protected]37ed3172013-09-24 23:49:30432
433 # Set good and bad revisions to be legit revisions.
434 if revlist:
435 if self.good_revision < self.bad_revision:
436 self.good_revision = revlist[0]
437 self.bad_revision = revlist[-1]
438 else:
439 self.bad_revision = revlist[0]
440 self.good_revision = revlist[-1]
441
442 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
443 if self.base_url == WEBKIT_BASE_URL:
444 revlist_all.sort()
445 self.good_revision = FixChromiumRevForBlink(revlist,
446 revlist_all,
447 self,
448 self.good_revision)
449 self.bad_revision = FixChromiumRevForBlink(revlist,
450 revlist_all,
451 self,
452 self.bad_revision)
[email protected]afe30662011-07-30 01:05:52453 return revlist
454
[email protected]fc3702e2013-11-09 04:23:00455def UnzipFilenameToDir(filename, directory):
456 """Unzip |filename| to |directory|."""
[email protected]afe30662011-07-30 01:05:52457 cwd = os.getcwd()
458 if not os.path.isabs(filename):
459 filename = os.path.join(cwd, filename)
[email protected]bd8dcb92010-03-31 01:05:24460 zf = zipfile.ZipFile(filename)
461 # Make base.
[email protected]fc3702e2013-11-09 04:23:00462 if not os.path.isdir(directory):
463 os.mkdir(directory)
464 os.chdir(directory)
[email protected]e29c08c2012-09-17 20:50:50465 # Extract files.
466 for info in zf.infolist():
467 name = info.filename
468 if name.endswith('/'): # dir
469 if not os.path.isdir(name):
470 os.makedirs(name)
471 else: # file
[email protected]fc3702e2013-11-09 04:23:00472 directory = os.path.dirname(name)
John Budorick06e5df12015-02-27 17:44:27473 if not os.path.isdir(directory):
[email protected]fc3702e2013-11-09 04:23:00474 os.makedirs(directory)
[email protected]e29c08c2012-09-17 20:50:50475 out = open(name, 'wb')
476 out.write(zf.read(name))
477 out.close()
478 # Set permissions. Permission info in external_attr is shifted 16 bits.
479 os.chmod(name, info.external_attr >> 16L)
480 os.chdir(cwd)
[email protected]bd8dcb92010-03-31 01:05:24481
[email protected]67e0bc62009-09-03 22:06:09482
[email protected]468a9772011-08-09 18:42:00483def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
[email protected]afe30662011-07-30 01:05:52484 """Downloads and unzips revision |rev|.
485 @param context A PathContext instance.
486 @param rev The Chromium revision number/tag to download.
487 @param filename The destination for the downloaded file.
488 @param quit_event A threading.Event which will be set by the master thread to
489 indicate that the download should be aborted.
[email protected]468a9772011-08-09 18:42:00490 @param progress_event A threading.Event which will be set by the master thread
491 to indicate that the progress of the download should be
492 displayed.
[email protected]afe30662011-07-30 01:05:52493 """
494 def ReportHook(blocknum, blocksize, totalsize):
[email protected]946be752011-10-25 23:34:21495 if quit_event and quit_event.isSet():
[email protected]4df583c2014-07-31 17:11:55496 raise RuntimeError('Aborting download of revision %s' % str(rev))
[email protected]946be752011-10-25 23:34:21497 if progress_event and progress_event.isSet():
[email protected]468a9772011-08-09 18:42:00498 size = blocknum * blocksize
499 if totalsize == -1: # Total size not known.
[email protected]4df583c2014-07-31 17:11:55500 progress = 'Received %d bytes' % size
[email protected]468a9772011-08-09 18:42:00501 else:
502 size = min(totalsize, size)
[email protected]4df583c2014-07-31 17:11:55503 progress = 'Received %d of %d bytes, %.2f%%' % (
[email protected]468a9772011-08-09 18:42:00504 size, totalsize, 100.0 * size / totalsize)
505 # Send a \r to let all progress messages use just one line of output.
[email protected]4df583c2014-07-31 17:11:55506 sys.stdout.write('\r' + progress)
[email protected]468a9772011-08-09 18:42:00507 sys.stdout.flush()
[email protected]afe30662011-07-30 01:05:52508 download_url = context.GetDownloadURL(rev)
509 try:
John Budorick06e5df12015-02-27 17:44:27510 urllib.urlretrieve(download_url, filename, ReportHook)
[email protected]946be752011-10-25 23:34:21511 if progress_event and progress_event.isSet():
[email protected]ecaba01e62011-10-26 05:33:28512 print
mikecasee2b6ce82015-02-06 18:22:39513
[email protected]4df583c2014-07-31 17:11:55514 except RuntimeError:
[email protected]afe30662011-07-30 01:05:52515 pass
[email protected]7ad66a72009-09-04 17:52:33516
[email protected]7ad66a72009-09-04 17:52:33517
[email protected]4df583c2014-07-31 17:11:55518def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
[email protected]afe30662011-07-30 01:05:52519 """Given a zipped revision, unzip it and run the test."""
[email protected]4df583c2014-07-31 17:11:55520 print 'Trying revision %s...' % str(revision)
[email protected]3ff00b72011-07-20 21:34:47521
[email protected]afe30662011-07-30 01:05:52522 # Create a temp directory and unzip the revision into it.
[email protected]7ad66a72009-09-04 17:52:33523 cwd = os.getcwd()
524 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
[email protected]4df583c2014-07-31 17:11:55525 UnzipFilenameToDir(zip_file, tempdir)
dmazzoni76e907d2015-01-22 08:14:49526
527 # Hack: Chrome OS archives are missing icudtl.dat; try to copy it from
528 # the local directory.
529 if context.platform == 'chromeos':
530 icudtl_path = 'third_party/icu/source/data/in/icudtl.dat'
531 if not os.access(icudtl_path, os.F_OK):
532 print 'Couldn\'t find: ' + icudtl_path
533 sys.exit()
534 os.system('cp %s %s/chrome-linux/' % (icudtl_path, tempdir))
535
[email protected]7ad66a72009-09-04 17:52:33536 os.chdir(tempdir)
[email protected]67e0bc62009-09-03 22:06:09537
[email protected]5e93cf162012-01-28 02:16:56538 # Run the build as many times as specified.
[email protected]4646a752013-07-19 22:14:34539 testargs = ['--user-data-dir=%s' % profile] + args
[email protected]d0149c5c2012-05-29 21:12:11540 # The sandbox must be run as root on Official Chrome, so bypass it.
Jason Kersey97bb027a2016-05-11 20:10:43541 if (context.flash_path and context.platform.startswith('linux')):
[email protected]d0149c5c2012-05-29 21:12:11542 testargs.append('--no-sandbox')
[email protected]fc3702e2013-11-09 04:23:00543 if context.flash_path:
544 testargs.append('--ppapi-flash-path=%s' % context.flash_path)
545 # We have to pass a large enough Flash version, which currently needs not
546 # be correct. Instead of requiring the user of the script to figure out and
547 # pass the correct version we just spoof it.
548 testargs.append('--ppapi-flash-version=99.9.999.999')
[email protected]d0149c5c2012-05-29 21:12:11549
[email protected]4646a752013-07-19 22:14:34550 runcommand = []
[email protected]61ea90a2013-09-26 10:17:34551 for token in shlex.split(command):
[email protected]4df583c2014-07-31 17:11:55552 if token == '%a':
[email protected]4646a752013-07-19 22:14:34553 runcommand.extend(testargs)
554 else:
[email protected]4df583c2014-07-31 17:11:55555 runcommand.append(
[email protected]011886692014-08-01 21:00:21556 token.replace('%p', os.path.abspath(context.GetLaunchPath(revision))).
557 replace('%s', ' '.join(testargs)))
[email protected]4646a752013-07-19 22:14:34558
[email protected]d59c8712014-02-11 21:04:57559 results = []
[email protected]4df583c2014-07-31 17:11:55560 for _ in range(num_runs):
[email protected]4646a752013-07-19 22:14:34561 subproc = subprocess.Popen(runcommand,
[email protected]5e93cf162012-01-28 02:16:56562 bufsize=-1,
563 stdout=subprocess.PIPE,
564 stderr=subprocess.PIPE)
565 (stdout, stderr) = subproc.communicate()
[email protected]d59c8712014-02-11 21:04:57566 results.append((subproc.returncode, stdout, stderr))
[email protected]7ad66a72009-09-04 17:52:33567 os.chdir(cwd)
[email protected]7ad66a72009-09-04 17:52:33568 try:
569 shutil.rmtree(tempdir, True)
[email protected]4df583c2014-07-31 17:11:55570 except Exception:
[email protected]7ad66a72009-09-04 17:52:33571 pass
[email protected]67e0bc62009-09-03 22:06:09572
[email protected]d59c8712014-02-11 21:04:57573 for (returncode, stdout, stderr) in results:
574 if returncode:
575 return (returncode, stdout, stderr)
576 return results[0]
[email protected]79f14742010-03-10 01:01:57577
[email protected]cb155a82011-11-29 17:25:34578
Jason Kersey97bb027a2016-05-11 20:10:43579# The arguments status, stdout and stderr are unused.
[email protected]4df583c2014-07-31 17:11:55580# They are present here because this function is passed to Bisect which then
581# calls it with 5 arguments.
582# pylint: disable=W0613
Jason Kersey97bb027a2016-05-11 20:10:43583def AskIsGoodBuild(rev, exit_status, stdout, stderr):
[email protected]4df583c2014-07-31 17:11:55584 """Asks the user whether build |rev| is good or bad."""
[email protected]79f14742010-03-10 01:01:57585 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09586 while True:
[email protected]4df583c2014-07-31 17:11:55587 response = raw_input('Revision %s is '
wangxianzhud8c4c562015-12-15 23:39:51588 '[(g)ood/(b)ad/(r)etry/(u)nknown/(s)tdout/(q)uit]: ' %
[email protected]53bb6342012-06-01 04:11:00589 str(rev))
wangxianzhud8c4c562015-12-15 23:39:51590 if response in ('g', 'b', 'r', 'u'):
[email protected]53bb6342012-06-01 04:11:00591 return response
wangxianzhud8c4c562015-12-15 23:39:51592 if response == 'q':
[email protected]afe30662011-07-30 01:05:52593 raise SystemExit()
wangxianzhud8c4c562015-12-15 23:39:51594 if response == 's':
595 print stdout
596 print stderr
[email protected]67e0bc62009-09-03 22:06:09597
[email protected]cb155a82011-11-29 17:25:34598
Jason Kersey97bb027a2016-05-11 20:10:43599def IsGoodASANBuild(rev, exit_status, stdout, stderr):
[email protected]011886692014-08-01 21:00:21600 """Determine if an ASAN build |rev| is good or bad
601
602 Will examine stderr looking for the error message emitted by ASAN. If not
603 found then will fallback to asking the user."""
604 if stderr:
605 bad_count = 0
606 for line in stderr.splitlines():
607 print line
608 if line.find('ERROR: AddressSanitizer:') != -1:
609 bad_count += 1
610 if bad_count > 0:
611 print 'Revision %d determined to be bad.' % rev
612 return 'b'
Jason Kersey97bb027a2016-05-11 20:10:43613 return AskIsGoodBuild(rev, exit_status, stdout, stderr)
skobes21b5cdfb2016-03-21 23:13:02614
615
Jason Kersey97bb027a2016-05-11 20:10:43616def DidCommandSucceed(rev, exit_status, stdout, stderr):
skobes21b5cdfb2016-03-21 23:13:02617 if exit_status:
618 print 'Bad revision: %s' % rev
619 return 'b'
620 else:
621 print 'Good revision: %s' % rev
622 return 'g'
623
[email protected]011886692014-08-01 21:00:21624
[email protected]53bb6342012-06-01 04:11:00625class DownloadJob(object):
626 """DownloadJob represents a task to download a given Chromium revision."""
[email protected]4df583c2014-07-31 17:11:55627
628 def __init__(self, context, name, rev, zip_file):
[email protected]53bb6342012-06-01 04:11:00629 super(DownloadJob, self).__init__()
630 # Store off the input parameters.
631 self.context = context
632 self.name = name
633 self.rev = rev
[email protected]4df583c2014-07-31 17:11:55634 self.zip_file = zip_file
[email protected]53bb6342012-06-01 04:11:00635 self.quit_event = threading.Event()
636 self.progress_event = threading.Event()
[email protected]4df583c2014-07-31 17:11:55637 self.thread = None
[email protected]53bb6342012-06-01 04:11:00638
639 def Start(self):
640 """Starts the download."""
641 fetchargs = (self.context,
642 self.rev,
[email protected]4df583c2014-07-31 17:11:55643 self.zip_file,
[email protected]53bb6342012-06-01 04:11:00644 self.quit_event,
645 self.progress_event)
646 self.thread = threading.Thread(target=FetchRevision,
647 name=self.name,
648 args=fetchargs)
649 self.thread.start()
650
651 def Stop(self):
652 """Stops the download which must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55653 assert self.thread, 'DownloadJob must be started before Stop is called.'
[email protected]53bb6342012-06-01 04:11:00654 self.quit_event.set()
655 self.thread.join()
[email protected]4df583c2014-07-31 17:11:55656 os.unlink(self.zip_file)
[email protected]53bb6342012-06-01 04:11:00657
658 def WaitFor(self):
659 """Prints a message and waits for the download to complete. The download
660 must have been started previously."""
[email protected]4df583c2014-07-31 17:11:55661 assert self.thread, 'DownloadJob must be started before WaitFor is called.'
662 print 'Downloading revision %s...' % str(self.rev)
[email protected]53bb6342012-06-01 04:11:00663 self.progress_event.set() # Display progress of download.
rob8a4543f2016-01-20 00:43:59664 try:
665 while self.thread.isAlive():
666 # The parameter to join is needed to keep the main thread responsive to
667 # signals. Without it, the program will not respond to interruptions.
668 self.thread.join(1)
669 except (KeyboardInterrupt, SystemExit):
670 self.Stop()
671 raise
[email protected]53bb6342012-06-01 04:11:00672
673
skobes21b5cdfb2016-03-21 23:13:02674def VerifyEndpoint(fetch, context, rev, profile, num_runs, command, try_args,
675 evaluate, expected_answer):
676 fetch.WaitFor()
677 try:
678 (exit_status, stdout, stderr) = RunRevision(
679 context, rev, fetch.zip_file, profile, num_runs, command, try_args)
680 except Exception, e:
681 print >> sys.stderr, e
Jason Kersey97bb027a2016-05-11 20:10:43682 if (evaluate(rev, exit_status, stdout, stderr) != expected_answer):
skobes21b5cdfb2016-03-21 23:13:02683 print 'Unexpected result at a range boundary! Your range is not correct.'
684 raise SystemExit
685
686
[email protected]2e0f2672014-08-13 20:32:58687def Bisect(context,
[email protected]5e93cf162012-01-28 02:16:56688 num_runs=1,
[email protected]4df583c2014-07-31 17:11:55689 command='%p %a',
[email protected]60ac66e32011-07-18 16:08:25690 try_args=(),
[email protected]afe30662011-07-30 01:05:52691 profile=None,
skobes21b5cdfb2016-03-21 23:13:02692 evaluate=AskIsGoodBuild,
693 verify_range=False):
[email protected]afe30662011-07-30 01:05:52694 """Given known good and known bad revisions, run a binary search on all
695 archived revisions to determine the last known good revision.
[email protected]60ac66e32011-07-18 16:08:25696
[email protected]2e0f2672014-08-13 20:32:58697 @param context PathContext object initialized with user provided parameters.
[email protected]5e93cf162012-01-28 02:16:56698 @param num_runs Number of times to run each build for asking good/bad.
[email protected]afe30662011-07-30 01:05:52699 @param try_args A tuple of arguments to pass to the test application.
700 @param profile The name of the user profile to run with.
[email protected]53bb6342012-06-01 04:11:00701 @param evaluate A function which returns 'g' if the argument build is good,
702 'b' if it's bad or 'u' if unknown.
skobes21b5cdfb2016-03-21 23:13:02703 @param verify_range If true, tests the first and last revisions in the range
704 before proceeding with the bisect.
[email protected]afe30662011-07-30 01:05:52705
706 Threading is used to fetch Chromium revisions in the background, speeding up
707 the user's experience. For example, suppose the bounds of the search are
708 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
709 whether revision 50 is good or bad, the next revision to check will be either
710 25 or 75. So, while revision 50 is being checked, the script will download
711 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
712 known:
713
714 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
715 is run on rev 75.
716
717 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
718 is run on rev 25.
[email protected]60ac66e32011-07-18 16:08:25719 """
720
[email protected]afe30662011-07-30 01:05:52721 if not profile:
722 profile = 'profile'
723
[email protected]2e0f2672014-08-13 20:32:58724 good_rev = context.good_revision
725 bad_rev = context.bad_revision
[email protected]afe30662011-07-30 01:05:52726 cwd = os.getcwd()
727
[email protected]28a3c122014-08-09 11:04:51728 print 'Downloading list of known revisions...',
Jason Kersey97bb027a2016-05-11 20:10:43729 if not context.use_local_cache:
rob724c9062015-01-22 00:26:42730 print '(use --use-local-cache to cache and re-use the list of revisions)'
[email protected]28a3c122014-08-09 11:04:51731 else:
732 print
[email protected]d0149c5c2012-05-29 21:12:11733 _GetDownloadPath = lambda rev: os.path.join(cwd,
734 '%s-%s' % (str(rev), context.archive_name))
Jason Kersey97bb027a2016-05-11 20:10:43735 revlist = context.GetRevList()
[email protected]afe30662011-07-30 01:05:52736
737 # Get a list of revisions to bisect across.
738 if len(revlist) < 2: # Don't have enough builds to bisect.
739 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
740 raise RuntimeError(msg)
741
742 # Figure out our bookends and first pivot point; fetch the pivot revision.
[email protected]eadd95d2012-11-02 22:42:09743 minrev = 0
744 maxrev = len(revlist) - 1
745 pivot = maxrev / 2
[email protected]afe30662011-07-30 01:05:52746 rev = revlist[pivot]
skobes21b5cdfb2016-03-21 23:13:02747 fetch = DownloadJob(context, 'initial_fetch', rev, _GetDownloadPath(rev))
[email protected]eadd95d2012-11-02 22:42:09748 fetch.Start()
skobes21b5cdfb2016-03-21 23:13:02749
750 if verify_range:
751 minrev_fetch = DownloadJob(
752 context, 'minrev_fetch', revlist[minrev],
753 _GetDownloadPath(revlist[minrev]))
754 maxrev_fetch = DownloadJob(
755 context, 'maxrev_fetch', revlist[maxrev],
756 _GetDownloadPath(revlist[maxrev]))
757 minrev_fetch.Start()
758 maxrev_fetch.Start()
759 try:
760 VerifyEndpoint(minrev_fetch, context, revlist[minrev], profile, num_runs,
761 command, try_args, evaluate, 'b' if bad_rev < good_rev else 'g')
762 VerifyEndpoint(maxrev_fetch, context, revlist[maxrev], profile, num_runs,
763 command, try_args, evaluate, 'g' if bad_rev < good_rev else 'b')
764 except (KeyboardInterrupt, SystemExit):
765 print 'Cleaning up...'
766 fetch.Stop()
767 sys.exit(0)
768 finally:
769 minrev_fetch.Stop()
770 maxrev_fetch.Stop()
771
[email protected]eadd95d2012-11-02 22:42:09772 fetch.WaitFor()
[email protected]60ac66e32011-07-18 16:08:25773
774 # Binary search time!
[email protected]4df583c2014-07-31 17:11:55775 while fetch and fetch.zip_file and maxrev - minrev > 1:
[email protected]eadd95d2012-11-02 22:42:09776 if bad_rev < good_rev:
[email protected]4df583c2014-07-31 17:11:55777 min_str, max_str = 'bad', 'good'
[email protected]eadd95d2012-11-02 22:42:09778 else:
[email protected]4df583c2014-07-31 17:11:55779 min_str, max_str = 'good', 'bad'
780 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str,
[email protected]eadd95d2012-11-02 22:42:09781 revlist[maxrev], max_str)
782
[email protected]afe30662011-07-30 01:05:52783 # Pre-fetch next two possible pivots
784 # - down_pivot is the next revision to check if the current revision turns
785 # out to be bad.
786 # - up_pivot is the next revision to check if the current revision turns
787 # out to be good.
[email protected]eadd95d2012-11-02 22:42:09788 down_pivot = int((pivot - minrev) / 2) + minrev
[email protected]53bb6342012-06-01 04:11:00789 down_fetch = None
[email protected]eadd95d2012-11-02 22:42:09790 if down_pivot != pivot and down_pivot != minrev:
[email protected]afe30662011-07-30 01:05:52791 down_rev = revlist[down_pivot]
[email protected]53bb6342012-06-01 04:11:00792 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
793 _GetDownloadPath(down_rev))
794 down_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25795
[email protected]eadd95d2012-11-02 22:42:09796 up_pivot = int((maxrev - pivot) / 2) + pivot
[email protected]53bb6342012-06-01 04:11:00797 up_fetch = None
[email protected]eadd95d2012-11-02 22:42:09798 if up_pivot != pivot and up_pivot != maxrev:
[email protected]afe30662011-07-30 01:05:52799 up_rev = revlist[up_pivot]
[email protected]53bb6342012-06-01 04:11:00800 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
801 _GetDownloadPath(up_rev))
802 up_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25803
[email protected]afe30662011-07-30 01:05:52804 # Run test on the pivot revision.
skobes21b5cdfb2016-03-21 23:13:02805 exit_status = None
[email protected]e29c08c2012-09-17 20:50:50806 stdout = None
807 stderr = None
808 try:
skobes21b5cdfb2016-03-21 23:13:02809 (exit_status, stdout, stderr) = RunRevision(
810 context, rev, fetch.zip_file, profile, num_runs, command, try_args)
[email protected]e29c08c2012-09-17 20:50:50811 except Exception, e:
[email protected]fc3702e2013-11-09 04:23:00812 print >> sys.stderr, e
[email protected]60ac66e32011-07-18 16:08:25813
[email protected]53bb6342012-06-01 04:11:00814 # Call the evaluate function to see if the current revision is good or bad.
[email protected]afe30662011-07-30 01:05:52815 # On that basis, kill one of the background downloads and complete the
816 # other, as described in the comments above.
817 try:
Jason Kersey97bb027a2016-05-11 20:10:43818 answer = evaluate(rev, exit_status, stdout, stderr)
[email protected]4df583c2014-07-31 17:11:55819 if ((answer == 'g' and good_rev < bad_rev)
820 or (answer == 'b' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12821 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09822 minrev = pivot
[email protected]53bb6342012-06-01 04:11:00823 if down_fetch:
824 down_fetch.Stop() # Kill the download of the older revision.
[email protected]1d4a06242013-08-20 22:53:12825 fetch = None
[email protected]53bb6342012-06-01 04:11:00826 if up_fetch:
827 up_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52828 pivot = up_pivot
[email protected]eadd95d2012-11-02 22:42:09829 fetch = up_fetch
[email protected]4df583c2014-07-31 17:11:55830 elif ((answer == 'b' and good_rev < bad_rev)
831 or (answer == 'g' and bad_rev < good_rev)):
[email protected]1d4a06242013-08-20 22:53:12832 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09833 maxrev = pivot
[email protected]53bb6342012-06-01 04:11:00834 if up_fetch:
835 up_fetch.Stop() # Kill the download of the newer revision.
[email protected]1d4a06242013-08-20 22:53:12836 fetch = None
[email protected]53bb6342012-06-01 04:11:00837 if down_fetch:
838 down_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52839 pivot = down_pivot
[email protected]eadd95d2012-11-02 22:42:09840 fetch = down_fetch
[email protected]1d4a06242013-08-20 22:53:12841 elif answer == 'r':
842 pass # Retry requires no changes.
[email protected]53bb6342012-06-01 04:11:00843 elif answer == 'u':
844 # Nuke the revision from the revlist and choose a new pivot.
[email protected]1d4a06242013-08-20 22:53:12845 fetch.Stop()
[email protected]53bb6342012-06-01 04:11:00846 revlist.pop(pivot)
[email protected]eadd95d2012-11-02 22:42:09847 maxrev -= 1 # Assumes maxrev >= pivot.
[email protected]53bb6342012-06-01 04:11:00848
[email protected]eadd95d2012-11-02 22:42:09849 if maxrev - minrev > 1:
[email protected]53bb6342012-06-01 04:11:00850 # Alternate between using down_pivot or up_pivot for the new pivot
851 # point, without affecting the range. Do this instead of setting the
852 # pivot to the midpoint of the new range because adjacent revisions
853 # are likely affected by the same issue that caused the (u)nknown
854 # response.
855 if up_fetch and down_fetch:
856 fetch = [up_fetch, down_fetch][len(revlist) % 2]
857 elif up_fetch:
858 fetch = up_fetch
859 else:
860 fetch = down_fetch
861 fetch.WaitFor()
862 if fetch == up_fetch:
863 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
864 else:
865 pivot = down_pivot
[email protected]53bb6342012-06-01 04:11:00866
867 if down_fetch and fetch != down_fetch:
868 down_fetch.Stop()
869 if up_fetch and fetch != up_fetch:
870 up_fetch.Stop()
871 else:
[email protected]4df583c2014-07-31 17:11:55872 assert False, 'Unexpected return value from evaluate(): ' + answer
skobes21b5cdfb2016-03-21 23:13:02873 except (KeyboardInterrupt, SystemExit):
[email protected]4df583c2014-07-31 17:11:55874 print 'Cleaning up...'
skobes21b5cdfb2016-03-21 23:13:02875 for f in [_GetDownloadPath(rev),
876 _GetDownloadPath(revlist[down_pivot]),
[email protected]5e93cf162012-01-28 02:16:56877 _GetDownloadPath(revlist[up_pivot])]:
[email protected]afe30662011-07-30 01:05:52878 try:
879 os.unlink(f)
880 except OSError:
881 pass
882 sys.exit(0)
883
884 rev = revlist[pivot]
885
[email protected]2e0f2672014-08-13 20:32:58886 return (revlist[minrev], revlist[maxrev], context)
[email protected]60ac66e32011-07-18 16:08:25887
888
pshenoycd6bd682014-09-10 20:50:22889def GetBlinkDEPSRevisionForChromiumRevision(self, rev):
[email protected]4c6fec6b2013-09-17 17:44:08890 """Returns the blink revision that was in REVISIONS file at
[email protected]b2fe7f22011-10-25 22:58:31891 chromium revision |rev|."""
pshenoycd6bd682014-09-10 20:50:22892
893 def _GetBlinkRev(url, blink_re):
894 m = blink_re.search(url.read())
895 url.close()
896 if m:
897 return m.group(1)
898
899 url = urllib.urlopen(DEPS_FILE_OLD % rev)
900 if url.getcode() == 200:
901 # . doesn't match newlines without re.DOTALL, so this is safe.
902 blink_re = re.compile(r'webkit_revision\D*(\d+)')
903 return int(_GetBlinkRev(url, blink_re))
[email protected]37ed3172013-09-24 23:49:30904 else:
pshenoycd6bd682014-09-10 20:50:22905 url = urllib.urlopen(DEPS_FILE_NEW % GetGitHashFromSVNRevision(rev))
906 if url.getcode() == 200:
907 blink_re = re.compile(r'webkit_revision\D*\d+;\D*\d+;(\w+)')
908 blink_git_sha = _GetBlinkRev(url, blink_re)
909 return self.GetSVNRevisionFromGitHash(blink_git_sha, 'blink')
910 raise Exception('Could not get Blink revision for Chromium rev %d' % rev)
[email protected]37ed3172013-09-24 23:49:30911
912
[email protected]2e0f2672014-08-13 20:32:58913def GetBlinkRevisionForChromiumRevision(context, rev):
[email protected]37ed3172013-09-24 23:49:30914 """Returns the blink revision that was in REVISIONS file at
915 chromium revision |rev|."""
[email protected]3e7c85322014-06-27 20:27:36916 def _IsRevisionNumber(revision):
917 if isinstance(revision, int):
918 return True
919 else:
920 return revision.isdigit()
[email protected]2e0f2672014-08-13 20:32:58921 if str(rev) in context.githash_svn_dict:
922 rev = context.githash_svn_dict[str(rev)]
923 file_url = '%s/%s%s/REVISIONS' % (context.base_url,
924 context._listing_platform_dir, rev)
[email protected]4c6fec6b2013-09-17 17:44:08925 url = urllib.urlopen(file_url)
[email protected]2e0f2672014-08-13 20:32:58926 if url.getcode() == 200:
927 try:
928 data = json.loads(url.read())
929 except ValueError:
930 print 'ValueError for JSON URL: %s' % file_url
931 raise ValueError
932 else:
933 raise ValueError
[email protected]b2fe7f22011-10-25 22:58:31934 url.close()
[email protected]4c6fec6b2013-09-17 17:44:08935 if 'webkit_revision' in data:
[email protected]3e7c85322014-06-27 20:27:36936 blink_rev = data['webkit_revision']
937 if not _IsRevisionNumber(blink_rev):
[email protected]2e0f2672014-08-13 20:32:58938 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink'))
[email protected]3e7c85322014-06-27 20:27:36939 return blink_rev
[email protected]b2fe7f22011-10-25 22:58:31940 else:
[email protected]ff50d1c2013-04-17 18:49:36941 raise Exception('Could not get blink revision for cr rev %d' % rev)
[email protected]b2fe7f22011-10-25 22:58:31942
[email protected]4df583c2014-07-31 17:11:55943
[email protected]37ed3172013-09-24 23:49:30944def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
945 """Returns the chromium revision that has the correct blink revision
946 for blink bisect, DEPS and REVISIONS file might not match since
947 blink snapshots point to tip of tree blink.
948 Note: The revisions_final variable might get modified to include
949 additional revisions."""
pshenoycd6bd682014-09-10 20:50:22950 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(self, rev)
[email protected]37ed3172013-09-24 23:49:30951
952 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
953 idx = revisions.index(rev)
954 if idx > 0:
955 rev = revisions[idx-1]
956 if rev not in revisions_final:
957 revisions_final.insert(0, rev)
958
959 revisions_final.sort()
960 return rev
[email protected]b2fe7f22011-10-25 22:58:31961
[email protected]4df583c2014-07-31 17:11:55962
[email protected]5980b752014-07-02 00:34:40963def GetChromiumRevision(context, url):
[email protected]801fb652012-07-20 20:13:50964 """Returns the chromium revision read from given URL."""
965 try:
966 # Location of the latest build revision number
[email protected]5980b752014-07-02 00:34:40967 latest_revision = urllib.urlopen(url).read()
968 if latest_revision.isdigit():
969 return int(latest_revision)
970 return context.GetSVNRevisionFromGitHash(latest_revision)
[email protected]4df583c2014-07-31 17:11:55971 except Exception:
972 print 'Could not determine latest revision. This could be bad...'
[email protected]801fb652012-07-20 20:13:50973 return 999999999
974
pshenoycd6bd682014-09-10 20:50:22975def GetGitHashFromSVNRevision(svn_revision):
976 crrev_url = CRREV_URL + str(svn_revision)
977 url = urllib.urlopen(crrev_url)
978 if url.getcode() == 200:
979 data = json.loads(url.read())
980 if 'git_sha' in data:
981 return data['git_sha']
982
pshenoy9ce271f2014-09-02 22:14:05983def PrintChangeLog(min_chromium_rev, max_chromium_rev):
984 """Prints the changelog URL."""
985
pshenoycd6bd682014-09-10 20:50:22986 print (' ' + CHANGELOG_URL % (GetGitHashFromSVNRevision(min_chromium_rev),
987 GetGitHashFromSVNRevision(max_chromium_rev)))
pshenoy9ce271f2014-09-02 22:14:05988
[email protected]801fb652012-07-20 20:13:50989
[email protected]67e0bc62009-09-03 22:06:09990def main():
[email protected]2c1d2732009-10-29 19:52:17991 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]887c9182013-02-12 20:30:31992 'Perform binary search on the snapshot builds to find a minimal\n'
993 'range of revisions where a behavior change happened. The\n'
994 'behaviors are described as "good" and "bad".\n'
995 'It is NOT assumed that the behavior of the later revision is\n'
[email protected]09c58da2013-01-07 21:30:17996 'the bad one.\n'
[email protected]178aab72010-10-08 17:21:38997 '\n'
[email protected]887c9182013-02-12 20:30:31998 'Revision numbers should use\n'
[email protected]887c9182013-02-12 20:30:31999 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
1000 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
1001 ' for earlier revs.\n'
1002 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
1003 ' are incorrect, they are from branches.\n'
1004 '\n'
[email protected]178aab72010-10-08 17:21:381005 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
[email protected]7ad66a72009-09-04 17:52:331006 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:571007 # Strangely, the default help output doesn't include the choice list.
mikecasea8cd284c2014-12-02 21:30:581008 choices = ['mac', 'mac64', 'win', 'win64', 'linux', 'linux64', 'linux-arm',
dmazzoni76e907d2015-01-22 08:14:491009 'chromeos']
[email protected]7ad66a72009-09-04 17:52:331010 parser.add_option('-a', '--archive',
[email protected]4df583c2014-07-31 17:11:551011 choices=choices,
1012 help='The buildbot archive to bisect [%s].' %
1013 '|'.join(choices))
[email protected]4df583c2014-07-31 17:11:551014 parser.add_option('-b', '--bad',
1015 type='str',
1016 help='A bad revision to start bisection. '
1017 'May be earlier or later than the good revision. '
1018 'Default is HEAD.')
1019 parser.add_option('-f', '--flash_path',
1020 type='str',
1021 help='Absolute path to a recent Adobe Pepper Flash '
1022 'binary to be used in this bisection (e.g. '
1023 'on Windows C:\...\pepflashplayer.dll and on Linux '
1024 '/opt/google/chrome/PepperFlash/'
1025 'libpepflashplayer.so).')
[email protected]4df583c2014-07-31 17:11:551026 parser.add_option('-g', '--good',
1027 type='str',
1028 help='A good revision to start bisection. ' +
1029 'May be earlier or later than the bad revision. ' +
1030 'Default is 0.')
1031 parser.add_option('-p', '--profile', '--user-data-dir',
1032 type='str',
1033 default='profile',
1034 help='Profile to use; this will not reset every run. '
1035 'Defaults to a clean profile.')
1036 parser.add_option('-t', '--times',
1037 type='int',
1038 default=1,
1039 help='Number of times to run each build before asking '
1040 'if it\'s good or bad. Temporary profiles are reused.')
1041 parser.add_option('-c', '--command',
1042 type='str',
1043 default='%p %a',
1044 help='Command to execute. %p and %a refer to Chrome '
1045 'executable and specified extra arguments '
1046 'respectively. Use %s to specify all extra arguments '
1047 'as one string. Defaults to "%p %a". Note that any '
1048 'extra paths specified should be absolute.')
1049 parser.add_option('-l', '--blink',
1050 action='store_true',
1051 help='Use Blink bisect instead of Chromium. ')
1052 parser.add_option('', '--not-interactive',
1053 action='store_true',
1054 default=False,
1055 help='Use command exit code to tell good/bad revision.')
[email protected]011886692014-08-01 21:00:211056 parser.add_option('--asan',
1057 dest='asan',
1058 action='store_true',
1059 default=False,
1060 help='Allow the script to bisect ASAN builds')
rob724c9062015-01-22 00:26:421061 parser.add_option('--use-local-cache',
1062 dest='use_local_cache',
[email protected]6a7a5d62014-07-09 04:45:501063 action='store_true',
1064 default=False,
rob724c9062015-01-22 00:26:421065 help='Use a local file in the current directory to cache '
1066 'a list of known revisions to speed up the '
1067 'initialization of this script.')
skobes21b5cdfb2016-03-21 23:13:021068 parser.add_option('--verify-range',
1069 dest='verify_range',
1070 action='store_true',
1071 default=False,
1072 help='Test the first and last revisions in the range ' +
1073 'before proceeding with the bisect.')
[email protected]b3b20512013-08-26 18:51:041074
[email protected]7ad66a72009-09-04 17:52:331075 (opts, args) = parser.parse_args()
1076
1077 if opts.archive is None:
[email protected]178aab72010-10-08 17:21:381078 print 'Error: missing required parameter: --archive'
1079 print
[email protected]7ad66a72009-09-04 17:52:331080 parser.print_help()
1081 return 1
1082
[email protected]011886692014-08-01 21:00:211083 if opts.asan:
1084 supported_platforms = ['linux', 'mac', 'win']
1085 if opts.archive not in supported_platforms:
1086 print 'Error: ASAN bisecting only supported on these platforms: [%s].' % (
1087 '|'.join(supported_platforms))
1088 return 1
[email protected]011886692014-08-01 21:00:211089
1090 if opts.asan:
1091 base_url = ASAN_BASE_URL
1092 elif opts.blink:
[email protected]4c6fec6b2013-09-17 17:44:081093 base_url = WEBKIT_BASE_URL
1094 else:
1095 base_url = CHROMIUM_BASE_URL
1096
[email protected]183706d92011-06-10 13:06:221097 # Create the context. Initialize 0 for the revisions as they are set below.
[email protected]2e0f2672014-08-13 20:32:581098 context = PathContext(base_url, opts.archive, opts.good, opts.bad,
Jason Kersey97bb027a2016-05-11 20:10:431099 opts.asan, opts.use_local_cache,
vitalybuka4d1e1e412015-07-06 17:21:061100 opts.flash_path)
mikecasea8cd284c2014-12-02 21:30:581101
[email protected]67e0bc62009-09-03 22:06:091102 # Pick a starting point, try to get HEAD for this.
[email protected]2e0f2672014-08-13 20:32:581103 if not opts.bad:
1104 context.bad_revision = '999.0.0.0'
1105 context.bad_revision = GetChromiumRevision(
1106 context, context.GetLastChangeURL())
[email protected]67e0bc62009-09-03 22:06:091107
1108 # Find out when we were good.
[email protected]2e0f2672014-08-13 20:32:581109 if not opts.good:
Jason Kersey97bb027a2016-05-11 20:10:431110 context.good_revision = 0
[email protected]801fb652012-07-20 20:13:501111
[email protected]fc3702e2013-11-09 04:23:001112 if opts.flash_path:
[email protected]2e0f2672014-08-13 20:32:581113 msg = 'Could not find Flash binary at %s' % opts.flash_path
1114 assert os.path.exists(opts.flash_path), msg
[email protected]fc3702e2013-11-09 04:23:001115
Jason Kersey97bb027a2016-05-11 20:10:431116 context.good_revision = int(context.good_revision)
1117 context.bad_revision = int(context.bad_revision)
[email protected]801fb652012-07-20 20:13:501118
[email protected]5e93cf162012-01-28 02:16:561119 if opts.times < 1:
1120 print('Number of times to run (%d) must be greater than or equal to 1.' %
1121 opts.times)
1122 parser.print_help()
1123 return 1
1124
skobes21b5cdfb2016-03-21 23:13:021125 if opts.not_interactive:
1126 evaluator = DidCommandSucceed
1127 elif opts.asan:
[email protected]011886692014-08-01 21:00:211128 evaluator = IsGoodASANBuild
1129 else:
1130 evaluator = AskIsGoodBuild
1131
[email protected]2e0f2672014-08-13 20:32:581132 # Save these revision numbers to compare when showing the changelog URL
1133 # after the bisect.
1134 good_rev = context.good_revision
1135 bad_rev = context.bad_revision
1136
1137 (min_chromium_rev, max_chromium_rev, context) = Bisect(
1138 context, opts.times, opts.command, args, opts.profile,
skobes21b5cdfb2016-03-21 23:13:021139 evaluator, opts.verify_range)
[email protected]67e0bc62009-09-03 22:06:091140
[email protected]ff50d1c2013-04-17 18:49:361141 # Get corresponding blink revisions.
[email protected]b2fe7f22011-10-25 22:58:311142 try:
[email protected]4c6fec6b2013-09-17 17:44:081143 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1144 min_chromium_rev)
1145 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1146 max_chromium_rev)
[email protected]4df583c2014-07-31 17:11:551147 except Exception:
[email protected]b2fe7f22011-10-25 22:58:311148 # Silently ignore the failure.
[email protected]ff50d1c2013-04-17 18:49:361149 min_blink_rev, max_blink_rev = 0, 0
[email protected]b2fe7f22011-10-25 22:58:311150
[email protected]3bdaa4752013-09-30 20:13:361151 if opts.blink:
1152 # We're done. Let the user know the results in an official manner.
1153 if good_rev > bad_rev:
1154 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
1155 else:
1156 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
[email protected]eadd95d2012-11-02 22:42:091157
[email protected]ff50d1c2013-04-17 18:49:361158 print 'BLINK CHANGELOG URL:'
1159 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
[email protected]3bdaa4752013-09-30 20:13:361160
[email protected]d0149c5c2012-05-29 21:12:111161 else:
[email protected]3bdaa4752013-09-30 20:13:361162 # We're done. Let the user know the results in an official manner.
1163 if good_rev > bad_rev:
1164 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
1165 str(max_chromium_rev))
1166 else:
1167 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
1168 str(max_chromium_rev))
1169 if min_blink_rev != max_blink_rev:
[email protected]4df583c2014-07-31 17:11:551170 print ('NOTE: There is a Blink roll in the range, '
1171 'you might also want to do a Blink bisect.')
[email protected]3bdaa4752013-09-30 20:13:361172
1173 print 'CHANGELOG URL:'
Jason Kersey97bb027a2016-05-11 20:10:431174 PrintChangeLog(min_chromium_rev, max_chromium_rev)
[email protected]cb155a82011-11-29 17:25:341175
[email protected]4df583c2014-07-31 17:11:551176
[email protected]67e0bc62009-09-03 22:06:091177if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:331178 sys.exit(main())