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

Unified 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 side-by-side diff with in-line comments
Download patch
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

Powered by Google App Engine
This is Rietveld 408576698