OLD | NEW |
---|---|
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 | |
martiniss
2016/10/10 18:53:12
nit: make a property to access _failed_checks.
| |
53 if rslt is not None: | |
54 msg = VerifySubset(rslt, step_odict) | |
55 if msg: | |
56 raise PostProcessError('post_process: steps'+msg) | |
martiniss
2016/10/10 18:53:12
nit: spacing around 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/10 18:53:12
Any particular reason you added this?
iannucci
2016/10/13 01:17:21
You mean besides the comment right above it? :D
| |
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 Loading... | |
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 Loading... | |
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) |
OLD | NEW |