| OLD | NEW |
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import json | 5 import json |
| 6 | 6 |
| 7 from recipe_engine.config import List | 7 from recipe_engine.config import List |
| 8 from recipe_engine.config import Single | 8 from recipe_engine.config import Single |
| 9 from recipe_engine.recipe_api import Property | 9 from recipe_engine.recipe_api import Property |
| 10 | 10 |
| 11 | 11 |
| 12 DEPS = [ | 12 DEPS = [ |
| 13 'chromium', | 13 'chromium', |
| 14 'chromium_tests', | 14 'chromium_tests', |
| 15 'findit', | 15 'findit', |
| 16 'depot_tools/gclient', | 16 'depot_tools/gclient', |
| 17 'recipe_engine/json', | 17 'recipe_engine/json', |
| 18 'recipe_engine/path', | 18 'recipe_engine/path', |
| 19 'recipe_engine/platform', | 19 'recipe_engine/platform', |
| 20 'recipe_engine/properties', | 20 'recipe_engine/properties', |
| 21 'recipe_engine/python', | 21 'recipe_engine/python', |
| 22 'recipe_engine/raw_io', |
| 22 'recipe_engine/step', | 23 'recipe_engine/step', |
| 23 ] | 24 ] |
| 24 | 25 |
| 25 | 26 |
| 26 PROPERTIES = { | 27 PROPERTIES = { |
| 27 'target_mastername': Property( | 28 'target_mastername': Property( |
| 28 kind=str, help='The target master to match compile config to.'), | 29 kind=str, help='The target master to match compile config to.'), |
| 29 'target_buildername': Property( | 30 'target_buildername': Property( |
| 30 kind=str, help='The target builder to match compile config to.'), | 31 kind=str, help='The target builder to match compile config to.'), |
| 31 'good_revision': Property( | 32 'good_revision': Property( |
| 32 kind=str, help='The last known good chromium revision.'), | 33 kind=str, help='The last known good chromium revision.'), |
| 33 'bad_revision': Property( | 34 'bad_revision': Property( |
| 34 kind=str, help='The first known bad chromium revision.'), | 35 kind=str, help='The first known bad chromium revision.'), |
| 35 'compile_targets': Property( | 36 'compile_targets': Property( |
| 36 kind=List(basestring), default=None, | 37 kind=List(basestring), default=None, |
| 37 help='The failed compile targets, eg: browser_tests, ' | 38 help='The failed compile targets, eg: browser_tests, ' |
| 38 'obj/path/to/source.o, gen/path/to/generated.cc, etc.'), | 39 'obj/path/to/source.o, gen/path/to/generated.cc, etc.'), |
| 39 'use_analyze': Property( | 40 'use_analyze': Property( |
| 40 kind=Single(bool, empty_val=False, required=False), default=True, | 41 kind=Single(bool, empty_val=False, required=False), default=True, |
| 41 help='Use analyze to filter out affected targets.'), | 42 help='Use analyze to filter out affected targets.'), |
| 43 'suspected_revisions': Property( |
| 44 kind=List(basestring), default=[], |
| 45 help='A list of suspected revisions from heuristic analysis.'), |
| 42 } | 46 } |
| 43 | 47 |
| 44 | 48 |
| 45 class CompileResult(object): | 49 class CompileResult(object): |
| 46 SKIPPED = 'skipped' # No compile is needed. | 50 SKIPPED = 'skipped' # No compile is needed. |
| 47 PASSED = 'passed' # Compile passed. | 51 PASSED = 'passed' # Compile passed. |
| 48 FAILED = 'failed' # Compile failed. | 52 FAILED = 'failed' # Compile failed. |
| 53 INFRA_FAILED = 'infra_failed' # Infra failed. |
| 49 | 54 |
| 50 | 55 |
| 51 def _run_compile_at_revision(api, target_mastername, target_buildername, | 56 def _run_compile_at_revision(api, target_mastername, target_buildername, |
| 52 revision, compile_targets, use_analyze): | 57 revision, compile_targets, use_analyze): |
| 53 with api.step.nest('test %s' % str(revision)): | 58 with api.step.nest('test %s' % str(revision)): |
| 54 # Checkout code at the given revision to recompile. | 59 # Checkout code at the given revision to recompile. |
| 55 bot_config = api.chromium_tests.create_bot_config_object( | 60 bot_config = api.chromium_tests.create_bot_config_object( |
| 56 target_mastername, target_buildername) | 61 target_mastername, target_buildername) |
| 57 bot_update_step, bot_db = api.chromium_tests.prepare_checkout( | 62 bot_update_step, bot_db = api.chromium_tests.prepare_checkout( |
| 58 bot_config, root_solution_revision=revision) | 63 bot_config, root_solution_revision=revision) |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 100 override_bot_type='builder_tester') | 105 override_bot_type='builder_tester') |
| 101 return CompileResult.PASSED | 106 return CompileResult.PASSED |
| 102 except api.step.InfraFailure: | 107 except api.step.InfraFailure: |
| 103 raise | 108 raise |
| 104 except api.step.StepFailure: | 109 except api.step.StepFailure: |
| 105 return CompileResult.FAILED | 110 return CompileResult.FAILED |
| 106 | 111 |
| 107 | 112 |
| 108 def RunSteps(api, target_mastername, target_buildername, | 113 def RunSteps(api, target_mastername, target_buildername, |
| 109 good_revision, bad_revision, | 114 good_revision, bad_revision, |
| 110 compile_targets, use_analyze): | 115 compile_targets, use_analyze, suspected_revisions): |
| 111 bot_config = api.chromium_tests.create_bot_config_object( | 116 bot_config = api.chromium_tests.create_bot_config_object( |
| 112 target_mastername, target_buildername) | 117 target_mastername, target_buildername) |
| 113 api.chromium_tests.configure_build( | 118 api.chromium_tests.configure_build( |
| 114 bot_config, override_bot_type='builder_tester') | 119 bot_config, override_bot_type='builder_tester') |
| 115 | 120 |
| 116 # Sync to bad revision, and retrieve revisions in the regression range. | 121 # Sync to bad revision, and retrieve revisions in the regression range. |
| 117 api.chromium_tests.prepare_checkout( | 122 api.chromium_tests.prepare_checkout( |
| 118 bot_config, | 123 bot_config, |
| 119 root_solution_revision=bad_revision) | 124 root_solution_revision=bad_revision) |
| 120 revisions_to_check = api.findit.revisions_between(good_revision, bad_revision) | 125 |
| 126 # Retrieve revisions in the regression range. The returned revisions are in |
| 127 # order from oldest to newest. |
| 128 all_revisions = api.findit.revisions_between(good_revision, bad_revision) |
| 129 |
| 130 # If suspected revisions are provided, divide the entire regression range into |
| 131 # a list of smaller sub-ranges. Because only a failure immediately following a |
| 132 # pass could identify the culprit, we rerun compile at the revision right |
| 133 # before a suspected revision and then at the suspected revision itself. So a |
| 134 # sub-range starts at the revision right before a suspected revision. |
| 135 # |
| 136 # Normally, heuristic analysis provides only 1 suspected revision and there |
| 137 # will be 2 sub-ranges. Example (previous build cycle passed at r0): |
| 138 # Entire regression range: [r1, r2, r3, r4, ..., r10] |
| 139 # Suspected revisions: [r5] |
| 140 # Then the sub-ranges are: |
| 141 # sub-range1: r4 and [r5, r6, ..., r10] |
| 142 # sub-range2: None and [r1, r2, r3] |
| 143 # In this example, compile is run at r4 first, and there will be a few cases: |
| 144 # 1) if r4 passes, the culprit is in [r5, r6, ..., r10]. Compile should be |
| 145 # rerun in order from r5 to r10. |
| 146 # 1.1) if a failure occurs at rN (5<=N<=10), rN is the actual culprit |
| 147 # because it is the first failure after a series of pass. |
| 148 # 1.2) if no failure occurs, the compile failure is a flaky one. This |
| 149 # sometimes happens and the compile log shows no error while the |
| 150 # step ran into an exception. |
| 151 # 2) if r4 fails, the culprit is either r4 itself or one of [r1, r2, r3]. |
| 152 # Compile should be rerun in order from r1 to r3. No compile is run at |
| 153 # r0, because it is the last known good revision. |
| 154 # 2.1) if a failure occurs at rN (1<=N<=3), rN is the actual culprit. |
| 155 # 2.2) if no failure occurs, r4 is the actual culprit instead. |
| 156 # |
| 157 # Occasionally, heuristic analysis provides 2+ suspected revisions (e.g. there |
| 158 # are conflicting commits). In this case, there will be 3+ sub-ranges. |
| 159 # For the above example, if the suspected revisions are [r5, r8], there will |
| 160 # be three sub-ranges: |
| 161 # sub-range1: r7 and [r8, r9, r10] |
| 162 # sub-range2: r4 and [r5, r6] |
| 163 # sub-range3: None and [r1, r2, r3] |
| 164 # Sub-ranges with newer revisions are tested first (sub-range1 -> sub-range2 |
| 165 # -> sub-range3), because it is more likely that a newer revision is the |
| 166 # beginning of the compile breakage. |
| 167 suspected_revision_index = [ |
| 168 all_revisions.index(r) |
| 169 for r in set(suspected_revisions) if r in all_revisions] |
| 170 if suspected_revision_index: |
| 171 sub_ranges = [] |
| 172 remaining_revisions = all_revisions[:] |
| 173 for index in sorted(suspected_revision_index, reverse=True): |
| 174 if index > 0: |
| 175 sub_ranges.append(remaining_revisions[index - 1:]) |
| 176 remaining_revisions = remaining_revisions[:index - 1] |
| 177 # None is a placeholder for the last known good revision. |
| 178 sub_ranges.append([None] + remaining_revisions) |
| 179 else: |
| 180 # Treat the entire regression range as a single sub-range. |
| 181 sub_ranges = [[None] + all_revisions] |
| 121 | 182 |
| 122 compile_results = {} | 183 compile_results = {} |
| 123 try_job_metadata = { | 184 try_job_metadata = { |
| 124 'regression_range_size': len(revisions_to_check) | 185 'regression_range_size': len(all_revisions), |
| 186 'sub_ranges': sub_ranges[:], |
| 125 } | 187 } |
| 126 report = { | 188 report = { |
| 127 'result': compile_results, | 189 'result': compile_results, |
| 128 'metadata': try_job_metadata, | 190 'metadata': try_job_metadata, |
| 129 } | 191 } |
| 130 | 192 |
| 193 suspected_revision = None |
| 194 revision_being_checked = None |
| 195 found = False |
| 131 try: | 196 try: |
| 132 for current_revision in revisions_to_check: | 197 while not found and sub_ranges: |
| 133 last_revision = None | 198 # Sub-ranges with newer revisions are tested first. |
| 134 compile_result = _run_compile_at_revision( | 199 first_revision = sub_ranges[0][0] |
| 135 api, target_mastername, target_buildername, | 200 following_revisions = sub_ranges[0][1:] |
| 136 current_revision, compile_targets, use_analyze) | 201 sub_ranges.pop(0) |
| 137 | 202 |
| 138 compile_results[current_revision] = compile_result | 203 if first_revision is not None: # No compile for last known good revision. |
| 139 last_revision = current_revision | 204 revision_being_checked = first_revision |
| 140 if compile_result == CompileResult.FAILED: | 205 compile_result = _run_compile_at_revision( |
| 141 break # Found the culprit, no need to check later revisions. | 206 api, target_mastername, target_buildername, |
| 207 first_revision, compile_targets, use_analyze) |
| 208 compile_results[first_revision] = compile_result |
| 209 if compile_result == CompileResult.FAILED: |
| 210 # The first revision of this sub-range already failed, thus either it |
| 211 # is the culprit or the culprit is in a sub-range with older |
| 212 # revisions. |
| 213 suspected_revision = first_revision |
| 214 continue |
| 215 |
| 216 # If the first revision passed, the culprit is either in the current range |
| 217 # or is the first revision of previous range with newer revisions as |
| 218 # identified above. |
| 219 for revision in following_revisions: |
| 220 revision_being_checked = revision |
| 221 compile_result = _run_compile_at_revision( |
| 222 api, target_mastername, target_buildername, |
| 223 revision, compile_targets, use_analyze) |
| 224 compile_results[revision] = compile_result |
| 225 if compile_result == CompileResult.FAILED: |
| 226 # First failure after a series of pass. |
| 227 suspected_revision = revision |
| 228 found = True |
| 229 break |
| 230 |
| 231 if not found and suspected_revision is not None: |
| 232 # If all revisions in the current range passed, and the first revision |
| 233 # of previous range failed, the culprit is found too. |
| 234 found = True |
| 235 except api.step.InfraFailure: |
| 236 compile_results[revision_being_checked] = CompileResult.INFRA_FAILED |
| 237 raise |
| 142 finally: | 238 finally: |
| 143 # Report the result. | 239 # Report the result. |
| 144 step_result = api.python.succeeding_step( | 240 step_result = api.python.succeeding_step( |
| 145 'report', [json.dumps(report, indent=2)], as_log='report') | 241 'report', [json.dumps(report, indent=2)], as_log='report') |
| 146 | 242 |
| 147 if (last_revision and | 243 if found: |
| 148 compile_results.get(last_revision) == CompileResult.FAILED): | 244 step_result.presentation.step_text = ( |
| 149 step_result.presentation.step_text = '<br/>Culprit: %s' % last_revision | 245 '<br/>Culprit: <a href="https://crrev.com/%s">%s</a>' % ( |
| 246 suspected_revision, suspected_revision)) |
| 150 | 247 |
| 151 # Set the report as a build property too, so that it will be reported back | 248 # Set the report as a build property too, so that it will be reported back |
| 152 # to Buildbucket and Findit will pull from there instead of buildbot master. | 249 # to Buildbucket and Findit will pull from there instead of buildbot master. |
| 153 step_result.presentation.properties['report'] = report | 250 step_result.presentation.properties['report'] = report |
| 154 | 251 |
| 155 return report | 252 return report |
| 156 | 253 |
| 157 | 254 |
| 158 def GenTests(api): | 255 def GenTests(api): |
| 159 def props(compile_targets=None, use_analyze=False): | 256 def props(compile_targets=None, use_analyze=False, |
| 257 good_revision=None, bad_revision=None, |
| 258 suspected_revisions=None): |
| 160 properties = { | 259 properties = { |
| 161 'mastername': 'tryserver.chromium.linux', | 260 'mastername': 'tryserver.chromium.linux', |
| 162 'buildername': 'linux_variable', | 261 'buildername': 'linux_variable', |
| 163 'slavename': 'build1-a1', | 262 'slavename': 'build1-a1', |
| 164 'buildnumber': '1', | 263 'buildnumber': '1', |
| 165 'target_mastername': 'chromium.linux', | 264 'target_mastername': 'chromium.linux', |
| 166 'target_buildername': 'Linux Builder', | 265 'target_buildername': 'Linux Builder', |
| 167 'good_revision': 'r0', | 266 'good_revision': good_revision or 'r0', |
| 168 'bad_revision': 'r1', | 267 'bad_revision': bad_revision or 'r1', |
| 169 'use_analyze': use_analyze, | 268 'use_analyze': use_analyze, |
| 170 } | 269 } |
| 171 if compile_targets: | 270 if compile_targets: |
| 172 properties['compile_targets'] = compile_targets | 271 properties['compile_targets'] = compile_targets |
| 272 if suspected_revisions: |
| 273 properties['suspected_revisions'] = suspected_revisions |
| 173 return api.properties(**properties) + api.platform.name('linux') | 274 return api.properties(**properties) + api.platform.name('linux') |
| 174 | 275 |
| 175 yield ( | 276 yield ( |
| 176 api.test('compile_specified_targets') + | 277 api.test('compile_specified_targets') + |
| 177 props(compile_targets=['target_name']) + | 278 props(compile_targets=['target_name']) + |
| 178 api.override_step_data('test r1.check_targets', | 279 api.override_step_data('test r1.check_targets', |
| 179 api.json.output({ | 280 api.json.output({ |
| 180 'found': ['target_name'], | 281 'found': ['target_name'], |
| 181 'not_found': [], | 282 'not_found': [], |
| 182 })) | 283 })) |
| (...skipping 127 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 310 })) + | 411 })) + |
| 311 api.override_step_data( | 412 api.override_step_data( |
| 312 'test r1.analyze', | 413 'test r1.analyze', |
| 313 api.json.output({ | 414 api.json.output({ |
| 314 'status': 'Found dependency', | 415 'status': 'Found dependency', |
| 315 'compile_targets': ['a', 'a_run'], | 416 'compile_targets': ['a', 'a_run'], |
| 316 'test_targets': ['a', 'a_run'], | 417 'test_targets': ['a', 'a_run'], |
| 317 }) | 418 }) |
| 318 ) | 419 ) |
| 319 ) | 420 ) |
| 421 |
| 422 # Entire regression range: (r1, r6] |
| 423 # Suspected_revisions: [r4] |
| 424 # Expected smaller ranges: [r3, [r4, r5, r6]], [None, [r2]] |
| 425 # Actual culprit: r4 |
| 426 # Should only run compile on r3, and then r4. |
| 427 yield ( |
| 428 api.test('find_culprit_in_middle_of_a_sub_range') + |
| 429 props(compile_targets=['target_name'], |
| 430 good_revision='r1', |
| 431 bad_revision='r6', |
| 432 suspected_revisions=['r4']) + |
| 433 api.override_step_data( |
| 434 'git commits in range', |
| 435 api.raw_io.stream_output( |
| 436 '\n'.join('r%d' % i for i in reversed(range(2, 7))))) + |
| 437 api.override_step_data('test r3.check_targets', |
| 438 api.json.output({ |
| 439 'found': ['target_name'], |
| 440 'not_found': [], |
| 441 })) + |
| 442 api.override_step_data('test r4.check_targets', |
| 443 api.json.output({ |
| 444 'found': ['target_name'], |
| 445 'not_found': [], |
| 446 })) + |
| 447 api.override_step_data('test r4.compile', retcode=1) |
| 448 ) |
| 449 |
| 450 # Entire regression range: (r1, r6] |
| 451 # Suspected_revisions: [r4] |
| 452 # Expected smaller ranges: [r3, [r4, r5, r6]], [None, [r2]] |
| 453 # Actual culprit: r3 |
| 454 # Should only run compile on r3, and then r2. |
| 455 yield ( |
| 456 api.test('find_culprit_at_first_revision_of_a_sub_range') + |
| 457 props(compile_targets=['target_name'], |
| 458 good_revision='r1', |
| 459 bad_revision='r6', |
| 460 suspected_revisions=['r4']) + |
| 461 api.override_step_data( |
| 462 'git commits in range', |
| 463 api.raw_io.stream_output( |
| 464 '\n'.join('r%d' % i for i in reversed(range(2, 7))))) + |
| 465 api.override_step_data('test r3.check_targets', |
| 466 api.json.output({ |
| 467 'found': ['target_name'], |
| 468 'not_found': [], |
| 469 })) + |
| 470 api.override_step_data('test r3.compile', retcode=1) + |
| 471 api.override_step_data('test r2.check_targets', |
| 472 api.json.output({ |
| 473 'found': ['target_name'], |
| 474 'not_found': [], |
| 475 })) |
| 476 ) |
| 477 |
| 478 # Entire regression range: (r1, r10] |
| 479 # Suspected_revisions: [r4, r8] |
| 480 # Expected smaller ranges: |
| 481 # [r7, [r8, r9, r10]], [r3, [r4, r5, r6]], [None, [r2]] |
| 482 # Actual culprit: r4 |
| 483 # Should only run compile on r7(failed), then r3(pass) and r4(failed). |
| 484 yield ( |
| 485 api.test('find_culprit_in_second_sub_range') + |
| 486 props(compile_targets=['target_name'], |
| 487 good_revision='r1', |
| 488 bad_revision='r6', |
| 489 suspected_revisions=['r4', 'r8']) + |
| 490 api.override_step_data( |
| 491 'git commits in range', |
| 492 api.raw_io.stream_output( |
| 493 '\n'.join('r%d' % i for i in reversed(range(2, 11))))) + |
| 494 api.override_step_data('test r7.check_targets', |
| 495 api.json.output({ |
| 496 'found': ['target_name'], |
| 497 'not_found': [], |
| 498 })) + |
| 499 api.override_step_data('test r7.compile', retcode=1) + |
| 500 api.override_step_data('test r3.check_targets', |
| 501 api.json.output({ |
| 502 'found': ['target_name'], |
| 503 'not_found': [], |
| 504 })) + |
| 505 api.override_step_data('test r4.check_targets', |
| 506 api.json.output({ |
| 507 'found': ['target_name'], |
| 508 'not_found': [], |
| 509 })) + |
| 510 api.override_step_data('test r4.compile', retcode=1) |
| 511 ) |
| 512 |
| 513 # Entire regression range: (r1, r5] |
| 514 # Suspected_revisions: [r2] |
| 515 # Expected smaller ranges: |
| 516 # [None, r2, r3, r4, r5] |
| 517 # Actual culprit: r2 |
| 518 # Should only run compile on r2(failed). |
| 519 yield ( |
| 520 api.test('find_culprit_as_first_revision_of_entire_range') + |
| 521 props(compile_targets=['target_name'], |
| 522 good_revision='r1', |
| 523 bad_revision='r5', |
| 524 suspected_revisions=['r2']) + |
| 525 api.override_step_data( |
| 526 'git commits in range', |
| 527 api.raw_io.stream_output( |
| 528 '\n'.join('r%d' % i for i in reversed(range(2, 6))))) + |
| 529 api.override_step_data('test r2.check_targets', |
| 530 api.json.output({ |
| 531 'found': ['target_name'], |
| 532 'not_found': [], |
| 533 })) + |
| 534 api.override_step_data('test r2.compile', retcode=1) |
| 535 ) |
| OLD | NEW |