| Index: scripts/slave/recipe_api.py
|
| diff --git a/scripts/slave/recipe_api.py b/scripts/slave/recipe_api.py
|
| index 6af63237d48ab5e610f71f7a4dbab450e2d49ec3..c6a0f3f5e022f8ad86fe7f8fc281847fb5c96a33 100644
|
| --- a/scripts/slave/recipe_api.py
|
| +++ b/scripts/slave/recipe_api.py
|
| @@ -3,50 +3,10 @@
|
| # found in the LICENSE file.
|
|
|
| import functools
|
| -import imp
|
| -import inspect
|
| -import os
|
| -import sys
|
| -import tempfile
|
|
|
| +from .recipe_test_api import DisabledTestData, ModuleTestData, StepTestData
|
|
|
| -class Placeholder(object):
|
| - """Base class for json placeholders. Do not use directly."""
|
| - def render(self, test_data): # pragma: no cover
|
| - """Return [cmd items]*"""
|
| - raise NotImplementedError
|
| -
|
| - def step_finished(self, presentation, step_result, test_data):
|
| - """Called after step completion. Intended to modify step_result."""
|
| - pass
|
| -
|
| -
|
| -class InputDataPlaceholder(Placeholder):
|
| - def __init__(self, data, suffix):
|
| - assert isinstance(data, basestring)
|
| - self.data = data
|
| - self.suffix = suffix
|
| - self.input_file = None
|
| - super(InputDataPlaceholder, self).__init__()
|
| -
|
| - def render(self, test_data):
|
| - if test_data is not None:
|
| - # cheat and pretend like we're going to pass the data on the
|
| - # cmdline for test expectation purposes.
|
| - return [self.data]
|
| - else: # pragma: no cover
|
| - input_fd, self.input_file = tempfile.mkstemp(suffix=self.suffix)
|
| - os.write(input_fd, self.data)
|
| - os.close(input_fd)
|
| - return [self.input_file]
|
| -
|
| - def step_finished(self, presentation, step_result, test_data):
|
| - if test_data is None: # pragma: no cover
|
| - os.unlink(self.input_file)
|
| -
|
| -
|
| -class ModuleInjectionSite(object):
|
| - pass
|
| +from .recipe_util import ModuleInjectionSite
|
|
|
|
|
| class RecipeApi(object):
|
| @@ -59,16 +19,54 @@ class RecipeApi(object):
|
|
|
| Dependency injection takes place in load_recipe_modules() below.
|
| """
|
| - def __init__(self, module=None, mock=None, **_kwargs):
|
| + def __init__(self, module=None, test_data=DisabledTestData(), **_kwargs):
|
| """Note: Injected dependencies are NOT available in __init__()."""
|
| self.c = None
|
| self._module = module
|
| - self._mock = mock
|
| +
|
| + 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()
|
|
|
| + # If our module has a test api, it gets injected here.
|
| + self.test_api = None
|
| +
|
| + @staticmethod
|
| + def inject_test_data(func):
|
| + """
|
| + Decorator which injects mock data from this module's test_api method into
|
| + the return value of the decorated function.
|
| +
|
| + The return value of func MUST be a single step dictionary (specifically,
|
| + |func| must not be a generator, nor must it return a list of steps, etc.)
|
| +
|
| + When the decorated function is called, |func| is called normally. If we are
|
| + in test mode, we will then also call self.test_api.<func.__name__>, whose
|
| + return value will be assigned into the step dictionary retuned by |func|.
|
| +
|
| + It is an error for the function to not exist in the test_api.
|
| + It is an error for the return value of |func| to already contain test data.
|
| + """
|
| + @functools.wraps(func)
|
| + def inner(self, *args, **kwargs):
|
| + ret = func(self, *args, **kwargs)
|
| + if self._mock is not None: # pylint: disable=W0212
|
| + test_fn = getattr(self.test_api, func.__name__, None)
|
| + assert test_fn, (
|
| + "Method %(meth)s in module %(mod)s is @inject_test_data, but test_api"
|
| + " does not contain %(meth)s."
|
| + % {
|
| + 'meth': func.__name__,
|
| + 'mod': self._module, # pylint: disable=W0212
|
| + })
|
| + assert 'default_test_data' not in ret
|
| + ret['default_test_data'] = test_fn(*args, **kwargs)
|
| + return ret
|
| + return inner
|
| +
|
| def get_config_defaults(self): # pylint: disable=R0201
|
| """
|
| Allows your api to dynamically determine static default values for configs.
|
| @@ -136,181 +134,42 @@ class RecipeApi(object):
|
| """Apply a named configuration to the provided config object or self."""
|
| self._module.CONFIG_CTX.CONFIG_ITEMS[config_name](config_object or self.c)
|
|
|
| + @property
|
| + def name(self):
|
| + return self._module.NAME
|
|
|
| -def load_recipe_modules(mod_dirs):
|
| - def patchup_module(submod):
|
| - submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None)
|
| - submod.API = getattr(submod, 'API', None)
|
| - submod.DEPS = frozenset(getattr(submod, 'DEPS', ()))
|
| -
|
| - if hasattr(submod, 'config'):
|
| - for v in submod.config.__dict__.itervalues():
|
| - if hasattr(v, 'I_AM_A_CONFIG_CTX'):
|
| - assert not submod.CONFIG_CTX, (
|
| - 'More than one configuration context: %s' % (submod.config))
|
| - submod.CONFIG_CTX = v
|
| - assert submod.CONFIG_CTX, 'Config file, but no config context?'
|
| -
|
| - for v in submod.api.__dict__.itervalues():
|
| - if inspect.isclass(v) and issubclass(v, RecipeApi):
|
| - assert not submod.API, (
|
| - 'More than one Api subclass: %s' % submod.api)
|
| - submod.API = v
|
| -
|
| - assert submod.API, 'Submodule has no api? %s' % (submod)
|
| -
|
| - RM = 'RECIPE_MODULES'
|
| - def find_and_load(fullname, modname, path):
|
| - if fullname not in sys.modules or fullname == RM:
|
| - try:
|
| - fil, pathname, descr = imp.find_module(modname,
|
| - [os.path.dirname(path)])
|
| - imp.load_module(fullname, fil, pathname, descr)
|
| - finally:
|
| - if fil:
|
| - fil.close()
|
| - return sys.modules[fullname]
|
| -
|
| - def recursive_import(path, prefix=None, skip_fn=lambda name: False):
|
| - modname = os.path.splitext(os.path.basename(path))[0]
|
| - if prefix:
|
| - fullname = '%s.%s' % (prefix, modname)
|
| - else:
|
| - fullname = RM
|
| - m = find_and_load(fullname, modname, path)
|
| - if not os.path.isdir(path):
|
| - return m
|
| -
|
| - for subitem in os.listdir(path):
|
| - subpath = os.path.join(path, subitem)
|
| - subname = os.path.splitext(subitem)[0]
|
| - if skip_fn(subname):
|
| - continue
|
| - if os.path.isdir(subpath):
|
| - if not os.path.exists(os.path.join(subpath, '__init__.py')):
|
| - continue
|
| - elif not subpath.endswith('.py') or subitem.startswith('__init__.py'):
|
| - continue
|
|
|
| - submod = recursive_import(subpath, fullname, skip_fn=skip_fn)
|
| -
|
| - if not hasattr(m, subname):
|
| - setattr(m, subname, submod)
|
| - else:
|
| - prev = getattr(m, subname)
|
| - assert submod is prev, (
|
| - 'Conflicting modules: %s and %s' % (prev, m))
|
| -
|
| - return m
|
| -
|
| - imp.acquire_lock()
|
| - try:
|
| - if RM not in sys.modules:
|
| - sys.modules[RM] = imp.new_module(RM)
|
| - # First import all the APIs and configs
|
| - for root in mod_dirs:
|
| - if os.path.isdir(root):
|
| - recursive_import(root, skip_fn=lambda name: name.endswith('_config'))
|
| -
|
| - # Then fixup all the modules
|
| - for name, submod in sys.modules[RM].__dict__.iteritems():
|
| - if name[0] == '_':
|
| - continue
|
| - patchup_module(submod)
|
| -
|
| - # Then import all the config extenders.
|
| - for root in mod_dirs:
|
| - if os.path.isdir(root):
|
| - recursive_import(root)
|
| - return sys.modules[RM]
|
| - finally:
|
| - imp.release_lock()
|
| -
|
| -
|
| -def CreateRecipeApi(names, mod_dirs, mocks=None, **kwargs):
|
| - """
|
| - Given a list of module names, return an instance of RecipeApi which contains
|
| - those modules as direct members.
|
| -
|
| - So, if you pass ['foobar'], you'll get an instance back which contains a
|
| - 'foobar' attribute which itself is a RecipeApi instance from the 'foobar'
|
| - module.
|
| -
|
| - Args:
|
| - names (list): A list of module names to include in the returned RecipeApi.
|
| - mod_dirs (list): A list of paths to directories which contain modules.
|
| - mocks (dict): An optional dict of {<modname>: <mock data>}. Each module
|
| - expects its own mock data.
|
| - **kwargs: Data passed to each module api. Usually this will contain:
|
| - properties (dict): the properties dictionary (used by the properties
|
| - module)
|
| - step_history (OrderedDict): the step history object (used by the
|
| - step_history module!)
|
| +def inject_test_data(func):
|
| """
|
| + Decorator which injects mock data from this module's test_api method into
|
| + the return value of the decorated function.
|
|
|
| - recipe_modules = load_recipe_modules(mod_dirs)
|
| -
|
| - inst_map = {None: RecipeApi()}
|
| - dep_map = {None: set(names)}
|
| - def create_maps(name):
|
| - if name not in dep_map:
|
| - module = getattr(recipe_modules, name)
|
| -
|
| - dep_map[name] = set(module.DEPS)
|
| - map(create_maps, dep_map[name])
|
| -
|
| - mock = None if mocks is None else mocks.get(name, {})
|
| - inst_map[name] = module.API(module=module, mock=mock, **kwargs)
|
| - map(create_maps, names)
|
| -
|
| - # NOTE: this is 'inefficient', but correct and compact.
|
| - did_something = True
|
| - while dep_map:
|
| - did_something = False
|
| - to_pop = []
|
| - for api_name, deps in dep_map.iteritems():
|
| - to_remove = []
|
| - for dep in [d for d in deps if d not in dep_map]:
|
| - # Grab the injection site
|
| - obj = inst_map[api_name].m
|
| - assert not hasattr(obj, dep)
|
| - setattr(obj, dep, inst_map[dep])
|
| - to_remove.append(dep)
|
| - did_something = True
|
| - map(deps.remove, to_remove)
|
| - if not deps:
|
| - to_pop.append(api_name)
|
| - did_something = True
|
| - map(dep_map.pop, to_pop)
|
| - assert did_something, 'Did nothing on this loop. %s' % dep_map
|
| + The return value of func MUST be a single step dictionary (specifically,
|
| + |func| must not be a generator, nor must it return a list of steps, etc.)
|
|
|
| - return inst_map[None]
|
| + When the decorated function is called, |func| is called normally. If we are
|
| + in test mode, we will then also call self.test_api.<func.__name__>, whose
|
| + return value will be assigned into the step dictionary retuned by |func|.
|
|
|
| -
|
| -def wrap_followup(kwargs, pre=False):
|
| + It is an error for the function to not exist in the test_api.
|
| + It is an error for the return value of |func| to already contain test data.
|
| """
|
| - Decorator for a new followup_fn.
|
| -
|
| - Will pop the existing fn out of kwargs (if any), and return a decorator for
|
| - the new folloup_fn.
|
| -
|
| - Args:
|
| - kwargs - dictionary possibly containing folloup_fn
|
| - pre - If true, the old folloup_fn is called before the wrapped function.
|
| - Otherwise, the old followup_fn is called after the wrapped function.
|
| - """
|
| - null_fn = lambda _: None
|
| - old_followup = kwargs.pop('followup_fn', null_fn)
|
| - def decorator(f):
|
| - @functools.wraps(f)
|
| - def _inner(step_result):
|
| - if pre:
|
| - old_followup(step_result)
|
| - f(step_result)
|
| - else:
|
| - f(step_result)
|
| - old_followup(step_result)
|
| - if old_followup is not null_fn:
|
| - _inner.__name__ += '[%s]' % old_followup.__name__
|
| - return _inner
|
| - return decorator
|
| + @functools.wraps(func)
|
| + def inner(self, *args, **kwargs):
|
| + assert isinstance(self, RecipeApi)
|
| + ret = func(self, *args, **kwargs)
|
| + if self._test_data.enabled: # pylint: disable=W0212
|
| + test_fn = getattr(self.test_api, func.__name__, None)
|
| + assert test_fn, (
|
| + "Method %(meth)s in module %(mod)s is @inject_test_data, but test_api"
|
| + " does not contain %(meth)s."
|
| + % {
|
| + 'meth': func.__name__,
|
| + 'mod': self._module, # pylint: disable=W0212
|
| + })
|
| + assert 'default_step_data' not in ret
|
| + data = test_fn(*args, **kwargs)
|
| + assert isinstance(data, StepTestData)
|
| + ret['default_step_data'] = data
|
| + return ret
|
| + return inner
|
|
|