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

Side by Side Diff: scripts/slave/recipe_loader.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: rebase 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
OLDNEW
(Empty)
1 import copy
2 import imp
3 import inspect
4 import os
5 import sys
6
7 from common import chromium_utils
8
9 from .recipe_util import RECIPE_DIRS, MODULE_DIRS
10 from .recipe_api import RecipeApi
11 from .recipe_test_api import RecipeTestApi, DisabledTestData, ModuleTestData
12
13
14 def load_recipe_modules(mod_dirs):
15 def patchup_module(name, submod):
16 submod.NAME = name
17 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None)
18 submod.DEPS = frozenset(getattr(submod, 'DEPS', ()))
19
20 if hasattr(submod, 'config'):
21 for v in submod.config.__dict__.itervalues():
22 if hasattr(v, 'I_AM_A_CONFIG_CTX'):
23 assert not submod.CONFIG_CTX, (
24 'More than one configuration context: %s' % (submod.config))
25 submod.CONFIG_CTX = v
26 assert submod.CONFIG_CTX, 'Config file, but no config context?'
27
28 submod.API = getattr(submod, 'API', None)
29 for v in submod.api.__dict__.itervalues():
30 if inspect.isclass(v) and issubclass(v, RecipeApi):
31 assert not submod.API, (
32 'More than one Api subclass: %s' % submod.api)
33 submod.API = v
34 assert submod.API, 'Submodule has no api? %s' % (submod)
35
36 submod.TEST_API = getattr(submod, 'TEST_API', None)
37 if hasattr(submod, 'test_api'):
38 for v in submod.test_api.__dict__.itervalues():
39 if inspect.isclass(v) and issubclass(v, RecipeTestApi):
40 assert not submod.TEST_API, (
41 'More than one TestApi subclass: %s' % submod.api)
42 submod.TEST_API = v
43 assert submod.API, (
44 'Submodule has test_api.py but no TestApi subclass? %s'
45 % (submod)
46 )
47
48 RM = 'RECIPE_MODULES'
49 def find_and_load(fullname, modname, path):
50 if fullname not in sys.modules or fullname == RM:
51 try:
52 fil, pathname, descr = imp.find_module(modname,
53 [os.path.dirname(path)])
54 imp.load_module(fullname, fil, pathname, descr)
55 finally:
56 if fil:
57 fil.close()
58 return sys.modules[fullname]
59
60 def recursive_import(path, prefix=None, skip_fn=lambda name: False):
61 modname = os.path.splitext(os.path.basename(path))[0]
62 if prefix:
63 fullname = '%s.%s' % (prefix, modname)
64 else:
65 fullname = RM
66 m = find_and_load(fullname, modname, path)
67 if not os.path.isdir(path):
68 return m
69
70 for subitem in os.listdir(path):
71 subpath = os.path.join(path, subitem)
72 subname = os.path.splitext(subitem)[0]
73 if skip_fn(subname):
74 continue
75 if os.path.isdir(subpath):
76 if not os.path.exists(os.path.join(subpath, '__init__.py')):
77 continue
78 elif not subpath.endswith('.py') or subitem.startswith('__init__.py'):
79 continue
80
81 submod = recursive_import(subpath, fullname, skip_fn=skip_fn)
82
83 if not hasattr(m, subname):
84 setattr(m, subname, submod)
85 else:
86 prev = getattr(m, subname)
87 assert submod is prev, (
88 'Conflicting modules: %s and %s' % (prev, m))
89
90 return m
91
92 imp.acquire_lock()
93 try:
94 if RM not in sys.modules:
95 sys.modules[RM] = imp.new_module(RM)
96 # First import all the APIs and configs
97 for root in mod_dirs:
98 if os.path.isdir(root):
99 recursive_import(root, skip_fn=lambda name: name.endswith('_config'))
100
101 # Then fixup all the modules
102 for name, submod in sys.modules[RM].__dict__.iteritems():
103 if name[0] == '_':
104 continue
105 patchup_module(name, submod)
106
107 # Then import all the config extenders.
108 for root in mod_dirs:
109 if os.path.isdir(root):
110 recursive_import(root)
111 return sys.modules[RM]
112 finally:
113 imp.release_lock()
114
115
116 def CreateApi(mod_dirs, names, test_data=DisabledTestData(), required=None,
117 optional=None, kwargs=None):
118 """
119 Given a list of module names, return an instance of RecipeApi which contains
120 those modules as direct members.
121
122 So, if you pass ['foobar'], you'll get an instance back which contains a
123 'foobar' attribute which itself is a RecipeApi instance from the 'foobar'
124 module.
125
126 Args:
127 names (list): A list of module names to include in the returned RecipeApi.
128 mod_dirs (list): A list of paths to directories which contain modules.
129 test_data (TestData): ...
130 kwargs: Data passed to each module api. Usually this will contain:
131 properties (dict): the properties dictionary (used by the properties
132 module)
133 step_history (OrderedDict): the step history object (used by the
134 step_history module!)
135 """
136 kwargs = kwargs or {}
137 recipe_modules = load_recipe_modules(mod_dirs)
138
139 inst_maps = {}
140 if required:
141 inst_maps[required[0]] = { None: required[1]() }
142 if optional:
143 inst_maps[optional[0]] = { None: optional[1]() }
144
145 dep_map = {None: set(names)}
146 def create_maps(name):
147 if name not in dep_map:
148 module = getattr(recipe_modules, name)
149
150 dep_map[name] = set(module.DEPS)
151 map(create_maps, dep_map[name])
152
153 mod_test = DisabledTestData()
154 if test_data.enabled:
155 mod_test = test_data.mod_data.get(name, ModuleTestData())
156
157 if required:
158 api = getattr(module, required[0])
159 inst_maps[required[0]][name] = api(module=module,
160 test_data=mod_test, **kwargs)
161 if optional:
162 api = getattr(module, optional[0], None) or optional[1]
163 inst_maps[optional[0]][name] = api(module=module,
164 test_data=mod_test)
165
166 map(create_maps, names)
167
168 if required:
169 MapDependencies(dep_map, inst_maps[required[0]])
170 if optional:
171 MapDependencies(dep_map, inst_maps[optional[0]])
172 if required:
173 for name, module in inst_maps[required[0]].iteritems():
174 module.test_api = inst_maps[optional[0]][name]
175
176 return inst_maps[(required or optional)[0]][None]
177
178
179 def MapDependencies(dep_map, inst_map):
180 # NOTE: this is 'inefficient', but correct and compact.
181 dep_map = copy.deepcopy(dep_map)
182 while dep_map:
183 did_something = False
184 to_pop = []
185 for api_name, deps in dep_map.iteritems():
186 to_remove = []
187 for dep in [d for d in deps if d not in dep_map]:
188 # Grab the injection site
189 obj = inst_map[api_name].m
190 assert not hasattr(obj, dep)
191 setattr(obj, dep, inst_map[dep])
192 to_remove.append(dep)
193 did_something = True
194 map(deps.remove, to_remove)
195 if not deps:
196 to_pop.append(api_name)
197 did_something = True
198 map(dep_map.pop, to_pop)
199 assert did_something, 'Did nothing on this loop. %s' % dep_map
200
201
202 def CreateTestApi(names):
203 return CreateApi(MODULE_DIRS(), names, optional=('TEST_API', RecipeTestApi))
204
205
206 def CreateRecipeApi(names, test_data=DisabledTestData(), **kwargs):
207 return CreateApi(MODULE_DIRS(), names, test_data=test_data, kwargs=kwargs,
208 required=('API', RecipeApi),
209 optional=('TEST_API', RecipeTestApi))
210
211
212 class NoSuchRecipe(Exception):
213 pass
214
215
216 def LoadRecipe(recipe):
217 # If the recipe is specified as "module:recipe", then it is an recipe
218 # contained in a recipe_module as an example. Look for it in the modules
219 # imported by load_recipe_modules instead of the normal search paths.
220 if ':' in recipe:
221 module_name, example = recipe.split(':')
222 assert example.endswith('example')
223 RECIPE_MODULES = load_recipe_modules(MODULE_DIRS())
224 try:
225 return getattr(getattr(RECIPE_MODULES, module_name), example)
226 except AttributeError:
227 pass
228 else:
229 for recipe_path in (os.path.join(p, recipe) for p in RECIPE_DIRS()):
230 recipe_module = chromium_utils.IsolatedImportFromPath(recipe_path)
231 if recipe_module:
232 return recipe_module
233 raise NoSuchRecipe(recipe)
234
235
236 def find_recipes(path, predicate):
237 for root, _dirs, files in os.walk(path):
238 for recipe in (f for f in files if predicate(f)):
239 recipe_path = os.path.join(root, recipe)
240 yield recipe_path
241
242
243 def loop_over_recipes():
244 for path in RECIPE_DIRS():
245 for recipe in find_recipes(
246 path, lambda f: f.endswith('.py') and f[0] != '_'):
247 yield recipe, recipe[len(path)+1:-len('.py')]
248 for path in MODULE_DIRS():
249 for recipe in find_recipes(
250 path, lambda f: f.endswith('example.py')):
251 module_name = os.path.dirname(recipe)[len(path)+1:]
252 yield recipe, '%s:example' % module_name
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698