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

Side by Side Diff: recipe_engine/recipe_test_api.py

Issue 1773273003: Make output placeholders like json.output index-able by name. (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/recipes-py@master
Patch Set: Created 4 years, 9 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
« no previous file with comments | « no previous file | recipe_engine/step_runner.py » ('j') | recipe_engine/step_runner.py » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2013-2015 The Chromium Authors. All rights reserved. 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 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 collections 5 import collections
6 import contextlib 6 import contextlib
7 7
8 from .util import ModuleInjectionSite, static_call, static_wraps 8 from .util import ModuleInjectionSite, Placeholder, static_call, static_wraps
9 from .types import freeze 9 from .types import freeze
10 10
11 def combineify(name, dest, a, b): 11 def combineify(name, dest, a, b, overwrite=False):
12 """ 12 """
13 Combines dictionary members in two objects into a third one using addition. 13 Combines dictionary members in two objects into a third one using addition.
14 14
15 Args: 15 Args:
16 name - the name of the member 16 name - the name of the member
17 dest - the destination object 17 dest - the destination object
18 a - the first source object 18 a - the first source object
19 b - the second source object 19 b - the second source object
20 overwrite - whether to over write the value from a with the same key.
iannucci 2016/03/10 03:17:42 So I think that now they're all named, there's no
stgao 2016/03/10 20:34:22 Unfortunately, we still have to do it this way, be
20 """ 21 """
21 dest_dict = getattr(dest, name) 22 dest_dict = getattr(dest, name)
22 dest_dict.update(getattr(a, name)) 23 dest_dict.update(getattr(a, name))
23 for k, v in getattr(b, name).iteritems(): 24 for k, v in getattr(b, name).iteritems():
24 if k in dest_dict: 25 if k in dest_dict:
25 dest_dict[k] += v 26 if not overwrite:
27 dest_dict[k] += v
28 else:
29 dest_dict[k] = v
26 else: 30 else:
27 dest_dict[k] = v 31 dest_dict[k] = v
28 32
29 33
30 class BaseTestData(object): 34 class BaseTestData(object):
31 def __init__(self, enabled=True): 35 def __init__(self, enabled=True):
32 super(BaseTestData, self).__init__() 36 super(BaseTestData, self).__init__()
33 self._enabled = enabled 37 self._enabled = enabled
34 38
35 @property 39 @property
36 def enabled(self): 40 def enabled(self):
37 return self._enabled 41 return self._enabled
38 42
39 43
40 class PlaceholderTestData(BaseTestData): 44 class PlaceholderTestData(BaseTestData):
41 def __init__(self, data=None): 45 def __init__(self, data=None, id=None):
42 super(PlaceholderTestData, self).__init__() 46 super(PlaceholderTestData, self).__init__()
43 self.data = data 47 self.data = data
48 self.id = id or Placeholder.DEFAULT_ID
44 49
45 def __repr__(self): 50 def __repr__(self):
46 return "PlaceholderTestData(%r)" % (self.data,) 51 return "PlaceholderTestData(%r)" % (self.data,)
47 52
48 53
49 class StepTestData(BaseTestData): 54 class StepTestData(BaseTestData):
50 """ 55 """
51 Mutable container for per-step test data. 56 Mutable container for per-step test data.
52 57
53 This data is consumed while running the recipe (during 58 This data is consumed while running the recipe (during
54 annotated_run.run_steps). 59 annotated_run.run_steps).
55 """ 60 """
56 def __init__(self): 61 def __init__(self):
57 super(StepTestData, self).__init__() 62 super(StepTestData, self).__init__()
58 # { (module, placeholder) -> [data] } 63 # { (module, placeholder, id) -> data }
59 self.placeholder_data = collections.defaultdict(list) 64 self.placeholder_data = {}
60 self.override = False 65 self.override = False
61 self._stdout = None 66 self._stdout = None
62 self._stderr = None 67 self._stderr = None
63 self._retcode = None 68 self._retcode = None
64 69
65 def __add__(self, other): 70 def __add__(self, other):
66 assert isinstance(other, StepTestData) 71 assert isinstance(other, StepTestData)
67 72
68 if other.override: 73 if other.override:
69 return other 74 return other
70 75
71 ret = StepTestData() 76 ret = StepTestData()
72 77
73 combineify('placeholder_data', ret, self, other) 78 combineify('placeholder_data', ret, self, other, overwrite=True)
74 79
75 # pylint: disable=W0212 80 # pylint: disable=W0212
76 ret._stdout = other._stdout or self._stdout 81 ret._stdout = other._stdout or self._stdout
77 ret._stderr = other._stderr or self._stderr 82 ret._stderr = other._stderr or self._stderr
78 ret._retcode = self._retcode 83 ret._retcode = self._retcode
79 if other._retcode is not None: 84 if other._retcode is not None:
80 assert ret._retcode is None 85 assert ret._retcode is None
81 ret._retcode = other._retcode 86 ret._retcode = other._retcode
82 87
83 return ret 88 return ret
84 89
85 def unwrap_placeholder(self): 90 def unwrap_placeholder(self):
86 # {(module, placeholder): [data]} => data. 91 # {(module, placeholder, id): data} => data.
87 assert len(self.placeholder_data) == 1 92 assert len(self.placeholder_data) == 1
88 data_list = self.placeholder_data.items()[0][1] 93 return self.placeholder_data.values()[0]
89 assert len(data_list) == 1
90 return data_list[0]
91 94
92 def pop_placeholder(self, name_pieces): 95 def pop_placeholder(self, module_name, placeholder_name, id):
93 l = self.placeholder_data[name_pieces] 96 return self.placeholder_data.pop((module_name, placeholder_name, id),
94 if l: 97 PlaceholderTestData())
95 return l.pop(0)
96 else:
97 return PlaceholderTestData()
98 98
99 @property 99 @property
100 def retcode(self): # pylint: disable=E0202 100 def retcode(self): # pylint: disable=E0202
101 return self._retcode or 0 101 return self._retcode or 0
102 102
103 @retcode.setter 103 @retcode.setter
104 def retcode(self, value): # pylint: disable=E0202 104 def retcode(self, value): # pylint: disable=E0202
105 self._retcode = value 105 self._retcode = value
106 106
107 @property 107 @property
(...skipping 142 matching lines...) Expand 10 before | Expand all | Expand 10 after
250 },) 250 },)
251 251
252 252
253 class DisabledTestData(BaseTestData): 253 class DisabledTestData(BaseTestData):
254 def __init__(self): 254 def __init__(self):
255 super(DisabledTestData, self).__init__(False) 255 super(DisabledTestData, self).__init__(False)
256 256
257 def __getattr__(self, name): 257 def __getattr__(self, name):
258 return self 258 return self
259 259
260 def pop_placeholder(self, _name_pieces): 260 def pop_placeholder(self, _module_name, _placeholder_name, _id):
261 return self 261 return self
262 262
263 def pop_step_test_data(self, _step_name, _step_test_data_fn): 263 def pop_step_test_data(self, _step_name, _step_test_data_fn):
264 return self 264 return self
265 265
266 def get_module_test_data(self, _module_name): 266 def get_module_test_data(self, _module_name):
267 return self 267 return self
268 268
269 @contextlib.contextmanager 269 @contextlib.contextmanager
270 def should_raise_exception(self, exception): # pylint: disable=unused-argument 270 def should_raise_exception(self, exception): # pylint: disable=unused-argument
(...skipping 10 matching lines...) Expand all
281 return ret 281 return ret
282 return inner 282 return inner
283 283
284 284
285 def placeholder_step_data(func): 285 def placeholder_step_data(func):
286 """Decorates RecipeTestApi member functions to allow those functions to 286 """Decorates RecipeTestApi member functions to allow those functions to
287 return just the placeholder data, instead of the normally required 287 return just the placeholder data, instead of the normally required
288 StepTestData() object. 288 StepTestData() object.
289 289
290 The wrapped function may return either: 290 The wrapped function may return either:
291 * <placeholder data>, <retcode or None> 291 * <placeholder data>, <retcode or None>, <id or None>
292 * StepTestData containing exactly one PlaceholderTestData and possible a 292 * StepTestData containing exactly one PlaceholderTestData and possible a
293 retcode. This is useful for returning the result of another method which 293 retcode. This is useful for returning the result of another method which
294 is wrapped with placeholder_step_data. 294 is wrapped with placeholder_step_data.
295 295
296 In either case, the wrapper function will return a StepTestData object with 296 In either case, the wrapper function will return a StepTestData object with
297 the retcode and placeholder datum inserted with a name of: 297 the retcode and placeholder datum inserted with a name of:
298 (<Test module name>, <wrapped function name>) 298 (<Test module name>, <wrapped function name>, <id>)
299 299
300 Say you had a 'foo_module' with the following RecipeTestApi: 300 Say you had a 'foo_module' with the following RecipeTestApi:
301 class FooTestApi(RecipeTestApi): 301 class FooTestApi(RecipeTestApi):
302 @placeholder_step_data 302 @placeholder_step_data
303 @staticmethod 303 @staticmethod
304 def cool_method(data, retcode=None): 304 def cool_method(data, retcode=None, id=None):
305 return ("Test data (%s)" % data), retcode 305 return ("Test data (%s)" % data), retcode, id
306 306
307 @placeholder_step_data 307 @placeholder_step_data
308 def other_method(self, retcode=None): 308 def other_method(self, retcode=None, id=None):
309 return self.cool_method('hammer time', retcode) 309 return self.cool_method('hammer time', retcode=retcode, id=id)
310 310
311 Code calling cool_method('hello') would get a StepTestData: 311 Code calling cool_method('hello', id='cool1') would get a StepTestData:
312 StepTestData( 312 StepTestData(
313 placeholder_data = { 313 placeholder_data = {
314 ('foo_module', 'cool_method'): [ 314 ('foo_module', 'cool_method', 'cool1') :
315 PlaceholderTestData('Test data (hello)') 315 PlaceholderTestData('Test data (hello)')
316 ]
317 }, 316 },
318 retcode = None 317 retcode = None
319 ) 318 )
320 319
321 Code calling other_method(50) would get a StepTestData: 320 Code calling other_method(retcode=50, id='other1') would get a StepTestData:
322 StepTestData( 321 StepTestData(
323 placeholder_data = { 322 placeholder_data = {
324 ('foo_module', 'other_method'): [ 323 ('foo_module', 'other_method', 'other1'):
325 PlaceholderTestData('Test data (hammer time)') 324 PlaceholderTestData('Test data (hammer time)')
326 ]
327 }, 325 },
328 retcode = 50 326 retcode = 50
329 ) 327 )
330 """ 328 """
331 @static_wraps(func) 329 @static_wraps(func)
332 def inner(self, *args, **kwargs): 330 def inner(self, *args, **kwargs):
333 assert isinstance(self, RecipeTestApi) 331 assert isinstance(self, RecipeTestApi)
334 mod_name = self._module.NAME # pylint: disable=W0212 332 mod_name = self._module.NAME # pylint: disable=W0212
335 data = static_call(self, func, *args, **kwargs) 333 data = static_call(self, func, *args, **kwargs)
336 if isinstance(data, StepTestData): 334 if isinstance(data, StepTestData):
337 all_data = [i 335 all_data = [i
338 for l in data.placeholder_data.values() 336 for l in data.placeholder_data.values()
339 for i in l] 337 for i in l]
340 assert len(all_data) == 1, ( 338 assert len(all_data) == 1, (
341 'placeholder_step_data is only expecting a single placeholder datum. ' 339 'placeholder_step_data is only expecting a single placeholder datum. '
342 'Got: %r' % data 340 'Got: %r' % data
343 ) 341 )
344 placeholder_data, retcode = all_data[0], data.retcode 342 placeholder_data, retcode = all_data[0], data.retcode
345 else: 343 else:
346 placeholder_data, retcode = data 344 placeholder_data, retcode, id = data
347 placeholder_data = PlaceholderTestData(placeholder_data) 345 placeholder_data = PlaceholderTestData(placeholder_data, id=id)
348 346
349 ret = StepTestData() 347 ret = StepTestData()
350 ret.placeholder_data[(mod_name, inner.__name__)].append(placeholder_data) 348 key = (mod_name, inner.__name__, placeholder_data.id)
349 ret.placeholder_data[key] = placeholder_data
351 ret.retcode = retcode 350 ret.retcode = retcode
352 return ret 351 return ret
353 return inner 352 return inner
354 353
355 354
356 class RecipeTestApi(object): 355 class RecipeTestApi(object):
357 """Provides testing interface for GenTest method. 356 """Provides testing interface for GenTest method.
358 357
359 There are two primary components to the test api: 358 There are two primary components to the test api:
360 * Test data creation methods (test and step_data) 359 * Test data creation methods (test and step_data)
361 * test_api's from all the modules in DEPS. 360 * test_api's from all the modules in DEPS.
362 361
363 Every test in GenTests(api) takes the form: 362 Every test in GenTests(api) takes the form:
364 yield <instance of TestData> 363 yield <instance of TestData>
365 364
366 There are 4 basic pieces to TestData: 365 There are 4 basic pieces to TestData:
367 name - The name of the test. 366 name - The name of the test.
368 properties - Simple key-value dictionary which is used as the combined 367 properties - Simple key-value dictionary which is used as the combined
369 build_properties and factory_properties for this test. 368 build_properties and factory_properties for this test.
370 mod_data - Module-specific testing data (see the platform module for a 369 mod_data - Module-specific testing data (see the platform module for a
371 good example). This is testing data which is only used once at 370 good example). This is testing data which is only used once at
372 the start of the execution of the recipe. Modules should 371 the start of the execution of the recipe. Modules should
373 provide methods to get their specific test information. See 372 provide methods to get their specific test information. See
374 the platform module's test_api for a good example of this. 373 the platform module's test_api for a good example of this.
375 step_data - Step-specific data. There are two major components to this. 374 step_data - Step-specific data. There are two major components to this.
376 retcode - The return code of the step 375 retcode - The return code of the step
377 placeholder_data - A mapping from placeholder name to the a list of 376 placeholder_data - A mapping from placeholder name to the list of
378 PlaceholderTestData objects, one for each instance 377 PlaceholderTestData objects, one for each instance
379 of that kind of Placeholder in the step. 378 of that kind of Placeholder in the step.
380 stdout, stderr - PlaceholderTestData objects for stdout and stderr. 379 stdout, stderr - PlaceholderTestData objects for stdout and stderr.
381 380
382 TestData objects are concatenatable, so it's convenient to phrase test cases 381 TestData objects are concatenatable, so it's convenient to phrase test cases
383 as a series of added TestData objects. For example: 382 as a series of added TestData objects. For example:
384 DEPS = ['properties', 'platform', 'json'] 383 DEPS = ['properties', 'platform', 'json']
385 def GenTests(api): 384 def GenTests(api):
386 yield ( 385 yield (
387 api.test('try_win64') + 386 api.test('try_win64') +
(...skipping 115 matching lines...) Expand 10 before | Expand all | Expand 10 after
503 502
504 def expect_exception(self, exc_type): #pylint: disable=R0201 503 def expect_exception(self, exc_type): #pylint: disable=R0201
505 ret = TestData(None) 504 ret = TestData(None)
506 ret.expect_exception(exc_type) 505 ret.expect_exception(exc_type)
507 return ret 506 return ret
508 507
509 def depend_on(self, recipe, properties, result): 508 def depend_on(self, recipe, properties, result):
510 ret = TestData() 509 ret = TestData()
511 ret.depend_on(recipe, properties, result) 510 ret.depend_on(recipe, properties, result)
512 return ret 511 return ret
OLDNEW
« no previous file with comments | « no previous file | recipe_engine/step_runner.py » ('j') | recipe_engine/step_runner.py » ('J')

Powered by Google App Engine
This is Rietveld 408576698