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

Side by Side Diff: scripts/slave/recipe_test_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: License headers 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/recipe_modules/step/example.py ('k') | scripts/slave/recipe_util.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4 import collections
5
6 from .recipe_util import ModuleInjectionSite, static_call, static_wraps
7
8 def combineify(name, dest, a, b):
9 """
10 Combines dictionary members in two objects into a third one using addition.
11
12 Args:
13 name - the name of the member
14 dest - the destination object
15 a - the first source object
16 b - the second source object
17 """
18 dest_dict = getattr(dest, name)
19 dest_dict.update(getattr(a, name))
20 for k, v in getattr(b, name).iteritems():
21 if k in dest_dict:
22 dest_dict[k] += v
23 else:
24 dest_dict[k] = v
25
26
27 class BaseTestData(object):
28 def __init__(self, enabled=True):
29 super(BaseTestData, self).__init__()
30 self._enabled = enabled
31
32 @property
33 def enabled(self):
34 return self._enabled
35
36
37 class PlaceholderTestData(BaseTestData):
38 def __init__(self, data=None):
39 super(PlaceholderTestData, self).__init__()
40 self.data = data
41
42 def __repr__(self):
43 return "PlaceholderTestData(%r)" % (self.data,)
44
45
46 class StepTestData(BaseTestData):
47 """
48 Mutable container for per-step test data.
49
50 This data is consumed while running the recipe (during
51 annotated_run.run_steps).
52 """
53 def __init__(self):
54 super(StepTestData, self).__init__()
55 # { (module, placeholder) -> [data] }
56 self.placeholder_data = collections.defaultdict(list)
57 self._retcode = None
58
59 def __add__(self, other):
60 assert isinstance(other, StepTestData)
61 ret = StepTestData()
62
63 combineify('placeholder_data', ret, self, other)
64
65 # pylint: disable=W0212
66 ret._retcode = self._retcode
67 if other._retcode is not None:
68 assert ret._retcode is None
69 ret._retcode = other._retcode
70 return ret
71
72 def pop_placeholder(self, name_pieces):
73 l = self.placeholder_data[name_pieces]
74 if l:
75 return l.pop(0)
76 else:
77 return PlaceholderTestData()
78
79 @property
80 def retcode(self): # pylint: disable=E0202
81 return self._retcode or 0
82
83 @retcode.setter
84 def retcode(self, value): # pylint: disable=E0202
85 self._retcode = value
86
87 def __repr__(self):
88 return "StepTestData(%r)" % ({
89 'placeholder_data': dict(self.placeholder_data.iteritems()),
90 'retcode': self._retcode,
91 },)
92
93
94 class ModuleTestData(BaseTestData, dict):
95 """
96 Mutable container for test data for a specific module.
97
98 This test data is consumed at module load time (i.e. when CreateRecipeApi
99 runs).
100 """
101 def __add__(self, other):
102 assert isinstance(other, ModuleTestData)
103 ret = ModuleTestData()
104 ret.update(self)
105 ret.update(other)
106 return ret
107
108 def __repr__(self):
109 return "ModuleTestData(%r)" % super(ModuleTestData, self).__repr__()
110
111
112 class TestData(BaseTestData):
113 def __init__(self, name=None):
114 super(TestData, self).__init__()
115 self.name = name
116 self.properties = {} # key -> val
117 self.mod_data = collections.defaultdict(ModuleTestData)
118 self.step_data = collections.defaultdict(StepTestData)
119
120 def __add__(self, other):
121 assert isinstance(other, TestData)
122 ret = TestData(self.name or other.name)
123
124 ret.properties.update(self.properties)
125 ret.properties.update(other.properties)
126
127 combineify('mod_data', ret, self, other)
128 combineify('step_data', ret, self, other)
129
130 return ret
131
132 def empty(self):
133 return not self.step_data
134
135 def __repr__(self):
136 return "TestData(%r)" % ({
137 'name': self.name,
138 'properties': self.properties,
139 'mod_data': dict(self.mod_data.iteritems()),
140 'step_data': dict(self.step_data.iteritems()),
141 },)
142
143
144 class DisabledTestData(BaseTestData):
145 def __init__(self):
146 super(DisabledTestData, self).__init__(False)
147
148 def __getattr__(self, name):
149 return self
150
151 def pop_placeholder(self, _name_pieces):
152 return self
153
154
155 def mod_test_data(func):
156 @static_wraps(func)
157 def inner(self, *args, **kwargs):
158 assert isinstance(self, RecipeTestApi)
159 mod_name = self._module.NAME # pylint: disable=W0212
160 ret = TestData(None)
161 data = static_call(self, func, *args, **kwargs)
162 ret.mod_data[mod_name][inner.__name__] = data
163 return ret
164 return inner
165
166
167 def placeholder_step_data(func):
168 """Decorates RecipeTestApi member functions to allow those functions to
169 return just the placeholder data, instead of the normally required
170 StepTestData() object.
171
172 The wrapped function may return either:
173 * <placeholder data>, <retcode or None>
174 * StepTestData containing exactly one PlaceholderTestData and possible a
175 retcode. This is useful for returning the result of another method which
176 is wrapped with placeholder_step_data.
177
178 In either case, the wrapper function will return a StepTestData object with
179 the retcode and placeholder datum inserted with a name of:
180 (<Test module name>, <wrapped function name>)
181
182 Say you had a 'foo_module' with the following RecipeTestApi:
183 class FooTestApi(RecipeTestApi):
184 @placeholder_step_data
185 @staticmethod
186 def cool_method(data, retcode=None):
187 return ("Test data (%s)" % data), retcode
188
189 @placeholder_step_data
190 def other_method(self, retcode=None):
191 return self.cool_method('hammer time', retcode)
192
193 Code calling cool_method('hello') would get a StepTestData:
194 StepTestData(
195 placeholder_data = {
196 ('foo_module', 'cool_method'): [
197 PlaceholderTestData('Test data (hello)')
198 ]
199 },
200 retcode = None
201 )
202
203 Code calling other_method(50) would get a StepTestData:
204 StepTestData(
205 placeholder_data = {
206 ('foo_module', 'other_method'): [
207 PlaceholderTestData('Test data (hammer time)')
208 ]
209 },
210 retcode = 50
211 )
212 """
213 @static_wraps(func)
214 def inner(self, *args, **kwargs):
215 assert isinstance(self, RecipeTestApi)
216 mod_name = self._module.NAME # pylint: disable=W0212
217 data = static_call(self, func, *args, **kwargs)
218 if isinstance(data, StepTestData):
219 all_data = [i
220 for l in data.placeholder_data.values()
221 for i in l]
222 assert len(all_data) == 1, (
223 'placeholder_step_data is only expecting a single placeholder datum. '
224 'Got: %r' % data
225 )
226 placeholder_data, retcode = all_data[0], data.retcode
227 else:
228 placeholder_data, retcode = data
229 placeholder_data = PlaceholderTestData(placeholder_data)
230
231 ret = StepTestData()
232 ret.placeholder_data[(mod_name, inner.__name__)].append(placeholder_data)
233 ret.retcode = retcode
234 return ret
235 return inner
236
237
238 class RecipeTestApi(object):
239 """Provides testing interface for GenTest method.
240
241 There are two primary components to the test api:
242 * Test data creation methods (test and step_data)
243 * test_api's from all the modules in DEPS.
244
245 Every test in GenTests(api) takes the form:
246 yield <instance of TestData>
247
248 There are 4 basic pieces to TestData:
249 name - The name of the test.
250 properties - Simple key-value dictionary which is used as the combined
251 build_properties and factory_properties for this test.
252 mod_data - Module-specific testing data (see the platform module for a
253 good example). This is testing data which is only used once at
254 the start of the execution of the recipe. Modules should
255 provide methods to get their specific test information. See
256 the platform module's test_api for a good example of this.
257 step_data - Step-specific data. There are two major components to this.
258 retcode - The return code of the step
259 placeholder_data - A mapping from placeholder name to the a list of
260 PlaceholderTestData objects, one for each instance
261 of that kind of Placeholder in the step.
262
263 TestData objects are concatenatable, so it's convienent to phrase test cases
264 as a series of added TestData objects. For example:
265 DEPS = ['properties', 'platform', 'json']
266 def GenTests(api):
267 yield (
268 api.test('try_win64') +
269 api.properties.tryserver(power_level=9001) +
270 api.platform('win', 64) +
271 api.step_data(
272 'some_step',
273 api.json.output("bobface"),
274 api.json.output({'key': 'value'})
275 )
276 )
277
278 This example would run a single test (named 'try_win64') with the standard
279 tryserver properties (plus an extra property 'power_level' whose value was
280 over 9000). The test would run as if it were being run on a 64-bit windows
281 installation, and the step named 'some_step' would have its first json output
282 placeholder be mocked to return '"bobface"', and its second json output
283 placeholder be mocked to return '{"key": "value"}'.
284
285 The properties.tryserver() call is documented in the 'properties' module's
286 test_api.
287 The platform() call is documented in the 'platform' module's test_api.
288 The json.output() call is documented in the 'json' module's test_api.
289 """
290 def __init__(self, module=None, test_data=DisabledTestData()):
291 """Note: Injected dependencies are NOT available in __init__()."""
292 # If we're the 'root' api, inject directly into 'self'.
293 # Otherwise inject into 'self.m'
294 self.m = self if module is None else ModuleInjectionSite()
295 self._module = module
296
297 assert isinstance(test_data, (ModuleTestData, DisabledTestData))
298 self._test_data = test_data
299
300 @staticmethod
301 def test(name):
302 """Returns a new empty TestData with the name filled in.
303
304 Use in GenTests:
305 def GenTests(api):
306 yield api.test('basic')
307 """
308 return TestData(name)
309
310 @staticmethod
311 def step_data(name, *data, **kwargs):
312 """Returns a new TestData with the mock data filled in for a single step.
313
314 Args:
315 name - The name of the step we're providing data for
316 data - Zero or more StepTestData objects. These may fill in placeholder
317 data for zero or more modules, as well as possibly setting the
318 retcode for this step.
319 retcode=(int or None) - Override the retcode for this step, even if it
320 was set by |data|. This must be set as a keyword arg.
321
322 Use in GenTests:
323 def GenTests(api):
324 yield (
325 api.test('more') +
326 api.step_data(
327 'init',
328 api.json.output({'some': ["cool", "json"]})
329 )
330 )
331 """
332 assert all(isinstance(d, StepTestData) for d in data)
333 ret = TestData(None)
334 if data:
335 ret.step_data[name] = reduce(sum, data)
336 if 'retcode' in kwargs:
337 ret.step_data[name].retcode = kwargs['retcode']
338 return ret
OLDNEW
« no previous file with comments | « scripts/slave/recipe_modules/step/example.py ('k') | scripts/slave/recipe_util.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698