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