OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2013 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 """Entry point for fully-annotated builds. | 6 """Entry point for fully-annotated builds. |
7 | 7 |
8 This script is part of the effort to move all builds to annotator-based | 8 This script is part of the effort to move all builds to annotator-based |
9 systems. Any builder configured to use the AnnotatorFactory.BaseFactory() | 9 systems. Any builder configured to use the AnnotatorFactory.BaseFactory() |
10 found in scripts/master/factory/annotator_factory.py executes a single | 10 found in scripts/master/factory/annotator_factory.py executes a single |
(...skipping 187 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
198 self.abort_reason = None | 198 self.abort_reason = None |
199 | 199 |
200 @property | 200 @property |
201 def step(self): | 201 def step(self): |
202 return copy.deepcopy(self._step) | 202 return copy.deepcopy(self._step) |
203 | 203 |
204 @property | 204 @property |
205 def retcode(self): | 205 def retcode(self): |
206 return self._retcode | 206 return self._retcode |
207 | 207 |
| 208 @retcode.setter |
| 209 def retcode(self, val): |
| 210 assert self._retcode is None, 'Can\'t override already-defined retcode' |
| 211 self._retcode = val |
| 212 |
208 @property | 213 @property |
209 def presentation(self): | 214 def presentation(self): |
210 return self._presentation | 215 return self._presentation |
211 | 216 |
212 # TODO(martiniss) update comment | 217 # TODO(martiniss) update comment |
213 # Result of 'render_step', fed into 'step_callback'. | 218 # Result of 'render_step', fed into 'step_callback'. |
214 Placeholders = collections.namedtuple( | 219 Placeholders = collections.namedtuple( |
215 'Placeholders', ['cmd', 'stdout', 'stderr', 'stdin']) | 220 'Placeholders', ['cmd', 'stdout', 'stderr', 'stdin']) |
216 | 221 |
217 | 222 |
(...skipping 222 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
440 return engine.run(steps, api) | 445 return engine.run(steps, api) |
441 | 446 |
442 | 447 |
443 class RecipeEngine(object): | 448 class RecipeEngine(object): |
444 """Knows how to execute steps emitted by a recipe, holds global state such as | 449 """Knows how to execute steps emitted by a recipe, holds global state such as |
445 step history and build properties. Each recipe module API has a reference to | 450 step history and build properties. Each recipe module API has a reference to |
446 this object. | 451 this object. |
447 | 452 |
448 Recipe modules that are aware of the engine: | 453 Recipe modules that are aware of the engine: |
449 * properties - uses engine.properties. | 454 * properties - uses engine.properties. |
450 * step_history - uses engine.step_history. | |
451 * step - uses engine.create_step(...). | 455 * step - uses engine.create_step(...). |
452 | 456 |
453 This class acts mostly as a documentation of expected public engine interface. | 457 This class acts mostly as a documentation of expected public engine interface. |
454 """ | 458 """ |
455 | 459 |
456 @staticmethod | 460 @staticmethod |
457 def create(stream, properties, test_data): | 461 def create(stream, properties, test_data): |
458 """Create a new instance of RecipeEngine based on 'engine' property.""" | 462 """Create a new instance of RecipeEngine based on 'engine' property.""" |
459 engine_cls_name = properties.get('engine', 'SequentialRecipeEngine') | 463 engine_cls_name = properties.get('engine', 'SequentialRecipeEngine') |
460 for cls in RecipeEngine.__subclasses__(): | 464 for cls in RecipeEngine.__subclasses__(): |
(...skipping 27 matching lines...) Expand all Loading... |
488 Args: | 492 Args: |
489 step: ConfigGroup object with information about the step, see | 493 step: ConfigGroup object with information about the step, see |
490 recipe_modules/step/config.py. | 494 recipe_modules/step/config.py. |
491 | 495 |
492 Returns: | 496 Returns: |
493 Opaque engine specific object that is understood by 'run_steps' method. | 497 Opaque engine specific object that is understood by 'run_steps' method. |
494 """ | 498 """ |
495 raise NotImplementedError | 499 raise NotImplementedError |
496 | 500 |
497 | 501 |
| 502 def _merge(*dicts): |
| 503 result = {} |
| 504 for d in dicts: |
| 505 result.update(d) |
| 506 return result |
| 507 |
| 508 |
498 class SequentialRecipeEngine(RecipeEngine): | 509 class SequentialRecipeEngine(RecipeEngine): |
499 """Always runs step sequentially. Currently the engine used by default.""" | 510 """Always runs step sequentially. Currently the engine used by default.""" |
500 def __init__(self, stream, properties, test_data): | 511 def __init__(self, stream, properties, test_data): |
501 super(SequentialRecipeEngine, self).__init__() | 512 super(SequentialRecipeEngine, self).__init__() |
502 self._stream = stream | 513 self._stream = stream |
503 self._properties = properties | 514 self._properties = properties |
504 self._test_data = test_data | 515 self._test_data = test_data |
505 self._step_history = collections.OrderedDict() | 516 self._step_results = collections.OrderedDict() |
| 517 self._step_disambiguation_index = {} |
506 | 518 |
507 self._previous_step_annotation = None | 519 self._annotation = None |
508 self._previous_step_result = None | 520 self._step_result = None |
509 self._api = None | 521 self._api = None |
510 | 522 |
511 @property | 523 @property |
512 def properties(self): | 524 def properties(self): |
513 return self._properties | 525 return self._properties |
514 | 526 |
515 @property | 527 @property |
516 def previous_step_result(self): | 528 def previous_step_result(self): |
517 """Allows api.step to get the active result from any context.""" | 529 """Allows api.step to get the active result from any context.""" |
518 return self._previous_step_result | 530 return self._step_result |
519 | 531 |
520 def _emit_results(self): | 532 def _emit_results(self): |
521 annotation = self._previous_step_annotation | 533 annotation = self._annotation |
522 step_result = self._previous_step_result | 534 step_result = self._step_result |
523 | 535 |
524 self._previous_step_annotation = None | 536 self._annotation = None |
525 self._previous_step_result = None | 537 self._step_result = None |
526 | 538 |
527 if not annotation or not step_result: | 539 if not annotation or not step_result: |
528 return | 540 return |
529 | 541 |
530 step_result.presentation.finalize(annotation) | 542 step_result.presentation.finalize(annotation) |
531 if self._test_data.enabled: | 543 if self._test_data.enabled: |
532 val = annotation.stream.getvalue() | 544 val = annotation.stream.getvalue() |
533 lines = filter(None, val.splitlines()) | 545 lines = filter(None, val.splitlines()) |
534 if lines: | 546 if lines: |
535 # note that '~' sorts after 'z' so that this will be last on each | 547 # note that '~' sorts after 'z' so that this will be last on each |
536 # step. also use _step to get access to the mutable step | 548 # step. also use _step to get access to the mutable step |
537 # dictionary. | 549 # dictionary. |
538 # pylint: disable=w0212 | 550 # pylint: disable=w0212 |
539 step_result._step['~followup_annotations'] = lines | 551 step_result._step['~followup_annotations'] = lines |
540 annotation.step_ended() | 552 annotation.step_ended() |
541 | 553 |
542 def run_step(self, step): | 554 def _disambiguate_name(self, step_name): |
543 ok_ret = step.pop('ok_ret') | 555 if step_name in self._step_disambiguation_index: |
544 infra_step = step.pop('infra_step') | 556 self._step_disambiguation_index[step_name] += 1 |
| 557 step_name += ' (%s)' % self._step_disambiguation_index[step_name] |
| 558 else: |
| 559 self._step_disambiguation_index[step_name] = 1 |
| 560 return step_name |
545 | 561 |
546 test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData) | 562 def _disambiguate_step(self, step): |
547 step_test = self._test_data.pop_step_test_data(step['name'], | 563 """Disambiguates step (destructively) by adding an index afterward. E.g. |
548 test_data_fn) | |
549 placeholders = render_step(step, step_test) | |
550 | 564 |
551 self._step_history[step['name']] = step | 565 gclient sync |
| 566 gclient sync (2) |
| 567 ... |
| 568 """ |
| 569 step['name'] = self._disambiguate_name(step['name']) |
| 570 |
| 571 def _subannotator(self): |
| 572 class Subannotator(object): |
| 573 # We use ann as the self argument because we are closing over the |
| 574 # SequentialRecipeEngine self. |
| 575 # pylint: disable=e0213 |
| 576 def BUILD_STEP(ann, name): |
| 577 self._open_step({'name': self._disambiguate_name(name)}) |
| 578 |
| 579 def STEP_WARNINGS(ann): |
| 580 self._step_result.presentation.status = 'WARNING' |
| 581 def STEP_FAILURE(ann): |
| 582 self._step_result.presentation.status = 'FAILURE' |
| 583 def STEP_EXCEPTION(ann): |
| 584 self._step_result.presentation.status = 'EXCEPTION' |
| 585 |
| 586 def STEP_TEXT(ann, msg): |
| 587 self._step_result.presentation.step_text = msg |
| 588 |
| 589 def STEP_LINK(ann, link_label, link_url): |
| 590 self._step_result.presentation.links[link_label] = link_url |
| 591 |
| 592 def STEP_LOG_LINE(ann, log_label, log_line): |
| 593 self._step_result.presentation.logs[log_label] += log_line |
| 594 def STEP_LOG_END(ann, log_label): |
| 595 # We do step finalization all at once. |
| 596 pass |
| 597 |
| 598 def SET_BUILD_PROPERTY(ann, name, value): |
| 599 self._step_result.presentation.properties[name] = json.loads(value) |
| 600 |
| 601 def STEP_SUMMARY_TEXT(ann, msg): |
| 602 self._step_result.presentation.step_summary_text = msg |
| 603 |
| 604 return Subannotator() |
| 605 |
| 606 def _open_step(self, step): |
552 self._emit_results() | 607 self._emit_results() |
| 608 step_result = StepData(step, None) |
| 609 self._step_results[step['name']] = step_result |
| 610 self._step_result = step_result |
| 611 self._annotation = self._stream.step(step['name']) |
| 612 self._annotation.step_started() |
| 613 if self._test_data.enabled: |
| 614 self._annotation.stream = cStringIO.StringIO() |
553 | 615 |
554 step_result = None | 616 def _step_kernel(self, step, step_test, subannotator=None): |
555 | |
556 if not self._test_data.enabled: | 617 if not self._test_data.enabled: |
557 self._previous_step_annotation, retcode = annotator.run_step( | 618 # Warning: run_step can change the current self._annotation and |
558 self._stream, **step) | 619 # self._step_result if it uses a subannotator. |
559 | 620 retcode = annotator.run_step( |
560 step_result = StepData(step, retcode) | 621 self._stream, |
561 self._previous_step_annotation.annotation_stream.step_cursor(step['name']) | 622 step_annotation=self._annotation, |
| 623 subannotator=subannotator, |
| 624 **step) |
| 625 self._step_result.retcode = retcode |
| 626 # TODO(luqui): What is the purpose of this line? |
| 627 self._annotation.annotation_stream.step_cursor( |
| 628 self._step_result.step['name']) |
562 else: | 629 else: |
563 self._previous_step_annotation = annotation = self._stream.step( | |
564 step['name']) | |
565 annotation.step_started() | |
566 try: | 630 try: |
567 annotation.stream = cStringIO.StringIO() | 631 self._step_result.retcode = step_test.retcode |
568 | |
569 step_result = StepData(step, step_test.retcode) | |
570 except OSError: | 632 except OSError: |
571 exc_type, exc_value, exc_tb = sys.exc_info() | 633 exc_type, exc_value, exc_tb = sys.exc_info() |
572 trace = traceback.format_exception(exc_type, exc_value, exc_tb) | 634 trace = traceback.format_exception(exc_type, exc_value, exc_tb) |
573 trace_lines = ''.join(trace).split('\n') | 635 trace_lines = ''.join(trace).split('\n') |
574 annotation.write_log_lines('exception', filter(None, trace_lines)) | 636 self._annotation.write_log_lines( |
575 annotation.step_exception() | 637 'exception', filter(None, trace_lines)) |
| 638 self._annotation.step_exception() |
576 | 639 |
577 get_placeholder_results(step_result, placeholders) | 640 return self._step_result.retcode |
578 self._previous_step_result = step_result | |
579 | 641 |
580 if step_result.retcode in ok_ret: | 642 def run_step(self, step): |
581 step_result.presentation.status = 'SUCCESS' | 643 self._disambiguate_step(step) |
582 return step_result | 644 ok_ret = step.pop('ok_ret') |
| 645 infra_step = step.pop('infra_step') |
| 646 allow_subannotations = step.get('allow_subannotations', False) |
| 647 |
| 648 test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData) |
| 649 step_test = self._test_data.pop_step_test_data(step['name'], test_data_fn) |
| 650 placeholders = render_step(step, step_test) |
| 651 |
| 652 if allow_subannotations: |
| 653 # TODO(luqui) Make this hierarchical. |
| 654 self._open_step(step) |
| 655 start_annotation = self._annotation |
| 656 retcode = self._step_kernel(step, step_test, |
| 657 subannotator=self._subannotator()) |
| 658 |
| 659 # Open a closing step for presentation modifications. |
| 660 if self._annotation != start_annotation: |
| 661 self._open_step({ 'name': step['name'] + ' (end)' }) |
| 662 self._step_result.retcode = retcode |
| 663 else: |
| 664 self._open_step(step) |
| 665 self._step_kernel(step, step_test) |
| 666 |
| 667 get_placeholder_results(self._step_result, placeholders) |
| 668 |
| 669 if self._step_result.retcode in ok_ret: |
| 670 self._step_result.presentation.status = 'SUCCESS' |
| 671 return self._step_result |
583 else: | 672 else: |
584 if not infra_step: | 673 if not infra_step: |
585 state = 'FAILURE' | 674 state = 'FAILURE' |
586 exc = recipe_api.StepFailure | 675 exc = recipe_api.StepFailure |
587 else: | 676 else: |
588 state = 'EXCEPTION' | 677 state = 'EXCEPTION' |
589 exc = recipe_api.InfraFailure | 678 exc = recipe_api.InfraFailure |
590 | 679 |
591 step_result.presentation.status = state | 680 self._step_result.presentation.status = state |
592 if step_test.enabled: | 681 if step_test.enabled: |
593 # To avoid cluttering the expectations, don't emit this in testmode. | 682 # To avoid cluttering the expectations, don't emit this in testmode. |
594 self._previous_step_annotation.emit( | 683 self._annotation.emit( |
595 'step returned non-zero exit code: %d' % step_result.retcode) | 684 'step returned non-zero exit code: %d' % self._step_result.retcode) |
596 | 685 |
597 raise exc(step['name'], step_result) | 686 raise exc(step['name'], self._step_result) |
598 | 687 |
599 | 688 |
600 def run(self, steps_function, api): | 689 def run(self, steps_function, api): |
601 self._api = api | 690 self._api = api |
602 retcode = None | 691 retcode = None |
603 final_result = None | 692 final_result = None |
604 | 693 |
605 try: | 694 try: |
606 try: | 695 try: |
607 retcode = steps_function(api) | 696 retcode = steps_function(api) |
608 assert retcode is None, ( | 697 assert retcode is None, ( |
609 "Non-None return from GenSteps is not supported yet") | 698 'Non-None return from GenSteps is not supported yet') |
610 | 699 |
611 assert not self._test_data.enabled or not self._test_data.step_data, ( | 700 assert not self._test_data.enabled or not self._test_data.step_data, ( |
612 "Unconsumed test data! %s" % (self._test_data.step_data,)) | 701 'Unconsumed test data! %s' % (self._test_data.step_data,)) |
613 finally: | 702 finally: |
614 self._emit_results() | 703 self._emit_results() |
615 except recipe_api.StepFailure as f: | 704 except recipe_api.StepFailure as f: |
616 retcode = f.retcode or 1 | 705 retcode = f.retcode or 1 |
617 final_result = { | 706 final_result = { |
618 "name": "$final_result", | 707 'name': '$final_result', |
619 "reason": f.reason, | 708 'reason': f.reason, |
620 "status_code": retcode | 709 'status_code': retcode |
621 } | 710 } |
622 | 711 |
623 except Exception as ex: | 712 except Exception as ex: |
624 unexpected_exception = self._test_data.is_unexpected_exception(ex) | 713 unexpected_exception = self._test_data.is_unexpected_exception(ex) |
625 | 714 |
626 retcode = -1 | 715 retcode = -1 |
627 final_result = { | 716 final_result = { |
628 "name": "$final_result", | 717 'name': '$final_result', |
629 "reason": "Uncaught Exception: %r" % ex, | 718 'reason': 'Uncaught Exception: %r' % ex, |
630 "status_code": retcode | 719 'status_code': retcode |
631 } | 720 } |
632 | 721 |
633 with self._stream.step('Uncaught Exception') as s: | 722 with self._stream.step('Uncaught Exception') as s: |
634 s.step_exception() | 723 s.step_exception() |
635 s.write_log_lines('exception', traceback.format_exc().splitlines()) | 724 s.write_log_lines('exception', traceback.format_exc().splitlines()) |
636 | 725 |
637 if unexpected_exception: | 726 if unexpected_exception: |
638 raise | 727 raise |
639 | 728 |
640 if final_result is not None: | 729 if final_result is not None: |
641 self._step_history[final_result['name']] = final_result | 730 self._step_results[final_result['name']] = ( |
| 731 StepData(final_result, final_result['status_code'])) |
642 | 732 |
643 return RecipeExecutionResult(retcode, self._step_history) | 733 return RecipeExecutionResult(retcode, self._step_results) |
644 | 734 |
645 def create_step(self, step): # pylint: disable=R0201 | 735 def create_step(self, step): # pylint: disable=R0201 |
646 # This version of engine doesn't do anything, just converts step to dict | 736 # This version of engine doesn't do anything, just converts step to dict |
647 # (that is consumed by annotator engine). | 737 # (that is consumed by annotator engine). |
648 return step.as_jsonish() | 738 return step.as_jsonish() |
649 | 739 |
650 | 740 |
651 class ParallelRecipeEngine(RecipeEngine): | 741 class ParallelRecipeEngine(RecipeEngine): |
652 """New engine that knows how to run steps in parallel. | 742 """New engine that knows how to run steps in parallel. |
653 | 743 |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
705 | 795 |
706 def shell_main(argv): | 796 def shell_main(argv): |
707 if update_scripts(): | 797 if update_scripts(): |
708 return subprocess.call([sys.executable] + argv) | 798 return subprocess.call([sys.executable] + argv) |
709 else: | 799 else: |
710 return main(argv) | 800 return main(argv) |
711 | 801 |
712 | 802 |
713 if __name__ == '__main__': | 803 if __name__ == '__main__': |
714 sys.exit(shell_main(sys.argv)) | 804 sys.exit(shell_main(sys.argv)) |
OLD | NEW |