blob: d9eecce63992f7483dd0ead25a7cc30e6b369678 [file] [log] [blame]
Salud Lemus055838a2019-08-19 13:37:28 -07001#!/usr/bin/env python2
2# -*- coding: utf-8 -*-
3# Copyright 2019 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Performs bisection on LLVM based off a .JSON file."""
8
9from __future__ import division
10from __future__ import print_function
11
12import argparse
Salud Lemusc856b682019-08-29 11:46:49 -070013import enum
Salud Lemus055838a2019-08-19 13:37:28 -070014import errno
15import json
16import os
Salud Lemusc856b682019-08-29 11:46:49 -070017import sys
Salud Lemus055838a2019-08-19 13:37:28 -070018
19from assert_not_in_chroot import VerifyOutsideChroot
Salud Lemus42235702019-08-23 16:49:28 -070020from get_llvm_hash import CreateTempLLVMRepo
Salud Lemus055838a2019-08-19 13:37:28 -070021from get_llvm_hash import LLVMHash
22from modify_a_tryjob import AddTryjob
23from patch_manager import _ConvertToASCII
24from update_tryjob_status import FindTryjobIndex
25from update_tryjob_status import TryjobStatus
26
27
Salud Lemusc856b682019-08-29 11:46:49 -070028class BisectionExitStatus(enum.Enum):
29 """Exit code when performing bisection."""
30
31 # Means that there are no more revisions available to bisect.
32 BISECTION_COMPLETE = 126
33
34
Salud Lemus055838a2019-08-19 13:37:28 -070035def is_file_and_json(json_file):
36 """Validates that the file exists and is a JSON file."""
37 return os.path.isfile(json_file) and json_file.endswith('.json')
38
39
40def GetCommandLineArgs():
41 """Parses the command line for the command line arguments."""
42
43 # Default path to the chroot if a path is not specified.
44 cros_root = os.path.expanduser('~')
45 cros_root = os.path.join(cros_root, 'chromiumos')
46
47 # Create parser and add optional command-line arguments.
48 parser = argparse.ArgumentParser(
49 description='Bisects LLVM via tracking a JSON file.')
50
51 # Add argument for other change lists that want to run alongside the tryjob
52 # which has a change list of updating a package's git hash.
53 parser.add_argument(
54 '--parallel',
55 type=int,
56 default=3,
57 help='How many tryjobs to create between the last good version and '
58 'the first bad version (default: %(default)s)')
59
60 # Add argument for the good LLVM revision for bisection.
61 parser.add_argument(
62 '--start_rev',
63 required=True,
64 type=int,
65 help='The good revision for the bisection.')
66
67 # Add argument for the bad LLVM revision for bisection.
68 parser.add_argument(
69 '--end_rev',
70 required=True,
71 type=int,
72 help='The bad revision for the bisection.')
73
74 # Add argument for the absolute path to the file that contains information on
75 # the previous tested svn version.
76 parser.add_argument(
77 '--last_tested',
78 required=True,
79 help='the absolute path to the file that contains the tryjobs')
80
81 # Add argument for the absolute path to the LLVM source tree.
82 parser.add_argument(
83 '--src_path',
84 help='the path to the LLVM source tree to use (used for retrieving the '
85 'git hash of each version between the last good version and first bad '
86 'version)')
87
88 # Add argument for other change lists that want to run alongside the tryjob
89 # which has a change list of updating a package's git hash.
90 parser.add_argument(
91 '--extra_change_lists',
92 type=int,
93 nargs='+',
94 help='change lists that would like to be run alongside the change list '
95 'of updating the packages')
96
97 # Add argument for custom options for the tryjob.
98 parser.add_argument(
99 '--options',
100 required=False,
101 nargs='+',
102 help='options to use for the tryjob testing')
103
104 # Add argument for the builder to use for the tryjob.
105 parser.add_argument(
106 '--builder', required=True, help='builder to use for the tryjob testing')
107
108 # Add argument for the description of the tryjob.
109 parser.add_argument(
110 '--description',
111 required=False,
112 nargs='+',
113 help='the description of the tryjob')
114
115 # Add argument for a specific chroot path.
116 parser.add_argument(
117 '--chroot_path',
118 default=cros_root,
119 help='the path to the chroot (default: %(default)s)')
120
121 # Add argument for the log level.
122 parser.add_argument(
123 '--log_level',
124 default='none',
125 choices=['none', 'quiet', 'average', 'verbose'],
126 help='the level for the logs (default: %(default)s)')
127
128 args_output = parser.parse_args()
129
130 assert args_output.start_rev < args_output.end_rev, (
131 'Start revision %d is >= end revision %d' % (args_output.start_rev,
132 args_output.end_rev))
133
134 if args_output.last_tested and not args_output.last_tested.endswith('.json'):
135 raise ValueError(
136 'Filed provided %s does not end in \'.json\'' % args_output.last_tested)
137
138 return args_output
139
140
141def _ValidateStartAndEndAgainstJSONStartAndEnd(start, end, json_start,
142 json_end):
143 """Valides that the command line arguments are the same as the JSON."""
144
145 if start != json_start or end != json_end:
146 raise ValueError('The start %d or the end %d version provided is '
147 'different than \'start\' %d or \'end\' %d in the .JSON '
148 'file' % (start, end, json_start, json_end))
149
150
151def GetStartAndEndRevision(start, end, tryjobs):
152 """Gets the start and end intervals in 'json_file'.
153
154 Args:
155 start: The start version of the bisection provided via the command line.
156 end: The end version of the bisection provided via the command line.
157 tryjobs: A list of tryjobs where each element is in the following format:
158 [
159 {[TRYJOB_INFORMATION]},
160 {[TRYJOB_INFORMATION]},
161 ...,
162 {[TRYJOB_INFORMATION]}
163 ]
164
165 Returns:
Salud Lemusffed65d2019-08-26 14:52:02 -0700166 The new start version and end version for bisection, a set of revisions
167 that are 'pending' and a set of revisions that are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700168
169 Raises:
170 ValueError: The value for 'status' is missing or there is a mismatch
171 between 'start' and 'end' compared to the 'start' and 'end' in the JSON
172 file.
173 AssertionError: The new start version is >= than the new end version.
174 """
175
176 if not tryjobs:
Salud Lemus02630632019-08-28 15:27:59 -0700177 return start, end, {}, {}
Salud Lemus055838a2019-08-19 13:37:28 -0700178
179 # Verify that each tryjob has a value for the 'status' key.
180 for cur_tryjob_dict in tryjobs:
181 if not cur_tryjob_dict.get('status', None):
182 raise ValueError('\'status\' is missing or has no value, please '
183 'go to %s and update it' % cur_tryjob_dict['link'])
184
185 all_bad_revisions = [end]
186 all_bad_revisions.extend(cur_tryjob['rev']
187 for cur_tryjob in tryjobs
188 if cur_tryjob['status'] == TryjobStatus.BAD.value)
189
190 # The minimum value for the 'bad' field in the tryjobs is the new end
191 # version.
192 bad_rev = min(all_bad_revisions)
193
194 all_good_revisions = [start]
195 all_good_revisions.extend(cur_tryjob['rev']
196 for cur_tryjob in tryjobs
197 if cur_tryjob['status'] == TryjobStatus.GOOD.value)
198
199 # The maximum value for the 'good' field in the tryjobs is the new start
200 # version.
201 good_rev = max(all_good_revisions)
202
203 # The good version should always be strictly less than the bad version;
204 # otherwise, bisection is broken.
205 assert good_rev < bad_rev, ('Bisection is broken because %d (good) is >= '
206 '%d (bad)' % (good_rev, bad_rev))
207
208 # Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'.
209 #
210 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
Salud Lemusffed65d2019-08-26 14:52:02 -0700211 # that have already been launched (this set is used when constructing the
Salud Lemus055838a2019-08-19 13:37:28 -0700212 # list of revisions to launch tryjobs for).
Salud Lemusffed65d2019-08-26 14:52:02 -0700213 pending_revisions = {
Salud Lemus055838a2019-08-19 13:37:28 -0700214 tryjob['rev']
215 for tryjob in tryjobs
216 if tryjob['status'] == TryjobStatus.PENDING.value and
217 good_rev < tryjob['rev'] < bad_rev
Salud Lemusffed65d2019-08-26 14:52:02 -0700218 }
Salud Lemus055838a2019-08-19 13:37:28 -0700219
Salud Lemusffed65d2019-08-26 14:52:02 -0700220 # Find all revisions that are to be skipped within 'good_rev' and 'bad_rev'.
221 #
222 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
223 # that have already been marked as 'skip' (this set is used when constructing
224 # the list of revisions to launch tryjobs for).
225 skip_revisions = {
226 tryjob['rev']
227 for tryjob in tryjobs
228 if tryjob['status'] == TryjobStatus.SKIP.value and
229 good_rev < tryjob['rev'] < bad_rev
230 }
231
232 return good_rev, bad_rev, pending_revisions, skip_revisions
Salud Lemus055838a2019-08-19 13:37:28 -0700233
234
235def GetRevisionsBetweenBisection(start, end, parallel, src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700236 pending_revisions, skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700237 """Gets the revisions between 'start' and 'end'.
238
239 Sometimes, the LLVM source tree's revisions do not increment by 1 (there is
240 a jump), so need to construct a list of all revisions that are NOT missing
241 between 'start' and 'end'. Then, the step amount (i.e. length of the list
242 divided by ('parallel' + 1)) will be used for indexing into the list.
243
244 Args:
245 start: The start revision.
246 end: The end revision.
247 parallel: The number of tryjobs to create between 'start' and 'end'.
248 src_path: The absolute path to the LLVM source tree to use.
Salud Lemusffed65d2019-08-26 14:52:02 -0700249 pending_revisions: A set containing 'pending' revisions that are between
250 'start' and 'end'.
251 skip_revisions: A set containing revisions between 'start' and 'end' that
252 are to be skipped.
Salud Lemus055838a2019-08-19 13:37:28 -0700253
254 Returns:
255 A list of revisions between 'start' and 'end'.
256 """
257
258 new_llvm = LLVMHash()
259
260 valid_revisions = []
261
262 # Start at ('start' + 1) because 'start' is the good revision.
263 #
264 # FIXME: Searching for each revision from ('start' + 1) up to 'end' in the
265 # LLVM source tree is a quadratic algorithm. It's a good idea to optimize
266 # this.
267 for cur_revision in range(start + 1, end):
268 try:
Salud Lemusffed65d2019-08-26 14:52:02 -0700269 if cur_revision not in pending_revisions and \
270 cur_revision not in skip_revisions:
Salud Lemus055838a2019-08-19 13:37:28 -0700271 # Verify that the current revision exists by finding its corresponding
272 # git hash in the LLVM source tree.
273 new_llvm.GetGitHashForVersion(src_path, cur_revision)
274 valid_revisions.append(cur_revision)
275 except ValueError:
276 # Could not find the git hash for the current revision.
277 continue
278
279 # ('parallel' + 1) so that the last revision in the list is not close to
280 # 'end' (have a bit more coverage).
281 index_step = len(valid_revisions) // (parallel + 1)
282
283 if not index_step:
284 index_step = 1
285
Salud Lemus055838a2019-08-19 13:37:28 -0700286 result = [valid_revisions[index] \
Salud Lemusffed65d2019-08-26 14:52:02 -0700287 for index in range(0, len(valid_revisions), index_step)]
Salud Lemus055838a2019-08-19 13:37:28 -0700288
289 return result
290
291
292def GetRevisionsListAndHashList(start, end, parallel, src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700293 pending_revisions, skip_revisions):
Salud Lemus055838a2019-08-19 13:37:28 -0700294 """Determines the revisions between start and end."""
295
296 new_llvm = LLVMHash()
297
298 with new_llvm.CreateTempDirectory() as temp_dir:
Salud Lemus42235702019-08-23 16:49:28 -0700299 with CreateTempLLVMRepo(temp_dir) as new_repo:
300 if not src_path:
301 src_path = new_repo
Salud Lemus055838a2019-08-19 13:37:28 -0700302
Salud Lemus42235702019-08-23 16:49:28 -0700303 # Get a list of revisions between start and end.
Salud Lemusffed65d2019-08-26 14:52:02 -0700304 revisions = GetRevisionsBetweenBisection(
305 start, end, parallel, src_path, pending_revisions, skip_revisions)
Salud Lemus055838a2019-08-19 13:37:28 -0700306
Salud Lemus42235702019-08-23 16:49:28 -0700307 git_hashes = [
308 new_llvm.GetGitHashForVersion(src_path, rev) for rev in revisions
309 ]
Salud Lemus055838a2019-08-19 13:37:28 -0700310
Salud Lemus055838a2019-08-19 13:37:28 -0700311 return revisions, git_hashes
312
313
Salud Lemusc856b682019-08-29 11:46:49 -0700314def DieWithNoRevisionsError(start, end, skip_revisions, pending_revisions):
315 """Raises a ValueError exception with useful information."""
316
317 no_revisions_message = ('No revisions between start %d and end '
318 '%d to create tryjobs' % (start, end))
319
320 if pending_revisions:
321 no_revisions_message += '\nThe following tryjobs are pending:\n' \
322 + '\n'.join(str(rev) for rev in pending_revisions)
323
324 if skip_revisions:
325 no_revisions_message += '\nThe following tryjobs were skipped:\n' \
326 + '\n'.join(str(rev) for rev in skip_revisions)
327
328 raise ValueError(no_revisions_message)
329
330
331def CheckForExistingTryjobsInRevisionsToLaunch(revisions, jobs):
332 """Checks if a revision in 'revisions' exists in 'jobs' list."""
333
334 for rev in revisions:
335 if FindTryjobIndex(rev, jobs) is not None:
336 raise ValueError('Revision %d exists already in \'jobs\'' % rev)
337
338
339def UpdateBisection(revisions, git_hashes, bisect_contents, last_tested,
340 update_packages, chroot_path, patch_metadata_file,
341 extra_change_lists, options, builder, log_level):
342 """Adds tryjobs and updates the status file with the new tryjobs."""
343
344 try:
345 for svn_revision, git_hash in zip(revisions, git_hashes):
346 tryjob_dict = AddTryjob(update_packages, git_hash, svn_revision,
347 chroot_path, patch_metadata_file,
348 extra_change_lists, options, builder, log_level,
349 svn_revision)
350
351 bisect_contents['jobs'].append(tryjob_dict)
352 finally:
353 # Do not want to lose progress if there is an exception.
354 if last_tested:
355 new_file = '%s.new' % last_tested
356 with open(new_file, 'w') as json_file:
357 json.dump(bisect_contents, json_file, indent=4, separators=(',', ': '))
358
359 os.rename(new_file, last_tested)
360
361
362def _NoteCompletedBisection(last_tested, src_path, end):
363 """Prints that bisection is complete."""
364
365 print('Finished bisecting for %s' % last_tested)
366
367 if src_path:
368 bad_llvm_hash = LLVMHash().GetGitHashForVersion(src_path, end)
369 else:
370 bad_llvm_hash = LLVMHash().GetLLVMHash(end)
371
372 print(
373 'The bad revision is %d and its commit hash is %s' % (end, bad_llvm_hash))
374
375
376def main(args_output):
Salud Lemus055838a2019-08-19 13:37:28 -0700377 """Bisects LLVM based off of a .JSON file.
378
379 Raises:
380 AssertionError: The script was run inside the chroot.
381 """
382
383 VerifyOutsideChroot()
384
Salud Lemus055838a2019-08-19 13:37:28 -0700385 update_packages = [
386 'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx',
387 'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind'
388 ]
389
390 patch_metadata_file = 'PATCHES.json'
391
392 start = args_output.start_rev
393 end = args_output.end_rev
394
395 try:
396 with open(args_output.last_tested) as f:
397 bisect_contents = _ConvertToASCII(json.load(f))
398 except IOError as err:
399 if err.errno != errno.ENOENT:
400 raise
401 bisect_contents = {'start': start, 'end': end, 'jobs': []}
402
403 _ValidateStartAndEndAgainstJSONStartAndEnd(
404 start, end, bisect_contents['start'], bisect_contents['end'])
Salud Lemusc856b682019-08-29 11:46:49 -0700405
Salud Lemusffed65d2019-08-26 14:52:02 -0700406 # Pending and skipped revisions are between 'start_revision' and
407 # 'end_revision'.
408 start_revision, end_revision, pending_revisions, skip_revisions = \
409 GetStartAndEndRevision(start, end, bisect_contents['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700410
411 revisions, git_hashes = GetRevisionsListAndHashList(
412 start_revision, end_revision, args_output.parallel, args_output.src_path,
Salud Lemusffed65d2019-08-26 14:52:02 -0700413 pending_revisions, skip_revisions)
414
Salud Lemusc856b682019-08-29 11:46:49 -0700415 # No more revisions between 'start_revision' and 'end_revision', so
416 # bisection is complete.
417 #
418 # This is determined by finding all valid revisions between 'start_revision'
419 # and 'end_revision' and that are NOT in the 'pending' and 'skipped' set.
Salud Lemusffed65d2019-08-26 14:52:02 -0700420 if not revisions:
Salud Lemusc856b682019-08-29 11:46:49 -0700421 # Successfully completed bisection where there are 2 cases:
422 # 1) 'start_revision' and 'end_revision' are back-to-back (example:
423 # 'start_revision' is 369410 and 'end_revision' is 369411).
424 #
425 # 2) 'start_revision' and 'end_revision' are NOT back-to-back, so there must
426 # be tryjobs in between which are labeled as 'skip' for their 'status'
427 # value.
428 #
429 # In either case, there are no 'pending' jobs.
430 if not pending_revisions:
431 _NoteCompletedBisection(args_output.last_tested, args_output.src_path,
432 end_revision)
Salud Lemusffed65d2019-08-26 14:52:02 -0700433
Salud Lemusc856b682019-08-29 11:46:49 -0700434 if skip_revisions:
435 skip_revisions_message = ('\nThe following revisions were skipped:\n' +
436 '\n'.join(str(rev) for rev in skip_revisions))
Salud Lemusffed65d2019-08-26 14:52:02 -0700437
Salud Lemusc856b682019-08-29 11:46:49 -0700438 print(skip_revisions_message)
Salud Lemus055838a2019-08-19 13:37:28 -0700439
Salud Lemusc856b682019-08-29 11:46:49 -0700440 return BisectionExitStatus.BISECTION_COMPLETE.value
Salud Lemus055838a2019-08-19 13:37:28 -0700441
Salud Lemusc856b682019-08-29 11:46:49 -0700442 # Some tryjobs are not finished which may change the actual bad
443 # commit/revision when those tryjobs are finished.
444 DieWithNoRevisionsError(start_revision, end_revision, skip_revisions,
445 pending_revisions)
Salud Lemus055838a2019-08-19 13:37:28 -0700446
Salud Lemusc856b682019-08-29 11:46:49 -0700447 CheckForExistingTryjobsInRevisionsToLaunch(revisions, bisect_contents['jobs'])
Salud Lemus055838a2019-08-19 13:37:28 -0700448
Salud Lemusc856b682019-08-29 11:46:49 -0700449 UpdateBisection(revisions, git_hashes, bisect_contents,
450 args_output.last_tested, update_packages,
451 args_output.chroot_path, patch_metadata_file,
452 args_output.extra_change_lists, args_output.options,
453 args_output.builder, args_output.log_level)
Salud Lemus055838a2019-08-19 13:37:28 -0700454
455
456if __name__ == '__main__':
Salud Lemusc856b682019-08-29 11:46:49 -0700457 args_output = GetCommandLineArgs()
458 sys.exit(main(args_output))