Chromium Code Reviews| 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 |