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

Side by Side Diff: recipe_engine/simulation_test.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 unified diff | Download patch
OLDNEW
1 # Copyright 2014 The LUCI Authors. All rights reserved. 1 # Copyright 2014 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 """Provides simulator test coverage for individual recipes.""" 5 """Provides simulator test coverage for individual recipes."""
6 6
7 import StringIO 7 import StringIO
8 import ast
8 import contextlib 9 import contextlib
10 import copy
9 import json 11 import json
10 import logging 12 import logging
11 import os 13 import os
12 import re 14 import re
13 import sys 15 import sys
16 import textwrap
17 import traceback
18 import inspect
19
20 from collections import OrderedDict, namedtuple
14 21
15 from . import env 22 from . import env
16 from . import stream 23 from . import stream
17 import expect_tests 24 import expect_tests
25 from .checker import Checker, VerifySubset
18 26
19 # This variable must be set in the dynamic scope of the functions in this file. 27 # This variable must be set in the dynamic scope of the functions in this file.
20 # We do this instead of passing because the threading system of expect tests 28 # We do this instead of passing because the threading system of expect tests
21 # doesn't know how to serialize it. 29 # doesn't know how to serialize it.
22 _UNIVERSE = None 30 _UNIVERSE = None
23 31
24 32
25 def RenderExpectation(test_data, raw_expectations): 33 class PostProcessError(ValueError):
26 """Applies the step filters (e.g. whitelists, etc.) to the raw_expectations, 34 pass
27 if the TestData actually contains any filters. 35
36
37 def _renderExpectation(test_data, step_odict):
38 """Applies the step post_process actions to the step_odict, if the
39 TestData actually contains any.
28 40
29 Returns the final expect_tests.Result.""" 41 Returns the final expect_tests.Result."""
30 if test_data.whitelist_data:
31 whitelist_data = dict(test_data.whitelist_data) # copy so we can mutate it
32 def filter_expectation(step):
33 whitelist = whitelist_data.pop(step['name'], None)
34 if whitelist is None:
35 return
36 42
37 whitelist = set(whitelist) # copy so we can mutate it 43 failed_checks = []
38 if len(whitelist) > 0:
39 whitelist.add('name')
40 step = {k: v for k, v in step.iteritems() if k in whitelist}
41 whitelist.difference_update(step.keys())
42 if whitelist:
43 raise ValueError(
44 "The whitelist includes fields %r in step %r, but those fields"
45 " don't exist."
46 % (whitelist, step['name']))
47 return step
48 raw_expectations = filter(filter_expectation, raw_expectations)
49 44
50 if whitelist_data: 45 for hook, args, kwargs, filename, lineno in test_data.post_process_hooks:
51 raise ValueError( 46 input_odict = copy.deepcopy(step_odict)
52 "The step names %r were included in the whitelist, but were never run." 47 # we ignore the input_odict so that it never gets printed in full. Usually
53 % [s['name'] for s in whitelist_data]) 48 # the check invocation itself will index the input_odict or will use it only
49 # for a key membership comparison, which provides enough debugging context.
50 checker = Checker(filename, lineno, hook, args, kwargs, input_odict)
51 rslt = hook(checker, input_odict, *args, **kwargs)
52 failed_checks += checker.failed_checks
53 if rslt is not None:
54 msg = VerifySubset(rslt, step_odict)
55 if msg:
56 raise PostProcessError('post_process: steps'+msg)
57 # restore 'name'
58 for k, v in rslt.iteritems():
59 if 'name' not in v:
60 v['name'] = k
61 step_odict = rslt
54 62
55 return expect_tests.Result(raw_expectations) 63 # empty means drop expectation
64 result_data = step_odict.values() if step_odict else None
65 return expect_tests.Result(result_data, failed_checks)
56 66
57 67
58 class SimulationAnnotatorStreamEngine(stream.AnnotatorStreamEngine): 68 class SimulationAnnotatorStreamEngine(stream.AnnotatorStreamEngine):
59 69
60 def __init__(self): 70 def __init__(self):
61 self._step_buffer_map = {} 71 self._step_buffer_map = {}
62 super(SimulationAnnotatorStreamEngine, self).__init__( 72 super(SimulationAnnotatorStreamEngine, self).__init__(
63 self.step_buffer(None)) 73 self.step_buffer(None))
64 74
65 def step_buffer(self, step_name): 75 def step_buffer(self, step_name):
66 return self._step_buffer_map.setdefault(step_name, StringIO.StringIO()) 76 return self._step_buffer_map.setdefault(step_name, StringIO.StringIO())
67 77
68 def new_step_stream(self, step_config): 78 def new_step_stream(self, step_config):
69 return self._create_step_stream(step_config, 79 return self._create_step_stream(step_config,
70 self.step_buffer(step_config.name)) 80 self.step_buffer(step_config.name))
71 81
72 82
73 def RunRecipe(test_data): 83 # This maps from (recipe_name,test_name) -> yielded test_data. It's outside of
84 # RunRecipe so that it can persist between RunRecipe calls in the same process.
85 _GEN_TEST_CACHE = {}
martiniss 2016/10/13 22:54:13 I don't see how this would do anything, is what I
iannucci 2016/10/13 23:05:05 We always run recipes many many times... one per t
86
87 def RunRecipe(recipe_name, test_name):
74 """Actually runs the recipe given the GenTests-supplied test_data.""" 88 """Actually runs the recipe given the GenTests-supplied test_data."""
75 from . import config_types 89 from . import config_types
76 from . import loader 90 from . import loader
77 from . import run 91 from . import run
78 from . import step_runner 92 from . import step_runner
79 from . import stream 93
94 if recipe_name not in _GEN_TEST_CACHE:
95 recipe_script = _UNIVERSE.load_recipe(recipe_name)
96 test_api = loader.create_test_api(recipe_script.LOADED_DEPS, _UNIVERSE)
97 for test_data in recipe_script.GenTests(test_api):
98 _GEN_TEST_CACHE[(recipe_name, test_data.name)] = test_data
99
100 test_data = _GEN_TEST_CACHE[(recipe_name, test_name)]
80 101
81 config_types.ResetTostringFns() 102 config_types.ResetTostringFns()
82 103
83 annotator = SimulationAnnotatorStreamEngine() 104 annotator = SimulationAnnotatorStreamEngine()
84 stream_engine = stream.ProductStreamEngine( 105 stream_engine = stream.ProductStreamEngine(
85 stream.StreamEngineInvariants(), 106 stream.StreamEngineInvariants(),
86 annotator) 107 annotator)
87 with stream_engine: 108 with stream_engine:
88 step_runner = step_runner.SimulationStepRunner(stream_engine, test_data, 109 step_runner = step_runner.SimulationStepRunner(stream_engine, test_data,
89 annotator) 110 annotator)
90 111
91 engine = run.RecipeEngine(step_runner, test_data.properties, _UNIVERSE) 112 props = test_data.properties.copy()
92 recipe_script = _UNIVERSE.load_recipe(test_data.properties['recipe']) 113 props['recipe'] = recipe_name
114 engine = run.RecipeEngine(step_runner, props, _UNIVERSE)
115 recipe_script = _UNIVERSE.load_recipe(recipe_name)
93 api = loader.create_recipe_api(recipe_script.LOADED_DEPS, engine, test_data) 116 api = loader.create_recipe_api(recipe_script.LOADED_DEPS, engine, test_data)
94 result = engine.run(recipe_script, api) 117 result = engine.run(recipe_script, api)
95 118
96 # Don't include tracebacks in expectations because they are too sensitive to 119 # Don't include tracebacks in expectations because they are too sensitive to
97 # change. 120 # change.
98 result.result.pop('traceback', None) 121 result.result.pop('traceback', None)
99 raw_expectations = step_runner.steps_ran + [result.result] 122 raw_expectations = step_runner.steps_ran.copy()
123 raw_expectations[result.result['name']] = result.result
100 124
101 try: 125 try:
102 return RenderExpectation(test_data, raw_expectations) 126 return _renderExpectation(test_data, raw_expectations)
103 except: 127 except:
104 print 128 print
105 print "The expectations would have been:" 129 print "The expectations would have been:"
106 json.dump(raw_expectations, sys.stdout, indent=2) 130 json.dump(raw_expectations, sys.stdout, indent=2)
107 raise 131 raise
108 132
109 133
110 def test_gen_coverage(): 134 def test_gen_coverage():
111 cover = [] 135 cover = []
112 136
(...skipping 10 matching lines...) Expand all
123 def cover_omit(): 147 def cover_omit():
124 omit = [ ] 148 omit = [ ]
125 149
126 for mod_dir_base in _UNIVERSE.module_dirs: 150 for mod_dir_base in _UNIVERSE.module_dirs:
127 if os.path.isdir(mod_dir_base): 151 if os.path.isdir(mod_dir_base):
128 omit.append(os.path.join(mod_dir_base, '*', 'resources', '*')) 152 omit.append(os.path.join(mod_dir_base, '*', 'resources', '*'))
129 153
130 return omit 154 return omit
131 155
132 156
133 class InsufficientTestCoverage(Exception): pass 157 class InsufficientTestCoverage(Exception):
158 pass
134 159
135 160
136 @expect_tests.covers(test_gen_coverage) 161 @expect_tests.covers(test_gen_coverage)
137 def GenerateTests(): 162 def GenerateTests():
138 from . import loader 163 from . import loader
139 164
140 cover_mods = [ ] 165 cover_mods = [ ]
141 for mod_dir_base in _UNIVERSE.module_dirs: 166 for mod_dir_base in _UNIVERSE.module_dirs:
142 if os.path.isdir(mod_dir_base): 167 if os.path.isdir(mod_dir_base):
143 cover_mods.append(os.path.join(mod_dir_base, '*.py')) 168 cover_mods.append(os.path.join(mod_dir_base, '*.py'))
144 169
145 for recipe_path, recipe_name in _UNIVERSE.loop_over_recipes(): 170 for recipe_path, recipe_name in _UNIVERSE.loop_over_recipes():
146 try: 171 try:
147 recipe = _UNIVERSE.load_recipe(recipe_name) 172 recipe = _UNIVERSE.load_recipe(recipe_name)
148 test_api = loader.create_test_api(recipe.LOADED_DEPS, _UNIVERSE) 173 test_api = loader.create_test_api(recipe.LOADED_DEPS, _UNIVERSE)
149 174
150 covers = cover_mods + [recipe_path] 175 covers = cover_mods + [recipe_path]
151 176
152 full_expectation_count = 0
153 for test_data in recipe.GenTests(test_api): 177 for test_data in recipe.GenTests(test_api):
154 if not test_data.whitelist_data:
155 full_expectation_count += 1
156 root, name = os.path.split(recipe_path) 178 root, name = os.path.split(recipe_path)
157 name = os.path.splitext(name)[0] 179 name = os.path.splitext(name)[0]
158 expect_path = os.path.join(root, '%s.expected' % name) 180 expect_path = os.path.join(root, '%s.expected' % name)
159
160 test_data.properties['recipe'] = recipe_name.replace('\\', '/')
161 yield expect_tests.Test( 181 yield expect_tests.Test(
162 '%s.%s' % (recipe_name, test_data.name), 182 '%s.%s' % (recipe_name, test_data.name),
163 expect_tests.FuncCall(RunRecipe, test_data), 183 expect_tests.FuncCall(RunRecipe, recipe_name, test_data.name),
164 expect_dir=expect_path, 184 expect_dir=expect_path,
165 expect_base=test_data.name, 185 expect_base=test_data.name,
166 covers=covers, 186 covers=covers,
167 break_funcs=(recipe.RunSteps,) 187 break_funcs=(recipe.RunSteps,)
168 ) 188 )
169
170 if full_expectation_count < 1:
171 raise InsufficientTestCoverage(
172 'Must have at least 1 test without a whitelist!')
173 except: 189 except:
174 info = sys.exc_info() 190 info = sys.exc_info()
175 new_exec = Exception('While generating results for %r: %s: %s' % ( 191 new_exec = Exception('While generating results for %r: %s: %s' % (
176 recipe_name, info[0].__name__, str(info[1]))) 192 recipe_name, info[0].__name__, str(info[1])))
177 raise new_exec.__class__, new_exec, info[2] 193 raise new_exec.__class__, new_exec, info[2]
178 194
179 195
180 def main(universe, args=None): 196 def main(universe, args=None):
181 """Runs simulation tests on a given repo of recipes. 197 """Runs simulation tests on a given repo of recipes.
182 198
(...skipping 10 matching lines...) Expand all
193 'TESTING_SLAVENAME']: 209 'TESTING_SLAVENAME']:
194 if env_var in os.environ: 210 if env_var in os.environ:
195 logging.warn("Ignoring %s environment variable." % env_var) 211 logging.warn("Ignoring %s environment variable." % env_var)
196 os.environ.pop(env_var) 212 os.environ.pop(env_var)
197 213
198 global _UNIVERSE 214 global _UNIVERSE
199 _UNIVERSE = universe 215 _UNIVERSE = universe
200 216
201 expect_tests.main('recipe_simulation_test', GenerateTests, 217 expect_tests.main('recipe_simulation_test', GenerateTests,
202 cover_omit=cover_omit(), args=args) 218 cover_omit=cover_omit(), args=args)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698