Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(197)

Side by Side Diff: scripts/slave/recipe_api.py

Issue 1151423002: Move recipe engine to third_party/recipe_engine. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Moved field_composer_test with its buddies Created 5 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « scripts/slave/isolation/recipes.isolate ('k') | scripts/slave/recipe_config.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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
OLDNEW
« no previous file with comments | « scripts/slave/isolation/recipes.isolate ('k') | scripts/slave/recipe_config.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698