Chromium Code Reviews| 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 |