Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2016 The LUCI Authors. All rights reserved. | 1 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 from __future__ import absolute_import | 5 from __future__ import absolute_import |
| 6 import contextlib | 6 import contextlib |
| 7 import collections | 7 import collections |
| 8 import keyword | 8 import keyword |
| 9 import re | 9 import re |
| 10 import types | 10 import types |
| 11 | 11 |
| 12 from functools import wraps | 12 from functools import wraps |
| 13 | 13 |
| 14 from .recipe_test_api import DisabledTestData, ModuleTestData | 14 from .recipe_test_api import DisabledTestData, ModuleTestData |
| 15 from .config import Single | 15 from .config import Single |
| 16 | 16 |
| 17 from .util import ModuleInjectionSite | 17 from .util import ModuleInjectionSite |
| 18 | 18 |
| 19 from . import field_composer | 19 from . import field_composer |
| 20 | 20 |
| 21 | 21 |
| 22 _StepConfig = collections.namedtuple('StepConfig', | 22 _TriggerSpec = collections.namedtuple('_TriggerSpec', |
| 23 ('bucket', 'builder_name', 'properties', 'buildbot_changes', 'tags', | |
| 24 'critical')) | |
| 25 | |
| 26 class TriggerSpec(_TriggerSpec): | |
| 27 """ | |
| 28 TriggerSpec is the internal representation of a raw trigger step. You should | |
| 29 use the standard 'step' recipe module, which will construct trigger specs | |
| 30 via API. | |
| 31 """ | |
| 32 | |
| 33 @classmethod | |
| 34 def _create(cls, builder_name, bucket=None, properties=None, | |
| 35 buildbot_changes=None, tags=None, critical=None): | |
| 36 """Creates a new TriggerSpec instance from its step API dictionary | |
| 37 keys/values. | |
| 38 | |
| 39 Args: | |
| 40 builder_name (str): The name of the builder to trigger. | |
| 41 bucket (str or None): The name of the trigger bucket. | |
| 42 properties (dict or None): Key/value properties dictionary. | |
| 43 buildbot_changes (list or None): Optional list of BuildBot change dicts. | |
| 44 tags (list or None): Optional list of tag strings. | |
| 45 critical (bool or None): If true and triggering fails asynchronously, fail | |
| 46 the entire build. If None, the step defaults to being True. | |
| 47 """ | |
| 48 if not isinstance(buildbot_changes, (types.NoneType, list)): | |
| 49 raise ValueError('buildbot_changes must be a list') | |
| 50 | |
| 51 return cls( | |
| 52 bucket=bucket, | |
| 53 builder_name=builder_name, | |
| 54 properties=properties, | |
| 55 buildbot_changes=buildbot_changes, | |
| 56 tags=tags, | |
| 57 critical=bool(critical) if critical is not None else (True), | |
| 58 ) | |
| 59 | |
| 60 def _render_to_dict(self): | |
| 61 d = dict((k, v) for k, v in self._asdict().iteritems() if v) | |
| 62 if d['critical']: | |
| 63 d.pop('critical') | |
| 64 return d | |
| 65 | |
| 66 | |
| 67 _StepConfig = collections.namedtuple('_StepConfig', | |
| 23 ('name', 'cmd', 'cwd', 'env', 'allow_subannotations', 'trigger_specs', | 68 ('name', 'cmd', 'cwd', 'env', 'allow_subannotations', 'trigger_specs', |
| 24 'timeout', 'infra_step', 'stdout', 'stderr', 'stdin', 'ok_ret', | 69 'timeout', 'infra_step', 'stdout', 'stderr', 'stdin', 'ok_ret', |
| 25 'step_test_data', 'nest_level')) | 70 'step_test_data', 'nest_level')) |
| 26 | 71 |
| 27 class StepConfig(_StepConfig): | 72 class StepConfig(_StepConfig): |
| 28 """ | 73 """ |
| 29 StepConfig parameters: | 74 StepConfig is the representation of a raw step as the recipe_engine sees it. |
| 30 name: name of the step, will appear in buildbots waterfall | 75 You should use the standard 'step' recipe module, which will construct and |
| 31 cmd: command to run, list of one or more strings | 76 pass this data to the engine for you, instead. The only reason why you would |
| 32 cwd: absolute path to working directory for the command | 77 need to worry about this object is if you're modifying the step module itself. |
| 33 env: dict with overrides for environment variables | 78 |
| 34 allow_subannotations: if True, lets the step emit its own annotations | 79 The optional "env" parameter provides optional overrides for environment |
| 35 trigger_specs: a list of trigger specifications, see also _trigger_builds. | 80 variables. Each value is % formatted with the entire existing os.environ. A |
| 36 timeout: if not None, a datetime.timedelta for the step timeout. | 81 value of `None` will remove that envvar from the environ. e.g. |
| 37 infra_step: if True, this is an infrastructure step. | 82 |
| 38 stdout: Path to a file to put step stdout into. If used, stdout won't | 83 { |
| 39 appear in annotator's stdout (and |allow_subannotations| is | 84 "envvar": "%(envvar)s;%(envvar2)s;extra", |
| 40 ignored). | 85 "delete_this": None, |
| 41 stderr: Path to a file to put step stderr into. If used, stderr won't | 86 "static_value": "something", |
| 42 appear in annotator's stderr. | 87 } |
| 43 stdin: Path to a file to read step stdin from. | |
| 44 ok_ret: Allowed return code list. | |
| 45 step_test_data: Possible associated step test data. | |
| 46 nest_level: the step's nesting level. | |
| 47 """ | 88 """ |
| 48 | 89 |
| 90 _RENDER_WHITELIST=frozenset(( | |
| 91 'cmd', | |
| 92 )) | |
| 49 | 93 |
| 50 def _make_step_config(**step): | 94 _RENDER_BLACKLIST=frozenset(( |
| 51 """Galvanize a step dictionary into a formal StepConfig.""" | 95 'nest_level', |
| 52 step_config = StepConfig( | 96 'ok_ret', |
| 53 name=step.pop('name'), | 97 'infra_step', |
| 54 cmd=step.pop('cmd', None), | 98 'step_test_data', |
| 55 cwd=step.pop('cwd', None), | 99 )) |
| 56 env=step.pop('env', None), | 100 |
| 57 allow_subannotations=step.pop('allow_subannotations', False), | 101 @classmethod |
| 58 trigger_specs=step.pop('trigger_specs', ()), | 102 def create(cls, name, cmd=None, cwd=None, env=None, |
| 59 timeout=step.pop('timeout', None), | 103 allow_subannotations=None, trigger_specs=None, timeout=None, |
| 60 infra_step=step.pop('infra_step', False), | 104 infra_step=None, stdout=None, stderr=None, stdin=None, |
| 61 stdout=step.pop('stdout', None), | 105 ok_ret=None, step_test_data=None, step_nest_level=None): |
| 62 stderr=step.pop('stderr', None), | 106 """ |
| 63 stdin=step.pop('stdin', None), | 107 Initializes a new StepConfig step API dictionary. |
| 64 ok_ret=step.pop('ok_ret', {0}), | 108 |
| 65 step_test_data=step.pop('step_test_data', None), | 109 Args: |
| 66 nest_level=step.pop('step_nest_level', 0), | 110 name (str): name of the step, will appear in buildbots waterfall |
| 67 ) | 111 cmd: command to run. Acceptable types: str, Path, Placeholder, or None. |
| 68 if step: | 112 cwd (str or None): absolute path to working directory for the command |
| 69 unknown_keys = sorted(step.iterkeys()) | 113 env (dict): overrides for environment variables, described above. |
| 70 raise KeyError('Unknown step dictionary keys: %s' % ( | 114 allow_subannotations (bool): if True, lets the step emit its own |
| 71 ', '.join(unknown_keys))) | 115 annotations. NOTE: Enabling this can cause some buggy behavior. Please |
| 72 return step_config | 116 strongly consider using step_result.presentation instead. If you have |
| 117 questions, please contact infra-dev@chromium.org. | |
| 118 trigger_specs: a list of trigger specifications, see also _trigger_builds. | |
| 119 timeout: if not None, a datetime.timedelta for the step timeout. | |
| 120 infra_step: if True, this is an infrastructure step. Failures will raise | |
| 121 InfraFailure instead of StepFailure. | |
| 122 stdout: Placeholder to put step stdout into. If used, stdout won't appear | |
| 123 in annotator's stdout (and |allow_subannotations| is ignored). | |
| 124 stderr: Placeholder to put step stderr into. If used, stderr won't appear | |
| 125 in annotator's stderr. | |
| 126 stdin: Placeholder to read step stdin from. | |
| 127 ok_ret (iter): set of return codes allowed. If the step process returns | |
| 128 something not on this list, it will raise a StepFailure (or | |
| 129 InfraFailure if infra_step is True). If omitted, {0} will be used. | |
| 130 step_test_data (func -> recipe_test_api.StepTestData): A factory which | |
| 131 returns a StepTestData object that will be used as the default test | |
| 132 data for this step. The recipe author can override/augment this object | |
| 133 in the GenTests function. | |
| 134 step_nest_level (int): the step's nesting level. | |
| 135 """ | |
| 136 return cls( | |
| 137 name=name, | |
| 138 cmd=cmd, | |
| 139 cwd=cwd, | |
| 140 env=env, | |
| 141 allow_subannotations=bool(allow_subannotations), | |
| 142 trigger_specs=[TriggerSpec._create(**trig) | |
| 143 for trig in (trigger_specs or ())], | |
| 144 timeout=timeout, | |
| 145 infra_step=bool(infra_step), | |
| 146 stdout=stdout, | |
| 147 stderr=stderr, | |
| 148 stdin=stdin, | |
| 149 ok_ret=frozenset(ok_ret or (0,)), | |
| 150 step_test_data=step_test_data, | |
| 151 nest_level=int(step_nest_level or 0), | |
| 152 ) | |
| 153 | |
| 154 def render_to_dict(self): | |
| 155 self = self._replace( | |
| 156 trigger_specs=[trig._render_to_dict() | |
| 157 for trig in (self.trigger_specs or ())], | |
| 158 ) | |
| 159 return dict((k, v) for k, v in self._asdict().iteritems() | |
| 160 if (v or k in self._RENDER_WHITELIST) | |
| 161 and k not in self._RENDER_BLACKLIST) | |
| 73 | 162 |
| 74 | 163 |
| 75 class StepFailure(Exception): | 164 class StepFailure(Exception): |
| 76 """ | 165 """ |
| 77 This is the base class for all step failures. | 166 This is the base class for all step failures. |
| 78 | 167 |
| 79 Raising a StepFailure counts as 'running a step' for the purpose of | 168 Raising a StepFailure counts as 'running a step' for the purpose of |
| 80 infer_composite_step's logic. | 169 infer_composite_step's logic. |
| 81 """ | 170 """ |
| 82 def __init__(self, name_or_reason, result=None): | 171 def __init__(self, name_or_reason, result=None): |
| (...skipping 416 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 499 return itm(base), params | 588 return itm(base), params |
| 500 except KeyError: | 589 except KeyError: |
| 501 if optional: | 590 if optional: |
| 502 return None, generic_params | 591 return None, generic_params |
| 503 else: # pragma: no cover | 592 else: # pragma: no cover |
| 504 raise # TODO(iannucci): raise a better exception. | 593 raise # TODO(iannucci): raise a better exception. |
| 505 | 594 |
| 506 def set_config(self, config_name=None, optional=False, **CONFIG_VARS): | 595 def set_config(self, config_name=None, optional=False, **CONFIG_VARS): |
| 507 """Sets the modules and its dependencies to the named configuration.""" | 596 """Sets the modules and its dependencies to the named configuration.""" |
| 508 assert self._module | 597 assert self._module |
| 509 config, params = self.make_config_params(config_name, optional, | 598 config, _ = self.make_config_params(config_name, optional, **CONFIG_VARS) |
| 510 **CONFIG_VARS) | |
| 511 if config: | 599 if config: |
| 512 self.c = config | 600 self.c = config |
| 513 | 601 |
| 514 def apply_config(self, config_name, config_object=None, optional=False): | 602 def apply_config(self, config_name, config_object=None, optional=False): |
| 515 """Apply a named configuration to the provided config object or self.""" | 603 """Apply a named configuration to the provided config object or self.""" |
| 516 assert config_name in self._module.CONFIG_CTX.CONFIG_ITEMS, ( | 604 assert config_name in self._module.CONFIG_CTX.CONFIG_ITEMS, ( |
| 517 config_name, self._module.CONFIG_CTX.CONFIG_ITEMS) | 605 config_name, self._module.CONFIG_CTX.CONFIG_ITEMS) |
| 518 self._module.CONFIG_CTX.CONFIG_ITEMS[config_name]( | 606 self._module.CONFIG_CTX.CONFIG_ITEMS[config_name]( |
| 519 config_object or self.c, optional=optional) | 607 config_object or self.c, optional=optional) |
| 520 | 608 |
| (...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 598 | 686 |
| 599 if name in ('self',): | 687 if name in ('self',): |
| 600 return False | 688 return False |
| 601 | 689 |
| 602 if keyword.iskeyword(name): | 690 if keyword.iskeyword(name): |
| 603 return False | 691 return False |
| 604 | 692 |
| 605 regex = r'^[a-zA-Z][a-zA-Z0-9_]*$' if is_param_name else r'^[a-zA-Z][.\w]*$' | 693 regex = r'^[a-zA-Z][a-zA-Z0-9_]*$' if is_param_name else r'^[a-zA-Z][.\w]*$' |
| 606 return bool(re.match(regex, name)) | 694 return bool(re.match(regex, name)) |
| 607 | 695 |
| 608 def __init__(self, default, help, kind, name, property_type, module, | 696 def __init__(self, default, helptext, kind, name, property_type, module, |
| 609 param_name=None): | 697 param_name=None): |
| 610 """ | 698 """ |
| 611 Constructor for BoundProperty. | 699 Constructor for BoundProperty. |
| 612 | 700 |
| 613 Args: | 701 Args: |
| 614 default: The default value for this Property. Note: A default | 702 default: The default value for this Property. Note: A default |
| 615 value of None is allowed. To have no default value, omit | 703 value of None is allowed. To have no default value, omit |
| 616 this argument. | 704 this argument. |
| 617 help: The help text for this Property. | 705 helptext: The help text for this Property. |
| 618 kind: The type of this Property. You can either pass in a raw python | 706 kind: The type of this Property. You can either pass in a raw python |
| 619 type, or a Config Type, using the recipe engine config system. | 707 type, or a Config Type, using the recipe engine config system. |
| 620 name: The name of this Property. | 708 name: The name of this Property. |
| 621 param_name: The name of the python function parameter this property | 709 param_name: The name of the python function parameter this property |
| 622 should be stored in. Can be used to allow for dotted property | 710 should be stored in. Can be used to allow for dotted property |
| 623 names, e.g. | 711 names, e.g. |
| 624 PROPERTIES = { | 712 PROPERTIES = { |
| 625 'foo.bar.bam': Property(param_name="bizbaz") | 713 'foo.bar.bam': Property(param_name="bizbaz") |
| 626 } | 714 } |
| 627 module: The module this Property is a part of. | 715 module: The module this Property is a part of. |
| 628 """ | 716 """ |
| 629 if not BoundProperty.legal_name(name): | 717 if not BoundProperty.legal_name(name): |
| 630 raise ValueError("Illegal name '{}'.".format(param_name)) | 718 raise ValueError("Illegal name '{}'.".format(param_name)) |
| 631 | 719 |
| 632 param_name = param_name or name | 720 param_name = param_name or name |
| 633 if not BoundProperty.legal_name(param_name, is_param_name=True): | 721 if not BoundProperty.legal_name(param_name, is_param_name=True): |
| 634 raise ValueError("Illegal param_name '{}'.".format(param_name)) | 722 raise ValueError("Illegal param_name '{}'.".format(param_name)) |
| 635 | 723 |
| 636 self.__default = default | 724 self.__default = default |
| 637 self.__help = help | 725 self.__help = helptext |
| 638 self.__kind = kind | 726 self.__kind = kind |
| 639 self.__name = name | 727 self.__name = name |
| 640 self.__property_type = property_type | 728 self.__property_type = property_type |
| 641 self.__param_name = param_name | 729 self.__param_name = param_name |
| 642 self.__module = module | 730 self.__module = module |
| 643 | 731 |
| 644 @property | 732 @property |
| 645 def name(self): | 733 def name(self): |
| 646 return self.__name | 734 return self.__name |
| 647 | 735 |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 684 return value | 772 return value |
| 685 | 773 |
| 686 if self.default is not PROPERTY_SENTINEL: | 774 if self.default is not PROPERTY_SENTINEL: |
| 687 return self.default | 775 return self.default |
| 688 | 776 |
| 689 raise ValueError( | 777 raise ValueError( |
| 690 "No default specified and no value provided for '{}' from {} '{}'".format( | 778 "No default specified and no value provided for '{}' from {} '{}'".format( |
| 691 self.name, self.__property_type, self.module)) | 779 self.name, self.__property_type, self.module)) |
| 692 | 780 |
| 693 class Property(object): | 781 class Property(object): |
| 694 def __init__(self, default=PROPERTY_SENTINEL, help="", kind=None, | 782 def __init__(self, default=PROPERTY_SENTINEL, helptext="", kind=None, |
| 695 param_name=None): | 783 param_name=None): |
| 696 """ | 784 """ |
| 697 Constructor for Property. | 785 Constructor for Property. |
| 698 | 786 |
| 699 Args: | 787 Args: |
| 700 default: The default value for this Property. Note: A default | 788 default: The default value for this Property. Note: A default |
| 701 value of None is allowed. To have no default value, omit | 789 value of None is allowed. To have no default value, omit |
| 702 this argument. | 790 this argument. |
| 703 help: The help text for this Property. | 791 helptext: The help text for this Property. |
| 704 kind: The type of this Property. You can either pass in a raw python | 792 kind: The type of this Property. You can either pass in a raw python |
| 705 type, or a Config Type, using the recipe engine config system. | 793 type, or a Config Type, using the recipe engine config system. |
| 706 """ | 794 """ |
| 707 self._default = default | 795 self._default = default |
| 708 self.help = help | 796 self.helptext = helptext |
|
martiniss
2016/09/13 17:09:16
This change is breaking all sorts of recipes downs
| |
| 709 self.param_name = param_name | 797 self.param_name = param_name |
| 710 | 798 |
| 711 if isinstance(kind, type): | 799 if isinstance(kind, type): |
| 712 if kind in (str, unicode): | 800 if kind in (str, unicode): |
| 713 kind = basestring | 801 kind = basestring |
| 714 kind = Single(kind) | 802 kind = Single(kind) |
| 715 self.kind = kind | 803 self.kind = kind |
| 716 | 804 |
| 717 def bind(self, name, property_type, module): | 805 def bind(self, name, property_type, module): |
| 718 """ | 806 """ |
| 719 Gets the BoundProperty version of this Property. Requires a name. | 807 Gets the BoundProperty version of this Property. Requires a name. |
| 720 """ | 808 """ |
| 721 return BoundProperty( | 809 return BoundProperty( |
| 722 self._default, self.help, self.kind, name, property_type, module, | 810 self._default, self.helptext, self.kind, name, property_type, module, |
| 723 self.param_name) | 811 self.param_name) |
| 724 | 812 |
| 725 class UndefinedPropertyException(TypeError): | 813 class UndefinedPropertyException(TypeError): |
| 726 pass | 814 pass |
| 727 | 815 |
| OLD | NEW |