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