| 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 bisect | 6 import bisect |
| 7 import collections | 7 import collections |
| 8 import contextlib | 8 import contextlib |
| 9 import copy | 9 import copy |
| 10 import json | 10 import json |
| 11 import keyword | 11 import keyword |
| 12 import re | 12 import re |
| 13 import types | 13 import types |
| 14 | 14 |
| 15 from functools import wraps | 15 from functools import wraps |
| 16 | 16 |
| 17 from .recipe_test_api import DisabledTestData, ModuleTestData | 17 from .recipe_test_api import DisabledTestData, ModuleTestData |
| 18 from .config import Single | 18 from .config import Single |
| 19 | 19 from .util import ModuleInjectionSite, Placeholder |
| 20 from .util import ModuleInjectionSite | |
| 21 | 20 |
| 22 | 21 |
| 23 class UnknownRequirementError(object): | 22 class UnknownRequirementError(object): |
| 24 """Raised by a requirement function when the referenced requirement is | 23 """Raised by a requirement function when the referenced requirement is |
| 25 unknown. | 24 unknown. |
| 26 """ | 25 """ |
| 27 | 26 |
| 28 def __init__(self, req): | 27 def __init__(self, req): |
| 29 super(UnknownRequirementError, self).__init__( | 28 super(UnknownRequirementError, self).__init__( |
| 30 'Unknown requirement [%s]' % (req,)) | 29 'Unknown requirement [%s]' % (req,)) |
| (...skipping 134 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 165 | 164 |
| 166 def get_properties(self): | 165 def get_properties(self): |
| 167 return copy.deepcopy(self._engine.properties) | 166 return copy.deepcopy(self._engine.properties) |
| 168 | 167 |
| 169 | 168 |
| 170 class StepClient(object): | 169 class StepClient(object): |
| 171 """A recipe engine client representing step running and introspection.""" | 170 """A recipe engine client representing step running and introspection.""" |
| 172 | 171 |
| 173 IDENT = 'step' | 172 IDENT = 'step' |
| 174 | 173 |
| 174 |
| 175 class StepConfig(collections.namedtuple('_StepConfig', ( |
| 176 'name', 'base_name', 'cmd', 'cwd', 'env', 'allow_subannotations', |
| 177 'trigger_specs', 'timeout', 'infra_step', 'stdout', 'stderr', 'stdin', |
| 178 'ok_ret', 'step_test_data', 'nest_level'))): |
| 179 |
| 180 """ |
| 181 StepConfig is the representation of a raw step as the recipe_engine sees it. |
| 182 You should use the standard 'step' recipe module, which will construct and |
| 183 pass this data to the engine for you, instead. The only reason why you would |
| 184 need to worry about this object is if you're modifying the step module |
| 185 itself. |
| 186 |
| 187 Fields: |
| 188 name (str): name of the step, will appear in buildbots waterfall |
| 189 base_name (str): the base name of the step. If the step has a derived |
| 190 name (e.g., nested may be concatenated with its parent), this is the |
| 191 name component of just this step. If None, this will be set to "name". |
| 192 cmd: command to run. Acceptable types: str, Path, Placeholder, or None. |
| 193 cwd (str or None): absolute path to working directory for the command |
| 194 env (dict): overrides for environment variables, described above. |
| 195 allow_subannotations (bool): if True, lets the step emit its own |
| 196 annotations. NOTE: Enabling this can cause some buggy behavior. Please |
| 197 strongly consider using step_result.presentation instead. If you have |
| 198 questions, please contact infra-dev@chromium.org. |
| 199 trigger_specs: a list of trigger specifications, see also _trigger_builds. |
| 200 timeout: if not None, a datetime.timedelta for the step timeout. |
| 201 infra_step: if True, this is an infrastructure step. Failures will raise |
| 202 InfraFailure instead of StepFailure. |
| 203 stdout: Placeholder to put step stdout into. If used, stdout won't appear |
| 204 in annotator's stdout (and |allow_subannotations| is ignored). |
| 205 stderr: Placeholder to put step stderr into. If used, stderr won't appear |
| 206 in annotator's stderr. |
| 207 stdin: Placeholder to read step stdin from. |
| 208 ok_ret (iter): set of return codes allowed. If the step process returns |
| 209 something not on this list, it will raise a StepFailure (or |
| 210 InfraFailure if infra_step is True). If omitted, {0} will be used. |
| 211 step_test_data (func -> recipe_test_api.StepTestData): A factory which |
| 212 returns a StepTestData object that will be used as the default test |
| 213 data for this step. The recipe author can override/augment this object |
| 214 in the GenTests function. |
| 215 nest_level (int): the step's nesting level. |
| 216 |
| 217 The optional "env" parameter provides optional overrides for environment |
| 218 variables. Each value is % formatted with the entire existing os.environ. A |
| 219 value of `None` will remove that envvar from the environ. e.g. |
| 220 |
| 221 { |
| 222 "envvar": "%(envvar)s;%(envvar2)s;extra", |
| 223 "delete_this": None, |
| 224 "static_value": "something", |
| 225 } |
| 226 """ |
| 227 |
| 228 _RENDER_WHITELIST=frozenset(( |
| 229 'cmd', |
| 230 )) |
| 231 |
| 232 _RENDER_BLACKLIST=frozenset(( |
| 233 'base_name', |
| 234 'nest_level', |
| 235 'ok_ret', |
| 236 'step_test_data', |
| 237 )) |
| 238 |
| 239 def __new__(cls, **kwargs): |
| 240 for field in cls._fields: |
| 241 kwargs.setdefault(field, None) |
| 242 sc = super(StepClient.StepConfig, cls).__new__(cls, **kwargs) |
| 243 |
| 244 return sc._replace( |
| 245 cmd=[(x if isinstance(x, Placeholder) else str(x)) |
| 246 for x in (sc.cmd or ())], |
| 247 cwd=(str(sc.cwd) if sc.cwd else (None)), |
| 248 base_name=sc.base_name or sc.name, |
| 249 allow_subannotations=bool(sc.allow_subannotations), |
| 250 trigger_specs=sc.trigger_specs or (), |
| 251 infra_step=bool(sc.infra_step), |
| 252 ok_ret=frozenset(sc.ok_ret or (0,)), |
| 253 nest_level=int(sc.nest_level or 0), |
| 254 ) |
| 255 |
| 256 def render_to_dict(self): |
| 257 sc = self._replace( |
| 258 trigger_specs=[trig._render_to_dict() |
| 259 for trig in (self.trigger_specs or ())], |
| 260 ) |
| 261 return dict((k, v) for k, v in sc._asdict().iteritems() |
| 262 if (v or k in sc._RENDER_WHITELIST) |
| 263 and k not in sc._RENDER_BLACKLIST) |
| 264 |
| 265 |
| 266 class TriggerSpec(collections.namedtuple('_TriggerSpec', ( |
| 267 'bucket', 'builder_name', 'properties', 'buildbot_changes', 'tags', |
| 268 'critical'))): |
| 269 |
| 270 """ |
| 271 TriggerSpec is the internal representation of a raw trigger step. You should |
| 272 use the standard 'step' recipe module, which will construct trigger specs |
| 273 via API. |
| 274 |
| 275 Fields: |
| 276 builder_name (str): The name of the builder to trigger. |
| 277 bucket (str or None): The name of the trigger bucket. |
| 278 properties (dict or None): Key/value properties dictionary. |
| 279 buildbot_changes (list or None): Optional list of BuildBot change dicts. |
| 280 tags (list or None): Optional list of tag strings. |
| 281 critical (bool or None): If true and triggering fails asynchronously, fail |
| 282 the entire build. If None, the step defaults to being True. |
| 283 """ |
| 284 |
| 285 def __new__(cls, **kwargs): |
| 286 for field in cls._fields: |
| 287 kwargs.setdefault(field, None) |
| 288 trig = super(StepClient.TriggerSpec, cls).__new__(cls, **kwargs) |
| 289 return trig._replace( |
| 290 critical=bool(trig.critical), |
| 291 ) |
| 292 |
| 293 def _render_to_dict(self): |
| 294 d = dict((k, v) for k, v in self._asdict().iteritems() if v) |
| 295 if d['critical']: |
| 296 d.pop('critical') |
| 297 return d |
| 298 |
| 299 |
| 175 def __init__(self, engine): | 300 def __init__(self, engine): |
| 176 self._engine = engine | 301 self._engine = engine |
| 177 | 302 |
| 178 def previous_step_result(self): | 303 def previous_step_result(self): |
| 179 """Allows api.step to get the active result from any context. | 304 """Allows api.step to get the active result from any context. |
| 180 | 305 |
| 181 This always returns the innermost nested step that is still open -- | 306 This always returns the innermost nested step that is still open -- |
| 182 presumably the one that just failed if we are in an exception handler.""" | 307 presumably the one that just failed if we are in an exception handler.""" |
| 183 if not self._engine._step_stack: | 308 if not self._engine._step_stack: |
| 184 raise ValueError( | 309 raise ValueError( |
| 185 'No steps have been run yet, and you are asking for a previous step ' | 310 'No steps have been run yet, and you are asking for a previous step ' |
| 186 'result.') | 311 'result.') |
| 187 return self._engine._step_stack[-1].step_result | 312 return self._engine._step_stack[-1].step_result |
| 188 | 313 |
| 189 def run_step(self, step_dict): | 314 def run_step(self, step_config): |
| 190 """ | 315 """ |
| 191 Runs a step. | 316 Runs a step from a StepConfig. |
| 192 | 317 |
| 193 Args: | 318 Args: |
| 194 step_dict (dict): A step dictionary to run. | 319 step_config: Keyword arguments to use to instantiate a StepConfig. |
| 195 | 320 |
| 196 Returns: | 321 Returns: |
| 197 A StepData object containing the result of running the step. | 322 A StepData object containing the result of running the step. |
| 198 """ | 323 """ |
| 199 return self._engine.run_step(StepConfig.create(**step_dict)) | 324 assert isinstance(step_config, self.StepConfig) |
| 325 return self._engine.run_step(step_config) |
| 200 | 326 |
| 201 | 327 |
| 202 class DependencyManagerClient(object): | 328 class DependencyManagerClient(object): |
| 203 """A recipe engine client representing the dependency manager.""" | 329 """A recipe engine client representing the dependency manager.""" |
| 204 | 330 |
| 205 IDENT = 'dependency_manager' | 331 IDENT = 'dependency_manager' |
| 206 | 332 |
| 207 def __init__(self, engine): | 333 def __init__(self, engine): |
| 208 self._engine = engine | 334 self._engine = engine |
| 209 | 335 |
| 210 def depend_on(self, recipe, properties, **kwargs): | 336 def depend_on(self, recipe, properties, **kwargs): |
| 211 return self._engine.depend_on(recipe, properties, **kwargs) | 337 return self._engine.depend_on(recipe, properties, **kwargs) |
| 212 | 338 |
| 213 | 339 |
| 214 _TriggerSpec = collections.namedtuple('_TriggerSpec', | |
| 215 ('bucket', 'builder_name', 'properties', 'buildbot_changes', 'tags', | |
| 216 'critical')) | |
| 217 | |
| 218 class TriggerSpec(_TriggerSpec): | |
| 219 """ | |
| 220 TriggerSpec is the internal representation of a raw trigger step. You should | |
| 221 use the standard 'step' recipe module, which will construct trigger specs | |
| 222 via API. | |
| 223 """ | |
| 224 | |
| 225 @classmethod | |
| 226 def _create(cls, builder_name, bucket=None, properties=None, | |
| 227 buildbot_changes=None, tags=None, critical=None): | |
| 228 """Creates a new TriggerSpec instance from its step API dictionary | |
| 229 keys/values. | |
| 230 | |
| 231 Args: | |
| 232 builder_name (str): The name of the builder to trigger. | |
| 233 bucket (str or None): The name of the trigger bucket. | |
| 234 properties (dict or None): Key/value properties dictionary. | |
| 235 buildbot_changes (list or None): Optional list of BuildBot change dicts. | |
| 236 tags (list or None): Optional list of tag strings. | |
| 237 critical (bool or None): If true and triggering fails asynchronously, fail | |
| 238 the entire build. If None, the step defaults to being True. | |
| 239 """ | |
| 240 if not isinstance(buildbot_changes, (types.NoneType, list)): | |
| 241 raise ValueError('buildbot_changes must be a list') | |
| 242 | |
| 243 return cls( | |
| 244 bucket=bucket, | |
| 245 builder_name=builder_name, | |
| 246 properties=properties, | |
| 247 buildbot_changes=buildbot_changes, | |
| 248 tags=tags, | |
| 249 critical=bool(critical) if critical is not None else (True), | |
| 250 ) | |
| 251 | |
| 252 def _render_to_dict(self): | |
| 253 d = dict((k, v) for k, v in self._asdict().iteritems() if v) | |
| 254 if d['critical']: | |
| 255 d.pop('critical') | |
| 256 return d | |
| 257 | |
| 258 | |
| 259 _StepConfig = collections.namedtuple('_StepConfig', | |
| 260 ('name', 'base_name', 'cmd', 'cwd', 'env', 'allow_subannotations', | |
| 261 'trigger_specs', 'timeout', 'infra_step', 'stdout', 'stderr', 'stdin', | |
| 262 'ok_ret', 'step_test_data', 'nest_level')) | |
| 263 | |
| 264 class StepConfig(_StepConfig): | |
| 265 """ | |
| 266 StepConfig is the representation of a raw step as the recipe_engine sees it. | |
| 267 You should use the standard 'step' recipe module, which will construct and | |
| 268 pass this data to the engine for you, instead. The only reason why you would | |
| 269 need to worry about this object is if you're modifying the step module itself. | |
| 270 | |
| 271 The optional "env" parameter provides optional overrides for environment | |
| 272 variables. Each value is % formatted with the entire existing os.environ. A | |
| 273 value of `None` will remove that envvar from the environ. e.g. | |
| 274 | |
| 275 { | |
| 276 "envvar": "%(envvar)s;%(envvar2)s;extra", | |
| 277 "delete_this": None, | |
| 278 "static_value": "something", | |
| 279 } | |
| 280 """ | |
| 281 | |
| 282 _RENDER_WHITELIST=frozenset(( | |
| 283 'cmd', | |
| 284 )) | |
| 285 | |
| 286 _RENDER_BLACKLIST=frozenset(( | |
| 287 'base_name', | |
| 288 'nest_level', | |
| 289 'ok_ret', | |
| 290 'step_test_data', | |
| 291 )) | |
| 292 | |
| 293 @classmethod | |
| 294 def create(cls, name, base_name=None, cmd=None, cwd=None, env=None, | |
| 295 allow_subannotations=None, trigger_specs=None, timeout=None, | |
| 296 infra_step=None, stdout=None, stderr=None, stdin=None, | |
| 297 ok_ret=None, step_test_data=None, step_nest_level=None): | |
| 298 """ | |
| 299 Initializes a new StepConfig step API dictionary. | |
| 300 | |
| 301 Args: | |
| 302 name (str): name of the step, will appear in buildbots waterfall | |
| 303 base_name (str): the base name of the step. If the step has a derived | |
| 304 name (e.g., nested may be concatenated with its parent), this is the | |
| 305 name component of just this step. If None, this will be set to "name". | |
| 306 cmd: command to run. Acceptable types: str, Path, Placeholder, or None. | |
| 307 cwd (str or None): absolute path to working directory for the command | |
| 308 env (dict): overrides for environment variables, described above. | |
| 309 allow_subannotations (bool): if True, lets the step emit its own | |
| 310 annotations. NOTE: Enabling this can cause some buggy behavior. Please | |
| 311 strongly consider using step_result.presentation instead. If you have | |
| 312 questions, please contact infra-dev@chromium.org. | |
| 313 trigger_specs: a list of trigger specifications, see also _trigger_builds. | |
| 314 timeout: if not None, a datetime.timedelta for the step timeout. | |
| 315 infra_step: if True, this is an infrastructure step. Failures will raise | |
| 316 InfraFailure instead of StepFailure. | |
| 317 stdout: Placeholder to put step stdout into. If used, stdout won't appear | |
| 318 in annotator's stdout (and |allow_subannotations| is ignored). | |
| 319 stderr: Placeholder to put step stderr into. If used, stderr won't appear | |
| 320 in annotator's stderr. | |
| 321 stdin: Placeholder to read step stdin from. | |
| 322 ok_ret (iter): set of return codes allowed. If the step process returns | |
| 323 something not on this list, it will raise a StepFailure (or | |
| 324 InfraFailure if infra_step is True). If omitted, {0} will be used. | |
| 325 step_test_data (func -> recipe_test_api.StepTestData): A factory which | |
| 326 returns a StepTestData object that will be used as the default test | |
| 327 data for this step. The recipe author can override/augment this object | |
| 328 in the GenTests function. | |
| 329 step_nest_level (int): the step's nesting level. | |
| 330 """ | |
| 331 return cls( | |
| 332 name=name, | |
| 333 base_name=(base_name or name), | |
| 334 cmd=cmd, | |
| 335 cwd=cwd, | |
| 336 env=env, | |
| 337 allow_subannotations=bool(allow_subannotations), | |
| 338 trigger_specs=[TriggerSpec._create(**trig) | |
| 339 for trig in (trigger_specs or ())], | |
| 340 timeout=timeout, | |
| 341 infra_step=bool(infra_step), | |
| 342 stdout=stdout, | |
| 343 stderr=stderr, | |
| 344 stdin=stdin, | |
| 345 ok_ret=frozenset(ok_ret or (0,)), | |
| 346 step_test_data=step_test_data, | |
| 347 nest_level=int(step_nest_level or 0), | |
| 348 ) | |
| 349 | |
| 350 def render_to_dict(self): | |
| 351 self = self._replace( | |
| 352 trigger_specs=[trig._render_to_dict() | |
| 353 for trig in (self.trigger_specs or ())], | |
| 354 ) | |
| 355 return dict((k, v) for k, v in self._asdict().iteritems() | |
| 356 if (v or k in self._RENDER_WHITELIST) | |
| 357 and k not in self._RENDER_BLACKLIST) | |
| 358 | |
| 359 | |
| 360 class StepFailure(Exception): | 340 class StepFailure(Exception): |
| 361 """ | 341 """ |
| 362 This is the base class for all step failures. | 342 This is the base class for all step failures. |
| 363 | 343 |
| 364 Raising a StepFailure counts as 'running a step' for the purpose of | 344 Raising a StepFailure counts as 'running a step' for the purpose of |
| 365 infer_composite_step's logic. | 345 infer_composite_step's logic. |
| 366 """ | 346 """ |
| 367 def __init__(self, name_or_reason, result=None): | 347 def __init__(self, name_or_reason, result=None): |
| 368 # Raising a StepFailure counts as running a step. | 348 # Raising a StepFailure counts as running a step. |
| 369 _DEFER_CONTEXT.mark_ran_step() | 349 _DEFER_CONTEXT.mark_ran_step() |
| (...skipping 705 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1075 def bind(self, name, property_type, full_decl_name): | 1055 def bind(self, name, property_type, full_decl_name): |
| 1076 """ | 1056 """ |
| 1077 Gets the BoundProperty version of this Property. Requires a name. | 1057 Gets the BoundProperty version of this Property. Requires a name. |
| 1078 """ | 1058 """ |
| 1079 return BoundProperty( | 1059 return BoundProperty( |
| 1080 self._default, self.help, self.kind, name, property_type, full_decl_name, | 1060 self._default, self.help, self.kind, name, property_type, full_decl_name, |
| 1081 self.param_name) | 1061 self.param_name) |
| 1082 | 1062 |
| 1083 class UndefinedPropertyException(TypeError): | 1063 class UndefinedPropertyException(TypeError): |
| 1084 pass | 1064 pass |
| OLD | NEW |