OLD | NEW |
---|---|
1 # Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import copy | |
5 import functools | 6 import functools |
6 import imp | 7 import imp |
7 import inspect | 8 import inspect |
8 import os | 9 import os |
9 import sys | 10 import sys |
10 import tempfile | 11 import tempfile |
11 | 12 |
12 | 13 |
13 class Placeholder(object): | 14 class Placeholder(object): |
14 """Base class for json placeholders. Do not use directly.""" | 15 """Base class for json placeholders. Do not use directly.""" |
(...skipping 26 matching lines...) Expand all Loading... | |
41 return [self.input_file] | 42 return [self.input_file] |
42 | 43 |
43 def step_finished(self, presentation, step_result, test_data): | 44 def step_finished(self, presentation, step_result, test_data): |
44 if test_data is None: # pragma: no cover | 45 if test_data is None: # pragma: no cover |
45 os.unlink(self.input_file) | 46 os.unlink(self.input_file) |
46 | 47 |
47 | 48 |
48 class ModuleInjectionSite(object): | 49 class ModuleInjectionSite(object): |
49 pass | 50 pass |
50 | 51 |
52 class RecipeTestApi(object): | |
53 def __init__(self, module=None): | |
54 """Note: Injected dependencies are NOT available in __init__().""" | |
55 # If we're the 'root' api, inject directly into 'self'. | |
56 # Otherwise inject into 'self.m' | |
57 self.m = self if module is None else ModuleInjectionSite() | |
51 | 58 |
52 class RecipeApi(object): | 59 class RecipeApi(object): |
53 """ | 60 """ |
54 Framework class for handling recipe_modules. | 61 Framework class for handling recipe_modules. |
55 | 62 |
56 Inherit from this in your recipe_modules/<name>/api.py . This class provides | 63 Inherit from this in your recipe_modules/<name>/api.py . This class provides |
57 wiring for your config context (in self.c and methods, and for dependency | 64 wiring for your config context (in self.c and methods, and for dependency |
58 injection (in self.m). | 65 injection (in self.m). |
59 | 66 |
60 Dependency injection takes place in load_recipe_modules() below. | 67 Dependency injection takes place in load_recipe_modules() below. |
61 """ | 68 """ |
62 def __init__(self, module=None, mock=None, **_kwargs): | 69 def __init__(self, module=None, mock=None, **_kwargs): |
63 """Note: Injected dependencies are NOT available in __init__().""" | 70 """Note: Injected dependencies are NOT available in __init__().""" |
64 self.c = None | 71 self.c = None |
65 self._module = module | 72 self._module = module |
66 self._mock = mock | 73 self._mock = mock |
67 | 74 |
68 # If we're the 'root' api, inject directly into 'self'. | 75 # If we're the 'root' api, inject directly into 'self'. |
69 # Otherwise inject into 'self.m' | 76 # Otherwise inject into 'self.m' |
70 self.m = self if module is None else ModuleInjectionSite() | 77 self.m = self if module is None else ModuleInjectionSite() |
71 | 78 |
79 # If our module has a test api, it gets injected here. | |
80 self.test_api = None | |
81 | |
82 | |
72 def _sanitize_config_vars(self, config_name, CONFIG_VARS): | 83 def _sanitize_config_vars(self, config_name, CONFIG_VARS): |
73 params = self.get_config_defaults(config_name) | 84 params = self.get_config_defaults(config_name) |
74 params.update(CONFIG_VARS) | 85 params.update(CONFIG_VARS) |
75 return params | 86 return params |
76 | 87 |
77 def get_config_defaults(self, _config_name): # pylint: disable=R0201 | 88 def get_config_defaults(self, _config_name): # pylint: disable=R0201 |
78 """ | 89 """ |
79 Allows your api to dynamically determine static default values for configs. | 90 Allows your api to dynamically determine static default values for configs. |
80 """ | 91 """ |
81 return {} | 92 return {} |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
114 | 125 |
115 def apply_config(self, config_name, config_object=None): | 126 def apply_config(self, config_name, config_object=None): |
116 """Apply a named configuration to the provided config object or self.""" | 127 """Apply a named configuration to the provided config object or self.""" |
117 self._module.CONFIG_CTX.CONFIG_ITEMS[config_name](config_object or self.c) | 128 self._module.CONFIG_CTX.CONFIG_ITEMS[config_name](config_object or self.c) |
118 | 129 |
119 | 130 |
120 def load_recipe_modules(mod_dirs): | 131 def load_recipe_modules(mod_dirs): |
121 def patchup_module(submod): | 132 def patchup_module(submod): |
122 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) | 133 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) |
123 submod.API = getattr(submod, 'API', None) | 134 submod.API = getattr(submod, 'API', None) |
135 submod.TEST_API = getattr(submod, 'TEST_API', None) | |
124 submod.DEPS = frozenset(getattr(submod, 'DEPS', ())) | 136 submod.DEPS = frozenset(getattr(submod, 'DEPS', ())) |
125 | 137 |
126 if hasattr(submod, 'config'): | 138 if hasattr(submod, 'config'): |
127 for v in submod.config.__dict__.itervalues(): | 139 for v in submod.config.__dict__.itervalues(): |
128 if hasattr(v, 'I_AM_A_CONFIG_CTX'): | 140 if hasattr(v, 'I_AM_A_CONFIG_CTX'): |
129 assert not submod.CONFIG_CTX, ( | 141 assert not submod.CONFIG_CTX, ( |
130 'More than one configuration context: %s' % (submod.config)) | 142 'More than one configuration context: %s' % (submod.config)) |
131 submod.CONFIG_CTX = v | 143 submod.CONFIG_CTX = v |
132 assert submod.CONFIG_CTX, 'Config file, but no config context?' | 144 assert submod.CONFIG_CTX, 'Config file, but no config context?' |
133 | 145 |
134 for v in submod.api.__dict__.itervalues(): | 146 for v in submod.api.__dict__.itervalues(): |
agable
2013/09/16 20:29:25
Since the API is the most basic part of the module
| |
135 if inspect.isclass(v) and issubclass(v, RecipeApi): | 147 if inspect.isclass(v) and issubclass(v, RecipeApi): |
136 assert not submod.API, ( | 148 assert not submod.API, ( |
137 'More than one Api subclass: %s' % submod.api) | 149 'More than one Api subclass: %s' % submod.api) |
138 submod.API = v | 150 submod.API = v |
139 | 151 |
140 assert submod.API, 'Submodule has no api? %s' % (submod) | 152 assert submod.API, 'Submodule has no api? %s' % (submod) |
141 | 153 |
154 if hasattr(submod, 'test_api'): | |
155 for v in submod.test_api.__dict__.itervalues(): | |
156 if inspect.isclass(v) and issubclass(v, RecipeTestApi): | |
157 assert not submod.TEST_API, ( | |
158 'More than one TestApi subclass: %s' % submod.api) | |
159 submod.TEST_API = v | |
160 assert submod.API, ( | |
161 'Submodule has test_api.py but no TestApi subclass? %s' | |
162 % (submod) | |
163 ) | |
164 | |
142 RM = 'RECIPE_MODULES' | 165 RM = 'RECIPE_MODULES' |
143 def find_and_load(fullname, modname, path): | 166 def find_and_load(fullname, modname, path): |
144 if fullname not in sys.modules or fullname == RM: | 167 if fullname not in sys.modules or fullname == RM: |
145 try: | 168 try: |
146 fil, pathname, descr = imp.find_module(modname, | 169 fil, pathname, descr = imp.find_module(modname, |
147 [os.path.dirname(path)]) | 170 [os.path.dirname(path)]) |
148 imp.load_module(fullname, fil, pathname, descr) | 171 imp.load_module(fullname, fil, pathname, descr) |
149 finally: | 172 finally: |
150 if fil: | 173 if fil: |
151 fil.close() | 174 fil.close() |
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
200 | 223 |
201 # Then import all the config extenders. | 224 # Then import all the config extenders. |
202 for root in mod_dirs: | 225 for root in mod_dirs: |
203 if os.path.isdir(root): | 226 if os.path.isdir(root): |
204 recursive_import(root) | 227 recursive_import(root) |
205 return sys.modules[RM] | 228 return sys.modules[RM] |
206 finally: | 229 finally: |
207 imp.release_lock() | 230 imp.release_lock() |
208 | 231 |
209 | 232 |
210 def CreateRecipeApi(names, mod_dirs, mocks=None, **kwargs): | 233 def CreateApi(mod_dirs, names, mocks=None, required=None, |
agable
2013/09/16 20:29:25
If 'required' and 'optional' are not antitheses of
| |
234 optional=None, kwargs=None): | |
211 """ | 235 """ |
212 Given a list of module names, return an instance of RecipeApi which contains | 236 Given a list of module names, return an instance of RecipeApi which contains |
agable
2013/09/16 20:29:25
Update docstring to reflect new argument ordering.
| |
213 those modules as direct members. | 237 those modules as direct members. |
214 | 238 |
215 So, if you pass ['foobar'], you'll get an instance back which contains a | 239 So, if you pass ['foobar'], you'll get an instance back which contains a |
216 'foobar' attribute which itself is a RecipeApi instance from the 'foobar' | 240 'foobar' attribute which itself is a RecipeApi instance from the 'foobar' |
217 module. | 241 module. |
218 | 242 |
219 Args: | 243 Args: |
220 names (list): A list of module names to include in the returned RecipeApi. | 244 names (list): A list of module names to include in the returned RecipeApi. |
221 mod_dirs (list): A list of paths to directories which contain modules. | 245 mod_dirs (list): A list of paths to directories which contain modules. |
222 mocks (dict): An optional dict of {<modname>: <mock data>}. Each module | 246 mocks (dict): An optional dict of {<modname>: <mock data>}. Each module |
223 expects its own mock data. | 247 expects its own mock data. |
224 **kwargs: Data passed to each module api. Usually this will contain: | 248 kwargs: Data passed to each module api. Usually this will contain: |
225 properties (dict): the properties dictionary (used by the properties | 249 properties (dict): the properties dictionary (used by the properties |
226 module) | 250 module) |
227 step_history (OrderedDict): the step history object (used by the | 251 step_history (OrderedDict): the step history object (used by the |
228 step_history module!) | 252 step_history module!) |
229 """ | 253 """ |
230 | 254 kwargs = kwargs or {} |
231 recipe_modules = load_recipe_modules(mod_dirs) | 255 recipe_modules = load_recipe_modules(mod_dirs) |
232 | 256 |
233 inst_map = {None: RecipeApi()} | 257 inst_maps = {} |
258 if required: | |
agable
2013/09/16 20:29:25
Definitely not a fan of the 'required' and 'option
| |
259 inst_maps[required[0]] = { None: required[1]() } | |
260 if optional: | |
261 inst_maps[optional[0]] = { None: optional[1]() } | |
262 | |
234 dep_map = {None: set(names)} | 263 dep_map = {None: set(names)} |
235 def create_maps(name): | 264 def create_maps(name): |
236 if name not in dep_map: | 265 if name not in dep_map: |
237 module = getattr(recipe_modules, name) | 266 module = getattr(recipe_modules, name) |
238 | 267 |
239 dep_map[name] = set(module.DEPS) | 268 dep_map[name] = set(module.DEPS) |
240 map(create_maps, dep_map[name]) | 269 map(create_maps, dep_map[name]) |
241 | 270 |
242 mock = None if mocks is None else mocks.get(name, {}) | 271 mock = None if mocks is None else mocks.get(name, {}) |
243 inst_map[name] = module.API(module=module, mock=mock, **kwargs) | 272 if required: |
273 api = getattr(module, required[0]) | |
274 inst_maps[required[0]][name] = api(module=module, mock=mock, **kwargs) | |
275 if optional: | |
276 api = getattr(module, optional[0], None) or optional[1] | |
277 inst_maps[optional[0]][name] = api(module=module) | |
agable
2013/09/16 20:29:25
As long as you're treating these as "required" and
| |
278 | |
244 map(create_maps, names) | 279 map(create_maps, names) |
245 | 280 |
281 if required: | |
282 MapDependencies(dep_map, inst_maps[required[0]]) | |
283 if optional: | |
284 MapDependencies(dep_map, inst_maps[optional[0]]) | |
285 if required: | |
286 for name, module in inst_maps[required[0]].iteritems(): | |
287 module.test_api = inst_maps[optional[0]][name] | |
288 | |
289 return inst_maps[(required or optional)[0]][None] | |
290 | |
291 | |
292 def MapDependencies(dep_map, inst_map): | |
agable
2013/09/16 20:29:25
While we're here, please rename "to_remove" and "t
| |
246 # NOTE: this is 'inefficient', but correct and compact. | 293 # NOTE: this is 'inefficient', but correct and compact. |
247 did_something = True | 294 dep_map = copy.deepcopy(dep_map) |
248 while dep_map: | 295 while dep_map: |
249 did_something = False | 296 did_something = False |
250 to_pop = [] | 297 to_pop = [] |
251 for api_name, deps in dep_map.iteritems(): | 298 for api_name, deps in dep_map.iteritems(): |
252 to_remove = [] | 299 to_remove = [] |
253 for dep in [d for d in deps if d not in dep_map]: | 300 for dep in [d for d in deps if d not in dep_map]: |
254 # Grab the injection site | 301 # Grab the injection site |
255 obj = inst_map[api_name].m | 302 obj = inst_map[api_name].m |
256 assert not hasattr(obj, dep) | 303 assert not hasattr(obj, dep) |
257 setattr(obj, dep, inst_map[dep]) | 304 setattr(obj, dep, inst_map[dep]) |
258 to_remove.append(dep) | 305 to_remove.append(dep) |
259 did_something = True | 306 did_something = True |
260 map(deps.remove, to_remove) | 307 map(deps.remove, to_remove) |
261 if not deps: | 308 if not deps: |
262 to_pop.append(api_name) | 309 to_pop.append(api_name) |
263 did_something = True | 310 did_something = True |
264 map(dep_map.pop, to_pop) | 311 map(dep_map.pop, to_pop) |
265 assert did_something, 'Did nothing on this loop. %s' % dep_map | 312 assert did_something, 'Did nothing on this loop. %s' % dep_map |
266 | 313 |
267 return inst_map[None] | 314 |
315 def CreateRecipeApi(mod_dirs, names, mocks=None, **kwargs): | |
316 return CreateApi(mod_dirs, names, mocks=mocks, kwargs=kwargs, | |
317 required=('API', RecipeApi), | |
318 optional=('TEST_API', RecipeTestApi)) | |
319 | |
320 | |
321 def CreateTestApi(mod_dirs, names): | |
322 return CreateApi(mod_dirs, names, optional=('TEST_API', RecipeTestApi)) | |
268 | 323 |
269 | 324 |
270 def wrap_followup(kwargs, pre=False): | 325 def wrap_followup(kwargs, pre=False): |
271 """ | 326 """ |
272 Decorator for a new followup_fn. | 327 Decorator for a new followup_fn. |
273 | 328 |
274 Will pop the existing fn out of kwargs (if any), and return a decorator for | 329 Will pop the existing fn out of kwargs (if any), and return a decorator for |
275 the new folloup_fn. | 330 the new folloup_fn. |
276 | 331 |
277 Args: | 332 Args: |
278 kwargs - dictionary possibly containing folloup_fn | 333 kwargs - dictionary possibly containing folloup_fn |
279 pre - If true, the old folloup_fn is called before the wrapped function. | 334 pre - If true, the old folloup_fn is called before the wrapped function. |
280 Otherwise, the old followup_fn is called after the wrapped function. | 335 Otherwise, the old followup_fn is called after the wrapped function. |
281 """ | 336 """ |
282 null_fn = lambda _: None | 337 null_fn = lambda _: None |
283 old_followup = kwargs.pop('followup_fn', null_fn) | 338 old_followup = kwargs.pop('followup_fn', null_fn) |
284 def decorator(f): | 339 def decorator(f): |
285 @functools.wraps(f) | 340 @functools.wraps(f) |
286 def _inner(step_result): | 341 def _inner(step_result): |
287 if pre: | 342 if pre: |
288 old_followup(step_result) | 343 old_followup(step_result) |
289 f(step_result) | 344 f(step_result) |
290 else: | 345 else: |
291 f(step_result) | 346 f(step_result) |
292 old_followup(step_result) | 347 old_followup(step_result) |
293 if old_followup is not null_fn: | 348 if old_followup is not null_fn: |
294 _inner.__name__ += '[%s]' % old_followup.__name__ | 349 _inner.__name__ += '[%s]' % old_followup.__name__ |
295 return _inner | 350 return _inner |
296 return decorator | 351 return decorator |
OLD | NEW |