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

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. 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') | no next file with comments »
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
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 PlaceholderTestData(BaseTestData):
41 def __init__(self, data=None): 46 def __init__(self, data=None, name=None):
42 super(PlaceholderTestData, self).__init__() 47 super(PlaceholderTestData, self).__init__()
43 self.data = data 48 self.data = data
49 self.name = name
44 50
45 def __repr__(self): 51 def __repr__(self):
46 return "PlaceholderTestData(%r)" % (self.data,) 52 if self.name is None:
53 return "PlaceholderTestData(DEFAULT, %r)" % (self.data,)
54 else:
55 return "PlaceholderTestData(%r, %r)" % (self.name, self.data,)
47 56
48 57
49 class StepTestData(BaseTestData): 58 class StepTestData(BaseTestData):
50 """ 59 """
51 Mutable container for per-step test data. 60 Mutable container for per-step test data.
52 61
53 This data is consumed while running the recipe (during 62 This data is consumed while running the recipe (during
54 annotated_run.run_steps). 63 annotated_run.run_steps).
55 """ 64 """
56 def __init__(self): 65 def __init__(self):
57 super(StepTestData, self).__init__() 66 super(StepTestData, self).__init__()
58 # { (module, placeholder) -> [data] }. These are for output placeholders. 67 # { (module, placeholder, name) -> data }. Data are for output placeholders.
59 self.placeholder_data = collections.defaultdict(list) 68 self.placeholder_data = collections.defaultdict(dict)
60 self.override = False 69 self.override = False
61 self._stdout = None 70 self._stdout = None
62 self._stderr = None 71 self._stderr = None
63 self._retcode = None 72 self._retcode = None
64 73
65 def __add__(self, other): 74 def __add__(self, other):
66 assert isinstance(other, StepTestData) 75 assert isinstance(other, StepTestData)
67 76
68 if other.override: 77 if other.override:
69 return other 78 return other
70 79
71 ret = StepTestData() 80 ret = StepTestData()
72 81
73 combineify('placeholder_data', ret, self, other) 82 combineify('placeholder_data', ret, self, other, overwrite=True)
74 83
75 # pylint: disable=W0212 84 # pylint: disable=W0212
76 ret._stdout = other._stdout or self._stdout 85 ret._stdout = other._stdout or self._stdout
77 ret._stderr = other._stderr or self._stderr 86 ret._stderr = other._stderr or self._stderr
78 ret._retcode = self._retcode 87 ret._retcode = self._retcode
79 if other._retcode is not None: 88 if other._retcode is not None:
80 assert ret._retcode is None 89 assert ret._retcode is None
81 ret._retcode = other._retcode 90 ret._retcode = other._retcode
82 91
83 return ret 92 return ret
84 93
85 def unwrap_placeholder(self): 94 def unwrap_placeholder(self):
86 # {(module, placeholder): [data]} => data. 95 # {(module, placeholder, name): data} => data.
87 assert len(self.placeholder_data) == 1 96 assert len(self.placeholder_data) == 1
88 data_list = self.placeholder_data.items()[0][1] 97 return self.placeholder_data.values()[0]
89 assert len(data_list) == 1
90 return data_list[0]
91 98
92 def pop_placeholder(self, name_pieces): 99 def pop_placeholder(self, module_name, placeholder_name, name):
93 l = self.placeholder_data[name_pieces] 100 return self.placeholder_data.pop(
94 if l: 101 (module_name, placeholder_name, name), PlaceholderTestData())
95 return l.pop(0)
96 else:
97 return PlaceholderTestData()
98 102
99 @property 103 @property
100 def retcode(self): # pylint: disable=E0202 104 def retcode(self): # pylint: disable=E0202
101 return self._retcode or 0 105 return self._retcode or 0
102 106
103 @retcode.setter 107 @retcode.setter
104 def retcode(self, value): # pylint: disable=E0202 108 def retcode(self, value): # pylint: disable=E0202
105 self._retcode = value 109 self._retcode = value
106 110
107 @property 111 @property
(...skipping 142 matching lines...) Expand 10 before | Expand all | Expand 10 after
250 },) 254 },)
251 255
252 256
253 class DisabledTestData(BaseTestData): 257 class DisabledTestData(BaseTestData):
254 def __init__(self): 258 def __init__(self):
255 super(DisabledTestData, self).__init__(False) 259 super(DisabledTestData, self).__init__(False)
256 260
257 def __getattr__(self, name): 261 def __getattr__(self, name):
258 return self 262 return self
259 263
260 def pop_placeholder(self, _name_pieces): 264 def pop_placeholder(self, _module_name, _placeholder_name, _name):
261 return self 265 return self
262 266
263 def pop_step_test_data(self, _step_name, _step_test_data_fn): 267 def pop_step_test_data(self, _step_name, _step_test_data_fn):
264 return self 268 return self
265 269
266 def get_module_test_data(self, _module_name): 270 def get_module_test_data(self, _module_name):
267 return self 271 return self
268 272
269 @contextlib.contextmanager 273 @contextlib.contextmanager
270 def should_raise_exception(self, exception): # pylint: disable=unused-argument 274 def should_raise_exception(self, exception): # pylint: disable=unused-argument
(...skipping 10 matching lines...) Expand all
281 return ret 285 return ret
282 return inner 286 return inner
283 287
284 288
285 def placeholder_step_data(func): 289 def placeholder_step_data(func):
286 """Decorates RecipeTestApi member functions to allow those functions to 290 """Decorates RecipeTestApi member functions to allow those functions to
287 return just the output placeholder data, instead of the normally required 291 return just the output placeholder data, instead of the normally required
288 StepTestData() object. 292 StepTestData() object.
289 293
290 The wrapped function may return either: 294 The wrapped function may return either:
291 * <placeholder data>, <retcode or None> 295 * <placeholder data>, <retcode or None>, <name or None>
292 * StepTestData containing exactly one PlaceholderTestData and possible a 296 * StepTestData containing exactly one PlaceholderTestData and possible a
293 retcode. This is useful for returning the result of another method which 297 retcode. This is useful for returning the result of another method which
294 is wrapped with placeholder_step_data. 298 is wrapped with placeholder_step_data.
295 299
296 In either case, the wrapper function will return a StepTestData object with 300 In either case, the wrapper function will return a StepTestData object with
297 the retcode and placeholder datum inserted with a name of: 301 the retcode and placeholder datum inserted with a name of:
298 (<Test module name>, <wrapped function name>) 302 (<Test module name>, <wrapped function name>, <name>)
299 303
300 Say you had a 'foo_module' with the following RecipeTestApi: 304 Say you had a 'foo_module' with the following RecipeTestApi:
301 class FooTestApi(RecipeTestApi): 305 class FooTestApi(RecipeTestApi):
302 @placeholder_step_data 306 @placeholder_step_data
303 @staticmethod 307 @staticmethod
304 def cool_method(data, retcode=None): 308 def cool_method(data, retcode=None, name=None):
305 return ("Test data (%s)" % data), retcode 309 return ("Test data (%s)" % data), retcode, name
306 310
307 @placeholder_step_data 311 @placeholder_step_data
308 def other_method(self, retcode=None): 312 def other_method(self, retcode=None, name=None):
309 return self.cool_method('hammer time', retcode) 313 return self.cool_method('hammer time', retcode=retcode, name=name)
310 314
311 Code calling cool_method('hello') would get a StepTestData: 315 Code calling cool_method('hello', name='cool1') would get a StepTestData:
312 StepTestData( 316 StepTestData(
313 placeholder_data = { 317 placeholder_data = {
314 ('foo_module', 'cool_method'): [ 318 ('foo_module', 'cool_method', 'cool1') :
315 PlaceholderTestData('Test data (hello)') 319 PlaceholderTestData('Test data (hello)')
316 ]
317 }, 320 },
318 retcode = None 321 retcode = None
319 ) 322 )
320 323
321 Code calling other_method(50) would get a StepTestData: 324 Code calling other_method(retcode=50, name='other1') would get a StepTestData:
322 StepTestData( 325 StepTestData(
323 placeholder_data = { 326 placeholder_data = {
324 ('foo_module', 'other_method'): [ 327 ('foo_module', 'other_method', 'other1'):
325 PlaceholderTestData('Test data (hammer time)') 328 PlaceholderTestData('Test data (hammer time)')
326 ]
327 }, 329 },
328 retcode = 50 330 retcode = 50
329 ) 331 )
330 """ 332 """
331 @static_wraps(func) 333 @static_wraps(func)
332 def inner(self, *args, **kwargs): 334 def inner(self, *args, **kwargs):
333 assert isinstance(self, RecipeTestApi) 335 assert isinstance(self, RecipeTestApi)
334 mod_name = self._module.NAME # pylint: disable=W0212 336 mod_name = self._module.NAME # pylint: disable=W0212
335 data = static_call(self, func, *args, **kwargs) 337 data = static_call(self, func, *args, **kwargs)
336 if isinstance(data, StepTestData): 338 if isinstance(data, StepTestData):
337 all_data = [i 339 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, ( 340 assert len(all_data) == 1, (
341 'placeholder_step_data is only expecting a single output placeholder ' 341 'placeholder_step_data is only expecting a single output placeholder '
342 'datum. Got: %r' % data 342 'datum. Got: %r' % data
343 ) 343 )
344 placeholder_data, retcode = all_data[0], data.retcode 344 placeholder_data, retcode = all_data[0], data.retcode
345 else: 345 else:
346 placeholder_data, retcode = data 346 placeholder_data, retcode, name = data
347 placeholder_data = PlaceholderTestData(placeholder_data) 347 placeholder_data = PlaceholderTestData(data=placeholder_data, name=name)
348 348
349 ret = StepTestData() 349 ret = StepTestData()
350 ret.placeholder_data[(mod_name, inner.__name__)].append(placeholder_data) 350 key = (mod_name, inner.__name__, placeholder_data.name)
351 ret.placeholder_data[key] = placeholder_data
351 ret.retcode = retcode 352 ret.retcode = retcode
352 return ret 353 return ret
353 return inner 354 return inner
354 355
355 356
356 class RecipeTestApi(object): 357 class RecipeTestApi(object):
357 """Provides testing interface for GenTest method. 358 """Provides testing interface for GenTest method.
358 359
359 There are two primary components to the test api: 360 There are two primary components to the test api:
360 * Test data creation methods (test and step_data) 361 * Test data creation methods (test and step_data)
361 * test_api's from all the modules in DEPS. 362 * test_api's from all the modules in DEPS.
362 363
363 Every test in GenTests(api) takes the form: 364 Every test in GenTests(api) takes the form:
364 yield <instance of TestData> 365 yield <instance of TestData>
365 366
366 There are 4 basic pieces to TestData: 367 There are 4 basic pieces to TestData:
367 name - The name of the test. 368 name - The name of the test.
368 properties - Simple key-value dictionary which is used as the combined 369 properties - Simple key-value dictionary which is used as the combined
369 build_properties and factory_properties for this test. 370 build_properties and factory_properties for this test.
370 mod_data - Module-specific testing data (see the platform module for a 371 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 372 good example). This is testing data which is only used once at
372 the start of the execution of the recipe. Modules should 373 the start of the execution of the recipe. Modules should
373 provide methods to get their specific test information. See 374 provide methods to get their specific test information. See
374 the platform module's test_api for a good example of this. 375 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. 376 step_data - Step-specific data. There are two major components to this.
376 retcode - The return code of the step 377 retcode - The return code of the step
377 placeholder_data - A mapping from placeholder name to the a list of 378 placeholder_data - A mapping from placeholder name to the
378 PlaceholderTestData objects, one for each instance 379 PlaceholderTestData object in the step.
379 of that kind of Placeholder in the step.
380 stdout, stderr - PlaceholderTestData objects for stdout and stderr. 380 stdout, stderr - PlaceholderTestData objects for stdout and stderr.
381 381
382 TestData objects are concatenatable, so it's convenient to phrase test cases 382 TestData objects are concatenatable, so it's convenient to phrase test cases
383 as a series of added TestData objects. For example: 383 as a series of added TestData objects. For example:
384 DEPS = ['properties', 'platform', 'json'] 384 DEPS = ['properties', 'platform', 'json']
385 def GenTests(api): 385 def GenTests(api):
386 yield ( 386 yield (
387 api.test('try_win64') + 387 api.test('try_win64') +
388 api.properties.tryserver(power_level=9001) + 388 api.properties.tryserver(power_level=9001) +
389 api.platform('win', 64) + 389 api.platform('win', 64) +
390 api.step_data( 390 api.step_data(
391 'some_step', 391 'some_step',
392 api.json.output("bobface"), 392 api.json.output("bobface", name="a"),
393 api.json.output({'key': 'value'}) 393 api.json.output({'key': 'value'}, name="b")
394 ) 394 )
395 ) 395 )
396 396
397 This example would run a single test (named 'try_win64') with the standard 397 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 398 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 399 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 400 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 401 the placeholder with name "a" be mocked to return '"bobface"', and the json
402 placeholder be mocked to return '{"key": "value"}'. 402 output of the placeholder with name "b" be mocked to return
403 '{"key": "value"}'.
403 404
404 The properties.tryserver() call is documented in the 'properties' module's 405 The properties.tryserver() call is documented in the 'properties' module's
405 test_api. 406 test_api.
406 The platform() call is documented in the 'platform' module's test_api. 407 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. 408 The json.output() call is documented in the 'json' module's test_api.
408 """ 409 """
409 def __init__(self, module=None): 410 def __init__(self, module=None):
410 """Note: Injected dependencies are NOT available in __init__().""" 411 """Note: Injected dependencies are NOT available in __init__()."""
411 # If we're the 'root' api, inject directly into 'self'. 412 # If we're the 'root' api, inject directly into 'self'.
412 # Otherwise inject into 'self.m' 413 # Otherwise inject into 'self.m'
(...skipping 19 matching lines...) Expand all
432 return TestData(None) 433 return TestData(None)
433 434
434 @staticmethod 435 @staticmethod
435 def _step_data(name, *data, **kwargs): 436 def _step_data(name, *data, **kwargs):
436 """Returns a new TestData with the mock data filled in for a single step. 437 """Returns a new TestData with the mock data filled in for a single step.
437 438
438 Used by step_data and override_step_data. 439 Used by step_data and override_step_data.
439 440
440 Args: 441 Args:
441 name - The name of the step we're providing data for 442 name - The name of the step we're providing data for
442 data - Zero or more StepTestData objects. These may fill in placeholder 443 data - Zero or more StepTestData objects. These may fill in output
443 data for zero or more modules, as well as possibly setting the 444 placeholder data for zero or more modules, as well as possibly
444 retcode for this step. 445 setting the retcode for this step.
445 retcode=(int or None) - Override the retcode for this step, even if it 446 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. 447 was set by |data|. This must be set as a keyword arg.
447 stdout - StepTestData object with a single output placeholder datum for a 448 stdout - StepTestData object with a single output placeholder datum for a
448 step's stdout. 449 step's stdout.
449 stderr - StepTestData object with a single output placeholder datum for a 450 stderr - StepTestData object with a single output placeholder datum for a
450 step's stderr. 451 step's stderr.
451 override=(bool) - This step data completely replaces any previously 452 override=(bool) - This step data completely replaces any previously
452 generated step data, instead of adding on to it. 453 generated step data, instead of adding on to it.
453 454
454 Use in GenTests: 455 Use in GenTests:
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after
505 506
506 def expect_exception(self, exc_type): #pylint: disable=R0201 507 def expect_exception(self, exc_type): #pylint: disable=R0201
507 ret = TestData(None) 508 ret = TestData(None)
508 ret.expect_exception(exc_type) 509 ret.expect_exception(exc_type)
509 return ret 510 return ret
510 511
511 def depend_on(self, recipe, properties, result): 512 def depend_on(self, recipe, properties, result):
512 ret = TestData() 513 ret = TestData()
513 ret.depend_on(recipe, properties, result) 514 ret.depend_on(recipe, properties, result)
514 return ret 515 return ret
OLDNEW
« no previous file with comments | « no previous file | recipe_engine/step_runner.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698