| OLD | NEW |
| (Empty) |
| 1 # Copyright 2013-2015 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 import contextlib | |
| 6 import keyword | |
| 7 import types | |
| 8 | |
| 9 from functools import wraps | |
| 10 | |
| 11 from .recipe_test_api import DisabledTestData, ModuleTestData | |
| 12 from .config import Single | |
| 13 | |
| 14 from .util import ModuleInjectionSite | |
| 15 | |
| 16 from . import field_composer | |
| 17 | |
| 18 | |
| 19 class StepFailure(Exception): | |
| 20 """ | |
| 21 This is the base class for all step failures. | |
| 22 | |
| 23 Raising a StepFailure counts as 'running a step' for the purpose of | |
| 24 infer_composite_step's logic. | |
| 25 """ | |
| 26 def __init__(self, name_or_reason, result=None): | |
| 27 _STEP_CONTEXT['ran_step'][0] = True | |
| 28 if result: | |
| 29 self.name = name_or_reason | |
| 30 self.result = result | |
| 31 self.reason = self.reason_message() | |
| 32 else: | |
| 33 self.name = None | |
| 34 self.result = None | |
| 35 self.reason = name_or_reason | |
| 36 | |
| 37 super(StepFailure, self).__init__(self.reason) | |
| 38 | |
| 39 def reason_message(self): | |
| 40 return "Step({!r}) failed with return_code {}".format( | |
| 41 self.name, self.result.retcode) | |
| 42 | |
| 43 def __str__(self): # pragma: no cover | |
| 44 return "Step Failure in %s" % self.name | |
| 45 | |
| 46 @property | |
| 47 def retcode(self): | |
| 48 """ | |
| 49 Returns the retcode of the step which failed. If this was a manual | |
| 50 failure, returns None | |
| 51 """ | |
| 52 if not self.result: | |
| 53 return None | |
| 54 return self.result.retcode | |
| 55 | |
| 56 | |
| 57 class StepWarning(StepFailure): | |
| 58 """ | |
| 59 A subclass of StepFailure, which still fails the build, but which is | |
| 60 a warning. Need to figure out how exactly this will be useful. | |
| 61 """ | |
| 62 def reason_message(self): # pragma: no cover | |
| 63 return "Warning: Step({!r}) returned {}".format( | |
| 64 self.name, self.result.retcode) | |
| 65 | |
| 66 def __str__(self): # pragma: no cover | |
| 67 return "Step Warning in %s" % self.name | |
| 68 | |
| 69 | |
| 70 class InfraFailure(StepFailure): | |
| 71 """ | |
| 72 A subclass of StepFailure, which fails the build due to problems with the | |
| 73 infrastructure. | |
| 74 """ | |
| 75 def reason_message(self): | |
| 76 return "Infra Failure: Step({!r}) returned {}".format( | |
| 77 self.name, self.result.retcode) | |
| 78 | |
| 79 def __str__(self): | |
| 80 return "Infra Failure in %s" % self.name | |
| 81 | |
| 82 | |
| 83 class AggregatedStepFailure(StepFailure): | |
| 84 def __init__(self, result): | |
| 85 super(AggregatedStepFailure, self).__init__( | |
| 86 "Aggregate step failure.", result=result) | |
| 87 | |
| 88 def reason_message(self): | |
| 89 msg = "{!r} out of {!r} aggregated steps failed. Failures: ".format( | |
| 90 len(self.result.failures), len(self.result.all_results)) | |
| 91 msg += ', '.join((f.reason or f.name) for f in self.result.failures) | |
| 92 return msg | |
| 93 | |
| 94 def __str__(self): # pragma: no cover | |
| 95 return "Aggregate Step Failure" | |
| 96 | |
| 97 | |
| 98 _FUNCTION_REGISTRY = { | |
| 99 'aggregated_result': {'combine': lambda a, b: b}, | |
| 100 'env': {'combine': lambda a, b: dict(a, **b)}, | |
| 101 'name': {'combine': lambda a, b: '%s.%s' % (a, b)}, | |
| 102 'nest_level': {'combine': lambda a, b: a + b}, | |
| 103 'ran_step': {'combine': lambda a, b: b}, | |
| 104 } | |
| 105 | |
| 106 | |
| 107 class AggregatedResult(object): | |
| 108 """Holds the result of an aggregated run of steps. | |
| 109 | |
| 110 Currently this is only used internally by defer_results, but it may be exposed | |
| 111 to the consumer of defer_results at some point in the future. For now it's | |
| 112 expected to be easier for defer_results consumers to do their own result | |
| 113 aggregation, as they may need to pick and chose (or label) which results they | |
| 114 really care about. | |
| 115 """ | |
| 116 def __init__(self): | |
| 117 self.successes = [] | |
| 118 self.failures = [] | |
| 119 | |
| 120 # Needs to be here to be able to treat this as a step result | |
| 121 self.retcode = None | |
| 122 | |
| 123 @property | |
| 124 def all_results(self): | |
| 125 """ | |
| 126 Return a list of two item tuples (x, y), where | |
| 127 x is whether or not the step succeeded, and | |
| 128 y is the result of the run | |
| 129 """ | |
| 130 res = [(True, result) for result in self.successes] | |
| 131 res.extend([(False, result) for result in self.failures]) | |
| 132 return res | |
| 133 | |
| 134 def add_success(self, result): | |
| 135 self.successes.append(result) | |
| 136 | |
| 137 def add_failure(self, exception): | |
| 138 self.failures.append(exception) | |
| 139 | |
| 140 | |
| 141 class DeferredResult(object): | |
| 142 def __init__(self, result, failure): | |
| 143 self._result = result | |
| 144 self._failure = failure | |
| 145 | |
| 146 @property | |
| 147 def is_ok(self): | |
| 148 return self._failure is None | |
| 149 | |
| 150 def get_result(self): | |
| 151 if not self.is_ok: | |
| 152 raise self.get_error() | |
| 153 return self._result | |
| 154 | |
| 155 def get_error(self): | |
| 156 assert self._failure, "WHAT IS IT ARE YOU DOING???!?!?!? SHTAP NAO" | |
| 157 return self._failure | |
| 158 | |
| 159 | |
| 160 _STEP_CONTEXT = field_composer.FieldComposer( | |
| 161 {'ran_step': [False]}, _FUNCTION_REGISTRY) | |
| 162 | |
| 163 | |
| 164 def non_step(func): | |
| 165 """A decorator which prevents a method from automatically being wrapped as | |
| 166 a infer_composite_step by RecipeApiMeta. | |
| 167 | |
| 168 This is needed for utility methods which don't run any steps, but which are | |
| 169 invoked within the context of a defer_results(). | |
| 170 | |
| 171 @see infer_composite_step, defer_results, RecipeApiMeta | |
| 172 """ | |
| 173 assert not hasattr(func, "_skip_inference"), \ | |
| 174 "Double-wrapped method %r?" % func | |
| 175 func._skip_inference = True # pylint: disable=protected-access | |
| 176 return func | |
| 177 | |
| 178 _skip_inference = non_step | |
| 179 | |
| 180 | |
| 181 @contextlib.contextmanager | |
| 182 def context(fields): | |
| 183 global _STEP_CONTEXT | |
| 184 old = _STEP_CONTEXT | |
| 185 try: | |
| 186 _STEP_CONTEXT = old.compose(fields) | |
| 187 yield | |
| 188 finally: | |
| 189 _STEP_CONTEXT = old | |
| 190 | |
| 191 | |
| 192 def infer_composite_step(func): | |
| 193 """A decorator which possibly makes this step act as a single step, for the | |
| 194 purposes of the defer_results function. | |
| 195 | |
| 196 Behaves as if this function were wrapped by composite_step, unless this | |
| 197 function: | |
| 198 * is already wrapped by non_step | |
| 199 * returns a result without calling api.step | |
| 200 * raises an exception which is not derived from StepFailure | |
| 201 | |
| 202 In any of these cases, this function will behave like a normal function. | |
| 203 | |
| 204 This decorator is automatically applied by RecipeApiMeta (or by inheriting | |
| 205 from RecipeApi). If you want to decalare a method's behavior explicitly, you | |
| 206 may decorate it with either composite_step or with non_step. | |
| 207 """ | |
| 208 if getattr(func, "_skip_inference", False): | |
| 209 return func | |
| 210 | |
| 211 @_skip_inference # to prevent double-wraps | |
| 212 @wraps(func) | |
| 213 def _inner(*a, **kw): | |
| 214 # We're not in a defer_results context, so just run the function normally. | |
| 215 if _STEP_CONTEXT.get('aggregated_result') is None: | |
| 216 return func(*a, **kw) | |
| 217 | |
| 218 agg = _STEP_CONTEXT['aggregated_result'] | |
| 219 | |
| 220 # Setting the aggregated_result to None allows the contents of func to be | |
| 221 # written in the same style (e.g. with exceptions) no matter how func is | |
| 222 # being called. | |
| 223 with context({'aggregated_result': None, 'ran_step': [False]}): | |
| 224 try: | |
| 225 ret = func(*a, **kw) | |
| 226 if not _STEP_CONTEXT.get('ran_step', [False])[0]: | |
| 227 return ret | |
| 228 agg.add_success(ret) | |
| 229 return DeferredResult(ret, None) | |
| 230 except StepFailure as ex: | |
| 231 agg.add_failure(ex) | |
| 232 return DeferredResult(None, ex) | |
| 233 return _inner | |
| 234 | |
| 235 | |
| 236 def composite_step(func): | |
| 237 """A decorator which makes this step act as a single step, for the purposes of | |
| 238 the defer_results function. | |
| 239 | |
| 240 This means that this function will not quit during the middle of its execution | |
| 241 because of a StepFailure, if there is an aggregator active. | |
| 242 | |
| 243 You may use this decorator explicitly if infer_composite_step is detecting | |
| 244 the behavior of your method incorrectly to force it to behave as a step. You | |
| 245 may also need to use this if your Api class inherits from RecipeApiPlain and | |
| 246 so doesn't have its methods automatically wrapped by infer_composite_step. | |
| 247 """ | |
| 248 @_skip_inference # to avoid double-wraps | |
| 249 @wraps(func) | |
| 250 def _inner(*a, **kw): | |
| 251 # always counts as running a step | |
| 252 _STEP_CONTEXT['ran_step'][0] = True | |
| 253 | |
| 254 if _STEP_CONTEXT.get('aggregated_result') is None: | |
| 255 return func(*a, **kw) | |
| 256 | |
| 257 agg = _STEP_CONTEXT['aggregated_result'] | |
| 258 | |
| 259 # Setting the aggregated_result to None allows the contents of func to be | |
| 260 # written in the same style (e.g. with exceptions) no matter how func is | |
| 261 # being called. | |
| 262 with context({'aggregated_result': None}): | |
| 263 try: | |
| 264 ret = func(*a, **kw) | |
| 265 agg.add_success(ret) | |
| 266 return DeferredResult(ret, None) | |
| 267 except StepFailure as ex: | |
| 268 agg.add_failure(ex) | |
| 269 return DeferredResult(None, ex) | |
| 270 return _inner | |
| 271 | |
| 272 | |
| 273 @contextlib.contextmanager | |
| 274 def defer_results(): | |
| 275 """ | |
| 276 Use this to defer step results in your code. All steps which would previously | |
| 277 return a result or throw an exception will instead return a DeferredResult. | |
| 278 | |
| 279 Any exceptions which were thrown during execution will be thrown when either: | |
| 280 a. You call get_result() on the step's result. | |
| 281 b. You exit the suite inside of the with statement | |
| 282 | |
| 283 Example: | |
| 284 with defer_results(): | |
| 285 api.step('a', ..) | |
| 286 api.step('b', ..) | |
| 287 result = api.m.module.im_a_composite_step(...) | |
| 288 api.m.echo('the data is', result.get_result()) | |
| 289 | |
| 290 If 'a' fails, 'b' and 'im a composite step' will still run. | |
| 291 If 'im a composite step' fails, then the get_result() call will raise | |
| 292 an exception. | |
| 293 If you don't try to use the result (don't call get_result()), an aggregate | |
| 294 failure will still be raised once you exit the suite inside | |
| 295 the with statement. | |
| 296 """ | |
| 297 assert _STEP_CONTEXT.get('aggregated_result') is None, ( | |
| 298 "may not call defer_results in an active defer_results context") | |
| 299 agg = AggregatedResult() | |
| 300 with context({'aggregated_result': agg}): | |
| 301 yield | |
| 302 if agg.failures: | |
| 303 raise AggregatedStepFailure(agg) | |
| 304 | |
| 305 | |
| 306 class RecipeApiMeta(type): | |
| 307 WHITELIST = ('__init__',) | |
| 308 def __new__(mcs, name, bases, attrs): | |
| 309 """Automatically wraps all methods of subclasses of RecipeApi with | |
| 310 @infer_composite_step. This allows defer_results to work as intended without | |
| 311 manually decorating every method. | |
| 312 """ | |
| 313 wrap = lambda f: infer_composite_step(f) if f else f | |
| 314 for attr in attrs: | |
| 315 if attr in RecipeApiMeta.WHITELIST: | |
| 316 continue | |
| 317 val = attrs[attr] | |
| 318 if isinstance(val, types.FunctionType): | |
| 319 attrs[attr] = wrap(val) | |
| 320 elif isinstance(val, property): | |
| 321 attrs[attr] = property( | |
| 322 wrap(val.fget), | |
| 323 wrap(val.fset), | |
| 324 wrap(val.fdel), | |
| 325 val.__doc__) | |
| 326 return super(RecipeApiMeta, mcs).__new__(mcs, name, bases, attrs) | |
| 327 | |
| 328 | |
| 329 class RecipeApiPlain(ModuleInjectionSite): | |
| 330 """ | |
| 331 Framework class for handling recipe_modules. | |
| 332 | |
| 333 Inherit from this in your recipe_modules/<name>/api.py . This class provides | |
| 334 wiring for your config context (in self.c and methods, and for dependency | |
| 335 injection (in self.m). | |
| 336 | |
| 337 Dependency injection takes place in load_recipe_modules() in recipe_loader.py. | |
| 338 | |
| 339 USE RecipeApi INSTEAD, UNLESS your RecipeApi subclass derives from something | |
| 340 which defines its own __metaclass__. Deriving from RecipeApi instead of | |
| 341 RecipeApiPlain allows your RecipeApi subclass to automatically work with | |
| 342 defer_results without needing to decorate every methods with | |
| 343 @infer_composite_step. | |
| 344 """ | |
| 345 | |
| 346 def __init__(self, module=None, engine=None, | |
| 347 test_data=DisabledTestData(), **_kwargs): | |
| 348 """Note: Injected dependencies are NOT available in __init__().""" | |
| 349 super(RecipeApiPlain, self).__init__() | |
| 350 | |
| 351 # |engine| is an instance of annotated_run.RecipeEngine. Modules should not | |
| 352 # generally use it unless they're low-level framework level modules. | |
| 353 self._engine = engine | |
| 354 self._module = module | |
| 355 | |
| 356 assert isinstance(test_data, (ModuleTestData, DisabledTestData)) | |
| 357 self._test_data = test_data | |
| 358 | |
| 359 # If we're the 'root' api, inject directly into 'self'. | |
| 360 # Otherwise inject into 'self.m' | |
| 361 self.m = self if module is None else ModuleInjectionSite(self) | |
| 362 | |
| 363 # If our module has a test api, it gets injected here. | |
| 364 self.test_api = None | |
| 365 | |
| 366 # Config goes here. | |
| 367 self.c = None | |
| 368 | |
| 369 def get_config_defaults(self): # pylint: disable=R0201 | |
| 370 """ | |
| 371 Allows your api to dynamically determine static default values for configs. | |
| 372 """ | |
| 373 return {} | |
| 374 | |
| 375 def make_config(self, config_name=None, optional=False, **CONFIG_VARS): | |
| 376 """Returns a 'config blob' for the current API.""" | |
| 377 return self.make_config_params(config_name, optional, **CONFIG_VARS)[0] | |
| 378 | |
| 379 def make_config_params(self, config_name, optional=False, **CONFIG_VARS): | |
| 380 """Returns a 'config blob' for the current API, and the computed params | |
| 381 for all dependent configurations. | |
| 382 | |
| 383 The params have the following order of precendence. Each subsequent param | |
| 384 is dict.update'd into the final parameters, so the order is from lowest to | |
| 385 higest precedence on a per-key basis: | |
| 386 * if config_name in CONFIG_CTX | |
| 387 * get_config_defaults() | |
| 388 * CONFIG_CTX[config_name].DEFAULT_CONFIG_VARS() | |
| 389 * CONFIG_VARS | |
| 390 * else | |
| 391 * get_config_defaults() | |
| 392 * CONFIG_VARS | |
| 393 """ | |
| 394 generic_params = self.get_config_defaults() # generic defaults | |
| 395 generic_params.update(CONFIG_VARS) # per-invocation values | |
| 396 | |
| 397 ctx = self._module.CONFIG_CTX | |
| 398 if optional and not ctx: | |
| 399 return None, generic_params | |
| 400 | |
| 401 assert ctx, '%s has no config context' % self | |
| 402 try: | |
| 403 params = self.get_config_defaults() # generic defaults | |
| 404 itm = ctx.CONFIG_ITEMS[config_name] if config_name else None | |
| 405 if itm: | |
| 406 params.update(itm.DEFAULT_CONFIG_VARS()) # per-item defaults | |
| 407 params.update(CONFIG_VARS) # per-invocation values | |
| 408 | |
| 409 base = ctx.CONFIG_SCHEMA(**params) | |
| 410 if config_name is None: | |
| 411 return base, params | |
| 412 else: | |
| 413 return itm(base), params | |
| 414 except KeyError: | |
| 415 if optional: | |
| 416 return None, generic_params | |
| 417 else: # pragma: no cover | |
| 418 raise # TODO(iannucci): raise a better exception. | |
| 419 | |
| 420 def set_config(self, config_name=None, optional=False, **CONFIG_VARS): | |
| 421 """Sets the modules and its dependencies to the named configuration.""" | |
| 422 assert self._module | |
| 423 config, params = self.make_config_params(config_name, optional, | |
| 424 **CONFIG_VARS) | |
| 425 if config: | |
| 426 self.c = config | |
| 427 | |
| 428 def apply_config(self, config_name, config_object=None, optional=False): | |
| 429 """Apply a named configuration to the provided config object or self.""" | |
| 430 self._module.CONFIG_CTX.CONFIG_ITEMS[config_name]( | |
| 431 config_object or self.c, optional=optional) | |
| 432 | |
| 433 def resource(self, *path): | |
| 434 """Returns path to a file under <recipe module>/resources/ directory. | |
| 435 | |
| 436 Args: | |
| 437 path: path relative to module's resources/ directory. | |
| 438 """ | |
| 439 # TODO(vadimsh): Verify that file exists. Including a case like: | |
| 440 # module.resource('dir').join('subdir', 'file.py') | |
| 441 return self._module.MODULE_DIRECTORY.join('resources', *path) | |
| 442 | |
| 443 @property | |
| 444 def name(self): | |
| 445 return self._module.NAME | |
| 446 | |
| 447 | |
| 448 class RecipeApi(RecipeApiPlain): | |
| 449 __metaclass__ = RecipeApiMeta | |
| 450 | |
| 451 | |
| 452 class Property(object): | |
| 453 sentinel = object() | |
| 454 | |
| 455 @staticmethod | |
| 456 def legal_name(name): | |
| 457 if name.startswith('_'): | |
| 458 return False | |
| 459 | |
| 460 if name in ('self',): | |
| 461 return False | |
| 462 | |
| 463 if keyword.iskeyword(name): | |
| 464 return False | |
| 465 | |
| 466 return True | |
| 467 | |
| 468 @property | |
| 469 def name(self): | |
| 470 return self._name | |
| 471 | |
| 472 @name.setter | |
| 473 def name(self, name): | |
| 474 if not Property.legal_name(name): | |
| 475 raise ValueError("Illegal name '{}'".format(name)) | |
| 476 | |
| 477 self._name = name | |
| 478 | |
| 479 def __init__(self, default=sentinel, help="", kind=None): | |
| 480 """ | |
| 481 Constructor for Property. | |
| 482 | |
| 483 Args: | |
| 484 default: The default value for this Property. Note: A default | |
| 485 value of None is allowed. To have no default value, omit | |
| 486 this argument. | |
| 487 help: The help text for this Property. | |
| 488 type: The type of this Property. You can either pass in a raw python | |
| 489 type, or a Config Type, using the recipe engine config system. | |
| 490 """ | |
| 491 self._default = default | |
| 492 self.help = help | |
| 493 self._name = None | |
| 494 | |
| 495 if isinstance(kind, type): | |
| 496 kind = Single(kind) | |
| 497 self.kind = kind | |
| 498 | |
| 499 def interpret(self, value): | |
| 500 """ | |
| 501 Interprets the value for this Property. | |
| 502 | |
| 503 Args: | |
| 504 value: The value to interpret. May be None, which | |
| 505 means no value provided. | |
| 506 | |
| 507 Returns: | |
| 508 The value to use for this property. Raises an error if | |
| 509 this property has no valid interpretation. | |
| 510 """ | |
| 511 if value is not Property.sentinel: | |
| 512 if self.kind is not None: | |
| 513 # The config system handles type checking for us here. | |
| 514 self.kind.set_val(value) | |
| 515 return value | |
| 516 | |
| 517 if self._default is not Property.sentinel: | |
| 518 return self._default | |
| 519 | |
| 520 raise ValueError( | |
| 521 "No default specified and no value provided for '{}'".format( | |
| 522 self.name)) | |
| 523 | |
| 524 class UndefinedPropertyException(TypeError): | |
| 525 pass | |
| OLD | NEW |