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

Side by Side Diff: third_party/recipe_engine/recipe_test_api.py

Issue 1241323004: Cross-repo recipe package system. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Roll to latest recipes-py Created 5 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 | « third_party/recipe_engine/recipe_api.py ('k') | third_party/recipe_engine/simulation_test.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 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
OLDNEW
« no previous file with comments | « third_party/recipe_engine/recipe_api.py ('k') | third_party/recipe_engine/simulation_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698