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