OLD | NEW |
| (Empty) |
1 # Copyright 2013-2015 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 | |
5 import collections | |
6 | |
7 from .util import ModuleInjectionSite, static_call, static_wraps | |
8 | |
9 def combineify(name, dest, a, b): | |
10 """ | |
11 Combines dictionary members in two objects into a third one using addition. | |
12 | |
13 Args: | |
14 name - the name of the member | |
15 dest - the destination object | |
16 a - the first source object | |
17 b - the second source object | |
18 """ | |
19 dest_dict = getattr(dest, name) | |
20 dest_dict.update(getattr(a, name)) | |
21 for k, v in getattr(b, name).iteritems(): | |
22 if k in dest_dict: | |
23 dest_dict[k] += v | |
24 else: | |
25 dest_dict[k] = v | |
26 | |
27 | |
28 class BaseTestData(object): | |
29 def __init__(self, enabled=True): | |
30 super(BaseTestData, self).__init__() | |
31 self._enabled = enabled | |
32 | |
33 @property | |
34 def enabled(self): | |
35 return self._enabled | |
36 | |
37 | |
38 class PlaceholderTestData(BaseTestData): | |
39 def __init__(self, data=None): | |
40 super(PlaceholderTestData, self).__init__() | |
41 self.data = data | |
42 | |
43 def __repr__(self): | |
44 return "PlaceholderTestData(%r)" % (self.data,) | |
45 | |
46 | |
47 class StepTestData(BaseTestData): | |
48 """ | |
49 Mutable container for per-step test data. | |
50 | |
51 This data is consumed while running the recipe (during | |
52 annotated_run.run_steps). | |
53 """ | |
54 def __init__(self): | |
55 super(StepTestData, self).__init__() | |
56 # { (module, placeholder) -> [data] } | |
57 self.placeholder_data = collections.defaultdict(list) | |
58 self.override = False | |
59 self._stdout = None | |
60 self._stderr = None | |
61 self._retcode = None | |
62 | |
63 def __add__(self, other): | |
64 assert isinstance(other, StepTestData) | |
65 | |
66 if other.override: | |
67 return other | |
68 | |
69 ret = StepTestData() | |
70 | |
71 combineify('placeholder_data', ret, self, other) | |
72 | |
73 # pylint: disable=W0212 | |
74 ret._stdout = other._stdout or self._stdout | |
75 ret._stderr = other._stderr or self._stderr | |
76 ret._retcode = self._retcode | |
77 if other._retcode is not None: | |
78 assert ret._retcode is None | |
79 ret._retcode = other._retcode | |
80 | |
81 return ret | |
82 | |
83 def unwrap_placeholder(self): | |
84 # {(module, placeholder): [data]} => data. | |
85 assert len(self.placeholder_data) == 1 | |
86 data_list = self.placeholder_data.items()[0][1] | |
87 assert len(data_list) == 1 | |
88 return data_list[0] | |
89 | |
90 def pop_placeholder(self, name_pieces): | |
91 l = self.placeholder_data[name_pieces] | |
92 if l: | |
93 return l.pop(0) | |
94 else: | |
95 return PlaceholderTestData() | |
96 | |
97 @property | |
98 def retcode(self): # pylint: disable=E0202 | |
99 return self._retcode or 0 | |
100 | |
101 @retcode.setter | |
102 def retcode(self, value): # pylint: disable=E0202 | |
103 self._retcode = value | |
104 | |
105 @property | |
106 def stdout(self): | |
107 return self._stdout or PlaceholderTestData(None) | |
108 | |
109 @stdout.setter | |
110 def stdout(self, value): | |
111 assert isinstance(value, PlaceholderTestData) | |
112 self._stdout = value | |
113 | |
114 @property | |
115 def stderr(self): | |
116 return self._stderr or PlaceholderTestData(None) | |
117 | |
118 @stderr.setter | |
119 def stderr(self, value): | |
120 assert isinstance(value, PlaceholderTestData) | |
121 self._stderr = value | |
122 | |
123 @property | |
124 def stdin(self): # pylint: disable=R0201 | |
125 return PlaceholderTestData(None) | |
126 | |
127 def __repr__(self): | |
128 return "StepTestData(%r)" % ({ | |
129 'placeholder_data': dict(self.placeholder_data.iteritems()), | |
130 'stdout': self._stdout, | |
131 'stderr': self._stderr, | |
132 'retcode': self._retcode, | |
133 'override': self.override, | |
134 },) | |
135 | |
136 | |
137 class ModuleTestData(BaseTestData, dict): | |
138 """ | |
139 Mutable container for test data for a specific module. | |
140 | |
141 This test data is consumed at module load time (i.e. when create_recipe_api | |
142 runs). | |
143 """ | |
144 def __add__(self, other): | |
145 assert isinstance(other, ModuleTestData) | |
146 ret = ModuleTestData() | |
147 ret.update(self) | |
148 ret.update(other) | |
149 return ret | |
150 | |
151 def __repr__(self): | |
152 return "ModuleTestData(%r)" % super(ModuleTestData, self).__repr__() | |
153 | |
154 | |
155 class TestData(BaseTestData): | |
156 def __init__(self, name=None): | |
157 super(TestData, self).__init__() | |
158 self.name = name | |
159 self.properties = {} # key -> val | |
160 self.mod_data = collections.defaultdict(ModuleTestData) | |
161 self.step_data = collections.defaultdict(StepTestData) | |
162 self.expected_exception = None | |
163 | |
164 def __add__(self, other): | |
165 assert isinstance(other, TestData) | |
166 ret = TestData(self.name or other.name) | |
167 | |
168 ret.properties.update(self.properties) | |
169 ret.properties.update(other.properties) | |
170 | |
171 combineify('mod_data', ret, self, other) | |
172 combineify('step_data', ret, self, other) | |
173 ret.expected_exception = self.expected_exception | |
174 if other.expected_exception: | |
175 ret.expected_exception = other.expected_exception | |
176 | |
177 return ret | |
178 | |
179 def empty(self): | |
180 return not self.step_data | |
181 | |
182 def pop_step_test_data(self, step_name, step_test_data_fn): | |
183 step_test_data = step_test_data_fn() | |
184 if step_name in self.step_data: | |
185 step_test_data += self.step_data.pop(step_name) | |
186 return step_test_data | |
187 | |
188 def get_module_test_data(self, module_name): | |
189 return self.mod_data.get(module_name, ModuleTestData()) | |
190 | |
191 def expect_exception(self, exception): | |
192 assert not self.expected_exception | |
193 self.expected_exception = exception | |
194 | |
195 def is_unexpected_exception(self, exception): | |
196 name = exception.__class__.__name__ | |
197 return not (self.enabled and name == self.expected_exception) | |
198 | |
199 def __repr__(self): | |
200 return "TestData(%r)" % ({ | |
201 'name': self.name, | |
202 'properties': self.properties, | |
203 'mod_data': dict(self.mod_data.iteritems()), | |
204 'step_data': dict(self.step_data.iteritems()), | |
205 'expected_exception': self.expected_exception, | |
206 },) | |
207 | |
208 | |
209 class DisabledTestData(BaseTestData): | |
210 def __init__(self): | |
211 super(DisabledTestData, self).__init__(False) | |
212 | |
213 def __getattr__(self, name): | |
214 return self | |
215 | |
216 def pop_placeholder(self, _name_pieces): | |
217 return self | |
218 | |
219 def pop_step_test_data(self, _step_name, _step_test_data_fn): | |
220 return self | |
221 | |
222 def get_module_test_data(self, _module_name): | |
223 return self | |
224 | |
225 def is_unexpected_exception(self, exception): #pylint: disable=R0201 | |
226 return False | |
227 | |
228 def mod_test_data(func): | |
229 @static_wraps(func) | |
230 def inner(self, *args, **kwargs): | |
231 assert isinstance(self, RecipeTestApi) | |
232 mod_name = self._module.NAME # pylint: disable=W0212 | |
233 ret = TestData(None) | |
234 data = static_call(self, func, *args, **kwargs) | |
235 ret.mod_data[mod_name][inner.__name__] = data | |
236 return ret | |
237 return inner | |
238 | |
239 | |
240 def placeholder_step_data(func): | |
241 """Decorates RecipeTestApi member functions to allow those functions to | |
242 return just the placeholder data, instead of the normally required | |
243 StepTestData() object. | |
244 | |
245 The wrapped function may return either: | |
246 * <placeholder data>, <retcode or None> | |
247 * StepTestData containing exactly one PlaceholderTestData and possible a | |
248 retcode. This is useful for returning the result of another method which | |
249 is wrapped with placeholder_step_data. | |
250 | |
251 In either case, the wrapper function will return a StepTestData object with | |
252 the retcode and placeholder datum inserted with a name of: | |
253 (<Test module name>, <wrapped function name>) | |
254 | |
255 Say you had a 'foo_module' with the following RecipeTestApi: | |
256 class FooTestApi(RecipeTestApi): | |
257 @placeholder_step_data | |
258 @staticmethod | |
259 def cool_method(data, retcode=None): | |
260 return ("Test data (%s)" % data), retcode | |
261 | |
262 @placeholder_step_data | |
263 def other_method(self, retcode=None): | |
264 return self.cool_method('hammer time', retcode) | |
265 | |
266 Code calling cool_method('hello') would get a StepTestData: | |
267 StepTestData( | |
268 placeholder_data = { | |
269 ('foo_module', 'cool_method'): [ | |
270 PlaceholderTestData('Test data (hello)') | |
271 ] | |
272 }, | |
273 retcode = None | |
274 ) | |
275 | |
276 Code calling other_method(50) would get a StepTestData: | |
277 StepTestData( | |
278 placeholder_data = { | |
279 ('foo_module', 'other_method'): [ | |
280 PlaceholderTestData('Test data (hammer time)') | |
281 ] | |
282 }, | |
283 retcode = 50 | |
284 ) | |
285 """ | |
286 @static_wraps(func) | |
287 def inner(self, *args, **kwargs): | |
288 assert isinstance(self, RecipeTestApi) | |
289 mod_name = self._module.NAME # pylint: disable=W0212 | |
290 data = static_call(self, func, *args, **kwargs) | |
291 if isinstance(data, StepTestData): | |
292 all_data = [i | |
293 for l in data.placeholder_data.values() | |
294 for i in l] | |
295 assert len(all_data) == 1, ( | |
296 'placeholder_step_data is only expecting a single placeholder datum. ' | |
297 'Got: %r' % data | |
298 ) | |
299 placeholder_data, retcode = all_data[0], data.retcode | |
300 else: | |
301 placeholder_data, retcode = data | |
302 placeholder_data = PlaceholderTestData(placeholder_data) | |
303 | |
304 ret = StepTestData() | |
305 ret.placeholder_data[(mod_name, inner.__name__)].append(placeholder_data) | |
306 ret.retcode = retcode | |
307 return ret | |
308 return inner | |
309 | |
310 | |
311 class RecipeTestApi(object): | |
312 """Provides testing interface for GenTest method. | |
313 | |
314 There are two primary components to the test api: | |
315 * Test data creation methods (test and step_data) | |
316 * test_api's from all the modules in DEPS. | |
317 | |
318 Every test in GenTests(api) takes the form: | |
319 yield <instance of TestData> | |
320 | |
321 There are 4 basic pieces to TestData: | |
322 name - The name of the test. | |
323 properties - Simple key-value dictionary which is used as the combined | |
324 build_properties and factory_properties for this test. | |
325 mod_data - Module-specific testing data (see the platform module for a | |
326 good example). This is testing data which is only used once at | |
327 the start of the execution of the recipe. Modules should | |
328 provide methods to get their specific test information. See | |
329 the platform module's test_api for a good example of this. | |
330 step_data - Step-specific data. There are two major components to this. | |
331 retcode - The return code of the step | |
332 placeholder_data - A mapping from placeholder name to the a list of | |
333 PlaceholderTestData objects, one for each instance | |
334 of that kind of Placeholder in the step. | |
335 stdout, stderr - PlaceholderTestData objects for stdout and stderr. | |
336 | |
337 TestData objects are concatenatable, so it's convenient to phrase test cases | |
338 as a series of added TestData objects. For example: | |
339 DEPS = ['properties', 'platform', 'json'] | |
340 def GenTests(api): | |
341 yield ( | |
342 api.test('try_win64') + | |
343 api.properties.tryserver(power_level=9001) + | |
344 api.platform('win', 64) + | |
345 api.step_data( | |
346 'some_step', | |
347 api.json.output("bobface"), | |
348 api.json.output({'key': 'value'}) | |
349 ) | |
350 ) | |
351 | |
352 This example would run a single test (named 'try_win64') with the standard | |
353 tryserver properties (plus an extra property 'power_level' whose value was | |
354 over 9000). The test would run as if it were being run on a 64-bit windows | |
355 installation, and the step named 'some_step' would have its first json output | |
356 placeholder be mocked to return '"bobface"', and its second json output | |
357 placeholder be mocked to return '{"key": "value"}'. | |
358 | |
359 The properties.tryserver() call is documented in the 'properties' module's | |
360 test_api. | |
361 The platform() call is documented in the 'platform' module's test_api. | |
362 The json.output() call is documented in the 'json' module's test_api. | |
363 """ | |
364 def __init__(self, module=None): | |
365 """Note: Injected dependencies are NOT available in __init__().""" | |
366 # If we're the 'root' api, inject directly into 'self'. | |
367 # Otherwise inject into 'self.m' | |
368 self.m = self if module is None else ModuleInjectionSite() | |
369 self._module = module | |
370 | |
371 @staticmethod | |
372 def test(name): | |
373 """Returns a new empty TestData with the name filled in. | |
374 | |
375 Use in GenTests: | |
376 def GenTests(api): | |
377 yield api.test('basic') | |
378 """ | |
379 return TestData(name) | |
380 | |
381 @staticmethod | |
382 def empty_test_data(): | |
383 """Returns a TestData with no information. | |
384 | |
385 This is the identity of the + operator for combining TestData. | |
386 """ | |
387 return TestData(None) | |
388 | |
389 @staticmethod | |
390 def _step_data(name, *data, **kwargs): | |
391 """Returns a new TestData with the mock data filled in for a single step. | |
392 | |
393 Used by step_data and override_step_data. | |
394 | |
395 Args: | |
396 name - The name of the step we're providing data for | |
397 data - Zero or more StepTestData objects. These may fill in placeholder | |
398 data for zero or more modules, as well as possibly setting the | |
399 retcode for this step. | |
400 retcode=(int or None) - Override the retcode for this step, even if it | |
401 was set by |data|. This must be set as a keyword arg. | |
402 stdout - StepTestData object with placeholder data for a step's stdout. | |
403 stderr - StepTestData object with placeholder data for a step's stderr. | |
404 override=(bool) - This step data completely replaces any previously | |
405 generated step data, instead of adding on to it. | |
406 | |
407 Use in GenTests: | |
408 # Hypothetically, suppose that your recipe has default test data for two | |
409 # steps 'init' and 'sync' (probably via recipe_api.inject_test_data()). | |
410 # For this example, lets say that the default test data looks like: | |
411 # api.step_data('init', api.json.output({'some': ["cool", "json"]})) | |
412 # AND | |
413 # api.step_data('sync', api.json.output({'src': {'rev': 100}})) | |
414 # Then, your GenTests code may augment or replace this data like: | |
415 | |
416 def GenTests(api): | |
417 yield ( | |
418 api.test('more') + | |
419 api.step_data( # Adds step data for a step with no default test data | |
420 'mystep', | |
421 api.json.output({'legend': ['...', 'DARY!']}) | |
422 ) + | |
423 api.step_data( # Adds retcode to default step_data for this step | |
424 'init', | |
425 retcode=1 | |
426 ) + | |
427 api.override_step_data( # Removes json output and overrides retcode | |
428 'sync', | |
429 retcode=100 | |
430 ) | |
431 ) | |
432 """ | |
433 assert all(isinstance(d, StepTestData) for d in data) | |
434 ret = TestData(None) | |
435 if data: | |
436 ret.step_data[name] = reduce(sum, data) | |
437 if 'retcode' in kwargs: | |
438 ret.step_data[name].retcode = kwargs['retcode'] | |
439 if 'override' in kwargs: | |
440 ret.step_data[name].override = kwargs['override'] | |
441 for key in ('stdout', 'stderr'): | |
442 if key in kwargs: | |
443 stdio_test_data = kwargs[key] | |
444 assert isinstance(stdio_test_data, StepTestData) | |
445 setattr(ret.step_data[name], key, stdio_test_data.unwrap_placeholder()) | |
446 return ret | |
447 | |
448 def step_data(self, name, *data, **kwargs): | |
449 """See _step_data()""" | |
450 return self._step_data(name, *data, **kwargs) | |
451 step_data.__doc__ = _step_data.__doc__ | |
452 | |
453 def override_step_data(self, name, *data, **kwargs): | |
454 """See _step_data()""" | |
455 kwargs['override'] = True | |
456 return self._step_data(name, *data, **kwargs) | |
457 override_step_data.__doc__ = _step_data.__doc__ | |
458 | |
459 def expect_exception(self, exc_type): #pylint: disable=R0201 | |
460 ret = TestData(None) | |
461 ret.expect_exception(exc_type) | |
462 return ret | |
OLD | NEW |