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

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

Powered by Google App Engine
This is Rietveld 408576698