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 214 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
432 return engine.run(steps, api) | 437 return engine.run(steps, api) |
433 | 438 |
434 | 439 |
435 class RecipeEngine(object): | 440 class RecipeEngine(object): |
436 """Knows how to execute steps emitted by a recipe, holds global state such as | 441 """Knows how to execute steps emitted by a recipe, holds global state such as |
437 step history and build properties. Each recipe module API has a reference to | 442 step history and build properties. Each recipe module API has a reference to |
438 this object. | 443 this object. |
439 | 444 |
440 Recipe modules that are aware of the engine: | 445 Recipe modules that are aware of the engine: |
441 * properties - uses engine.properties. | 446 * properties - uses engine.properties. |
442 * step_history - uses engine.step_history. | |
443 * step - uses engine.create_step(...). | 447 * step - uses engine.create_step(...). |
444 | 448 |
445 This class acts mostly as a documentation of expected public engine interface. | 449 This class acts mostly as a documentation of expected public engine interface. |
446 """ | 450 """ |
447 | 451 |
448 @staticmethod | 452 @staticmethod |
449 def create(stream, properties, test_data): | 453 def create(stream, properties, test_data): |
450 """Create a new instance of RecipeEngine based on 'engine' property.""" | 454 """Create a new instance of RecipeEngine based on 'engine' property.""" |
451 engine_cls_name = properties.get('engine', 'SequentialRecipeEngine') | 455 engine_cls_name = properties.get('engine', 'SequentialRecipeEngine') |
452 for cls in RecipeEngine.__subclasses__(): | 456 for cls in RecipeEngine.__subclasses__(): |
(...skipping 27 matching lines...) Expand all Loading... | |
480 Args: | 484 Args: |
481 step: ConfigGroup object with information about the step, see | 485 step: ConfigGroup object with information about the step, see |
482 recipe_modules/step/config.py. | 486 recipe_modules/step/config.py. |
483 | 487 |
484 Returns: | 488 Returns: |
485 Opaque engine specific object that is understood by 'run_steps' method. | 489 Opaque engine specific object that is understood by 'run_steps' method. |
486 """ | 490 """ |
487 raise NotImplementedError | 491 raise NotImplementedError |
488 | 492 |
489 | 493 |
494 def _merge(*dicts): | |
495 result = {} | |
496 for d in dicts: | |
497 result.update(d) | |
498 return result | |
499 | |
500 | |
490 class SequentialRecipeEngine(RecipeEngine): | 501 class SequentialRecipeEngine(RecipeEngine): |
491 """Always runs step sequentially. Currently the engine used by default.""" | 502 """Always runs step sequentially. Currently the engine used by default.""" |
492 def __init__(self, stream, properties, test_data): | 503 def __init__(self, stream, properties, test_data): |
493 super(SequentialRecipeEngine, self).__init__() | 504 super(SequentialRecipeEngine, self).__init__() |
494 self._stream = stream | 505 self._stream = stream |
495 self._properties = properties | 506 self._properties = properties |
496 self._test_data = test_data | 507 self._test_data = test_data |
497 self._step_history = collections.OrderedDict() | 508 self._step_results = collections.OrderedDict() |
509 self._step_disambiguation_index = {} | |
498 | 510 |
499 self._previous_step_annotation = None | 511 self._annotation = None |
500 self._previous_step_result = None | 512 self._step_result = None |
501 self._api = None | 513 self._api = None |
502 | 514 |
503 @property | 515 @property |
504 def properties(self): | 516 def properties(self): |
505 return self._properties | 517 return self._properties |
506 | 518 |
507 @property | 519 @property |
508 def previous_step_result(self): | 520 def previous_step_result(self): |
509 """Allows api.step to get the active result from any context.""" | 521 """Allows api.step to get the active result from any context.""" |
510 return self._previous_step_result | 522 return self._step_result |
511 | 523 |
512 def _emit_results(self): | 524 def _emit_results(self): |
513 annotation = self._previous_step_annotation | 525 annotation = self._annotation |
514 step_result = self._previous_step_result | 526 step_result = self._step_result |
515 | 527 |
516 self._previous_step_annotation = None | 528 self._annotation = None |
517 self._previous_step_result = None | 529 self._step_result = None |
518 | 530 |
519 if not annotation or not step_result: | 531 if not annotation or not step_result: |
520 return | 532 return |
521 | 533 |
522 step_result.presentation.finalize(annotation) | 534 step_result.presentation.finalize(annotation) |
523 if self._test_data.enabled: | 535 if self._test_data.enabled: |
524 val = annotation.stream.getvalue() | 536 val = annotation.stream.getvalue() |
525 lines = filter(None, val.splitlines()) | 537 lines = filter(None, val.splitlines()) |
526 if lines: | 538 if lines: |
527 # note that '~' sorts after 'z' so that this will be last on each | 539 # note that '~' sorts after 'z' so that this will be last on each |
528 # step. also use _step to get access to the mutable step | 540 # step. also use _step to get access to the mutable step |
529 # dictionary. | 541 # dictionary. |
530 # pylint: disable=w0212 | 542 # pylint: disable=w0212 |
531 step_result._step['~followup_annotations'] = lines | 543 step_result._step['~followup_annotations'] = lines |
532 annotation.step_ended() | 544 annotation.step_ended() |
533 | 545 |
534 def run_step(self, step): | 546 def _disambiguate_name(self, step_name): |
535 ok_ret = step.pop('ok_ret') | 547 if step_name in self._step_disambiguation_index: |
536 infra_step = step.pop('infra_step') | 548 self._step_disambiguation_index[step_name] += 1 |
549 step_name += ' (%s)' % self._step_disambiguation_index[step_name] | |
550 else: | |
551 self._step_disambiguation_index[step_name] = 1 | |
552 return step_name | |
537 | 553 |
538 test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData) | 554 def _disambiguate_step(self, step): |
539 step_test = self._test_data.pop_step_test_data(step['name'], | 555 """Disambiguates step (destructively) by adding an index afterward. E.g. |
540 test_data_fn) | |
541 placeholders = render_step(step, step_test) | |
542 | 556 |
543 self._step_history[step['name']] = step | 557 gclient sync |
558 gclient sync (2) | |
559 ... | |
560 """ | |
561 step['name'] = self._disambiguate_name(step['name']) | |
562 | |
563 def _subannotator(self): | |
564 class Subannotator(object): | |
565 def BUILD_STEP(ann, name): | |
566 # TODO(luqui): disambiguate | |
567 self._open_step({'name': self._disambiguate_name(name)}) | |
568 | |
569 def STEP_WARNINGS(ann): | |
570 self._step_result.presentation.status = 'WARNING' | |
571 def STEP_FAILURE(ann): | |
572 self._step_result.presentation.status = 'FAILURE' | |
573 def STEP_EXCEPTION(ann): | |
574 self._step_result.presentation.status = 'EXCEPTION' | |
575 | |
576 def STEP_TEXT(ann, msg): | |
577 self._step_result.presentation.step_text = msg | |
578 | |
579 def STEP_LINK(ann, link_label, link_url): | |
580 self._step_result.presentation.links[link_label] = link_url | |
581 | |
582 def STEP_LOG_LINE(ann, log_label, log_line): | |
583 self._step_result.presentation.logs[log_label] += log_line | |
584 def STEP_LOG_END(ann, log_label): | |
585 # We do step finalization all at once. | |
586 pass | |
587 | |
588 def SET_BUILD_PROPERTY(ann, name, value): | |
589 self._step_result.presentation.properties[name] = value | |
590 | |
591 def STEP_SUMMARY_TEXT(ann, msg): | |
592 self._step_result.presentation.step_summary_text = msg | |
593 | |
594 return Subannotator() | |
595 | |
596 def _open_step(self, step): | |
544 self._emit_results() | 597 self._emit_results() |
598 step_result = StepData(step, None) | |
599 self._step_results[step['name']] = step_result | |
600 self._step_result = step_result | |
601 self._annotation = self._stream.step(step['name']) | |
602 self._annotation.step_started() | |
603 if self._test_data.enabled: | |
604 self._annotation.stream = cStringIO.StringIO() | |
545 | 605 |
546 step_result = None | 606 def _step_kernel(self, step, step_test, subannotator=None): |
547 | |
548 if not self._test_data.enabled: | 607 if not self._test_data.enabled: |
549 self._previous_step_annotation, retcode = annotator.run_step( | 608 # Warning: run_step cah change the current self._annotation and |
550 self._stream, **step) | 609 # self._step_result if it uses a subannotator. |
551 | 610 retcode = annotator.run_step( |
552 step_result = StepData(step, retcode) | 611 self._stream, |
553 self._previous_step_annotation.annotation_stream.step_cursor(step['name']) | 612 step_annotation=self._annotation, |
613 subannotator=subannotator, | |
614 **step) | |
615 self._step_result.retcode = retcode | |
616 # TODO(luqui): what is the purpose of this line? | |
617 self._annotation.annotation_stream.step_cursor( | |
618 self._step_result.step['name']) | |
554 else: | 619 else: |
555 self._previous_step_annotation = annotation = self._stream.step( | |
556 step['name']) | |
557 annotation.step_started() | |
558 try: | 620 try: |
559 annotation.stream = cStringIO.StringIO() | 621 self._step_result.retcode = step_test.retcode |
560 | |
561 step_result = StepData(step, step_test.retcode) | |
562 except OSError: | 622 except OSError: |
563 exc_type, exc_value, exc_tb = sys.exc_info() | 623 exc_type, exc_value, exc_tb = sys.exc_info() |
564 trace = traceback.format_exception(exc_type, exc_value, exc_tb) | 624 trace = traceback.format_exception(exc_type, exc_value, exc_tb) |
565 trace_lines = ''.join(trace).split('\n') | 625 trace_lines = ''.join(trace).split('\n') |
566 annotation.write_log_lines('exception', filter(None, trace_lines)) | 626 self._annotation.write_log_lines( |
567 annotation.step_exception() | 627 'exception', filter(None, trace_lines)) |
628 self._annotation.step_exception() | |
568 | 629 |
569 get_placeholder_results(step_result, placeholders) | 630 return self._step_result.retcode |
570 self._previous_step_result = step_result | |
571 | 631 |
572 if step_result.retcode in ok_ret: | 632 def run_step(self, step): |
573 step_result.presentation.status = 'SUCCESS' | 633 self._disambiguate_step(step) |
574 return step_result | 634 ok_ret = step.pop('ok_ret') |
635 infra_step = step.pop('infra_step') | |
636 allow_subannotations = step.get('allow_subannotations', False) | |
637 | |
638 test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData) | |
639 step_test = self._test_data.pop_step_test_data(step['name'], test_data_fn) | |
640 placeholders = render_step(step, step_test) | |
641 | |
642 if allow_subannotations: | |
643 # TODO(luqui) make this hierarchical | |
644 start_step = _merge(step, { 'name': step['name'] + ' (start)' }) | |
luqui
2015/03/23 23:26:32
Just emit name. Only emit (end) step if any suban
| |
645 self._open_step(start_step) | |
646 retcode = self._step_kernel(start_step, step_test, | |
647 subannotator=self._subannotator()) | |
648 | |
649 # Open a closing step for presentation modifications. | |
650 self._open_step({ 'name': step['name'] + ' (end)' }) | |
651 self._step_result.retcode = retcode | |
652 else: | |
653 self._open_step(step) | |
654 self._step_kernel(step, step_test) | |
655 | |
656 get_placeholder_results(self._step_result, placeholders) | |
657 | |
658 if self._step_result.retcode in ok_ret: | |
659 self._step_result.presentation.status = 'SUCCESS' | |
660 return self._step_result | |
575 else: | 661 else: |
576 if not infra_step: | 662 if not infra_step: |
577 state = 'FAILURE' | 663 state = 'FAILURE' |
578 exc = recipe_api.StepFailure | 664 exc = recipe_api.StepFailure |
579 else: | 665 else: |
580 state = 'EXCEPTION' | 666 state = 'EXCEPTION' |
581 exc = recipe_api.InfraFailure | 667 exc = recipe_api.InfraFailure |
582 | 668 |
583 step_result.presentation.status = state | 669 self._step_result.presentation.status = state |
584 if step_test.enabled: | 670 if step_test.enabled: |
585 # To avoid cluttering the expectations, don't emit this in testmode. | 671 # To avoid cluttering the expectations, don't emit this in testmode. |
586 self._previous_step_annotation.emit( | 672 self._annotation.emit( |
587 'step returned non-zero exit code: %d' % step_result.retcode) | 673 'step returned non-zero exit code: %d' % self._step_result.retcode) |
588 | 674 |
589 raise exc(step['name'], step_result) | 675 raise exc(step['name'], self._step_result) |
590 | 676 |
591 | 677 |
592 def run(self, steps_function, api): | 678 def run(self, steps_function, api): |
593 self._api = api | 679 self._api = api |
594 retcode = None | 680 retcode = None |
595 final_result = None | 681 final_result = None |
596 | 682 |
597 try: | 683 try: |
598 try: | 684 try: |
599 retcode = steps_function(api) | 685 retcode = steps_function(api) |
600 assert retcode is None, ( | 686 assert retcode is None, ( |
601 "Non-None return from GenSteps is not supported yet") | 687 'Non-None return from GenSteps is not supported yet') |
602 | 688 |
603 assert not self._test_data.enabled or not self._test_data.step_data, ( | 689 assert not self._test_data.enabled or not self._test_data.step_data, ( |
604 "Unconsumed test data! %s" % (self._test_data.step_data,)) | 690 'Unconsumed test data! %s' % (self._test_data.step_data,)) |
605 finally: | 691 finally: |
606 self._emit_results() | 692 self._emit_results() |
607 except recipe_api.StepFailure as f: | 693 except recipe_api.StepFailure as f: |
608 retcode = f.retcode or 1 | 694 retcode = f.retcode or 1 |
609 final_result = { | 695 final_result = { |
610 "name": "$final_result", | 696 'name': '$final_result', |
611 "reason": f.reason, | 697 'reason': f.reason, |
612 "status_code": retcode | 698 'status_code': retcode |
613 } | 699 } |
614 | 700 |
615 except Exception as ex: | 701 except Exception as ex: |
616 unexpected_exception = self._test_data.is_unexpected_exception(ex) | 702 unexpected_exception = self._test_data.is_unexpected_exception(ex) |
617 | 703 |
618 retcode = -1 | 704 retcode = -1 |
619 final_result = { | 705 final_result = { |
620 "name": "$final_result", | 706 'name': '$final_result', |
621 "reason": "Uncaught Exception: %r" % ex, | 707 'reason': 'Uncaught Exception: %r' % ex, |
622 "status_code": retcode | 708 'status_code': retcode |
623 } | 709 } |
624 | 710 |
625 with self._stream.step('Uncaught Exception') as s: | 711 with self._stream.step('Uncaught Exception') as s: |
626 s.step_exception() | 712 s.step_exception() |
627 s.write_log_lines('exception', traceback.format_exc().splitlines()) | 713 s.write_log_lines('exception', traceback.format_exc().splitlines()) |
628 | 714 |
629 if unexpected_exception: | 715 if unexpected_exception: |
630 raise | 716 raise |
631 | 717 |
632 if final_result is not None: | 718 if final_result is not None: |
633 self._step_history[final_result['name']] = final_result | 719 self._step_results[final_result['name']] = ( |
720 StepData(final_result, final_result['status_code'])) | |
634 | 721 |
635 return RecipeExecutionResult(retcode, self._step_history) | 722 return RecipeExecutionResult(retcode, self._step_results) |
636 | 723 |
637 def create_step(self, step): # pylint: disable=R0201 | 724 def create_step(self, step): # pylint: disable=R0201 |
638 # This version of engine doesn't do anything, just converts step to dict | 725 # This version of engine doesn't do anything, just converts step to dict |
639 # (that is consumed by annotator engine). | 726 # (that is consumed by annotator engine). |
640 return step.as_jsonish() | 727 return step.as_jsonish() |
641 | 728 |
642 | 729 |
643 class ParallelRecipeEngine(RecipeEngine): | 730 class ParallelRecipeEngine(RecipeEngine): |
644 """New engine that knows how to run steps in parallel. | 731 """New engine that knows how to run steps in parallel. |
645 | 732 |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
697 | 784 |
698 def shell_main(argv): | 785 def shell_main(argv): |
699 if update_scripts(): | 786 if update_scripts(): |
700 return subprocess.call([sys.executable] + argv) | 787 return subprocess.call([sys.executable] + argv) |
701 else: | 788 else: |
702 return main(argv) | 789 return main(argv) |
703 | 790 |
704 | 791 |
705 if __name__ == '__main__': | 792 if __name__ == '__main__': |
706 sys.exit(shell_main(sys.argv)) | 793 sys.exit(shell_main(sys.argv)) |
OLD | NEW |