Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1010)

Side by Side Diff: tools/bisect-builds.py

Issue 6995117: bisect-builds.py: Use Google Common Data Storage instead of build.chromium.org. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 9 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Snapshot Build Bisect Tool 6 """Snapshot Build Bisect Tool
7 7
8 This script bisects a snapshot archive using binary search. It starts at 8 This script bisects a snapshot archive using binary search. It starts at
9 a bad revision (it will try to guess HEAD) and asks for a last known-good 9 a bad revision (it will try to guess HEAD) and asks for a last known-good
10 revision. It will then binary search across this revision range by downloading, 10 revision. It will then binary search across this revision range by downloading,
11 unzipping, and opening Chromium for you. After testing the specific revision, 11 unzipping, and opening Chromium for you. After testing the specific revision,
12 it will ask you whether it is good or bad before continuing the search. 12 it will ask you whether it is good or bad before continuing the search.
13 """ 13 """
14 14
15 # Base URL to download snapshots from. 15 # The root URL for storage.
16 BUILD_BASE_URL = 'http://build.chromium.org/f/chromium/continuous/' 16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-continuous'
17
18 # The index file that lists all the builds. This lives in BUILD_BASE_URL.
19 BUILD_INDEX_FILE = 'all_builds.txt'
20
21 # The type (platform) of the build archive. This is what's passed in to the
22 # '-a/--archive' option.
23 BUILD_ARCHIVE_TYPE = ''
24
25 # The location of the builds. Format this with a (date, revision) tuple, which
26 # can be obtained through ParseIndexLine().
27 BUILD_ARCHIVE_URL = '/%s/%d/'
28
29 # Name of the build archive.
30 BUILD_ZIP_NAME = ''
31
32 # Directory name inside the archive.
33 BUILD_DIR_NAME = ''
34
35 # Name of the executable.
36 BUILD_EXE_NAME = ''
37 17
38 # URL to the ViewVC commit page. 18 # URL to the ViewVC commit page.
39 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' 19 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d'
40 20
41 # Changelogs URL 21 # Changelogs URL.
42 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ 22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
43 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' 23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d'
44 24
45 ############################################################################### 25 ###############################################################################
46 26
47 import math 27 import math
48 import optparse 28 import optparse
49 import os 29 import os
50 import pipes 30 import pipes
51 import re 31 import re
52 import shutil 32 import shutil
53 import sys 33 import sys
54 import tempfile 34 import tempfile
55 import urllib 35 import urllib
36 from xml.etree import ElementTree
56 import zipfile 37 import zipfile
57 38
39 class PathContext(object):
40 """A PathContext is used to carry the information used to construct URLs and
41 paths when dealing with the storage server and archives."""
42 def __init__(self, platform, good_revision, bad_revision):
43 super(PathContext, self).__init__()
44 # Store off the input parameters.
45 self.platform = platform # What's passed in to the '-a/--archive' option.
46 self.good_revision = good_revision
47 self.bad_revision = bad_revision
48
49 # The name of the ZIP file in a revision directory on the server.
50 self.archive_name = None
51
52 # Set some internal members:
53 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
54 # _archive_extract_dir = Uncompressed directory in the archive_name file.
55 # _binary_name = The name of the executable to run.
56 if self.platform == 'linux' or self.platform == 'linux-64':
57 self._listing_platform_dir = 'Linux/'
58 self.archive_name = 'chrome-linux.zip'
59 self._archive_extract_dir = 'chrome-linux'
60 self._binary_name = 'chrome'
61 # Linux and x64 share all the same path data except for the archive dir.
62 if self.platform == 'linux-64':
63 self._listing_platform_dir = 'Linux_x64/'
64 elif self.platform == 'mac':
65 self._listing_platform_dir = 'Mac/'
66 self.archive_name = 'chrome-mac.zip'
67 self._archive_extract_dir = 'chrome-mac'
68 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
69 elif self.platform == 'win':
70 self._listing_platform_dir = 'Win/'
71 self.archive_name = 'chrome-win32.zip'
72 self._archive_extract_dir = 'chrome-win32'
73 self._binary_name = 'chrome.exe'
74 else:
75 raise Exception("Invalid platform")
76
77 def GetListingURL(self, revision_offset=None):
78 """Returns the URL for a directory listing, with an optional starting
79 revision."""
80 marker = ''
81 if revision_offset:
82 marker = '&marker=' + self._listing_platform_dir + \
83 str(revision_offset) + '/'
84 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \
85 marker
86
87 def GetDownloadURL(self, revision):
88 """Gets the download URL for a build archive of a specific revision."""
89 return BASE_URL + '/' + self._listing_platform_dir + str(revision) + '/' + \
90 self.archive_name
91
92 def GetLastChangeURL(self):
93 """Returns a URL to the LAST_CHANGE file."""
94 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE'
95
96 def GetLaunchPath(self):
97 """Returns a relative path (presumably from the archive extraction location)
98 that is used to run the executable."""
99 return os.path.join(self._archive_extract_dir, self._binary_name)
100
58 101
59 def UnzipFilenameToDir(filename, dir): 102 def UnzipFilenameToDir(filename, dir):
60 """Unzip |filename| to directory |dir|.""" 103 """Unzip |filename| to directory |dir|."""
61 zf = zipfile.ZipFile(filename) 104 zf = zipfile.ZipFile(filename)
62 # Make base. 105 # Make base.
63 pushd = os.getcwd() 106 pushd = os.getcwd()
64 try: 107 try:
65 if not os.path.isdir(dir): 108 if not os.path.isdir(dir):
66 os.mkdir(dir) 109 os.mkdir(dir)
67 os.chdir(dir) 110 os.chdir(dir)
(...skipping 11 matching lines...) Expand all
79 out.write(zf.read(name)) 122 out.write(zf.read(name))
80 out.close() 123 out.close()
81 # Set permissions. Permission info in external_attr is shifted 16 bits. 124 # Set permissions. Permission info in external_attr is shifted 16 bits.
82 os.chmod(name, info.external_attr >> 16L) 125 os.chmod(name, info.external_attr >> 16L)
83 os.chdir(pushd) 126 os.chdir(pushd)
84 except Exception, e: 127 except Exception, e:
85 print >>sys.stderr, e 128 print >>sys.stderr, e
86 sys.exit(1) 129 sys.exit(1)
87 130
88 131
89 def SetArchiveVars(archive): 132 def ParseDirectoryIndex(context):
90 """Set a bunch of global variables appropriate for the specified archive.""" 133 """Parses the Google Storage directory listing into a list of revision
91 global BUILD_ARCHIVE_TYPE 134 numbers. The range starts with context.good_revision and goes until the latest
92 global BUILD_ZIP_NAME 135 revision."""
93 global BUILD_DIR_NAME 136 def _FetchAndParse(url):
94 global BUILD_EXE_NAME 137 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
95 global BUILD_BASE_URL 138 next-marker is not None, then the listing is a partial listing and another
139 fetch should be performed with next-marker being the marker= GET
140 parameter."""
141 handle = urllib.urlopen(url)
142 document = ElementTree.parse(handle)
96 143
97 BUILD_ARCHIVE_TYPE = archive 144 # All nodes in the tree are namespaced. Get the root's tag name to extract
145 # the namespace. Etree does namespaces as |{namespace}tag|.
146 root_tag = document.getroot().tag
147 end_ns_pos = root_tag.find('}')
148 if end_ns_pos == -1:
149 raise Exception("Could not locate end namespace for directory index")
150 namespace = root_tag[:end_ns_pos + 1]
98 151
99 if BUILD_ARCHIVE_TYPE in ('linux', 'linux64', 'linux-chromiumos'): 152 # Find the prefix (_listing_platform_dir) and whether or not the list is
100 BUILD_ZIP_NAME = 'chrome-linux.zip' 153 # truncated.
101 BUILD_DIR_NAME = 'chrome-linux' 154 prefix = document.find(namespace + 'Prefix').text
102 BUILD_EXE_NAME = 'chrome' 155 next_marker = None
103 elif BUILD_ARCHIVE_TYPE in ('mac'): 156 is_truncated = document.find(namespace + 'IsTruncated')
104 BUILD_ZIP_NAME = 'chrome-mac.zip' 157 if is_truncated is not None and is_truncated.text.lower() == 'true':
105 BUILD_DIR_NAME = 'chrome-mac' 158 next_marker = document.find(namespace + 'NextMarker').text
106 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium' 159
107 elif BUILD_ARCHIVE_TYPE in ('win'): 160 # Get a list of all the revisions.
108 BUILD_ZIP_NAME = 'chrome-win32.zip' 161 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
109 BUILD_DIR_NAME = 'chrome-win32' 162 namespace + 'Prefix')
110 BUILD_EXE_NAME = 'chrome.exe' 163 # The <Prefix> nodes have content of the form of
164 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
165 # trailing slash to just have a number.
166 revisions = map(lambda x: x.text[len(prefix):-1], all_prefixes)
167 return (revisions, next_marker)
168
169 # Set the marker to be the good revision, as bisecting before that is not
170 # necessary.
171 (revisions, next_marker) = _FetchAndParse(
172 context.GetListingURL(context.good_revision))
nsylvain 2011/06/09 21:12:03 I'm not sure this works well. I don't think the o
Robert Sesek 2011/06/09 23:38:58 Ah, thanks. I didn't realize that; was just trying
173 # If the result list was truncated, refetch with the next marker. Do this
174 # until an entire directory listing is done.
175 while next_marker:
176 (new_revisions, next_marker) = _FetchAndParse(
177 context.GetListingURL(next_marker))
178 revisions.extend(new_revisions)
179
180 return revisions
111 181
112 182
113 def ParseDirectoryIndex(url): 183 def GetRevList(context):
114 """Parses the all_builds.txt index file. The format of this file is: 184 """Gets the list of revision numbers between |good_revision| and
115 mac/2011-02-16/75130 185 |bad_revision| of the |context|."""
116 mac/2011-02-16/75218 186 # Download the revlist and filter for just the range between good and bad.
117 mac/2011-02-16/75226 187 rev_range = range(context.good_revision, context.bad_revision)
118 mac/2011-02-16/75234 188 revlist = map(int, ParseDirectoryIndex(context))
119 mac/2011-02-16/75184 189 revlist = filter(lambda r: r in rev_range, revlist)
120 This function will return a list of DATE/REVISION strings for the platform
121 specified by BUILD_ARCHIVE_TYPE.
122 """
123 handle = urllib.urlopen(url)
124 dirindex = handle.readlines()
125 handle.close()
126
127 # Only return values for the specified platform. Include the trailing slash to
128 # not confuse linux and linux64.
129 archtype = BUILD_ARCHIVE_TYPE + '/'
130 dirindex = filter(lambda l: l.startswith(archtype), dirindex)
131
132 # Remove the newline separator and the platform token.
133 dirindex = map(lambda l: l[len(archtype):].strip(), dirindex)
134 dirindex.sort()
135 return dirindex
136
137
138 def ParseIndexLine(iline):
139 """Takes an index line returned by ParseDirectoryIndex() and returns a
140 2-tuple of (date, revision). |date| is a string and |revision| is an int."""
141 split = iline.split('/')
142 assert(len(split) == 2)
143 return (split[0], int(split[1]))
144
145
146 def GetRevision(iline):
147 """Takes an index line, parses it, and returns the revision."""
148 return ParseIndexLine(iline)[1]
149
150
151 def GetRevList(good, bad):
152 """Gets the list of revision numbers between |good| and |bad|."""
153 # Download the main revlist.
154 revlist = ParseDirectoryIndex(BUILD_BASE_URL + BUILD_INDEX_FILE)
155 revrange = range(good, bad)
156 revlist = filter(lambda r: GetRevision(r) in revrange, revlist)
157 revlist.sort() 190 revlist.sort()
158 return revlist 191 return revlist
159 192
160 193
161 def TryRevision(iline, profile, args): 194 def TryRevision(context, rev, profile, args):
162 """Downloads revision from |iline|, unzips it, and opens it for the user to 195 """Downloads revision |rev|, unzips it, and opens it for the user to test.
163 test. |profile| is the profile to use.""" 196 |profile| is the profile to use."""
164 # Do this in a temp dir so we don't collide with user files. 197 # Do this in a temp dir so we don't collide with user files.
165 cwd = os.getcwd() 198 cwd = os.getcwd()
166 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') 199 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
167 os.chdir(tempdir) 200 os.chdir(tempdir)
168 201
169 # Download the file. 202 # Download the file.
170 download_url = BUILD_BASE_URL + BUILD_ARCHIVE_TYPE + \ 203 download_url = context.GetDownloadURL(rev)
171 (BUILD_ARCHIVE_URL % ParseIndexLine(iline)) + BUILD_ZIP_NAME
172 def _ReportHook(blocknum, blocksize, totalsize): 204 def _ReportHook(blocknum, blocksize, totalsize):
173 size = blocknum * blocksize 205 size = blocknum * blocksize
174 if totalsize == -1: # Total size not known. 206 if totalsize == -1: # Total size not known.
175 progress = "Received %d bytes" % size 207 progress = "Received %d bytes" % size
176 else: 208 else:
177 size = min(totalsize, size) 209 size = min(totalsize, size)
178 progress = "Received %d of %d bytes, %.2f%%" % ( 210 progress = "Received %d of %d bytes, %.2f%%" % (
179 size, totalsize, 100.0 * size / totalsize) 211 size, totalsize, 100.0 * size / totalsize)
180 # Send a \r to let all progress messages use just one line of output. 212 # Send a \r to let all progress messages use just one line of output.
181 sys.stdout.write("\r" + progress) 213 sys.stdout.write("\r" + progress)
182 sys.stdout.flush() 214 sys.stdout.flush()
183 try: 215 try:
184 print 'Fetching ' + download_url 216 print 'Fetching ' + download_url
185 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _ReportHook) 217 urllib.urlretrieve(download_url, context.archive_name, _ReportHook)
186 print 218 print
187 except Exception, e: 219 except Exception, e:
188 print('Could not retrieve the download. Sorry.') 220 print('Could not retrieve the download. Sorry.')
189 sys.exit(-1) 221 sys.exit(-1)
190 222
191 # Unzip the file. 223 # Unzip the file.
192 print 'Unzipping ...' 224 print 'Unzipping ...'
193 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir) 225 UnzipFilenameToDir(context.archive_name, os.curdir)
194 226
195 # Tell the system to open the app. 227 # Tell the system to open the app.
196 args = ['--user-data-dir=%s' % profile] + args 228 args = ['--user-data-dir=%s' % profile] + args
197 flags = ' '.join(map(pipes.quote, args)) 229 flags = ' '.join(map(pipes.quote, args))
198 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME) 230 cmd = '%s %s' % (context.GetLaunchPath(), flags)
199 cmd = '%s %s' % (exe, flags)
200 print 'Running %s' % cmd 231 print 'Running %s' % cmd
201 os.system(cmd) 232 os.system(cmd)
202 233
203 os.chdir(cwd) 234 os.chdir(cwd)
204 print 'Cleaning temp dir ...' 235 print 'Cleaning temp dir ...'
205 try: 236 try:
206 shutil.rmtree(tempdir, True) 237 shutil.rmtree(tempdir, True)
207 except Exception, e: 238 except Exception, e:
208 pass 239 pass
209 240
210 241
211 def AskIsGoodBuild(iline): 242 def AskIsGoodBuild(rev):
212 """Ask the user whether build from index line |iline| is good or bad.""" 243 """Ask the user whether build |rev| is good or bad."""
213 # Loop until we get a response that we can parse. 244 # Loop until we get a response that we can parse.
214 while True: 245 while True:
215 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % GetRevision(iline)) 246 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev))
216 if response and response in ('g', 'b'): 247 if response and response in ('g', 'b'):
217 return response == 'g' 248 return response == 'g'
218 249
250
219 def main(): 251 def main():
220 usage = ('%prog [options] [-- chromium-options]\n' 252 usage = ('%prog [options] [-- chromium-options]\n'
221 'Perform binary search on the snapshot builds.\n' 253 'Perform binary search on the snapshot builds.\n'
222 '\n' 254 '\n'
223 'Tip: add "-- --no-first-run" to bypass the first run prompts.') 255 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
224 parser = optparse.OptionParser(usage=usage) 256 parser = optparse.OptionParser(usage=usage)
225 # Strangely, the default help output doesn't include the choice list. 257 # Strangely, the default help output doesn't include the choice list.
226 choices = ['mac', 'win', 'linux', 'linux64'] 258 choices = ['mac', 'win', 'linux', 'linux64']
227 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 259 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
228 parser.add_option('-a', '--archive', 260 parser.add_option('-a', '--archive',
(...skipping 14 matching lines...) Expand all
243 print 275 print
244 parser.print_help() 276 parser.print_help()
245 return 1 277 return 1
246 278
247 if opts.bad and opts.good and (opts.good > opts.bad): 279 if opts.bad and opts.good and (opts.good > opts.bad):
248 print ('The good revision (%d) must precede the bad revision (%d).\n' % 280 print ('The good revision (%d) must precede the bad revision (%d).\n' %
249 (opts.good, opts.bad)) 281 (opts.good, opts.bad))
250 parser.print_help() 282 parser.print_help()
251 return 1 283 return 1
252 284
253 SetArchiveVars(opts.archive) 285 # Create the context. Initialize 0 for the revisions as they are set below.
286 context = PathContext(opts.archive, 0, 0)
254 287
255 # Pick a starting point, try to get HEAD for this. 288 # Pick a starting point, try to get HEAD for this.
256 if opts.bad: 289 if opts.bad:
257 bad_rev = opts.bad 290 bad_rev = opts.bad
258 else: 291 else:
259 bad_rev = 0 292 bad_rev = 0
260 try: 293 try:
261 # Location of the latest build revision number 294 # Location of the latest build revision number
262 BUILD_LATEST_URL = '%s/LATEST/REVISION' % (BUILD_BASE_URL) 295 nh = urllib.urlopen(context.GetLastChangeURL())
263 nh = urllib.urlopen(BUILD_LATEST_URL)
264 latest = int(nh.read()) 296 latest = int(nh.read())
265 nh.close() 297 nh.close()
266 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) 298 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest)
267 if (bad_rev == ''): 299 if (bad_rev == ''):
268 bad_rev = latest 300 bad_rev = latest
269 bad_rev = int(bad_rev) 301 bad_rev = int(bad_rev)
270 except Exception, e: 302 except Exception, e:
271 print('Could not determine latest revision. This could be bad...') 303 print('Could not determine latest revision. This could be bad...')
272 bad_rev = int(raw_input('Bad revision: ')) 304 bad_rev = int(raw_input('Bad revision: '))
273 305
274 # Find out when we were good. 306 # Find out when we were good.
275 if opts.good: 307 if opts.good:
276 good_rev = opts.good 308 good_rev = opts.good
277 else: 309 else:
278 good_rev = 0 310 good_rev = 0
279 try: 311 try:
280 good_rev = int(raw_input('Last known good [0]: ')) 312 good_rev = int(raw_input('Last known good [0]: '))
281 except Exception, e: 313 except Exception, e:
282 pass 314 pass
283 315
316 # Set the input parameters now that they've been validated.
317 context.good_revision = good_rev
318 context.bad_revision = bad_rev
319
284 # Get a list of revisions to bisect across. 320 # Get a list of revisions to bisect across.
285 revlist = GetRevList(good_rev, bad_rev) 321 revlist = GetRevList(context)
286 if len(revlist) < 2: # Don't have enough builds to bisect 322 if len(revlist) < 2: # Don't have enough builds to bisect
287 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist 323 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist
288 sys.exit(1) 324 sys.exit(1)
289 325
290 # If we don't have a |good_rev|, set it to be the first revision possible. 326 # If we don't have a |good_rev|, set it to be the first revision possible.
291 if good_rev == 0: 327 if good_rev == 0:
292 good_rev = revlist[0] 328 good_rev = revlist[0]
293 329
294 # These are indexes of |revlist|. 330 # These are indexes of |revlist|.
295 good = 0 331 good = 0
296 bad = len(revlist) - 1 332 bad = len(revlist) - 1
297 last_known_good_rev = revlist[good] 333 last_known_good_rev = revlist[good]
298 334
299 # Binary search time! 335 # Binary search time!
300 while good < bad: 336 while good < bad:
301 candidates = revlist[good:bad] 337 candidates = revlist[good:bad]
302 num_poss = len(candidates) 338 num_poss = len(candidates)
303 if num_poss > 10: 339 if num_poss > 10:
304 print('%d candidates. %d tries left.' % 340 print('%d candidates. %d tries left.' %
305 (num_poss, round(math.log(num_poss, 2)))) 341 (num_poss, round(math.log(num_poss, 2))))
306 else: 342 else:
307 print('Candidates: %s' % map(GetRevision, revlist[good:bad])) 343 print('Candidates: %s' % revlist[good:bad])
308 344
309 # Cut the problem in half... 345 # Cut the problem in half...
310 test = int((bad - good) / 2) + good 346 test = int((bad - good) / 2) + good
311 test_rev = revlist[test] 347 test_rev = revlist[test]
312 348
313 # Let the user give this rev a spin (in her own profile, if she wants). 349 # Let the user give this rev a spin (in her own profile, if she wants).
314 profile = opts.profile 350 profile = opts.profile
315 if not profile: 351 if not profile:
316 profile = 'profile' # In a temp dir. 352 profile = 'profile' # In a temp dir.
317 TryRevision(test_rev, profile, args) 353 TryRevision(context, test_rev, profile, args)
318 if AskIsGoodBuild(test_rev): 354 if AskIsGoodBuild(test_rev):
319 last_known_good_rev = revlist[good] 355 last_known_good_rev = revlist[good]
320 good = test + 1 356 good = test + 1
321 else: 357 else:
322 bad = test 358 bad = test
323 359
324 # We're done. Let the user know the results in an official manner. 360 # We're done. Let the user know the results in an official manner.
325 bad_revision = GetRevision(revlist[bad]) 361 print('You are probably looking for build %d.' % revlist[bad])
326 print('You are probably looking for build %d.' % bad_revision)
327 print('CHANGELOG URL:') 362 print('CHANGELOG URL:')
328 print(CHANGELOG_URL % (GetRevision(last_known_good_rev), bad_revision)) 363 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad]))
329 print('Built at revision:') 364 print('Built at revision:')
330 print(BUILD_VIEWVC_URL % bad_revision) 365 print(BUILD_VIEWVC_URL % revlist[bad])
331 366
332 if __name__ == '__main__': 367 if __name__ == '__main__':
333 sys.exit(main()) 368 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698