| Index: recipe_engine/recipe_test_api.py
|
| diff --git a/recipe_engine/recipe_test_api.py b/recipe_engine/recipe_test_api.py
|
| index 629686e9e3f70189066afc3425c07df0440cae24..e432491d3757079f6a12b2e6580eb53be24e2112 100644
|
| --- a/recipe_engine/recipe_test_api.py
|
| +++ b/recipe_engine/recipe_test_api.py
|
| @@ -1,9 +1,11 @@
|
| -# Copyright 2013 The LUCI Authors. All rights reserved.
|
| +# Copyright 2016 The LUCI Authors. All rights reserved.
|
| # Use of this source code is governed under the Apache License, Version 2.0
|
| # that can be found in the LICENSE file.
|
|
|
| -import collections
|
| import contextlib
|
| +import inspect
|
| +
|
| +from collections import namedtuple, defaultdict
|
|
|
| from .util import ModuleInjectionSite, static_call, static_wraps
|
| from .types import freeze
|
| @@ -65,7 +67,7 @@ class StepTestData(BaseTestData):
|
| def __init__(self):
|
| super(StepTestData, self).__init__()
|
| # { (module, placeholder, name) -> data }. Data are for output placeholders.
|
| - self.placeholder_data = collections.defaultdict(dict)
|
| + self.placeholder_data = defaultdict(dict)
|
| self.override = False
|
| self._stdout = None
|
| self._stderr = None
|
| @@ -181,16 +183,20 @@ class ModuleTestData(BaseTestData, dict):
|
| return "ModuleTestData(%r)" % super(ModuleTestData, self).__repr__()
|
|
|
|
|
| +PostprocessHook = namedtuple(
|
| + 'PostprocessHook', 'func args kwargs filename lineno')
|
| +
|
| +
|
| class TestData(BaseTestData):
|
| def __init__(self, name=None):
|
| super(TestData, self).__init__()
|
| self.name = name
|
| self.properties = {} # key -> val
|
| - self.mod_data = collections.defaultdict(ModuleTestData)
|
| - self.step_data = collections.defaultdict(StepTestData)
|
| + self.mod_data = defaultdict(ModuleTestData)
|
| + self.step_data = defaultdict(StepTestData)
|
| self.depend_on_data = {}
|
| self.expected_exception = None
|
| - self.whitelist_data = {} # step_name -> fields
|
| + self.post_process_hooks = [] # list(PostprocessHook)
|
|
|
| def __add__(self, other):
|
| assert isinstance(other, TestData)
|
| @@ -202,7 +208,10 @@ class TestData(BaseTestData):
|
| combineify('mod_data', ret, self, other)
|
| combineify('step_data', ret, self, other)
|
| combineify('depend_on_data', ret, self, other)
|
| - combineify('whitelist_data', ret, self, other)
|
| +
|
| + ret.post_process_hooks.extend(self.post_process_hooks)
|
| + ret.post_process_hooks.extend(other.post_process_hooks)
|
| +
|
| ret.expected_exception = self.expected_exception
|
| if other.expected_exception:
|
| ret.expected_exception = other.expected_exception
|
| @@ -271,8 +280,9 @@ class TestData(BaseTestData):
|
| % tup)
|
| self.depend_on_data[tup] = freeze(result)
|
|
|
| - def whitelist(self, step_name, fields):
|
| - self.whitelist_data[step_name] = frozenset(fields)
|
| + def post_process(self, func, args, kwargs, filename, lineno):
|
| + self.post_process_hooks.append(PostprocessHook(
|
| + func, args, kwargs, filename, lineno))
|
|
|
| def __repr__(self):
|
| return "TestData(%r)" % ({
|
| @@ -550,39 +560,96 @@ class RecipeTestApi(object):
|
| ret.depend_on(recipe, properties, result)
|
| return ret
|
|
|
| - def whitelist(self, step_name, *fields):
|
| - """Calling this enables step whitelisting for the expectations on this test.
|
| - You may call it multiple times, once per step_name that you want to have
|
| - show in the JSON expectations file for this test.
|
| + def post_process(self, func, *args, **kwargs):
|
| + """Calling this adds a post-processing hook for this test's expectations.
|
| +
|
| + `func` should be a callable whose signature is in the form of:
|
| + func(check, step_odict, *args, **kwargs) -> (step_odict or None)
|
| +
|
| + Where:
|
| + * `step_odict` is an ordered dictionary of step dictionaries, as would be
|
| + recorded into the JSON expectation file for this test. The dictionary key
|
| + is the step's name.
|
| +
|
| + * `check` is a semi-magical function which you can use to test things.
|
| + Using `check` will allow you to see all the violated assertions from your
|
| + post_process functions simultaneously. Always call `check` directly (i.e.
|
| + with parens) to produce helpful check messages. `check` also has a second
|
| + form that takes a human hint to print when the `check` fails. Hints should
|
| + be written as the ___ in the sentence 'check that ___.'. Essentially,
|
| + check has the function signatures:
|
| +
|
| + `def check(<bool expression>)`
|
| + `def check(hint, <bool expression>)`
|
| +
|
| + If the hint is omitted, then the boolean expression itself becomes the
|
| + hint when the check failure message is printed.
|
| +
|
| + Note that check DOES NOT stop your function. It is not an assert. Your
|
| + function will continue to execute after invoking the check function. If
|
| + the boolean expression is False, the check will produce a helpful error
|
| + message and cause the test case to fail.
|
| +
|
| + * args and kwargs are optional, and completely up to your implementation.
|
| + They will be passed straight through to your function, and are provided to
|
| + eliminate an extra `lambda` if your function needs to take additional
|
| + inputs.
|
| +
|
| + Raising an exception will print the exception and will halt the
|
| + postprocessing chain entirely.
|
| +
|
| + The function must return either `None`, or it may return a filtered subset
|
| + of step_odict (e.g. ommitting some steps and/or dictionary keys). This will
|
| + be the new value of step_odict for the test. Returning an empty dict or
|
| + OrderedDict will remove the expectations from disk altogether. Returning
|
| + `None` (Python's implicit default return value) is equivalent to returning
|
| + the unmodified step_odict. 'name' will always be preserved in every step,
|
| + even if you remove it.
|
| +
|
| + Calling post_process multiple times will apply each function in order,
|
| + chaining the output of one function to the input of the next function. This
|
| + is intended to be use to compose the effects of multiple re-usable
|
| + postprocessing functions, some of which are pre-defined in
|
| + `recipe_engine.post_process` which you can import in your recipe.
|
|
|
| - You may also optionally specify fields that you want to show up in the JSON
|
| - expectations. By default, all fields of the step will appear, but you may
|
| - only be interested in e.g. 'cmd' or 'env', for example. The 'name' field is
|
| - always included, regardless.
|
| + Example:
|
| + from recipe_engine.post_process import (Filter, DoesNotRun,
|
| + DropExpectation)
|
|
|
| - Keep in mind that the ultimate result of the recipe (the return value from
|
| - RunSteps) is on a virtual step named '$result'.
|
| + def GenTests(api):
|
| + yield api.test('no post processing')
|
|
|
| - Example:
|
| - yield api.test('assert entire recipe')
|
| + yield (api.test('only thing_step')
|
| + + api.post_process(Filter('thing_step'))
|
| + )
|
|
|
| - yield (api.test('assert only thing step')
|
| - + api.whitelist('thing step')
|
| - )
|
| + tstepFilt = Filter()
|
| + tstepFilt = tstepFilt.include('thing_step', 'cmd')
|
| + yield (api.test('only thing_step\'s cmd')
|
| + + api.post_process(tstepFilt)
|
| + )
|
|
|
| - yield (api.test('assert only thing step\'s cmd')
|
| - + api.whitelist('thing step', 'cmd')
|
| - )
|
| + yield (api.test('assert bob_step does not run')
|
| + + api.post_process(DoesNotRun, 'bob_step')
|
| + )
|
|
|
| - yield (api.test('assert thing step and other step')
|
| - + api.whitelist('thing step')
|
| - + api.whitelist('other step')
|
| - )
|
| + yield (api.test('only care one step and the result')
|
| + + api.post_process(Filter('one_step', '$result'))
|
| + )
|
|
|
| - yield (api.test('only care about the result')
|
| - + api.whitelist('$result')
|
| - )
|
| + def assertStuff(check, step_odict, to_check):
|
| + check(to_check in step_odict['step_name']['cmd'])
|
| +
|
| + yield (api.test('assert something and have NO expectation file')
|
| + + api.post_process(assertStuff, 'to_check_arg')
|
| + + api.post_process(DropExpectation)
|
| + )
|
| """
|
| ret = TestData()
|
| - ret.whitelist(step_name, fields)
|
| + try:
|
| + stk = inspect.stack()
|
| + _, filename, lineno, _, _, _ = stk[1]
|
| + finally:
|
| + del stk
|
| + ret.post_process(func, args, kwargs, filename, lineno)
|
| return ret
|
|
|