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

Side by Side Diff: recipe_engine/recipe_test_api.py

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

Powered by Google App Engine
This is Rietveld 408576698