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

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

Issue 23889036: Refactor the way that TestApi works so that it is actually useful. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Move gclient test_api to got_revisions cl Created 7 years, 3 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/annotated_run.py ('k') | scripts/slave/recipe_modules/android/api.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
OLDNEW
« no previous file with comments | « scripts/slave/annotated_run.py ('k') | scripts/slave/recipe_modules/android/api.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698