OLD | NEW |
---|---|
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 |
OLD | NEW |