| Index: third_party/recipe_engine/recipe_api.py
|
| diff --git a/third_party/recipe_engine/recipe_api.py b/third_party/recipe_engine/recipe_api.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..72661a9eda4b737a25d80f5c4b2ae27bedd0039c
|
| --- /dev/null
|
| +++ b/third_party/recipe_engine/recipe_api.py
|
| @@ -0,0 +1,525 @@
|
| +# Copyright 2013-2015 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +import contextlib
|
| +import keyword
|
| +import types
|
| +
|
| +from functools import wraps
|
| +
|
| +from .recipe_test_api import DisabledTestData, ModuleTestData
|
| +from .config import Single
|
| +
|
| +from .util import ModuleInjectionSite
|
| +
|
| +from . import field_composer
|
| +
|
| +
|
| +class StepFailure(Exception):
|
| + """
|
| + This is the base class for all step failures.
|
| +
|
| + Raising a StepFailure counts as 'running a step' for the purpose of
|
| + infer_composite_step's logic.
|
| + """
|
| + def __init__(self, name_or_reason, result=None):
|
| + _STEP_CONTEXT['ran_step'][0] = True
|
| + if result:
|
| + self.name = name_or_reason
|
| + self.result = result
|
| + self.reason = self.reason_message()
|
| + else:
|
| + self.name = None
|
| + self.result = None
|
| + self.reason = name_or_reason
|
| +
|
| + super(StepFailure, self).__init__(self.reason)
|
| +
|
| + def reason_message(self):
|
| + return "Step({!r}) failed with return_code {}".format(
|
| + self.name, self.result.retcode)
|
| +
|
| + def __str__(self): # pragma: no cover
|
| + return "Step Failure in %s" % self.name
|
| +
|
| + @property
|
| + def retcode(self):
|
| + """
|
| + Returns the retcode of the step which failed. If this was a manual
|
| + failure, returns None
|
| + """
|
| + if not self.result:
|
| + return None
|
| + return self.result.retcode
|
| +
|
| +
|
| +class StepWarning(StepFailure):
|
| + """
|
| + A subclass of StepFailure, which still fails the build, but which is
|
| + a warning. Need to figure out how exactly this will be useful.
|
| + """
|
| + def reason_message(self): # pragma: no cover
|
| + return "Warning: Step({!r}) returned {}".format(
|
| + self.name, self.result.retcode)
|
| +
|
| + def __str__(self): # pragma: no cover
|
| + return "Step Warning in %s" % self.name
|
| +
|
| +
|
| +class InfraFailure(StepFailure):
|
| + """
|
| + A subclass of StepFailure, which fails the build due to problems with the
|
| + infrastructure.
|
| + """
|
| + def reason_message(self):
|
| + return "Infra Failure: Step({!r}) returned {}".format(
|
| + self.name, self.result.retcode)
|
| +
|
| + def __str__(self):
|
| + return "Infra Failure in %s" % self.name
|
| +
|
| +
|
| +class AggregatedStepFailure(StepFailure):
|
| + def __init__(self, result):
|
| + super(AggregatedStepFailure, self).__init__(
|
| + "Aggregate step failure.", result=result)
|
| +
|
| + def reason_message(self):
|
| + msg = "{!r} out of {!r} aggregated steps failed. Failures: ".format(
|
| + len(self.result.failures), len(self.result.all_results))
|
| + msg += ', '.join((f.reason or f.name) for f in self.result.failures)
|
| + return msg
|
| +
|
| + def __str__(self): # pragma: no cover
|
| + return "Aggregate Step Failure"
|
| +
|
| +
|
| +_FUNCTION_REGISTRY = {
|
| + 'aggregated_result': {'combine': lambda a, b: b},
|
| + 'env': {'combine': lambda a, b: dict(a, **b)},
|
| + 'name': {'combine': lambda a, b: '%s.%s' % (a, b)},
|
| + 'nest_level': {'combine': lambda a, b: a + b},
|
| + 'ran_step': {'combine': lambda a, b: b},
|
| +}
|
| +
|
| +
|
| +class AggregatedResult(object):
|
| + """Holds the result of an aggregated run of steps.
|
| +
|
| + Currently this is only used internally by defer_results, but it may be exposed
|
| + to the consumer of defer_results at some point in the future. For now it's
|
| + expected to be easier for defer_results consumers to do their own result
|
| + aggregation, as they may need to pick and chose (or label) which results they
|
| + really care about.
|
| + """
|
| + def __init__(self):
|
| + self.successes = []
|
| + self.failures = []
|
| +
|
| + # Needs to be here to be able to treat this as a step result
|
| + self.retcode = None
|
| +
|
| + @property
|
| + def all_results(self):
|
| + """
|
| + Return a list of two item tuples (x, y), where
|
| + x is whether or not the step succeeded, and
|
| + y is the result of the run
|
| + """
|
| + res = [(True, result) for result in self.successes]
|
| + res.extend([(False, result) for result in self.failures])
|
| + return res
|
| +
|
| + def add_success(self, result):
|
| + self.successes.append(result)
|
| +
|
| + def add_failure(self, exception):
|
| + self.failures.append(exception)
|
| +
|
| +
|
| +class DeferredResult(object):
|
| + def __init__(self, result, failure):
|
| + self._result = result
|
| + self._failure = failure
|
| +
|
| + @property
|
| + def is_ok(self):
|
| + return self._failure is None
|
| +
|
| + def get_result(self):
|
| + if not self.is_ok:
|
| + raise self.get_error()
|
| + return self._result
|
| +
|
| + def get_error(self):
|
| + assert self._failure, "WHAT IS IT ARE YOU DOING???!?!?!? SHTAP NAO"
|
| + return self._failure
|
| +
|
| +
|
| +_STEP_CONTEXT = field_composer.FieldComposer(
|
| + {'ran_step': [False]}, _FUNCTION_REGISTRY)
|
| +
|
| +
|
| +def non_step(func):
|
| + """A decorator which prevents a method from automatically being wrapped as
|
| + a infer_composite_step by RecipeApiMeta.
|
| +
|
| + This is needed for utility methods which don't run any steps, but which are
|
| + invoked within the context of a defer_results().
|
| +
|
| + @see infer_composite_step, defer_results, RecipeApiMeta
|
| + """
|
| + assert not hasattr(func, "_skip_inference"), \
|
| + "Double-wrapped method %r?" % func
|
| + func._skip_inference = True # pylint: disable=protected-access
|
| + return func
|
| +
|
| +_skip_inference = non_step
|
| +
|
| +
|
| +@contextlib.contextmanager
|
| +def context(fields):
|
| + global _STEP_CONTEXT
|
| + old = _STEP_CONTEXT
|
| + try:
|
| + _STEP_CONTEXT = old.compose(fields)
|
| + yield
|
| + finally:
|
| + _STEP_CONTEXT = old
|
| +
|
| +
|
| +def infer_composite_step(func):
|
| + """A decorator which possibly makes this step act as a single step, for the
|
| + purposes of the defer_results function.
|
| +
|
| + Behaves as if this function were wrapped by composite_step, unless this
|
| + function:
|
| + * is already wrapped by non_step
|
| + * returns a result without calling api.step
|
| + * raises an exception which is not derived from StepFailure
|
| +
|
| + In any of these cases, this function will behave like a normal function.
|
| +
|
| + This decorator is automatically applied by RecipeApiMeta (or by inheriting
|
| + from RecipeApi). If you want to decalare a method's behavior explicitly, you
|
| + may decorate it with either composite_step or with non_step.
|
| + """
|
| + if getattr(func, "_skip_inference", False):
|
| + return func
|
| +
|
| + @_skip_inference # to prevent double-wraps
|
| + @wraps(func)
|
| + def _inner(*a, **kw):
|
| + # We're not in a defer_results context, so just run the function normally.
|
| + if _STEP_CONTEXT.get('aggregated_result') is None:
|
| + return func(*a, **kw)
|
| +
|
| + agg = _STEP_CONTEXT['aggregated_result']
|
| +
|
| + # Setting the aggregated_result to None allows the contents of func to be
|
| + # written in the same style (e.g. with exceptions) no matter how func is
|
| + # being called.
|
| + with context({'aggregated_result': None, 'ran_step': [False]}):
|
| + try:
|
| + ret = func(*a, **kw)
|
| + if not _STEP_CONTEXT.get('ran_step', [False])[0]:
|
| + return ret
|
| + agg.add_success(ret)
|
| + return DeferredResult(ret, None)
|
| + except StepFailure as ex:
|
| + agg.add_failure(ex)
|
| + return DeferredResult(None, ex)
|
| + return _inner
|
| +
|
| +
|
| +def composite_step(func):
|
| + """A decorator which makes this step act as a single step, for the purposes of
|
| + the defer_results function.
|
| +
|
| + This means that this function will not quit during the middle of its execution
|
| + because of a StepFailure, if there is an aggregator active.
|
| +
|
| + You may use this decorator explicitly if infer_composite_step is detecting
|
| + the behavior of your method incorrectly to force it to behave as a step. You
|
| + may also need to use this if your Api class inherits from RecipeApiPlain and
|
| + so doesn't have its methods automatically wrapped by infer_composite_step.
|
| + """
|
| + @_skip_inference # to avoid double-wraps
|
| + @wraps(func)
|
| + def _inner(*a, **kw):
|
| + # always counts as running a step
|
| + _STEP_CONTEXT['ran_step'][0] = True
|
| +
|
| + if _STEP_CONTEXT.get('aggregated_result') is None:
|
| + return func(*a, **kw)
|
| +
|
| + agg = _STEP_CONTEXT['aggregated_result']
|
| +
|
| + # Setting the aggregated_result to None allows the contents of func to be
|
| + # written in the same style (e.g. with exceptions) no matter how func is
|
| + # being called.
|
| + with context({'aggregated_result': None}):
|
| + try:
|
| + ret = func(*a, **kw)
|
| + agg.add_success(ret)
|
| + return DeferredResult(ret, None)
|
| + except StepFailure as ex:
|
| + agg.add_failure(ex)
|
| + return DeferredResult(None, ex)
|
| + return _inner
|
| +
|
| +
|
| +@contextlib.contextmanager
|
| +def defer_results():
|
| + """
|
| + Use this to defer step results in your code. All steps which would previously
|
| + return a result or throw an exception will instead return a DeferredResult.
|
| +
|
| + Any exceptions which were thrown during execution will be thrown when either:
|
| + a. You call get_result() on the step's result.
|
| + b. You exit the suite inside of the with statement
|
| +
|
| + Example:
|
| + with defer_results():
|
| + api.step('a', ..)
|
| + api.step('b', ..)
|
| + result = api.m.module.im_a_composite_step(...)
|
| + api.m.echo('the data is', result.get_result())
|
| +
|
| + If 'a' fails, 'b' and 'im a composite step' will still run.
|
| + If 'im a composite step' fails, then the get_result() call will raise
|
| + an exception.
|
| + If you don't try to use the result (don't call get_result()), an aggregate
|
| + failure will still be raised once you exit the suite inside
|
| + the with statement.
|
| + """
|
| + assert _STEP_CONTEXT.get('aggregated_result') is None, (
|
| + "may not call defer_results in an active defer_results context")
|
| + agg = AggregatedResult()
|
| + with context({'aggregated_result': agg}):
|
| + yield
|
| + if agg.failures:
|
| + raise AggregatedStepFailure(agg)
|
| +
|
| +
|
| +class RecipeApiMeta(type):
|
| + WHITELIST = ('__init__',)
|
| + def __new__(mcs, name, bases, attrs):
|
| + """Automatically wraps all methods of subclasses of RecipeApi with
|
| + @infer_composite_step. This allows defer_results to work as intended without
|
| + manually decorating every method.
|
| + """
|
| + wrap = lambda f: infer_composite_step(f) if f else f
|
| + for attr in attrs:
|
| + if attr in RecipeApiMeta.WHITELIST:
|
| + continue
|
| + val = attrs[attr]
|
| + if isinstance(val, types.FunctionType):
|
| + attrs[attr] = wrap(val)
|
| + elif isinstance(val, property):
|
| + attrs[attr] = property(
|
| + wrap(val.fget),
|
| + wrap(val.fset),
|
| + wrap(val.fdel),
|
| + val.__doc__)
|
| + return super(RecipeApiMeta, mcs).__new__(mcs, name, bases, attrs)
|
| +
|
| +
|
| +class RecipeApiPlain(ModuleInjectionSite):
|
| + """
|
| + Framework class for handling recipe_modules.
|
| +
|
| + Inherit from this in your recipe_modules/<name>/api.py . This class provides
|
| + wiring for your config context (in self.c and methods, and for dependency
|
| + injection (in self.m).
|
| +
|
| + Dependency injection takes place in load_recipe_modules() in recipe_loader.py.
|
| +
|
| + USE RecipeApi INSTEAD, UNLESS your RecipeApi subclass derives from something
|
| + which defines its own __metaclass__. Deriving from RecipeApi instead of
|
| + RecipeApiPlain allows your RecipeApi subclass to automatically work with
|
| + defer_results without needing to decorate every methods with
|
| + @infer_composite_step.
|
| + """
|
| +
|
| + def __init__(self, module=None, engine=None,
|
| + test_data=DisabledTestData(), **_kwargs):
|
| + """Note: Injected dependencies are NOT available in __init__()."""
|
| + super(RecipeApiPlain, self).__init__()
|
| +
|
| + # |engine| is an instance of annotated_run.RecipeEngine. Modules should not
|
| + # generally use it unless they're low-level framework level modules.
|
| + self._engine = engine
|
| + self._module = module
|
| +
|
| + assert isinstance(test_data, (ModuleTestData, DisabledTestData))
|
| + self._test_data = test_data
|
| +
|
| + # If we're the 'root' api, inject directly into 'self'.
|
| + # Otherwise inject into 'self.m'
|
| + self.m = self if module is None else ModuleInjectionSite(self)
|
| +
|
| + # If our module has a test api, it gets injected here.
|
| + self.test_api = None
|
| +
|
| + # Config goes here.
|
| + self.c = None
|
| +
|
| + def get_config_defaults(self): # pylint: disable=R0201
|
| + """
|
| + Allows your api to dynamically determine static default values for configs.
|
| + """
|
| + return {}
|
| +
|
| + def make_config(self, config_name=None, optional=False, **CONFIG_VARS):
|
| + """Returns a 'config blob' for the current API."""
|
| + return self.make_config_params(config_name, optional, **CONFIG_VARS)[0]
|
| +
|
| + def make_config_params(self, config_name, optional=False, **CONFIG_VARS):
|
| + """Returns a 'config blob' for the current API, and the computed params
|
| + for all dependent configurations.
|
| +
|
| + The params have the following order of precendence. Each subsequent param
|
| + is dict.update'd into the final parameters, so the order is from lowest to
|
| + higest precedence on a per-key basis:
|
| + * if config_name in CONFIG_CTX
|
| + * get_config_defaults()
|
| + * CONFIG_CTX[config_name].DEFAULT_CONFIG_VARS()
|
| + * CONFIG_VARS
|
| + * else
|
| + * get_config_defaults()
|
| + * CONFIG_VARS
|
| + """
|
| + generic_params = self.get_config_defaults() # generic defaults
|
| + generic_params.update(CONFIG_VARS) # per-invocation values
|
| +
|
| + ctx = self._module.CONFIG_CTX
|
| + if optional and not ctx:
|
| + return None, generic_params
|
| +
|
| + assert ctx, '%s has no config context' % self
|
| + try:
|
| + params = self.get_config_defaults() # generic defaults
|
| + itm = ctx.CONFIG_ITEMS[config_name] if config_name else None
|
| + if itm:
|
| + params.update(itm.DEFAULT_CONFIG_VARS()) # per-item defaults
|
| + params.update(CONFIG_VARS) # per-invocation values
|
| +
|
| + base = ctx.CONFIG_SCHEMA(**params)
|
| + if config_name is None:
|
| + return base, params
|
| + else:
|
| + return itm(base), params
|
| + except KeyError:
|
| + if optional:
|
| + return None, generic_params
|
| + else: # pragma: no cover
|
| + raise # TODO(iannucci): raise a better exception.
|
| +
|
| + def set_config(self, config_name=None, optional=False, **CONFIG_VARS):
|
| + """Sets the modules and its dependencies to the named configuration."""
|
| + assert self._module
|
| + config, params = self.make_config_params(config_name, optional,
|
| + **CONFIG_VARS)
|
| + if config:
|
| + self.c = config
|
| +
|
| + def apply_config(self, config_name, config_object=None, optional=False):
|
| + """Apply a named configuration to the provided config object or self."""
|
| + self._module.CONFIG_CTX.CONFIG_ITEMS[config_name](
|
| + config_object or self.c, optional=optional)
|
| +
|
| + def resource(self, *path):
|
| + """Returns path to a file under <recipe module>/resources/ directory.
|
| +
|
| + Args:
|
| + path: path relative to module's resources/ directory.
|
| + """
|
| + # TODO(vadimsh): Verify that file exists. Including a case like:
|
| + # module.resource('dir').join('subdir', 'file.py')
|
| + return self._module.MODULE_DIRECTORY.join('resources', *path)
|
| +
|
| + @property
|
| + def name(self):
|
| + return self._module.NAME
|
| +
|
| +
|
| +class RecipeApi(RecipeApiPlain):
|
| + __metaclass__ = RecipeApiMeta
|
| +
|
| +
|
| +class Property(object):
|
| + sentinel = object()
|
| +
|
| + @staticmethod
|
| + def legal_name(name):
|
| + if name.startswith('_'):
|
| + return False
|
| +
|
| + if name in ('self',):
|
| + return False
|
| +
|
| + if keyword.iskeyword(name):
|
| + return False
|
| +
|
| + return True
|
| +
|
| + @property
|
| + def name(self):
|
| + return self._name
|
| +
|
| + @name.setter
|
| + def name(self, name):
|
| + if not Property.legal_name(name):
|
| + raise ValueError("Illegal name '{}'".format(name))
|
| +
|
| + self._name = name
|
| +
|
| + def __init__(self, default=sentinel, help="", kind=None):
|
| + """
|
| + Constructor for Property.
|
| +
|
| + Args:
|
| + default: The default value for this Property. Note: A default
|
| + value of None is allowed. To have no default value, omit
|
| + this argument.
|
| + help: The help text for this Property.
|
| + type: The type of this Property. You can either pass in a raw python
|
| + type, or a Config Type, using the recipe engine config system.
|
| + """
|
| + self._default = default
|
| + self.help = help
|
| + self._name = None
|
| +
|
| + if isinstance(kind, type):
|
| + kind = Single(kind)
|
| + self.kind = kind
|
| +
|
| + def interpret(self, value):
|
| + """
|
| + Interprets the value for this Property.
|
| +
|
| + Args:
|
| + value: The value to interpret. May be None, which
|
| + means no value provided.
|
| +
|
| + Returns:
|
| + The value to use for this property. Raises an error if
|
| + this property has no valid interpretation.
|
| + """
|
| + if value is not Property.sentinel:
|
| + if self.kind is not None:
|
| + # The config system handles type checking for us here.
|
| + self.kind.set_val(value)
|
| + return value
|
| +
|
| + if self._default is not Property.sentinel:
|
| + return self._default
|
| +
|
| + raise ValueError(
|
| + "No default specified and no value provided for '{}'".format(
|
| + self.name))
|
| +
|
| +class UndefinedPropertyException(TypeError):
|
| + pass
|
|
|