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

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: Address comments. 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, 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 - if True, for the same key, overwrite value from a with the one
21 from b; otherwise, use addition to merge them.
20 """ 22 """
21 dest_dict = getattr(dest, name) 23 dest_dict = getattr(dest, name)
22 dest_dict.update(getattr(a, name)) 24 dest_dict.update(getattr(a, name))
23 for k, v in getattr(b, name).iteritems(): 25 for k, v in getattr(b, name).iteritems():
24 if k in dest_dict: 26 if k in dest_dict:
25 dest_dict[k] += v 27 if not overwrite:
28 dest_dict[k] += v
29 else:
30 dest_dict[k] = v
iannucci 2016/03/22 22:54:24 I don't think this needs distinction any more: all
stgao 2016/03/22 23:40:25 We still need this distinction, as we discussed of
26 else: 31 else:
27 dest_dict[k] = v 32 dest_dict[k] = v
28 33
29 34
30 class BaseTestData(object): 35 class BaseTestData(object):
31 def __init__(self, enabled=True): 36 def __init__(self, enabled=True):
32 super(BaseTestData, self).__init__() 37 super(BaseTestData, self).__init__()
33 self._enabled = enabled 38 self._enabled = enabled
34 39
35 @property 40 @property
36 def enabled(self): 41 def enabled(self):
37 return self._enabled 42 return self._enabled
38 43
39 44
40 class PlaceholderTestData(BaseTestData): 45 class InputPlaceholderTestData(BaseTestData):
41 def __init__(self, data=None): 46 def __init__(self, data=None):
42 super(PlaceholderTestData, self).__init__() 47 super(InputPlaceholderTestData, self).__init__()
43 self.data = data 48 self.data = data
44 49
45 def __repr__(self): 50 def __repr__(self):
46 return "PlaceholderTestData(%r)" % (self.data,) 51 return "InputPlaceholderTestData(%r)" % (self.data,)
iannucci 2016/03/22 22:54:24 we don't need this class at all
stgao 2016/03/22 23:40:25 We need this one for the stdin below at line #139.
52
53
54 class OutputPlaceholderTestData(BaseTestData):
55 def __init__(self, data=None, name=None):
56 super(OutputPlaceholderTestData, self).__init__()
57 self.data = data
58 self.name = name
59
60 def __repr__(self):
61 if self.name is None:
62 return "OutputPlaceholderTestData(%r)" % (self.data,)
iannucci 2016/03/22 22:54:24 maybe `OutputPlaceholderTestData(DEFAULT, %r)` ?
stgao 2016/03/22 23:40:25 Done.
63 else:
64 return "OutputPlaceholderTestData(%r, %r)" % (self.name, self.data,)
47 65
48 66
49 class StepTestData(BaseTestData): 67 class StepTestData(BaseTestData):
50 """ 68 """
51 Mutable container for per-step test data. 69 Mutable container for per-step test data.
52 70
53 This data is consumed while running the recipe (during 71 This data is consumed while running the recipe (during
54 annotated_run.run_steps). 72 annotated_run.run_steps).
55 """ 73 """
56 def __init__(self): 74 def __init__(self):
57 super(StepTestData, self).__init__() 75 super(StepTestData, self).__init__()
58 # { (module, placeholder) -> [data] }. These are for output placeholders. 76 # { (module, placeholder, name) -> data }. Data are for output placeholders.
59 self.placeholder_data = collections.defaultdict(list) 77 self.placeholder_data = collections.defaultdict(dict)
60 self.override = False 78 self.override = False
61 self._stdout = None 79 self._stdout = None
62 self._stderr = None 80 self._stderr = None
63 self._retcode = None 81 self._retcode = None
64 82
65 def __add__(self, other): 83 def __add__(self, other):
66 assert isinstance(other, StepTestData) 84 assert isinstance(other, StepTestData)
67 85
68 if other.override: 86 if other.override:
69 return other 87 return other
70 88
71 ret = StepTestData() 89 ret = StepTestData()
72 90
73 combineify('placeholder_data', ret, self, other) 91 combineify('placeholder_data', ret, self, other, overwrite=True)
74 92
75 # pylint: disable=W0212 93 # pylint: disable=W0212
76 ret._stdout = other._stdout or self._stdout 94 ret._stdout = other._stdout or self._stdout
77 ret._stderr = other._stderr or self._stderr 95 ret._stderr = other._stderr or self._stderr
78 ret._retcode = self._retcode 96 ret._retcode = self._retcode
79 if other._retcode is not None: 97 if other._retcode is not None:
80 assert ret._retcode is None 98 assert ret._retcode is None
81 ret._retcode = other._retcode 99 ret._retcode = other._retcode
82 100
83 return ret 101 return ret
84 102
85 def unwrap_placeholder(self): 103 def unwrap_placeholder(self):
86 # {(module, placeholder): [data]} => data. 104 # {(module, placeholder, name): data} => data.
87 assert len(self.placeholder_data) == 1 105 assert len(self.placeholder_data) == 1
88 data_list = self.placeholder_data.items()[0][1] 106 return self.placeholder_data.values()[0]
89 assert len(data_list) == 1
90 return data_list[0]
91 107
92 def pop_placeholder(self, name_pieces): 108 def pop_placeholder(self, module_name, placeholder_name, name):
93 l = self.placeholder_data[name_pieces] 109 return self.placeholder_data.pop(
94 if l: 110 (module_name, placeholder_name, name), OutputPlaceholderTestData())
95 return l.pop(0)
96 else:
97 return PlaceholderTestData()
98 111
99 @property 112 @property
100 def retcode(self): # pylint: disable=E0202 113 def retcode(self): # pylint: disable=E0202
101 return self._retcode or 0 114 return self._retcode or 0
102 115
103 @retcode.setter 116 @retcode.setter
104 def retcode(self, value): # pylint: disable=E0202 117 def retcode(self, value): # pylint: disable=E0202
105 self._retcode = value 118 self._retcode = value
106 119
107 @property 120 @property
108 def stdout(self): 121 def stdout(self):
109 return self._stdout or PlaceholderTestData(None) 122 return self._stdout or OutputPlaceholderTestData(None)
110 123
111 @stdout.setter 124 @stdout.setter
112 def stdout(self, value): 125 def stdout(self, value):
113 assert isinstance(value, PlaceholderTestData) 126 assert isinstance(value, OutputPlaceholderTestData)
114 self._stdout = value 127 self._stdout = value
115 128
116 @property 129 @property
117 def stderr(self): 130 def stderr(self):
118 return self._stderr or PlaceholderTestData(None) 131 return self._stderr or OutputPlaceholderTestData(None)
119 132
120 @stderr.setter 133 @stderr.setter
121 def stderr(self, value): 134 def stderr(self, value):
122 assert isinstance(value, PlaceholderTestData) 135 assert isinstance(value, OutputPlaceholderTestData)
123 self._stderr = value 136 self._stderr = value
124 137
125 @property 138 @property
126 def stdin(self): # pylint: disable=R0201 139 def stdin(self): # pylint: disable=R0201
127 return PlaceholderTestData(None) 140 return InputPlaceholderTestData(None)
128 141
129 def __repr__(self): 142 def __repr__(self):
130 return "StepTestData(%r)" % ({ 143 return "StepTestData(%r)" % ({
131 'placeholder_data': dict(self.placeholder_data.iteritems()), 144 'placeholder_data': dict(self.placeholder_data.iteritems()),
132 'stdout': self._stdout, 145 'stdout': self._stdout,
133 'stderr': self._stderr, 146 'stderr': self._stderr,
134 'retcode': self._retcode, 147 'retcode': self._retcode,
135 'override': self.override, 148 'override': self.override,
136 },) 149 },)
137 150
(...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after
250 },) 263 },)
251 264
252 265
253 class DisabledTestData(BaseTestData): 266 class DisabledTestData(BaseTestData):
254 def __init__(self): 267 def __init__(self):
255 super(DisabledTestData, self).__init__(False) 268 super(DisabledTestData, self).__init__(False)
256 269
257 def __getattr__(self, name): 270 def __getattr__(self, name):
258 return self 271 return self
259 272
260 def pop_placeholder(self, _name_pieces): 273 def pop_placeholder(self, _module_name, _placeholder_name, _name):
261 return self 274 return self
262 275
263 def pop_step_test_data(self, _step_name, _step_test_data_fn): 276 def pop_step_test_data(self, _step_name, _step_test_data_fn):
264 return self 277 return self
265 278
266 def get_module_test_data(self, _module_name): 279 def get_module_test_data(self, _module_name):
267 return self 280 return self
268 281
269 @contextlib.contextmanager 282 @contextlib.contextmanager
270 def should_raise_exception(self, exception): # pylint: disable=unused-argument 283 def should_raise_exception(self, exception): # pylint: disable=unused-argument
(...skipping 10 matching lines...) Expand all
281 return ret 294 return ret
282 return inner 295 return inner
283 296
284 297
285 def placeholder_step_data(func): 298 def placeholder_step_data(func):
286 """Decorates RecipeTestApi member functions to allow those functions to 299 """Decorates RecipeTestApi member functions to allow those functions to
287 return just the output placeholder data, instead of the normally required 300 return just the output placeholder data, instead of the normally required
288 StepTestData() object. 301 StepTestData() object.
289 302
290 The wrapped function may return either: 303 The wrapped function may return either:
291 * <placeholder data>, <retcode or None> 304 * <placeholder data>, <retcode or None>, <name or None>
292 * StepTestData containing exactly one PlaceholderTestData and possible a 305 * StepTestData containing exactly one OutputPlaceholderTestData and possible
293 retcode. This is useful for returning the result of another method which 306 a retcode. This is useful for returning the result of another method which
294 is wrapped with placeholder_step_data. 307 is wrapped with placeholder_step_data.
295 308
296 In either case, the wrapper function will return a StepTestData object with 309 In either case, the wrapper function will return a StepTestData object with
297 the retcode and placeholder datum inserted with a name of: 310 the retcode and placeholder datum inserted with a name of:
298 (<Test module name>, <wrapped function name>) 311 (<Test module name>, <wrapped function name>, <name>)
299 312
300 Say you had a 'foo_module' with the following RecipeTestApi: 313 Say you had a 'foo_module' with the following RecipeTestApi:
301 class FooTestApi(RecipeTestApi): 314 class FooTestApi(RecipeTestApi):
302 @placeholder_step_data 315 @placeholder_step_data
303 @staticmethod 316 @staticmethod
304 def cool_method(data, retcode=None): 317 def cool_method(data, retcode=None, name=None):
305 return ("Test data (%s)" % data), retcode 318 return ("Test data (%s)" % data), retcode, name
306 319
307 @placeholder_step_data 320 @placeholder_step_data
308 def other_method(self, retcode=None): 321 def other_method(self, retcode=None, name=None):
309 return self.cool_method('hammer time', retcode) 322 return self.cool_method('hammer time', retcode=retcode, name=name)
310 323
311 Code calling cool_method('hello') would get a StepTestData: 324 Code calling cool_method('hello', name='cool1') would get a StepTestData:
312 StepTestData( 325 StepTestData(
313 placeholder_data = { 326 placeholder_data = {
314 ('foo_module', 'cool_method'): [ 327 ('foo_module', 'cool_method', 'cool1') :
315 PlaceholderTestData('Test data (hello)') 328 OutputPlaceholderTestData('Test data (hello)')
316 ]
317 }, 329 },
318 retcode = None 330 retcode = None
319 ) 331 )
320 332
321 Code calling other_method(50) would get a StepTestData: 333 Code calling other_method(retcode=50, name='other1') would get a StepTestData:
322 StepTestData( 334 StepTestData(
323 placeholder_data = { 335 placeholder_data = {
324 ('foo_module', 'other_method'): [ 336 ('foo_module', 'other_method', 'other1'):
325 PlaceholderTestData('Test data (hammer time)') 337 OutputPlaceholderTestData('Test data (hammer time)')
326 ]
327 }, 338 },
328 retcode = 50 339 retcode = 50
329 ) 340 )
330 """ 341 """
331 @static_wraps(func) 342 @static_wraps(func)
332 def inner(self, *args, **kwargs): 343 def inner(self, *args, **kwargs):
333 assert isinstance(self, RecipeTestApi) 344 assert isinstance(self, RecipeTestApi)
334 mod_name = self._module.NAME # pylint: disable=W0212 345 mod_name = self._module.NAME # pylint: disable=W0212
335 data = static_call(self, func, *args, **kwargs) 346 data = static_call(self, func, *args, **kwargs)
336 if isinstance(data, StepTestData): 347 if isinstance(data, StepTestData):
337 all_data = [i 348 all_data = data.placeholder_data.values()
338 for l in data.placeholder_data.values()
339 for i in l]
340 assert len(all_data) == 1, ( 349 assert len(all_data) == 1, (
341 'placeholder_step_data is only expecting a single output placeholder ' 350 'placeholder_step_data is only expecting a single output placeholder '
342 'datum. Got: %r' % data 351 'datum. Got: %r' % data
343 ) 352 )
344 placeholder_data, retcode = all_data[0], data.retcode 353 placeholder_data, retcode = all_data[0], data.retcode
345 else: 354 else:
346 placeholder_data, retcode = data 355 placeholder_data, retcode, name = data
347 placeholder_data = PlaceholderTestData(placeholder_data) 356 placeholder_data = OutputPlaceholderTestData(
357 data=placeholder_data, name=name)
348 358
349 ret = StepTestData() 359 ret = StepTestData()
350 ret.placeholder_data[(mod_name, inner.__name__)].append(placeholder_data) 360 key = (mod_name, inner.__name__, placeholder_data.name)
361 ret.placeholder_data[key] = placeholder_data
351 ret.retcode = retcode 362 ret.retcode = retcode
352 return ret 363 return ret
353 return inner 364 return inner
354 365
355 366
356 class RecipeTestApi(object): 367 class RecipeTestApi(object):
357 """Provides testing interface for GenTest method. 368 """Provides testing interface for GenTest method.
358 369
359 There are two primary components to the test api: 370 There are two primary components to the test api:
360 * Test data creation methods (test and step_data) 371 * Test data creation methods (test and step_data)
361 * test_api's from all the modules in DEPS. 372 * test_api's from all the modules in DEPS.
362 373
363 Every test in GenTests(api) takes the form: 374 Every test in GenTests(api) takes the form:
364 yield <instance of TestData> 375 yield <instance of TestData>
365 376
366 There are 4 basic pieces to TestData: 377 There are 4 basic pieces to TestData:
367 name - The name of the test. 378 name - The name of the test.
368 properties - Simple key-value dictionary which is used as the combined 379 properties - Simple key-value dictionary which is used as the combined
369 build_properties and factory_properties for this test. 380 build_properties and factory_properties for this test.
370 mod_data - Module-specific testing data (see the platform module for a 381 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 382 good example). This is testing data which is only used once at
372 the start of the execution of the recipe. Modules should 383 the start of the execution of the recipe. Modules should
373 provide methods to get their specific test information. See 384 provide methods to get their specific test information. See
374 the platform module's test_api for a good example of this. 385 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. 386 step_data - Step-specific data. There are two major components to this.
376 retcode - The return code of the step 387 retcode - The return code of the step
377 placeholder_data - A mapping from placeholder name to the a list of 388 placeholder_data - A mapping from placeholder name to the
378 PlaceholderTestData objects, one for each instance 389 OutputPlaceholderTestData object in the step.
379 of that kind of Placeholder in the step. 390 stdout, stderr - OutputPlaceholderTestData objects for stdout and
380 stdout, stderr - PlaceholderTestData objects for stdout and stderr. 391 stderr.
381 392
382 TestData objects are concatenatable, so it's convenient to phrase test cases 393 TestData objects are concatenatable, so it's convenient to phrase test cases
383 as a series of added TestData objects. For example: 394 as a series of added TestData objects. For example:
384 DEPS = ['properties', 'platform', 'json'] 395 DEPS = ['properties', 'platform', 'json']
385 def GenTests(api): 396 def GenTests(api):
386 yield ( 397 yield (
387 api.test('try_win64') + 398 api.test('try_win64') +
388 api.properties.tryserver(power_level=9001) + 399 api.properties.tryserver(power_level=9001) +
389 api.platform('win', 64) + 400 api.platform('win', 64) +
390 api.step_data( 401 api.step_data(
391 'some_step', 402 'some_step',
392 api.json.output("bobface"), 403 api.json.output("bobface", name="a"),
393 api.json.output({'key': 'value'}) 404 api.json.output({'key': 'value'}, name="b")
394 ) 405 )
395 ) 406 )
396 407
397 This example would run a single test (named 'try_win64') with the standard 408 This example would run a single test (named 'try_win64') with the standard
398 tryserver properties (plus an extra property 'power_level' whose value was 409 tryserver properties (plus an extra property 'power_level' whose value was
399 over 9000). The test would run as if it were being run on a 64-bit windows 410 over 9000). The test would run as if it were being run on a 64-bit windows
400 installation, and the step named 'some_step' would have its first json output 411 installation, and the step named 'some_step' would have the json output of
401 placeholder be mocked to return '"bobface"', and its second json output 412 the placeholder with name "a" be mocked to return '"bobface"', and the json
402 placeholder be mocked to return '{"key": "value"}'. 413 output of the placeholder with name "b" be mocked to return
414 '{"key": "value"}'.
403 415
404 The properties.tryserver() call is documented in the 'properties' module's 416 The properties.tryserver() call is documented in the 'properties' module's
405 test_api. 417 test_api.
406 The platform() call is documented in the 'platform' module's test_api. 418 The platform() call is documented in the 'platform' module's test_api.
407 The json.output() call is documented in the 'json' module's test_api. 419 The json.output() call is documented in the 'json' module's test_api.
408 """ 420 """
409 def __init__(self, module=None): 421 def __init__(self, module=None):
410 """Note: Injected dependencies are NOT available in __init__().""" 422 """Note: Injected dependencies are NOT available in __init__()."""
411 # If we're the 'root' api, inject directly into 'self'. 423 # If we're the 'root' api, inject directly into 'self'.
412 # Otherwise inject into 'self.m' 424 # Otherwise inject into 'self.m'
(...skipping 19 matching lines...) Expand all
432 return TestData(None) 444 return TestData(None)
433 445
434 @staticmethod 446 @staticmethod
435 def _step_data(name, *data, **kwargs): 447 def _step_data(name, *data, **kwargs):
436 """Returns a new TestData with the mock data filled in for a single step. 448 """Returns a new TestData with the mock data filled in for a single step.
437 449
438 Used by step_data and override_step_data. 450 Used by step_data and override_step_data.
439 451
440 Args: 452 Args:
441 name - The name of the step we're providing data for 453 name - The name of the step we're providing data for
442 data - Zero or more StepTestData objects. These may fill in placeholder 454 data - Zero or more StepTestData objects. These may fill in output
443 data for zero or more modules, as well as possibly setting the 455 placeholder data for zero or more modules, as well as possibly
444 retcode for this step. 456 setting the retcode for this step.
445 retcode=(int or None) - Override the retcode for this step, even if it 457 retcode=(int or None) - Override the retcode for this step, even if it
446 was set by |data|. This must be set as a keyword arg. 458 was set by |data|. This must be set as a keyword arg.
447 stdout - StepTestData object with a single output placeholder datum for a 459 stdout - StepTestData object with a single output placeholder datum for a
448 step's stdout. 460 step's stdout.
449 stderr - StepTestData object with a single output placeholder datum for a 461 stderr - StepTestData object with a single output placeholder datum for a
450 step's stderr. 462 step's stderr.
451 override=(bool) - This step data completely replaces any previously 463 override=(bool) - This step data completely replaces any previously
452 generated step data, instead of adding on to it. 464 generated step data, instead of adding on to it.
453 465
454 Use in GenTests: 466 Use in GenTests:
(...skipping 27 matching lines...) Expand all
482 if data: 494 if data:
483 ret.step_data[name] = reduce(lambda x,y: x + y, data) 495 ret.step_data[name] = reduce(lambda x,y: x + y, data)
484 if 'retcode' in kwargs: 496 if 'retcode' in kwargs:
485 ret.step_data[name].retcode = kwargs['retcode'] 497 ret.step_data[name].retcode = kwargs['retcode']
486 if 'override' in kwargs: 498 if 'override' in kwargs:
487 ret.step_data[name].override = kwargs['override'] 499 ret.step_data[name].override = kwargs['override']
488 for key in ('stdout', 'stderr'): 500 for key in ('stdout', 'stderr'):
489 if key in kwargs: 501 if key in kwargs:
490 stdio_test_data = kwargs[key] 502 stdio_test_data = kwargs[key]
491 assert isinstance(stdio_test_data, StepTestData) 503 assert isinstance(stdio_test_data, StepTestData)
492 setattr(ret.step_data[name], key, stdio_test_data.unwrap_placeholder()) 504 setattr(ret.step_data[name], key,
505 stdio_test_data.unwrap_placeholder())
493 return ret 506 return ret
494 507
495 def step_data(self, name, *data, **kwargs): 508 def step_data(self, name, *data, **kwargs):
496 """See _step_data()""" 509 """See _step_data()"""
497 return self._step_data(name, *data, **kwargs) 510 return self._step_data(name, *data, **kwargs)
498 step_data.__doc__ = _step_data.__doc__ 511 step_data.__doc__ = _step_data.__doc__
499 512
500 def override_step_data(self, name, *data, **kwargs): 513 def override_step_data(self, name, *data, **kwargs):
501 """See _step_data()""" 514 """See _step_data()"""
502 kwargs['override'] = True 515 kwargs['override'] = True
503 return self._step_data(name, *data, **kwargs) 516 return self._step_data(name, *data, **kwargs)
504 override_step_data.__doc__ = _step_data.__doc__ 517 override_step_data.__doc__ = _step_data.__doc__
505 518
506 def expect_exception(self, exc_type): #pylint: disable=R0201 519 def expect_exception(self, exc_type): #pylint: disable=R0201
507 ret = TestData(None) 520 ret = TestData(None)
508 ret.expect_exception(exc_type) 521 ret.expect_exception(exc_type)
509 return ret 522 return ret
510 523
511 def depend_on(self, recipe, properties, result): 524 def depend_on(self, recipe, properties, result):
512 ret = TestData() 525 ret = TestData()
513 ret.depend_on(recipe, properties, result) 526 ret.depend_on(recipe, properties, result)
514 return ret 527 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