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

Side by Side Diff: recipe_engine/recipe_test_api.py

Issue 2387763003: Add initial postprocess unit test thingy. (Closed)
Patch Set: rewrite parser code Created 4 years, 2 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
OLDNEW
1 # Copyright 2013 The LUCI Authors. All rights reserved. 1 # Copyright 2016 The LUCI Authors. All rights reserved.
2 # Use of this source code is governed under the Apache License, Version 2.0 2 # Use of this source code is governed under the Apache License, Version 2.0
3 # that can be found in the LICENSE file. 3 # that can be found in the LICENSE file.
4 4
5 import collections
6 import contextlib 5 import contextlib
6 import inspect
7
8 from collections import namedtuple, defaultdict
7 9
8 from .util import ModuleInjectionSite, static_call, static_wraps 10 from .util import ModuleInjectionSite, static_call, static_wraps
9 from .types import freeze 11 from .types import freeze
10 12
11 def combineify(name, dest, a, b, overwrite=False): 13 def combineify(name, dest, a, b, overwrite=False):
12 """ 14 """
13 Combines dictionary members in two objects into a third one using addition. 15 Combines dictionary members in two objects into a third one using addition.
14 16
15 Args: 17 Args:
16 name - the name of the member 18 name - the name of the member
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
58 class StepTestData(BaseTestData): 60 class StepTestData(BaseTestData):
59 """ 61 """
60 Mutable container for per-step test data. 62 Mutable container for per-step test data.
61 63
62 This data is consumed while running the recipe (during 64 This data is consumed while running the recipe (during
63 annotated_run.run_steps). 65 annotated_run.run_steps).
64 """ 66 """
65 def __init__(self): 67 def __init__(self):
66 super(StepTestData, self).__init__() 68 super(StepTestData, self).__init__()
67 # { (module, placeholder, name) -> data }. Data are for output placeholders. 69 # { (module, placeholder, name) -> data }. Data are for output placeholders.
68 self.placeholder_data = collections.defaultdict(dict) 70 self.placeholder_data = defaultdict(dict)
69 self.override = False 71 self.override = False
70 self._stdout = None 72 self._stdout = None
71 self._stderr = None 73 self._stderr = None
72 self._retcode = None 74 self._retcode = None
73 self._times_out_after = None 75 self._times_out_after = None
74 76
75 def __add__(self, other): 77 def __add__(self, other):
76 assert isinstance(other, StepTestData) 78 assert isinstance(other, StepTestData)
77 79
78 if other.override: 80 if other.override:
(...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after
174 assert isinstance(other, ModuleTestData) 176 assert isinstance(other, ModuleTestData)
175 ret = ModuleTestData() 177 ret = ModuleTestData()
176 ret.update(self) 178 ret.update(self)
177 ret.update(other) 179 ret.update(other)
178 return ret 180 return ret
179 181
180 def __repr__(self): 182 def __repr__(self):
181 return "ModuleTestData(%r)" % super(ModuleTestData, self).__repr__() 183 return "ModuleTestData(%r)" % super(ModuleTestData, self).__repr__()
182 184
183 185
186 PostprocessHook = namedtuple(
187 'PostprocessHook', 'func args kwargs filename lineno')
188
189
184 class TestData(BaseTestData): 190 class TestData(BaseTestData):
185 def __init__(self, name=None): 191 def __init__(self, name=None):
186 super(TestData, self).__init__() 192 super(TestData, self).__init__()
187 self.name = name 193 self.name = name
188 self.properties = {} # key -> val 194 self.properties = {} # key -> val
189 self.mod_data = collections.defaultdict(ModuleTestData) 195 self.mod_data = defaultdict(ModuleTestData)
190 self.step_data = collections.defaultdict(StepTestData) 196 self.step_data = defaultdict(StepTestData)
191 self.depend_on_data = {} 197 self.depend_on_data = {}
192 self.expected_exception = None 198 self.expected_exception = None
193 self.whitelist_data = {} # step_name -> fields 199 self.post_process_hooks = [] # list(PostprocessHook)
194 200
195 def __add__(self, other): 201 def __add__(self, other):
196 assert isinstance(other, TestData) 202 assert isinstance(other, TestData)
197 ret = TestData(self.name or other.name) 203 ret = TestData(self.name or other.name)
198 204
199 ret.properties.update(self.properties) 205 ret.properties.update(self.properties)
200 ret.properties.update(other.properties) 206 ret.properties.update(other.properties)
201 207
202 combineify('mod_data', ret, self, other) 208 combineify('mod_data', ret, self, other)
203 combineify('step_data', ret, self, other) 209 combineify('step_data', ret, self, other)
204 combineify('depend_on_data', ret, self, other) 210 combineify('depend_on_data', ret, self, other)
205 combineify('whitelist_data', ret, self, other) 211
212 ret.post_process_hooks.extend(self.post_process_hooks)
213 ret.post_process_hooks.extend(other.post_process_hooks)
214
206 ret.expected_exception = self.expected_exception 215 ret.expected_exception = self.expected_exception
207 if other.expected_exception: 216 if other.expected_exception:
208 ret.expected_exception = other.expected_exception 217 ret.expected_exception = other.expected_exception
209 218
210 return ret 219 return ret
211 220
212 @property 221 @property
213 def consumed(self): 222 def consumed(self):
214 return not (self.step_data or self.expected_exception) 223 return not (self.step_data or self.expected_exception)
215 224
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
264 if not should_raise: 273 if not should_raise:
265 self.expected_exception = None 274 self.expected_exception = None
266 275
267 def depend_on(self, recipe, properties, result): 276 def depend_on(self, recipe, properties, result):
268 tup = freeze((recipe, properties)) 277 tup = freeze((recipe, properties))
269 if tup in self.depend_on_data: 278 if tup in self.depend_on_data:
270 raise ValueError('Already gave test data for recipe %s with properties %r' 279 raise ValueError('Already gave test data for recipe %s with properties %r'
271 % tup) 280 % tup)
272 self.depend_on_data[tup] = freeze(result) 281 self.depend_on_data[tup] = freeze(result)
273 282
274 def whitelist(self, step_name, fields): 283 def post_process(self, func, args, kwargs, filename, lineno):
275 self.whitelist_data[step_name] = frozenset(fields) 284 self.post_process_hooks.append(PostprocessHook(
285 func, args, kwargs, filename, lineno))
276 286
277 def __repr__(self): 287 def __repr__(self):
278 return "TestData(%r)" % ({ 288 return "TestData(%r)" % ({
279 'name': self.name, 289 'name': self.name,
280 'properties': self.properties, 290 'properties': self.properties,
281 'mod_data': dict(self.mod_data.iteritems()), 291 'mod_data': dict(self.mod_data.iteritems()),
282 'step_data': dict(self.step_data.iteritems()), 292 'step_data': dict(self.step_data.iteritems()),
283 'expected_exception': self.expected_exception, 293 'expected_exception': self.expected_exception,
284 'depend_on_data': self.depend_on_data, 294 'depend_on_data': self.depend_on_data,
285 },) 295 },)
(...skipping 257 matching lines...) Expand 10 before | Expand all | Expand 10 after
543 def expect_exception(self, exc_type): #pylint: disable=R0201 553 def expect_exception(self, exc_type): #pylint: disable=R0201
544 ret = TestData(None) 554 ret = TestData(None)
545 ret.expect_exception(exc_type) 555 ret.expect_exception(exc_type)
546 return ret 556 return ret
547 557
548 def depend_on(self, recipe, properties, result): 558 def depend_on(self, recipe, properties, result):
549 ret = TestData() 559 ret = TestData()
550 ret.depend_on(recipe, properties, result) 560 ret.depend_on(recipe, properties, result)
551 return ret 561 return ret
552 562
553 def whitelist(self, step_name, *fields): 563 def post_process(self, func, *args, **kwargs):
554 """Calling this enables step whitelisting for the expectations on this test. 564 """Calling this adds a post-processing hook for this test's expectations.
555 You may call it multiple times, once per step_name that you want to have
556 show in the JSON expectations file for this test.
557 565
558 You may also optionally specify fields that you want to show up in the JSON 566 `func` should be a callable whose signature is in the form of:
559 expectations. By default, all fields of the step will appear, but you may 567 func(check, step_odict, *args, **kwargs) -> (step_odict or None)
560 only be interested in e.g. 'cmd' or 'env', for example. The 'name' field is
561 always included, regardless.
562 568
563 Keep in mind that the ultimate result of the recipe (the return value from 569 Where:
564 RunSteps) is on a virtual step named '$result'. 570 * `step_odict` is an ordered dictionary of step dictionaries, as would be
571 recorded into the JSON expectation file for this test. The dictionary key
572 is the step's name.
573
574 * `check` is a semi-magical function which you can use to test things.
575 Using `check` will allow you to see all the violated assertions from your
576 post_process functions simultaneously. Always call `check` directly (i.e.
577 with parens) to produce helpful check messages. `check` also has a second
578 form that takes a human hint to print when the `check` fails. Hints should
579 be written as the ___ in the sentence 'check that ___.'. Essentially,
580 check has the function signatures:
581
582 `def check(<bool expression>)`
583 `def check(hint, <bool expression>)`
584
585 If the hint is omitted, then the boolean expression itself becomes the
586 hint when the check failure message is printed.
587
588 Note that check DOES NOT stop your function. It is not an assert. Your
589 function will continue to execute after invoking the check function. If
590 the boolean expression is False, the check will produce a helpful error
591 message and cause the test case to fail.
592
593 * args and kwargs are optional, and completely up to your implementation.
594 They will be passed straight through to your function, and are provided to
595 eliminate an extra `lambda` if your function needs to take additional
596 inputs.
597
598 Raising an exception will print the exception and will halt the
599 postprocessing chain entirely.
600
601 The function must return either `None`, or it may return a filtered subset
602 of step_odict (e.g. ommitting some steps and/or dictionary keys). This will
603 be the new value of step_odict for the test. Returning an empty dict or
604 OrderedDict will remove the expectations from disk altogether. Returning
605 `None` (Python's implicit default return value) is equivalent to returning
606 the unmodified step_odict. 'name' will always be preserved in every step,
607 even if you remove it.
608
609 Calling post_process multiple times will apply each function in order,
610 chaining the output of one function to the input of the next function. This
611 is intended to be use to compose the effects of multiple re-usable
612 postprocessing functions, some of which are pre-defined in
613 `recipe_engine.post_process` which you can import in your recipe.
565 614
566 Example: 615 Example:
567 yield api.test('assert entire recipe') 616 from recipe_engine.post_process import (Filter, DoesNotRun,
617 DropExpectation)
568 618
569 yield (api.test('assert only thing step') 619 def GenTests(api):
570 + api.whitelist('thing step') 620 yield api.test('no post processing')
571 )
572 621
573 yield (api.test('assert only thing step\'s cmd') 622 yield (api.test('only thing_step')
574 + api.whitelist('thing step', 'cmd') 623 + api.post_process(Filter('thing_step'))
575 ) 624 )
576 625
577 yield (api.test('assert thing step and other step') 626 tstepFilt = Filter()
578 + api.whitelist('thing step') 627 tstepFilt = tstepFilt.include('thing_step', 'cmd')
579 + api.whitelist('other step') 628 yield (api.test('only thing_step\'s cmd')
580 ) 629 + api.post_process(tstepFilt)
630 )
581 631
582 yield (api.test('only care about the result') 632 yield (api.test('assert bob_step does not run')
583 + api.whitelist('$result') 633 + api.post_process(DoesNotRun, 'bob_step')
584 ) 634 )
635
636 yield (api.test('only care one step and the result')
637 + api.post_process(Filter('one_step', '$result'))
638 )
639
640 def assertStuff(check, step_odict, to_check):
641 check(to_check in step_odict['step_name']['cmd'])
642
643 yield (api.test('assert something and have NO expectation file')
644 + api.post_process(assertStuff, 'to_check_arg')
645 + api.post_process(DropExpectation)
646 )
585 """ 647 """
586 ret = TestData() 648 ret = TestData()
587 ret.whitelist(step_name, fields) 649 try:
650 stk = inspect.stack()
651 _, filename, lineno, _, _, _ = stk[1]
652 finally:
653 del stk
654 ret.post_process(func, args, kwargs, filename, lineno)
588 return ret 655 return ret
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698