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

Unified Diff: recipe_engine/simulation_test.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 side-by-side diff with in-line comments
Download patch
Index: recipe_engine/simulation_test.py
diff --git a/recipe_engine/simulation_test.py b/recipe_engine/simulation_test.py
index 17c1d1e511689cdb38340f1af3a4585b640e46df..96c0d479e92b16e7fd80bb730fa6f301a44900d1 100644
--- a/recipe_engine/simulation_test.py
+++ b/recipe_engine/simulation_test.py
@@ -5,15 +5,22 @@
"""Provides simulator test coverage for individual recipes."""
import StringIO
+import ast
import contextlib
+import copy
import json
import logging
import os
import re
import sys
+import textwrap
+import traceback
+
+from collections import OrderedDict
from . import env
from . import stream
+import astunparse
import expect_tests
# This variable must be set in the dynamic scope of the functions in this file.
@@ -22,37 +29,159 @@ import expect_tests
_UNIVERSE = None
-def RenderExpectation(test_data, raw_expectations):
- """Applies the step filters (e.g. whitelists, etc.) to the raw_expectations,
- if the TestData actually contains any filters.
- Returns the final expect_tests.Result."""
- if test_data.whitelist_data:
- whitelist_data = dict(test_data.whitelist_data) # copy so we can mutate it
- def filter_expectation(step):
- whitelist = whitelist_data.pop(step['name'], None)
- if whitelist is None:
- return
-
- whitelist = set(whitelist) # copy so we can mutate it
- if len(whitelist) > 0:
- whitelist.add('name')
- step = {k: v for k, v in step.iteritems() if k in whitelist}
- whitelist.difference_update(step.keys())
- if whitelist:
- raise ValueError(
- "The whitelist includes fields %r in step %r, but those fields"
- " don't exist."
- % (whitelist, step['name']))
- return step
- raw_expectations = filter(filter_expectation, raw_expectations)
+linecache = {}
+def getLines(filename):
dnj 2016/10/04 16:42:35 nit: Add underscores to these global functions and
iannucci 2016/10/06 20:28:09 Done.
+ filename = os.path.abspath(filename)
+ if filename in linecache:
+ return linecache[filename]
+
+ with open(filename, 'r') as f:
+ ret = f.readlines()
+ linecache[filename] = ret
+ return ret
+
+
+def getReferencedNamesAndNode(filename, lineno):
+ lines = getLines(filename)
+ i = lineno-1
+ for i in range(lineno-1, 0, -1):
dnj 2016/10/04 16:42:35 nit: xrange
dnj 2016/10/06 21:20:19 (This and other comments in this file).
iannucci 2016/10/06 22:47:11 oops
+ try:
+ to_parse = ''.join(lines[i:lineno]).strip()
+ node = ast.parse(to_parse)
+ break
+ except SyntaxError:
+ continue
martiniss 2016/10/03 19:14:19 Why is this here? comment? :)
iannucci 2016/10/06 22:47:11 Done.
+ names = set(n.id for n in ast.walk(node) if isinstance(n, ast.Name))
+ return (names, node, i)
+
+
+class Checker(object):
+ def __init__(self):
+ self._failed_checks = []
+ self._ignore_set = [self]
martiniss 2016/10/03 19:14:19 Don't really understand ignore set
iannucci 2016/10/06 22:47:11 Done.
+
+ def _ignore(self, obj):
+ self._ignore_set.extend(obj)
+
+ def _inner(self, hint, exp):
martiniss 2016/10/03 19:14:19 Some docs on the magic in this function would be n
dnj 2016/10/04 16:42:34 Since "_inner" isn't recursive or anything, let's
dnj 2016/10/04 16:42:35 +176 on docs here. This is probably the most magic
iannucci 2016/10/06 22:47:11 Done.
+ if exp:
+ # this check passed
+ return
+
+ stk = None
dnj 2016/10/04 16:42:35 nit: this shouldn't be needed, since all paths cau
iannucci 2016/10/06 22:47:11 Done.
+ try:
+ raise ZeroDivisionError
+ except ZeroDivisionError:
+ stk = sys.exc_info()[2].tb_frame.f_back.f_back
+
+ refNames, node, actual_lineno = getReferencedNamesAndNode(
+ stk.f_code.co_filename, stk.f_lineno)
+ local_vars = OrderedDict()
+ for k, v in stk.f_locals.iteritems():
+ if k not in refNames:
+ continue
+ if any(v is itm for itm in self._ignore_set):
+ continue
+ local_vars[k] = v
+ self._failed_checks.append(expect_tests.Check(
+ hint,
+ astunparse.unparse(node),
+ local_vars,
+ stk.f_code.co_filename,
+ actual_lineno,
+ False
+ ))
+
+ def __call__(self, arg1, arg2=None):
+ if arg2 is not None:
+ hint = arg1
+ exp = arg2
+ else:
+ hint = None
+ exp = arg1
+ self._inner(hint, exp)
+
+
+def checkIsSubset(a, b):
martiniss 2016/10/03 19:14:19 tests?
dnj 2016/10/04 16:42:34 docs?
iannucci 2016/10/06 22:47:11 Done.
+ # both a and b should be ordered step lists.
+ if a is b:
dnj 2016/10/04 16:42:35 Could use "==" here to catch duplicates that aren'
iannucci 2016/10/06 22:47:11 I'm not certain how that would ever happen in prac
+ return
+
+ for step_name, step in a.iteritems():
+ if step_name not in b:
+ raise ValueError(
dnj 2016/10/04 16:42:34 Maybe define a PostProcessError type?
iannucci 2016/10/06 22:47:10 Done.
+ 'post_process introduced new step %r' % step_name)
- if whitelist_data:
+ if 'name' not in step:
raise ValueError(
- "The step names %r were included in the whitelist, but were never run."
- % [s['name'] for s in whitelist_data])
+ 'post_process removed "name" from step %r' % step_name)
dnj 2016/10/04 16:42:35 Should document that this is not allowed in "post_
iannucci 2016/10/06 22:47:10 Done.
- return expect_tests.Result(raw_expectations)
+ orig = b[step_name]
+ if step is orig:
dnj 2016/10/04 16:42:34 == ?
+ continue
+
+ for k, v in step.iteritems():
+ if k not in orig:
+ raise ValueError(
+ 'post_process introduced new key %r in step %r' % (
+ k, step_name))
+
+ orig_v = orig[k]
+ if v is orig_v:
dnj 2016/10/04 16:42:34 == ?
iannucci 2016/10/06 22:47:11 Again, not sure how this would happen in practice,
+ continue
+
+ if type(v) != type(orig_v):
+ raise ValueError(
+ 'post_process changed type of key %r in step %r from %s to %s' % (
+ k, step_name, type(orig_v), type(v)))
dnj 2016/10/04 16:42:35 For rendering type names, "type(x).__name__" is a
iannucci 2016/10/06 22:47:11 Done.
+
+ if isinstance(v, list):
dnj 2016/10/04 16:42:35 Unless we specifically care about "list" and "dict
iannucci 2016/10/06 22:47:10 We do, expectations will never have something othe
+ if len(v) > len(orig_v):
+ raise ValueError(
+ 'post_process %r in step %r is longer than the original: %d v %d' %
+ (k, step_name, len(v), len(orig_v)))
+ iov = iv = 0
martiniss 2016/10/03 19:14:19 not immediately obvious what these variables are f
iannucci 2016/10/06 22:47:11 Done.
+ while iov < len(orig_v) - 1 and iv < len(v) - 1:
+ if v[iv] == orig_v[iov]:
+ iv += 1
+ iov += 1
+ if iv != len(v) - 1:
+ raise ValueError(
+ 'post_process added elements to %r in step %r: %r' % (
+ k, step_name, v[iv:]))
+ elif isinstance(v, dict):
+ for subk, subv in v.iteritems():
+ if subk not in orig_v:
dnj 2016/10/04 16:42:35 Can get by with only one lookup: orig_item = orig
iannucci 2016/10/06 22:47:10 Done.
+ raise ValueError(
+ 'post_process added element %s[%r] in step %r' % (
+ k, subk, step_name))
+ if subv != orig_v[subk]:
+ raise ValueError(
+ 'post_process changed element %s[%r] in step %r: %r to %r' % (
+ k, subk, step_name, subv, orig_v[subk]))
+ elif v != orig_v:
+ raise ValueError(
+ 'post_process changed value of key %r in step %r from %s to %s' % (
+ k, step_name, orig_v, v))
+
+
+def RenderExpectation(test_data, step_odict):
+ """Applies the step post_process actions to the step_odict, if the
+ TestData actually contains any.
+
+ Returns the final expect_tests.Result."""
+ checker = Checker()
+
+ for hook, args, kwargs in test_data.post_process_hooks:
+ input_odict = copy.deepcopy(step_odict)
+ checker._ignore(input_odict)
martiniss 2016/10/03 19:14:19 why do we always ignore the input dict?
iannucci 2016/10/06 22:47:10 Done.
+ rslt = hook(checker, input_odict, *args, **kwargs)
+ if rslt is not None:
+ checkIsSubset(rslt, step_odict)
+ step_odict = rslt
+
+ return expect_tests.Result(step_odict.values(), checker._failed_checks)
class SimulationAnnotatorStreamEngine(stream.AnnotatorStreamEngine):
@@ -70,13 +199,24 @@ class SimulationAnnotatorStreamEngine(stream.AnnotatorStreamEngine):
self.step_buffer(step_config.name))
-def RunRecipe(test_data):
+# This maps from (recipe_name,test_name) -> yielded test_data. It's outside of
dnj 2016/10/04 16:42:35 Can you make this a namedtuple?
iannucci 2016/10/06 22:47:11 I tried, it makes it more verbose for very little
+# RunRecipe so that it can persist between RunRecipe calls in the same process.
+_GEN_TEST_CACHE = {}
+
+def RunRecipe(recipe_name, test_name):
"""Actually runs the recipe given the GenTests-supplied test_data."""
from . import config_types
from . import loader
from . import run
from . import step_runner
- from . import stream
+
+ if recipe_name not in _GEN_TEST_CACHE:
+ recipe_script = _UNIVERSE.load_recipe(recipe_name)
+ test_api = loader.create_test_api(recipe_script.LOADED_DEPS, _UNIVERSE)
+ for test_data in recipe_script.GenTests(test_api):
+ _GEN_TEST_CACHE[(recipe_name, test_data.name)] = test_data
+
+ test_data = _GEN_TEST_CACHE[(recipe_name, test_name)]
config_types.ResetTostringFns()
@@ -88,15 +228,18 @@ def RunRecipe(test_data):
step_runner = step_runner.SimulationStepRunner(stream_engine, test_data,
annotator)
- engine = run.RecipeEngine(step_runner, test_data.properties, _UNIVERSE)
- recipe_script = _UNIVERSE.load_recipe(test_data.properties['recipe'])
+ props = test_data.properties.copy()
dnj 2016/10/04 16:42:34 deepcopy? I think properties can be nested.
iannucci 2016/10/06 22:47:11 they can, but it doesn't matter.
+ props['recipe'] = recipe_name
+ engine = run.RecipeEngine(step_runner, props, _UNIVERSE)
+ recipe_script = _UNIVERSE.load_recipe(recipe_name)
api = loader.create_recipe_api(recipe_script.LOADED_DEPS, engine, test_data)
result = engine.run(recipe_script, api)
# Don't include tracebacks in expectations because they are too sensitive to
# change.
result.result.pop('traceback', None)
- raw_expectations = step_runner.steps_ran + [result.result]
+ raw_expectations = step_runner.steps_ran.copy()
+ raw_expectations[result.result['name']] = result.result
try:
return RenderExpectation(test_data, raw_expectations)
@@ -130,7 +273,8 @@ def cover_omit():
return omit
-class InsufficientTestCoverage(Exception): pass
+class InsufficientTestCoverage(Exception):
+ pass
@expect_tests.covers(test_gen_coverage)
@@ -149,27 +293,18 @@ def GenerateTests():
covers = cover_mods + [recipe_path]
- full_expectation_count = 0
for test_data in recipe.GenTests(test_api):
- if not test_data.whitelist_data:
- full_expectation_count += 1
root, name = os.path.split(recipe_path)
name = os.path.splitext(name)[0]
expect_path = os.path.join(root, '%s.expected' % name)
-
- test_data.properties['recipe'] = recipe_name.replace('\\', '/')
yield expect_tests.Test(
'%s.%s' % (recipe_name, test_data.name),
- expect_tests.FuncCall(RunRecipe, test_data),
+ expect_tests.FuncCall(RunRecipe, recipe_name, test_data.name),
expect_dir=expect_path,
expect_base=test_data.name,
covers=covers,
break_funcs=(recipe.RunSteps,)
)
-
- if full_expectation_count < 1:
- raise InsufficientTestCoverage(
- 'Must have at least 1 test without a whitelist!')
except:
info = sys.exc_info()
new_exec = Exception('While generating results for %r: %s: %s' % (

Powered by Google App Engine
This is Rietveld 408576698