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 |