blob: 9cf2f44793d54a6b3bbb1b913f679760090dff49 [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
13import errno
14import json
15import os
16
17from assert_not_in_chroot import VerifyOutsideChroot
18from get_llvm_hash import LLVMHash
19from modify_a_tryjob import AddTryjob
20from patch_manager import _ConvertToASCII
21from update_tryjob_status import FindTryjobIndex
22from update_tryjob_status import TryjobStatus
23
24
25def is_file_and_json(json_file):
26 """Validates that the file exists and is a JSON file."""
27 return os.path.isfile(json_file) and json_file.endswith('.json')
28
29
30def GetCommandLineArgs():
31 """Parses the command line for the command line arguments."""
32
33 # Default path to the chroot if a path is not specified.
34 cros_root = os.path.expanduser('~')
35 cros_root = os.path.join(cros_root, 'chromiumos')
36
37 # Create parser and add optional command-line arguments.
38 parser = argparse.ArgumentParser(
39 description='Bisects LLVM via tracking a JSON file.')
40
41 # Add argument for other change lists that want to run alongside the tryjob
42 # which has a change list of updating a package's git hash.
43 parser.add_argument(
44 '--parallel',
45 type=int,
46 default=3,
47 help='How many tryjobs to create between the last good version and '
48 'the first bad version (default: %(default)s)')
49
50 # Add argument for the good LLVM revision for bisection.
51 parser.add_argument(
52 '--start_rev',
53 required=True,
54 type=int,
55 help='The good revision for the bisection.')
56
57 # Add argument for the bad LLVM revision for bisection.
58 parser.add_argument(
59 '--end_rev',
60 required=True,
61 type=int,
62 help='The bad revision for the bisection.')
63
64 # Add argument for the absolute path to the file that contains information on
65 # the previous tested svn version.
66 parser.add_argument(
67 '--last_tested',
68 required=True,
69 help='the absolute path to the file that contains the tryjobs')
70
71 # Add argument for the absolute path to the LLVM source tree.
72 parser.add_argument(
73 '--src_path',
74 help='the path to the LLVM source tree to use (used for retrieving the '
75 'git hash of each version between the last good version and first bad '
76 'version)')
77
78 # Add argument for other change lists that want to run alongside the tryjob
79 # which has a change list of updating a package's git hash.
80 parser.add_argument(
81 '--extra_change_lists',
82 type=int,
83 nargs='+',
84 help='change lists that would like to be run alongside the change list '
85 'of updating the packages')
86
87 # Add argument for custom options for the tryjob.
88 parser.add_argument(
89 '--options',
90 required=False,
91 nargs='+',
92 help='options to use for the tryjob testing')
93
94 # Add argument for the builder to use for the tryjob.
95 parser.add_argument(
96 '--builder', required=True, help='builder to use for the tryjob testing')
97
98 # Add argument for the description of the tryjob.
99 parser.add_argument(
100 '--description',
101 required=False,
102 nargs='+',
103 help='the description of the tryjob')
104
105 # Add argument for a specific chroot path.
106 parser.add_argument(
107 '--chroot_path',
108 default=cros_root,
109 help='the path to the chroot (default: %(default)s)')
110
111 # Add argument for the log level.
112 parser.add_argument(
113 '--log_level',
114 default='none',
115 choices=['none', 'quiet', 'average', 'verbose'],
116 help='the level for the logs (default: %(default)s)')
117
118 args_output = parser.parse_args()
119
120 assert args_output.start_rev < args_output.end_rev, (
121 'Start revision %d is >= end revision %d' % (args_output.start_rev,
122 args_output.end_rev))
123
124 if args_output.last_tested and not args_output.last_tested.endswith('.json'):
125 raise ValueError(
126 'Filed provided %s does not end in \'.json\'' % args_output.last_tested)
127
128 return args_output
129
130
131def _ValidateStartAndEndAgainstJSONStartAndEnd(start, end, json_start,
132 json_end):
133 """Valides that the command line arguments are the same as the JSON."""
134
135 if start != json_start or end != json_end:
136 raise ValueError('The start %d or the end %d version provided is '
137 'different than \'start\' %d or \'end\' %d in the .JSON '
138 'file' % (start, end, json_start, json_end))
139
140
141def GetStartAndEndRevision(start, end, tryjobs):
142 """Gets the start and end intervals in 'json_file'.
143
144 Args:
145 start: The start version of the bisection provided via the command line.
146 end: The end version of the bisection provided via the command line.
147 tryjobs: A list of tryjobs where each element is in the following format:
148 [
149 {[TRYJOB_INFORMATION]},
150 {[TRYJOB_INFORMATION]},
151 ...,
152 {[TRYJOB_INFORMATION]}
153 ]
154
155 Returns:
156 The new start version and end version for bisection.
157
158 Raises:
159 ValueError: The value for 'status' is missing or there is a mismatch
160 between 'start' and 'end' compared to the 'start' and 'end' in the JSON
161 file.
162 AssertionError: The new start version is >= than the new end version.
163 """
164
165 if not tryjobs:
166 return start, end, []
167
168 # Verify that each tryjob has a value for the 'status' key.
169 for cur_tryjob_dict in tryjobs:
170 if not cur_tryjob_dict.get('status', None):
171 raise ValueError('\'status\' is missing or has no value, please '
172 'go to %s and update it' % cur_tryjob_dict['link'])
173
174 all_bad_revisions = [end]
175 all_bad_revisions.extend(cur_tryjob['rev']
176 for cur_tryjob in tryjobs
177 if cur_tryjob['status'] == TryjobStatus.BAD.value)
178
179 # The minimum value for the 'bad' field in the tryjobs is the new end
180 # version.
181 bad_rev = min(all_bad_revisions)
182
183 all_good_revisions = [start]
184 all_good_revisions.extend(cur_tryjob['rev']
185 for cur_tryjob in tryjobs
186 if cur_tryjob['status'] == TryjobStatus.GOOD.value)
187
188 # The maximum value for the 'good' field in the tryjobs is the new start
189 # version.
190 good_rev = max(all_good_revisions)
191
192 # The good version should always be strictly less than the bad version;
193 # otherwise, bisection is broken.
194 assert good_rev < bad_rev, ('Bisection is broken because %d (good) is >= '
195 '%d (bad)' % (good_rev, bad_rev))
196
197 # Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'.
198 #
199 # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
200 # that have already been launched (this list is used when constructing the
201 # list of revisions to launch tryjobs for).
202 pending_revisions = [
203 tryjob['rev']
204 for tryjob in tryjobs
205 if tryjob['status'] == TryjobStatus.PENDING.value and
206 good_rev < tryjob['rev'] < bad_rev
207 ]
208
209 return good_rev, bad_rev, pending_revisions
210
211
212def GetRevisionsBetweenBisection(start, end, parallel, src_path,
213 pending_revisions):
214 """Gets the revisions between 'start' and 'end'.
215
216 Sometimes, the LLVM source tree's revisions do not increment by 1 (there is
217 a jump), so need to construct a list of all revisions that are NOT missing
218 between 'start' and 'end'. Then, the step amount (i.e. length of the list
219 divided by ('parallel' + 1)) will be used for indexing into the list.
220
221 Args:
222 start: The start revision.
223 end: The end revision.
224 parallel: The number of tryjobs to create between 'start' and 'end'.
225 src_path: The absolute path to the LLVM source tree to use.
226 pending_revisions: A list of 'pending' revisions that are between 'start'
227 and 'end'.
228
229 Returns:
230 A list of revisions between 'start' and 'end'.
231 """
232
233 new_llvm = LLVMHash()
234
235 valid_revisions = []
236
237 # Start at ('start' + 1) because 'start' is the good revision.
238 #
239 # FIXME: Searching for each revision from ('start' + 1) up to 'end' in the
240 # LLVM source tree is a quadratic algorithm. It's a good idea to optimize
241 # this.
242 for cur_revision in range(start + 1, end):
243 try:
244 if cur_revision not in pending_revisions:
245 # Verify that the current revision exists by finding its corresponding
246 # git hash in the LLVM source tree.
247 new_llvm.GetGitHashForVersion(src_path, cur_revision)
248 valid_revisions.append(cur_revision)
249 except ValueError:
250 # Could not find the git hash for the current revision.
251 continue
252
253 # ('parallel' + 1) so that the last revision in the list is not close to
254 # 'end' (have a bit more coverage).
255 index_step = len(valid_revisions) // (parallel + 1)
256
257 if not index_step:
258 index_step = 1
259
260 # Starting at 'index_step' because the first element would be close to
261 # 'start' (similar to ('parallel' + 1) for the last element).
262 result = [valid_revisions[index] \
263 for index in range(index_step, len(valid_revisions), index_step)]
264
265 return result
266
267
268def GetRevisionsListAndHashList(start, end, parallel, src_path,
269 pending_revisions):
270 """Determines the revisions between start and end."""
271
272 new_llvm = LLVMHash()
273
274 with new_llvm.CreateTempDirectory() as temp_dir:
275 if not src_path:
276 new_llvm.CloneLLVMRepo(temp_dir)
277 src_path = temp_dir
278
279 # Get a list of revisions between start and end.
280 revisions = GetRevisionsBetweenBisection(start, end, parallel, src_path,
281 pending_revisions)
282
283 git_hashes = [
284 new_llvm.GetGitHashForVersion(src_path, rev) for rev in revisions
285 ]
286
287 assert revisions, ('No revisions between start %d and end %d to create '
288 'tryjobs.' % (start, end))
289
290 return revisions, git_hashes
291
292
293def main():
294 """Bisects LLVM based off of a .JSON file.
295
296 Raises:
297 AssertionError: The script was run inside the chroot.
298 """
299
300 VerifyOutsideChroot()
301
302 args_output = GetCommandLineArgs()
303
304 update_packages = [
305 'sys-devel/llvm', 'sys-libs/compiler-rt', 'sys-libs/libcxx',
306 'sys-libs/libcxxabi', 'sys-libs/llvm-libunwind'
307 ]
308
309 patch_metadata_file = 'PATCHES.json'
310
311 start = args_output.start_rev
312 end = args_output.end_rev
313
314 try:
315 with open(args_output.last_tested) as f:
316 bisect_contents = _ConvertToASCII(json.load(f))
317 except IOError as err:
318 if err.errno != errno.ENOENT:
319 raise
320 bisect_contents = {'start': start, 'end': end, 'jobs': []}
321
322 _ValidateStartAndEndAgainstJSONStartAndEnd(
323 start, end, bisect_contents['start'], bisect_contents['end'])
324
325 # Pending revisions are between 'start_revision' and 'end_revision'.
326 start_revision, end_revision, pending_revisions = GetStartAndEndRevision(
327 start, end, bisect_contents['jobs'])
328
329 revisions, git_hashes = GetRevisionsListAndHashList(
330 start_revision, end_revision, args_output.parallel, args_output.src_path,
331 pending_revisions)
332
333 # Check if any revisions that are going to be added as a tryjob exist already
334 # in the 'jobs' list.
335 for rev in revisions:
336 if FindTryjobIndex(rev, bisect_contents['jobs']) is not None:
337 raise ValueError('Revision %d exists already in \'jobs\'' % rev)
338
339 try:
340 for svn_revision, git_hash in zip(revisions, git_hashes):
341 tryjob_dict = AddTryjob(update_packages, git_hash, svn_revision,
342 args_output.chroot_path, patch_metadata_file,
343 args_output.extra_change_lists,
344 args_output.options, args_output.builder,
345 args_output.log_level, svn_revision)
346
347 bisect_contents['jobs'].append(tryjob_dict)
348 finally:
349 # Do not want to lose progress if there is an exception.
350 if args_output.last_tested:
351 new_file = '%s.new' % args_output.last_tested
352 with open(new_file, 'w') as json_file:
353 json.dump(bisect_contents, json_file, indent=4, separators=(',', ': '))
354
355 os.rename(new_file, args_output.last_tested)
356
357
358if __name__ == '__main__':
359 main()