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 |
24 import astunparse | |
17 import expect_tests | 25 import expect_tests |
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 class _checkTransformer(ast.NodeTransformer): |
25 def RenderExpectation(test_data, raw_expectations): | 33 """_checkTransformer is an ast NodeTransformer which extracts the helpful |
26 """Applies the step filters (e.g. whitelists, etc.) to the raw_expectations, | 34 subexpressions from a python expression (specificially, from an invocation of |
Michael Achenbach
2016/10/07 16:32:04
nit: s/specificially/specifically
| |
27 if the TestData actually contains any filters. | 35 the Checker). These subexpressions will be printed along with the check's |
36 source code statement to provide context for the failed check. | |
37 | |
38 It knows the following transformations: | |
39 * all python identifiers will be resolved to their local variable meaning. | |
40 * `___ in <instance of dict>` will cause dict.keys() to be printed in lieu | |
41 of the entire dictionary. | |
42 * `a[b][c]` will cause `a[b]` and `a[b][c]` to be printed (for an arbitrary | |
43 level of recursion) | |
44 | |
45 The transformed ast is NOT a valid python AST... In particular, every reduced | |
46 subexpression will be an ast.Name() where the id is the code for the | |
47 subexpression (which may not be a valid name! It could be `foo.bar()`.), and | |
48 the ctx will be the eval'd value for that element. | |
49 | |
50 In addition to this, there will be a list of ast.Name nodes in the | |
51 transformer's `extra` attribute for additional expressions which should be | |
52 printed for debugging usefulness, but didn't fit into the ast tree anywhere. | |
53 """ | |
54 | |
55 def __init__(self, lvars, gvars): | |
56 self.lvars = lvars | |
57 self.gvars = gvars | |
58 self.extras = [] | |
59 | |
60 def _eval(self, node): | |
61 code = astunparse.unparse(node).strip() | |
62 try: | |
63 thing = eval(code, self.gvars, self.lvars) | |
64 return ast.Name(code, thing) | |
65 except NameError: | |
66 return node | |
67 | |
68 def visit_Compare(self, node): | |
69 # match `___ in instanceof(dict)` | |
70 node = self.generic_visit(node) | |
71 | |
72 if len(node.ops) == 1 and isinstance(node.ops[0], ast.In): | |
73 cmps = node.comparators | |
74 if len(cmps) == 1 and isinstance(cmps[0], ast.Name): | |
75 name = cmps[0] | |
76 if isinstance(name.ctx, dict): | |
77 node = ast.Compare( | |
78 node.left, | |
79 node.ops, | |
80 [ast.Name(name.id+".keys()", name.ctx.keys())]) | |
81 | |
82 return node | |
83 | |
84 def visit_Subscript(self, node): | |
85 # match __[a] | |
86 node = self.generic_visit(node) | |
87 if isinstance(node.slice, ast.Index): | |
88 if isinstance(node.slice.value, ast.Name): | |
89 self.extras.append(self._eval(node.slice.value)) | |
90 node = self._eval(node) | |
91 return node | |
92 | |
93 def visit_Name(self, node): | |
94 # match foo | |
95 return self._eval(node) | |
96 | |
97 | |
98 def render_user_value(val): | |
99 """Takes a subexpression user value, and attempts to render it in the most | |
100 useful way possible. | |
101 | |
102 Currently this will use render_re for compiled regular expressions, and will | |
103 fall back to repr() for everything else. | |
104 | |
105 It should be the goal of this function to return an `eval`able string that | |
106 would yield the equivalent value in a python interpreter. | |
107 """ | |
108 if isinstance(val, re._pattern_type): | |
109 return render_re(val) | |
110 return repr(val) | |
111 | |
112 | |
113 def render_re(regex): | |
114 """Renders a repr()-style value for a compiled regular expression.""" | |
115 actual_flags = [] | |
116 if regex.flags: | |
117 flags = [ | |
118 (re.IGNORECASE, 'IGNORECASE'), | |
119 (re.LOCALE, 'LOCALE'), | |
120 (re.UNICODE, 'UNICODE'), | |
121 (re.MULTILINE, 'MULTILINE'), | |
122 (re.DOTALL, 'DOTALL'), | |
123 (re.VERBOSE, 'VERBOSE'), | |
124 ] | |
125 for val, name in flags: | |
126 if regex.flags & val: | |
127 actual_flags.append(name) | |
128 if actual_flags: | |
129 return 're.compile(%r, %s)' % (regex.pattern, '|'.join(actual_flags)) | |
130 else: | |
131 return 're.compile(%r)' % regex.pattern | |
132 | |
133 | |
134 class _checker(object): | |
135 def __init__(self, filename, lineno, funcname, args, kwargs, *ignores): | |
136 self._failed_checks = [] | |
137 | |
138 # _ignore_set is the set of objects that we should never print as local | |
139 # variables. We start this set off by including the actual _checker object, | |
140 # since there's no value to printing that. | |
141 self._ignore_set = {id(x) for x in ignores+(self,)} | |
142 | |
143 self._ctx_filename = filename | |
144 self._ctx_lineno = lineno | |
145 self._ctx_funcname = funcname | |
146 self._ctx_args = map(repr, args) | |
147 self._ctx_kwargs = {k: repr(v) for k, v in kwargs.iteritems()} | |
148 | |
149 def _process_frame(self, frame, with_vars): | |
150 """This processes a stack frame into an expect_tests.CheckFrame, which | |
151 includes file name, line number, function name (of the function containing | |
152 the frame), the parsed statement at that line, and the relevant local | |
153 variables/subexpressions (if with_vars is True). | |
154 | |
155 In addition to transforming the expression with _checkTransformer, this | |
156 will: | |
157 * omit subexpressions which resolve to callable()'s | |
158 * omit the overall step ordered dictionary | |
159 * transform all subexpression values using render_user_value(). | |
160 """ | |
161 raw_frame, filename, lineno, func_name, _, _ = frame | |
162 | |
163 filelines, _ = inspect.findsource(raw_frame) | |
164 | |
165 i = lineno-1 | |
166 # this dumb little loop will try to parse a node out of the ast which ends | |
167 # at the line that shows up in the frame. To do this, tries parsing that | |
168 # line, and if it fails, it adds a prefix line. It keeps doing this until | |
169 # it gets a successful parse. | |
170 for i in xrange(lineno-1, 0, -1): | |
171 try: | |
172 to_parse = ''.join(filelines[i:lineno]).strip() | |
173 node = ast.parse(to_parse) | |
174 break | |
175 except SyntaxError: | |
176 continue | |
177 varmap = None | |
178 if with_vars: | |
179 xfrmr = _checkTransformer(raw_frame.f_locals, raw_frame.f_globals) | |
180 | |
181 varmap = {} | |
182 def add_node(n): | |
183 if isinstance(n, ast.Name): | |
184 val = n.ctx | |
185 if isinstance(val, ast.AST): | |
186 return | |
187 if callable(val) or id(val) in self._ignore_set: | |
188 return | |
189 if n.id not in varmap: | |
190 varmap[n.id] = render_user_value(val) | |
191 map(add_node, ast.walk(xfrmr.visit(copy.deepcopy(node)))) | |
192 # TODO(iannucci): only add extras if verbose is True | |
193 map(add_node, xfrmr.extras) | |
194 | |
195 return expect_tests.CheckFrame( | |
196 filename, | |
197 lineno, | |
198 func_name, | |
199 astunparse.unparse(node).strip(), | |
200 varmap | |
201 ) | |
202 | |
203 def _call_impl(self, hint, exp): | |
204 """This implements the bulk of what happens when you run `check(exp)`. It | |
205 will crawl back up the stack and extract information about all of the frames | |
206 which are relevent to the check, including file:lineno and the code | |
207 statement which occurs at that location for all the frames. | |
208 | |
209 On the last frame (the one that actually contains the check call), it will | |
210 also try to obtain relevant local values in the check so they can be printed | |
211 with the check to aid in debugging and diagnosis. It uses the parsed | |
212 statement found at that line to find all referenced local variables in that | |
213 frame. | |
214 """ | |
215 | |
216 if exp: | |
217 # TODO(iannucci): collect this in verbose mode. | |
218 # this check passed | |
219 return | |
220 | |
221 try: | |
222 frames = inspect.stack()[2:] | |
223 | |
224 # grab all frames which have self as a local variable (e.g. frames | |
225 # associated with this checker), excluding self.__call__. | |
226 try: | |
227 i = 0 | |
228 for i, f in enumerate(frames): | |
229 if self not in f[0].f_locals.itervalues(): | |
230 break | |
231 keep_frames = [self._process_frame(f, j == 0) | |
232 for j, f in enumerate(frames[:i-1])] | |
233 finally: | |
234 del f | |
235 | |
236 # order it so that innermost frame is at the bottom | |
237 keep_frames = keep_frames[::-1] | |
238 | |
239 self._failed_checks.append(expect_tests.Check( | |
240 hint, | |
241 self._ctx_filename, | |
242 self._ctx_lineno, | |
243 self._ctx_funcname, | |
244 self._ctx_args, | |
245 self._ctx_kwargs, | |
246 keep_frames, | |
247 False | |
248 )) | |
249 finally: | |
250 # avoid reference cycle as suggested by inspect docs. | |
251 del frames | |
252 | |
253 def __call__(self, arg1, arg2=None): | |
254 if arg2 is not None: | |
255 hint = arg1 | |
256 exp = arg2 | |
257 else: | |
258 hint = None | |
259 exp = arg1 | |
260 self._call_impl(hint, exp) | |
261 | |
262 | |
263 class PostProcessError(ValueError): | |
264 def __init__(self, msg): | |
265 super(PostProcessError, self).__init__("post_process %s" % (msg)) | |
266 | |
267 | |
268 class PostProcessStepError(ValueError): | |
269 def __init__(self, msg, step_name): | |
270 super(PostProcessStepError, self).__init__( | |
271 msg + " in step " + repr(step_name)) | |
272 | |
273 | |
274 def _checkIsSubset(a, b): | |
275 """This checks to see that a is a subset of b, where both a and b are | |
276 OrderedDicts. This will raise a PostProcessError if any data in a is not | |
277 present in b.""" | |
278 | |
279 # both a and b should be ordered step lists. | |
280 if a is b: | |
281 return | |
282 | |
283 typeOK = ( | |
284 len(b) == 0 and isinstance(b, (OrderedDict, dict)) or | |
285 len(b) != 0 and isinstance(b, OrderedDict)) | |
286 if not typeOK: | |
287 raise PostProcessError( | |
288 'must always return an OrderedDict() or the empty {}.') | |
289 | |
290 for step_name, step in a.iteritems(): | |
291 if step_name not in b: | |
292 raise PostProcessError('introduced new step %r' % step_name) | |
293 | |
294 if 'name' not in step: | |
295 raise PostProcessStepError('removed "name"', step_name) | |
296 | |
297 orig = b[step_name] | |
298 if step is orig: | |
299 continue | |
300 | |
301 for k, v in step.iteritems(): | |
302 if k not in orig: | |
303 raise PostProcessStepError('introduced new key %r' % k, step_name) | |
304 | |
305 orig_v = orig[k] | |
306 if v is orig_v: | |
307 continue | |
308 | |
309 if type(v) != type(orig_v): | |
310 raise PostProcessStepError( | |
311 'changed type of key %r from %s to %s' % ( | |
312 k, type(orig_v).__name__, type(v).__name__), step_name) | |
313 | |
314 if isinstance(v, list): | |
315 if len(v) > len(orig_v): | |
316 raise PostProcessStepError( | |
317 '%r is longer than the original (%d v %d)' % | |
318 (k, len(v), len(orig_v)), step_name) | |
319 idx_orig_v = idx_v = 0 | |
320 while idx_orig_v < len(orig_v) - 1 and idx_v < len(v) - 1: | |
321 if v[idx_v] == orig_v[idx_orig_v]: | |
322 idx_v += 1 | |
323 idx_orig_v += 1 | |
324 if idx_v != len(v) - 1: | |
325 raise PostProcessStepError( | |
326 'added elements to %r (%r)' % (k, v[idx_v:]), step_name) | |
327 elif isinstance(v, dict): | |
328 for subk, subv in v.iteritems(): | |
329 el = orig_v.get(subk) | |
330 if el is None: | |
331 raise PostProcessStepError('added element %s[%r]' % (k, subk), | |
332 step_name) | |
333 if subv != el: | |
334 raise PostProcessError( | |
335 'changed element %s[%r] in step %r: %r to %r' % ( | |
336 k, subk, step_name, subv, orig_v[subk])) | |
337 elif v != orig_v: | |
338 raise PostProcessStepError( | |
339 'changed value of key %r from %s to %s' % (k, orig_v, v), step_name) | |
340 | |
341 | |
342 def _nameOfCallable(c): | |
343 if inspect.isfunction(c): | |
344 return c.__name__ | |
345 if inspect.ismethod(c): | |
346 return c.im_class.__name__+'.'+c.__name__ | |
347 if hasattr(c, '__class__') and hasattr(c, '__call__'): | |
348 return c.__class__.__name__+'.__call__' | |
349 return repr(c) | |
350 | |
351 | |
352 def _renderExpectation(test_data, step_odict): | |
353 """Applies the step post_process actions to the step_odict, if the | |
354 TestData actually contains any. | |
28 | 355 |
29 Returns the final expect_tests.Result.""" | 356 Returns the final expect_tests.Result.""" |
30 if test_data.whitelist_data: | 357 |
31 whitelist_data = dict(test_data.whitelist_data) # copy so we can mutate it | 358 failed_checks = [] |
32 def filter_expectation(step): | 359 |
33 whitelist = whitelist_data.pop(step['name'], None) | 360 for hook, args, kwargs, filename, lineno in test_data.post_process_hooks: |
34 if whitelist is None: | 361 input_odict = copy.deepcopy(step_odict) |
35 return | 362 # we ignore the input_odict so that it never gets printed in full. Usually |
36 | 363 # the check invocation itself will index the input_odict or will use it only |
37 whitelist = set(whitelist) # copy so we can mutate it | 364 # for a key membership comparison, which provides enough debugging context. |
38 if len(whitelist) > 0: | 365 checker = _checker(filename, lineno, _nameOfCallable(hook), args, kwargs, |
39 whitelist.add('name') | 366 input_odict) |
40 step = {k: v for k, v in step.iteritems() if k in whitelist} | 367 rslt = hook(checker, input_odict, *args, **kwargs) |
41 whitelist.difference_update(step.keys()) | 368 failed_checks += checker._failed_checks |
42 if whitelist: | 369 if rslt is not None: |
43 raise ValueError( | 370 _checkIsSubset(rslt, step_odict) |
44 "The whitelist includes fields %r in step %r, but those fields" | 371 step_odict = rslt |
45 " don't exist." | 372 |
46 % (whitelist, step['name'])) | 373 # empty means drop expectation |
47 return step | 374 result_data = step_odict.values() if step_odict else None |
48 raw_expectations = filter(filter_expectation, raw_expectations) | 375 return expect_tests.Result(result_data, failed_checks) |
49 | |
50 if whitelist_data: | |
51 raise ValueError( | |
52 "The step names %r were included in the whitelist, but were never run." | |
53 % [s['name'] for s in whitelist_data]) | |
54 | |
55 return expect_tests.Result(raw_expectations) | |
56 | 376 |
57 | 377 |
58 class SimulationAnnotatorStreamEngine(stream.AnnotatorStreamEngine): | 378 class SimulationAnnotatorStreamEngine(stream.AnnotatorStreamEngine): |
59 | 379 |
60 def __init__(self): | 380 def __init__(self): |
61 self._step_buffer_map = {} | 381 self._step_buffer_map = {} |
62 super(SimulationAnnotatorStreamEngine, self).__init__( | 382 super(SimulationAnnotatorStreamEngine, self).__init__( |
63 self.step_buffer(None)) | 383 self.step_buffer(None)) |
64 | 384 |
65 def step_buffer(self, step_name): | 385 def step_buffer(self, step_name): |
66 return self._step_buffer_map.setdefault(step_name, StringIO.StringIO()) | 386 return self._step_buffer_map.setdefault(step_name, StringIO.StringIO()) |
67 | 387 |
68 def new_step_stream(self, step_config): | 388 def new_step_stream(self, step_config): |
69 return self._create_step_stream(step_config, | 389 return self._create_step_stream(step_config, |
70 self.step_buffer(step_config.name)) | 390 self.step_buffer(step_config.name)) |
71 | 391 |
72 | 392 |
73 def RunRecipe(test_data): | 393 # This maps from (recipe_name,test_name) -> yielded test_data. It's outside of |
394 # RunRecipe so that it can persist between RunRecipe calls in the same process. | |
395 _GEN_TEST_CACHE = {} | |
396 | |
397 def RunRecipe(recipe_name, test_name): | |
74 """Actually runs the recipe given the GenTests-supplied test_data.""" | 398 """Actually runs the recipe given the GenTests-supplied test_data.""" |
75 from . import config_types | 399 from . import config_types |
76 from . import loader | 400 from . import loader |
77 from . import run | 401 from . import run |
78 from . import step_runner | 402 from . import step_runner |
79 from . import stream | 403 |
404 if recipe_name not in _GEN_TEST_CACHE: | |
405 recipe_script = _UNIVERSE.load_recipe(recipe_name) | |
406 test_api = loader.create_test_api(recipe_script.LOADED_DEPS, _UNIVERSE) | |
407 for test_data in recipe_script.GenTests(test_api): | |
408 _GEN_TEST_CACHE[(recipe_name, test_data.name)] = test_data | |
409 | |
410 test_data = _GEN_TEST_CACHE[(recipe_name, test_name)] | |
80 | 411 |
81 config_types.ResetTostringFns() | 412 config_types.ResetTostringFns() |
82 | 413 |
83 annotator = SimulationAnnotatorStreamEngine() | 414 annotator = SimulationAnnotatorStreamEngine() |
84 stream_engine = stream.ProductStreamEngine( | 415 stream_engine = stream.ProductStreamEngine( |
85 stream.StreamEngineInvariants(), | 416 stream.StreamEngineInvariants(), |
86 annotator) | 417 annotator) |
87 with stream_engine: | 418 with stream_engine: |
88 step_runner = step_runner.SimulationStepRunner(stream_engine, test_data, | 419 step_runner = step_runner.SimulationStepRunner(stream_engine, test_data, |
89 annotator) | 420 annotator) |
90 | 421 |
91 engine = run.RecipeEngine(step_runner, test_data.properties, _UNIVERSE) | 422 props = test_data.properties.copy() |
92 recipe_script = _UNIVERSE.load_recipe(test_data.properties['recipe']) | 423 props['recipe'] = recipe_name |
424 engine = run.RecipeEngine(step_runner, props, _UNIVERSE) | |
425 recipe_script = _UNIVERSE.load_recipe(recipe_name) | |
93 api = loader.create_recipe_api(recipe_script.LOADED_DEPS, engine, test_data) | 426 api = loader.create_recipe_api(recipe_script.LOADED_DEPS, engine, test_data) |
94 result = engine.run(recipe_script, api) | 427 result = engine.run(recipe_script, api) |
95 | 428 |
96 # Don't include tracebacks in expectations because they are too sensitive to | 429 # Don't include tracebacks in expectations because they are too sensitive to |
97 # change. | 430 # change. |
98 result.result.pop('traceback', None) | 431 result.result.pop('traceback', None) |
99 raw_expectations = step_runner.steps_ran + [result.result] | 432 raw_expectations = step_runner.steps_ran.copy() |
433 raw_expectations[result.result['name']] = result.result | |
100 | 434 |
101 try: | 435 try: |
102 return RenderExpectation(test_data, raw_expectations) | 436 return _renderExpectation(test_data, raw_expectations) |
103 except: | 437 except: |
104 print | 438 print |
105 print "The expectations would have been:" | 439 print "The expectations would have been:" |
106 json.dump(raw_expectations, sys.stdout, indent=2) | 440 json.dump(raw_expectations, sys.stdout, indent=2) |
107 raise | 441 raise |
108 | 442 |
109 | 443 |
110 def test_gen_coverage(): | 444 def test_gen_coverage(): |
111 cover = [] | 445 cover = [] |
112 | 446 |
(...skipping 10 matching lines...) Expand all Loading... | |
123 def cover_omit(): | 457 def cover_omit(): |
124 omit = [ ] | 458 omit = [ ] |
125 | 459 |
126 for mod_dir_base in _UNIVERSE.module_dirs: | 460 for mod_dir_base in _UNIVERSE.module_dirs: |
127 if os.path.isdir(mod_dir_base): | 461 if os.path.isdir(mod_dir_base): |
128 omit.append(os.path.join(mod_dir_base, '*', 'resources', '*')) | 462 omit.append(os.path.join(mod_dir_base, '*', 'resources', '*')) |
129 | 463 |
130 return omit | 464 return omit |
131 | 465 |
132 | 466 |
133 class InsufficientTestCoverage(Exception): pass | 467 class InsufficientTestCoverage(Exception): |
468 pass | |
134 | 469 |
135 | 470 |
136 @expect_tests.covers(test_gen_coverage) | 471 @expect_tests.covers(test_gen_coverage) |
137 def GenerateTests(): | 472 def GenerateTests(): |
138 from . import loader | 473 from . import loader |
139 | 474 |
140 cover_mods = [ ] | 475 cover_mods = [ ] |
141 for mod_dir_base in _UNIVERSE.module_dirs: | 476 for mod_dir_base in _UNIVERSE.module_dirs: |
142 if os.path.isdir(mod_dir_base): | 477 if os.path.isdir(mod_dir_base): |
143 cover_mods.append(os.path.join(mod_dir_base, '*.py')) | 478 cover_mods.append(os.path.join(mod_dir_base, '*.py')) |
144 | 479 |
145 for recipe_path, recipe_name in _UNIVERSE.loop_over_recipes(): | 480 for recipe_path, recipe_name in _UNIVERSE.loop_over_recipes(): |
146 try: | 481 try: |
147 recipe = _UNIVERSE.load_recipe(recipe_name) | 482 recipe = _UNIVERSE.load_recipe(recipe_name) |
148 test_api = loader.create_test_api(recipe.LOADED_DEPS, _UNIVERSE) | 483 test_api = loader.create_test_api(recipe.LOADED_DEPS, _UNIVERSE) |
149 | 484 |
150 covers = cover_mods + [recipe_path] | 485 covers = cover_mods + [recipe_path] |
151 | 486 |
152 full_expectation_count = 0 | |
153 for test_data in recipe.GenTests(test_api): | 487 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) | 488 root, name = os.path.split(recipe_path) |
157 name = os.path.splitext(name)[0] | 489 name = os.path.splitext(name)[0] |
158 expect_path = os.path.join(root, '%s.expected' % name) | 490 expect_path = os.path.join(root, '%s.expected' % name) |
159 | |
160 test_data.properties['recipe'] = recipe_name.replace('\\', '/') | |
161 yield expect_tests.Test( | 491 yield expect_tests.Test( |
162 '%s.%s' % (recipe_name, test_data.name), | 492 '%s.%s' % (recipe_name, test_data.name), |
163 expect_tests.FuncCall(RunRecipe, test_data), | 493 expect_tests.FuncCall(RunRecipe, recipe_name, test_data.name), |
164 expect_dir=expect_path, | 494 expect_dir=expect_path, |
165 expect_base=test_data.name, | 495 expect_base=test_data.name, |
166 covers=covers, | 496 covers=covers, |
167 break_funcs=(recipe.RunSteps,) | 497 break_funcs=(recipe.RunSteps,) |
168 ) | 498 ) |
169 | |
170 if full_expectation_count < 1: | |
171 raise InsufficientTestCoverage( | |
172 'Must have at least 1 test without a whitelist!') | |
173 except: | 499 except: |
174 info = sys.exc_info() | 500 info = sys.exc_info() |
175 new_exec = Exception('While generating results for %r: %s: %s' % ( | 501 new_exec = Exception('While generating results for %r: %s: %s' % ( |
176 recipe_name, info[0].__name__, str(info[1]))) | 502 recipe_name, info[0].__name__, str(info[1]))) |
177 raise new_exec.__class__, new_exec, info[2] | 503 raise new_exec.__class__, new_exec, info[2] |
178 | 504 |
179 | 505 |
180 def main(universe, args=None): | 506 def main(universe, args=None): |
181 """Runs simulation tests on a given repo of recipes. | 507 """Runs simulation tests on a given repo of recipes. |
182 | 508 |
(...skipping 10 matching lines...) Expand all Loading... | |
193 'TESTING_SLAVENAME']: | 519 'TESTING_SLAVENAME']: |
194 if env_var in os.environ: | 520 if env_var in os.environ: |
195 logging.warn("Ignoring %s environment variable." % env_var) | 521 logging.warn("Ignoring %s environment variable." % env_var) |
196 os.environ.pop(env_var) | 522 os.environ.pop(env_var) |
197 | 523 |
198 global _UNIVERSE | 524 global _UNIVERSE |
199 _UNIVERSE = universe | 525 _UNIVERSE = universe |
200 | 526 |
201 expect_tests.main('recipe_simulation_test', GenerateTests, | 527 expect_tests.main('recipe_simulation_test', GenerateTests, |
202 cover_omit=cover_omit(), args=args) | 528 cover_omit=cover_omit(), args=args) |
OLD | NEW |