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

Side by Side Diff: recipe_engine/recipe_test_api.py

Issue 1785543004: Split Placeholder into InputPlaceholder and OutputPlaceholder. (Closed) Base URL: https://chromium.googlesource.com/external/github.com/luci/recipes-py@master
Patch Set: Fix nits. 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 . 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):
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
(...skipping 30 matching lines...) Expand all
49 class StepTestData(BaseTestData): 49 class StepTestData(BaseTestData):
50 """ 50 """
51 Mutable container for per-step test data. 51 Mutable container for per-step test data.
52 52
53 This data is consumed while running the recipe (during 53 This data is consumed while running the recipe (during
54 annotated_run.run_steps). 54 annotated_run.run_steps).
55 """ 55 """
56 def __init__(self): 56 def __init__(self):
57 super(StepTestData, self).__init__() 57 super(StepTestData, self).__init__()
58 # { (module, placeholder) -> [data] } 58 # { (module, placeholder) -> [data] }
59 self.placeholder_data = collections.defaultdict(list) 59 self.input_placeholder_data = collections.defaultdict(list)
iannucci 2016/03/12 03:36:12 I don't think there needs to be input_placeholder_
stgao 2016/03/22 05:56:43 Done. Also update other docstrings and error messa
60 self.output_placeholder_data = collections.defaultdict(list)
60 self.override = False 61 self.override = False
61 self._stdout = None 62 self._stdout = None
62 self._stderr = None 63 self._stderr = None
63 self._retcode = None 64 self._retcode = None
64 65
65 def __add__(self, other): 66 def __add__(self, other):
66 assert isinstance(other, StepTestData) 67 assert isinstance(other, StepTestData)
67 68
68 if other.override: 69 if other.override:
69 return other 70 return other
70 71
71 ret = StepTestData() 72 ret = StepTestData()
72 73
73 combineify('placeholder_data', ret, self, other) 74 combineify('input_placeholder_data', ret, self, other)
75 combineify('output_placeholder_data', ret, self, other)
74 76
75 # pylint: disable=W0212 77 # pylint: disable=W0212
76 ret._stdout = other._stdout or self._stdout 78 ret._stdout = other._stdout or self._stdout
77 ret._stderr = other._stderr or self._stderr 79 ret._stderr = other._stderr or self._stderr
78 ret._retcode = self._retcode 80 ret._retcode = self._retcode
79 if other._retcode is not None: 81 if other._retcode is not None:
80 assert ret._retcode is None 82 assert ret._retcode is None
81 ret._retcode = other._retcode 83 ret._retcode = other._retcode
82 84
83 return ret 85 return ret
84 86
85 def unwrap_placeholder(self): 87 def unwrap_output_placeholder(self):
86 # {(module, placeholder): [data]} => data. 88 # {(module, placeholder): [data]} => data.
87 assert len(self.placeholder_data) == 1 89 assert len(self.output_placeholder_data) == 1
88 data_list = self.placeholder_data.items()[0][1] 90 data_list = self.output_placeholder_data.items()[0][1]
89 assert len(data_list) == 1 91 assert len(data_list) == 1
90 return data_list[0] 92 return data_list[0]
91 93
92 def pop_placeholder(self, name_pieces): 94 def _pop_placeholder(self, name_pieces, placeholder_data):
93 l = self.placeholder_data[name_pieces] 95 l = placeholder_data[name_pieces]
94 if l: 96 if l:
95 return l.pop(0) 97 return l.pop(0)
96 else: 98 else:
97 return PlaceholderTestData() 99 return PlaceholderTestData()
98 100
101 def pop_input_placeholder(self, name_pieces):
102 return self._pop_placeholder(name_pieces, self.input_placeholder_data)
103
104 def pop_output_placeholder(self, name_pieces):
105 return self._pop_placeholder(name_pieces, self.output_placeholder_data)
106
99 @property 107 @property
100 def retcode(self): # pylint: disable=E0202 108 def retcode(self): # pylint: disable=E0202
101 return self._retcode or 0 109 return self._retcode or 0
102 110
103 @retcode.setter 111 @retcode.setter
104 def retcode(self, value): # pylint: disable=E0202 112 def retcode(self, value): # pylint: disable=E0202
105 self._retcode = value 113 self._retcode = value
106 114
107 @property 115 @property
108 def stdout(self): 116 def stdout(self):
(...skipping 141 matching lines...) Expand 10 before | Expand all | Expand 10 after
250 },) 258 },)
251 259
252 260
253 class DisabledTestData(BaseTestData): 261 class DisabledTestData(BaseTestData):
254 def __init__(self): 262 def __init__(self):
255 super(DisabledTestData, self).__init__(False) 263 super(DisabledTestData, self).__init__(False)
256 264
257 def __getattr__(self, name): 265 def __getattr__(self, name):
258 return self 266 return self
259 267
260 def pop_placeholder(self, _name_pieces): 268 def pop_input_placeholder(self, _name_pieces):
269 return self
270
271 def pop_output_placeholder(self, _name_pieces):
261 return self 272 return self
262 273
263 def pop_step_test_data(self, _step_name, _step_test_data_fn): 274 def pop_step_test_data(self, _step_name, _step_test_data_fn):
264 return self 275 return self
265 276
266 def get_module_test_data(self, _module_name): 277 def get_module_test_data(self, _module_name):
267 return self 278 return self
268 279
269 @contextlib.contextmanager 280 @contextlib.contextmanager
270 def should_raise_exception(self, exception): # pylint: disable=unused-argument 281 def should_raise_exception(self, exception): # pylint: disable=unused-argument
271 yield True 282 yield True
272 283
273 def mod_test_data(func): 284 def mod_test_data(func):
274 @static_wraps(func) 285 @util.static_wraps(func)
275 def inner(self, *args, **kwargs): 286 def inner(self, *args, **kwargs):
276 assert isinstance(self, RecipeTestApi) 287 assert isinstance(self, RecipeTestApi)
277 mod_name = self._module.NAME # pylint: disable=W0212 288 mod_name = self._module.NAME # pylint: disable=W0212
278 ret = TestData(None) 289 ret = TestData(None)
279 data = static_call(self, func, *args, **kwargs) 290 data = util.static_call(self, func, *args, **kwargs)
280 ret.mod_data[mod_name][inner.__name__] = data 291 ret.mod_data[mod_name][inner.__name__] = data
281 return ret 292 return ret
282 return inner 293 return inner
283 294
284 295
285 def placeholder_step_data(func): 296 def placeholder_step_data(func):
286 """Decorates RecipeTestApi member functions to allow those functions to 297 """Decorates RecipeTestApi member functions to allow those functions to
287 return just the placeholder data, instead of the normally required 298 return just the output placeholder data, instead of the normally required
288 StepTestData() object. 299 StepTestData() object.
289 300
290 The wrapped function may return either: 301 The wrapped function may return either:
291 * <placeholder data>, <retcode or None> 302 * <placeholder data>, <retcode or None>
292 * StepTestData containing exactly one PlaceholderTestData and possible a 303 * StepTestData containing exactly one PlaceholderTestData and possible a
293 retcode. This is useful for returning the result of another method which 304 retcode. This is useful for returning the result of another method which
294 is wrapped with placeholder_step_data. 305 is wrapped with placeholder_step_data.
295 306
296 In either case, the wrapper function will return a StepTestData object with 307 In either case, the wrapper function will return a StepTestData object with
297 the retcode and placeholder datum inserted with a name of: 308 the retcode and placeholder datum inserted with a name of:
298 (<Test module name>, <wrapped function name>) 309 (<Test module name>, <wrapped function name>)
299 310
300 Say you had a 'foo_module' with the following RecipeTestApi: 311 Say you had a 'foo_module' with the following RecipeTestApi:
301 class FooTestApi(RecipeTestApi): 312 class FooTestApi(RecipeTestApi):
302 @placeholder_step_data 313 @placeholder_step_data
303 @staticmethod 314 @staticmethod
304 def cool_method(data, retcode=None): 315 def cool_method(data, retcode=None):
305 return ("Test data (%s)" % data), retcode 316 return ("Test data (%s)" % data), retcode
306 317
307 @placeholder_step_data 318 @placeholder_step_data
308 def other_method(self, retcode=None): 319 def other_method(self, retcode=None):
309 return self.cool_method('hammer time', retcode) 320 return self.cool_method('hammer time', retcode)
310 321
311 Code calling cool_method('hello') would get a StepTestData: 322 Code calling cool_method('hello') would get a StepTestData:
312 StepTestData( 323 StepTestData(
313 placeholder_data = { 324 output_placeholder_data = {
314 ('foo_module', 'cool_method'): [ 325 ('foo_module', 'cool_method'): [
315 PlaceholderTestData('Test data (hello)') 326 PlaceholderTestData('Test data (hello)')
316 ] 327 ]
317 }, 328 },
318 retcode = None 329 retcode = None
319 ) 330 )
320 331
321 Code calling other_method(50) would get a StepTestData: 332 Code calling other_method(50) would get a StepTestData:
322 StepTestData( 333 StepTestData(
323 placeholder_data = { 334 output_placeholder_data = {
324 ('foo_module', 'other_method'): [ 335 ('foo_module', 'other_method'): [
325 PlaceholderTestData('Test data (hammer time)') 336 PlaceholderTestData('Test data (hammer time)')
326 ] 337 ]
327 }, 338 },
328 retcode = 50 339 retcode = 50
329 ) 340 )
330 """ 341 """
331 @static_wraps(func) 342 @util.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 = util.static_call(self, func, *args, **kwargs)
336 if isinstance(data, StepTestData): 347 if isinstance(data, StepTestData):
337 all_data = [i 348 all_data = data.output_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 placeholder datum. ' 350 'placeholder_step_data is only expecting a single output placeholder '
342 '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 = data
347 placeholder_data = PlaceholderTestData(placeholder_data) 356 placeholder_data = PlaceholderTestData(placeholder_data)
348 357
349 ret = StepTestData() 358 ret = StepTestData()
350 ret.placeholder_data[(mod_name, inner.__name__)].append(placeholder_data) 359 ret.output_placeholder_data[(mod_name, inner.__name__)].append(
360 placeholder_data)
351 ret.retcode = retcode 361 ret.retcode = retcode
352 return ret 362 return ret
353 return inner 363 return inner
354 364
355 365
356 class RecipeTestApi(object): 366 class RecipeTestApi(object):
357 """Provides testing interface for GenTest method. 367 """Provides testing interface for GenTest method.
358 368
359 There are two primary components to the test api: 369 There are two primary components to the test api:
360 * Test data creation methods (test and step_data) 370 * Test data creation methods (test and step_data)
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after
403 413
404 The properties.tryserver() call is documented in the 'properties' module's 414 The properties.tryserver() call is documented in the 'properties' module's
405 test_api. 415 test_api.
406 The platform() call is documented in the 'platform' module's test_api. 416 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. 417 The json.output() call is documented in the 'json' module's test_api.
408 """ 418 """
409 def __init__(self, module=None): 419 def __init__(self, module=None):
410 """Note: Injected dependencies are NOT available in __init__().""" 420 """Note: Injected dependencies are NOT available in __init__()."""
411 # If we're the 'root' api, inject directly into 'self'. 421 # If we're the 'root' api, inject directly into 'self'.
412 # Otherwise inject into 'self.m' 422 # Otherwise inject into 'self.m'
413 self.m = self if module is None else ModuleInjectionSite() 423 self.m = self if module is None else util.ModuleInjectionSite()
414 self._module = module 424 self._module = module
415 425
416 @staticmethod 426 @staticmethod
417 def test(name): 427 def test(name):
418 """Returns a new empty TestData with the name filled in. 428 """Returns a new empty TestData with the name filled in.
419 429
420 Use in GenTests: 430 Use in GenTests:
421 def GenTests(api): 431 def GenTests(api):
422 yield api.test('basic') 432 yield api.test('basic')
423 """ 433 """
(...skipping 13 matching lines...) Expand all
437 447
438 Used by step_data and override_step_data. 448 Used by step_data and override_step_data.
439 449
440 Args: 450 Args:
441 name - The name of the step we're providing data for 451 name - The name of the step we're providing data for
442 data - Zero or more StepTestData objects. These may fill in placeholder 452 data - Zero or more StepTestData objects. These may fill in placeholder
443 data for zero or more modules, as well as possibly setting the 453 data for zero or more modules, as well as possibly setting the
444 retcode for this step. 454 retcode for this step.
445 retcode=(int or None) - Override the retcode for this step, even if it 455 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. 456 was set by |data|. This must be set as a keyword arg.
447 stdout - StepTestData object with placeholder data for a step's stdout. 457 stdout - StepTestData object with a single output placeholder datum for a
448 stderr - StepTestData object with placeholder data for a step's stderr. 458 step's stdout.
459 stderr - StepTestData object with a single output placeholder datum for a
460 step's stderr.
449 override=(bool) - This step data completely replaces any previously 461 override=(bool) - This step data completely replaces any previously
450 generated step data, instead of adding on to it. 462 generated step data, instead of adding on to it.
451 463
452 Use in GenTests: 464 Use in GenTests:
453 # Hypothetically, suppose that your recipe has default test data for two 465 # Hypothetically, suppose that your recipe has default test data for two
454 # steps 'init' and 'sync' (probably via recipe_api.inject_test_data()). 466 # steps 'init' and 'sync' (probably via recipe_api.inject_test_data()).
455 # For this example, lets say that the default test data looks like: 467 # For this example, lets say that the default test data looks like:
456 # api.step_data('init', api.json.output({'some': ["cool", "json"]})) 468 # api.step_data('init', api.json.output({'some': ["cool", "json"]}))
457 # AND 469 # AND
458 # api.step_data('sync', api.json.output({'src': {'rev': 100}})) 470 # api.step_data('sync', api.json.output({'src': {'rev': 100}}))
(...skipping 21 matching lines...) Expand all
480 if data: 492 if data:
481 ret.step_data[name] = reduce(lambda x,y: x + y, data) 493 ret.step_data[name] = reduce(lambda x,y: x + y, data)
482 if 'retcode' in kwargs: 494 if 'retcode' in kwargs:
483 ret.step_data[name].retcode = kwargs['retcode'] 495 ret.step_data[name].retcode = kwargs['retcode']
484 if 'override' in kwargs: 496 if 'override' in kwargs:
485 ret.step_data[name].override = kwargs['override'] 497 ret.step_data[name].override = kwargs['override']
486 for key in ('stdout', 'stderr'): 498 for key in ('stdout', 'stderr'):
487 if key in kwargs: 499 if key in kwargs:
488 stdio_test_data = kwargs[key] 500 stdio_test_data = kwargs[key]
489 assert isinstance(stdio_test_data, StepTestData) 501 assert isinstance(stdio_test_data, StepTestData)
490 setattr(ret.step_data[name], key, stdio_test_data.unwrap_placeholder()) 502 setattr(ret.step_data[name], key,
503 stdio_test_data.unwrap_output_placeholder())
491 return ret 504 return ret
492 505
493 def step_data(self, name, *data, **kwargs): 506 def step_data(self, name, *data, **kwargs):
494 """See _step_data()""" 507 """See _step_data()"""
495 return self._step_data(name, *data, **kwargs) 508 return self._step_data(name, *data, **kwargs)
496 step_data.__doc__ = _step_data.__doc__ 509 step_data.__doc__ = _step_data.__doc__
497 510
498 def override_step_data(self, name, *data, **kwargs): 511 def override_step_data(self, name, *data, **kwargs):
499 """See _step_data()""" 512 """See _step_data()"""
500 kwargs['override'] = True 513 kwargs['override'] = True
501 return self._step_data(name, *data, **kwargs) 514 return self._step_data(name, *data, **kwargs)
502 override_step_data.__doc__ = _step_data.__doc__ 515 override_step_data.__doc__ = _step_data.__doc__
503 516
504 def expect_exception(self, exc_type): #pylint: disable=R0201 517 def expect_exception(self, exc_type): #pylint: disable=R0201
505 ret = TestData(None) 518 ret = TestData(None)
506 ret.expect_exception(exc_type) 519 ret.expect_exception(exc_type)
507 return ret 520 return ret
508 521
509 def depend_on(self, recipe, properties, result): 522 def depend_on(self, recipe, properties, result):
510 ret = TestData() 523 ret = TestData()
511 ret.depend_on(recipe, properties, result) 524 ret.depend_on(recipe, properties, result)
512 return ret 525 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