OLD | NEW |
(Empty) | |
| 1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
| 2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
| 3 |
| 4 """Run tests in the farm sub-directory. Designed for nose.""" |
| 5 |
| 6 import difflib |
| 7 import filecmp |
| 8 import fnmatch |
| 9 import glob |
| 10 import os |
| 11 import re |
| 12 import shutil |
| 13 import sys |
| 14 import unittest |
| 15 |
| 16 from nose.plugins.skip import SkipTest |
| 17 |
| 18 from coverage.test_helpers import ModuleAwareMixin, SysPathAwareMixin, change_di
r, saved_sys_path |
| 19 from tests.helpers import run_command |
| 20 from tests.backtest import execfile # pylint: disable=redefined-builtin |
| 21 |
| 22 from coverage.debug import _TEST_NAME_FILE |
| 23 |
| 24 |
| 25 def test_farm(clean_only=False): |
| 26 """A test-generating function for nose to find and run.""" |
| 27 for fname in glob.glob("tests/farm/*/*.py"): |
| 28 case = FarmTestCase(fname, clean_only) |
| 29 yield (case,) |
| 30 |
| 31 |
| 32 # "rU" was deprecated in 3.4 |
| 33 READ_MODE = "rU" if sys.version_info < (3, 4) else "r" |
| 34 |
| 35 |
| 36 class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): |
| 37 """A test case from the farm tree. |
| 38 |
| 39 Tests are short Python script files, often called run.py: |
| 40 |
| 41 copy("src", "out") |
| 42 run(''' |
| 43 coverage run white.py |
| 44 coverage annotate white.py |
| 45 ''', rundir="out") |
| 46 compare("out", "gold", "*,cover") |
| 47 clean("out") |
| 48 |
| 49 Verbs (copy, run, compare, clean) are methods in this class. FarmTestCase |
| 50 has options to allow various uses of the test cases (normal execution, |
| 51 cleaning-only, or run and leave the results for debugging). |
| 52 |
| 53 This class is a unittest.TestCase so that we can use behavior-modifying |
| 54 mixins, but it's only useful as a nose test function. Yes, this is |
| 55 confusing. |
| 56 |
| 57 """ |
| 58 |
| 59 # We don't want test runners finding this and instantiating it themselves. |
| 60 __test__ = False |
| 61 |
| 62 def __init__(self, runpy, clean_only=False, dont_clean=False): |
| 63 """Create a test case from a run.py file. |
| 64 |
| 65 `clean_only` means that only the clean() action is executed. |
| 66 `dont_clean` means that the clean() action is not executed. |
| 67 |
| 68 """ |
| 69 super(FarmTestCase, self).__init__() |
| 70 |
| 71 self.description = runpy |
| 72 self.dir, self.runpy = os.path.split(runpy) |
| 73 self.clean_only = clean_only |
| 74 self.dont_clean = dont_clean |
| 75 self.ok = True |
| 76 |
| 77 def setUp(self): |
| 78 """Test set up, run by nose before __call__.""" |
| 79 super(FarmTestCase, self).setUp() |
| 80 # Modules should be importable from the current directory. |
| 81 sys.path.insert(0, '') |
| 82 |
| 83 def tearDown(self): |
| 84 """Test tear down, run by nose after __call__.""" |
| 85 # Make sure the test is cleaned up, unless we never want to, or if the |
| 86 # test failed. |
| 87 if not self.dont_clean and self.ok: # pragma: part covered |
| 88 self.clean_only = True |
| 89 self() |
| 90 |
| 91 super(FarmTestCase, self).tearDown() |
| 92 |
| 93 # This object will be run by nose via the __call__ method, and nose |
| 94 # doesn't do cleanups in that case. Do them now. |
| 95 self.doCleanups() |
| 96 |
| 97 def runTest(self): |
| 98 """Here to make unittest.TestCase happy, but will never be invoked.""" |
| 99 raise Exception("runTest isn't used in this class!") |
| 100 |
| 101 def __call__(self): |
| 102 """Execute the test from the run.py file.""" |
| 103 if _TEST_NAME_FILE: # pragma: debugging |
| 104 with open(_TEST_NAME_FILE, "w") as f: |
| 105 f.write(self.description.replace("/", "_")) |
| 106 |
| 107 # Prepare a dictionary of globals for the run.py files to use. |
| 108 fns = """ |
| 109 copy run runfunc clean skip |
| 110 compare contains contains_any doesnt_contain |
| 111 """.split() |
| 112 if self.clean_only: |
| 113 glo = dict((fn, noop) for fn in fns) |
| 114 glo['clean'] = clean |
| 115 else: |
| 116 glo = dict((fn, globals()[fn]) for fn in fns) |
| 117 if self.dont_clean: # pragma: not covered |
| 118 glo['clean'] = noop |
| 119 |
| 120 with change_dir(self.dir): |
| 121 try: |
| 122 execfile(self.runpy, glo) |
| 123 except Exception: |
| 124 self.ok = False |
| 125 raise |
| 126 |
| 127 def run_fully(self): # pragma: not covered |
| 128 """Run as a full test case, with setUp and tearDown.""" |
| 129 self.setUp() |
| 130 try: |
| 131 self() |
| 132 finally: |
| 133 self.tearDown() |
| 134 |
| 135 |
| 136 # Functions usable inside farm run.py files |
| 137 |
| 138 def noop(*args_unused, **kwargs_unused): |
| 139 """A no-op function to stub out run, copy, etc, when only cleaning.""" |
| 140 pass |
| 141 |
| 142 |
| 143 def copy(src, dst): |
| 144 """Copy a directory.""" |
| 145 if os.path.exists(dst): |
| 146 shutil.rmtree(dst) |
| 147 shutil.copytree(src, dst) |
| 148 |
| 149 |
| 150 def run(cmds, rundir="src", outfile=None): |
| 151 """Run a list of commands. |
| 152 |
| 153 `cmds` is a string, commands separated by newlines. |
| 154 `rundir` is the directory in which to run the commands. |
| 155 `outfile` is a file name to redirect stdout to. |
| 156 |
| 157 """ |
| 158 with change_dir(rundir): |
| 159 if outfile: |
| 160 fout = open(outfile, "a+") |
| 161 try: |
| 162 for cmd in cmds.split("\n"): |
| 163 cmd = cmd.strip() |
| 164 if not cmd: |
| 165 continue |
| 166 retcode, output = run_command(cmd) |
| 167 print(output.rstrip()) |
| 168 if outfile: |
| 169 fout.write(output) |
| 170 if retcode: |
| 171 raise Exception("command exited abnormally") |
| 172 finally: |
| 173 if outfile: |
| 174 fout.close() |
| 175 |
| 176 |
| 177 def runfunc(fn, rundir="src", addtopath=None): |
| 178 """Run a function. |
| 179 |
| 180 `fn` is a callable. |
| 181 `rundir` is the directory in which to run the function. |
| 182 |
| 183 """ |
| 184 with change_dir(rundir): |
| 185 with saved_sys_path(): |
| 186 if addtopath is not None: |
| 187 sys.path.insert(0, addtopath) |
| 188 fn() |
| 189 |
| 190 |
| 191 def compare( |
| 192 dir1, dir2, file_pattern=None, size_within=0, |
| 193 left_extra=False, right_extra=False, scrubs=None |
| 194 ): |
| 195 """Compare files matching `file_pattern` in `dir1` and `dir2`. |
| 196 |
| 197 `dir2` is interpreted as a prefix, with Python version numbers appended |
| 198 to find the actual directory to compare with. "foo" will compare |
| 199 against "foo_v241", "foo_v24", "foo_v2", or "foo", depending on which |
| 200 directory is found first. |
| 201 |
| 202 `size_within` is a percentage delta for the file sizes. If non-zero, |
| 203 then the file contents are not compared (since they are expected to |
| 204 often be different), but the file sizes must be within this amount. |
| 205 For example, size_within=10 means that the two files' sizes must be |
| 206 within 10 percent of each other to compare equal. |
| 207 |
| 208 `left_extra` true means the left directory can have extra files in it |
| 209 without triggering an assertion. `right_extra` means the right |
| 210 directory can. |
| 211 |
| 212 `scrubs` is a list of pairs, regexes to find and literal strings to |
| 213 replace them with to scrub the files of unimportant differences. |
| 214 |
| 215 An assertion will be raised if the directories fail one of their |
| 216 matches. |
| 217 |
| 218 """ |
| 219 # Search for a dir2 with a version suffix. |
| 220 version_suff = ''.join(map(str, sys.version_info[:3])) |
| 221 while version_suff: |
| 222 trydir = dir2 + '_v' + version_suff |
| 223 if os.path.exists(trydir): |
| 224 dir2 = trydir |
| 225 break |
| 226 version_suff = version_suff[:-1] |
| 227 |
| 228 assert os.path.exists(dir1), "Left directory missing: %s" % dir1 |
| 229 assert os.path.exists(dir2), "Right directory missing: %s" % dir2 |
| 230 |
| 231 dc = filecmp.dircmp(dir1, dir2) |
| 232 diff_files = fnmatch_list(dc.diff_files, file_pattern) |
| 233 left_only = fnmatch_list(dc.left_only, file_pattern) |
| 234 right_only = fnmatch_list(dc.right_only, file_pattern) |
| 235 show_diff = True |
| 236 |
| 237 if size_within: |
| 238 # The files were already compared, use the diff_files list as a |
| 239 # guide for size comparison. |
| 240 wrong_size = [] |
| 241 for f in diff_files: |
| 242 with open(os.path.join(dir1, f), "rb") as fobj: |
| 243 left = fobj.read() |
| 244 with open(os.path.join(dir2, f), "rb") as fobj: |
| 245 right = fobj.read() |
| 246 size_l, size_r = len(left), len(right) |
| 247 big, little = max(size_l, size_r), min(size_l, size_r) |
| 248 if (big - little) / float(little) > size_within/100.0: |
| 249 # print "%d %d" % (big, little) |
| 250 # print "Left: ---\n%s\n-----\n%s" % (left, right) |
| 251 wrong_size.append("%s (%s,%s)" % (f, size_l, size_r)) |
| 252 if wrong_size: |
| 253 print("File sizes differ between %s and %s: %s" % ( |
| 254 dir1, dir2, ", ".join(wrong_size) |
| 255 )) |
| 256 |
| 257 # We'll show the diff iff the files differed enough in size. |
| 258 show_diff = bool(wrong_size) |
| 259 |
| 260 if show_diff: |
| 261 # filecmp only compares in binary mode, but we want text mode. So |
| 262 # look through the list of different files, and compare them |
| 263 # ourselves. |
| 264 text_diff = [] |
| 265 for f in diff_files: |
| 266 with open(os.path.join(dir1, f), READ_MODE) as fobj: |
| 267 left = fobj.read() |
| 268 with open(os.path.join(dir2, f), READ_MODE) as fobj: |
| 269 right = fobj.read() |
| 270 if scrubs: |
| 271 left = scrub(left, scrubs) |
| 272 right = scrub(right, scrubs) |
| 273 if left != right: |
| 274 text_diff.append(f) |
| 275 left = left.splitlines() |
| 276 right = right.splitlines() |
| 277 print("\n".join(difflib.Differ().compare(left, right))) |
| 278 assert not text_diff, "Files differ: %s" % text_diff |
| 279 |
| 280 if not left_extra: |
| 281 assert not left_only, "Files in %s only: %s" % (dir1, left_only) |
| 282 if not right_extra: |
| 283 assert not right_only, "Files in %s only: %s" % (dir2, right_only) |
| 284 |
| 285 |
| 286 def contains(filename, *strlist): |
| 287 """Check that the file contains all of a list of strings. |
| 288 |
| 289 An assert will be raised if one of the arguments in `strlist` is |
| 290 missing in `filename`. |
| 291 |
| 292 """ |
| 293 with open(filename, "r") as fobj: |
| 294 text = fobj.read() |
| 295 for s in strlist: |
| 296 assert s in text, "Missing content in %s: %r" % (filename, s) |
| 297 |
| 298 |
| 299 def contains_any(filename, *strlist): |
| 300 """Check that the file contains at least one of a list of strings. |
| 301 |
| 302 An assert will be raised if none of the arguments in `strlist` is in |
| 303 `filename`. |
| 304 |
| 305 """ |
| 306 with open(filename, "r") as fobj: |
| 307 text = fobj.read() |
| 308 for s in strlist: |
| 309 if s in text: |
| 310 return |
| 311 assert False, "Missing content in %s: %r [1 of %d]" % (filename, strlist[0],
len(strlist),) |
| 312 |
| 313 |
| 314 def doesnt_contain(filename, *strlist): |
| 315 """Check that the file contains none of a list of strings. |
| 316 |
| 317 An assert will be raised if any of the strings in `strlist` appears in |
| 318 `filename`. |
| 319 |
| 320 """ |
| 321 with open(filename, "r") as fobj: |
| 322 text = fobj.read() |
| 323 for s in strlist: |
| 324 assert s not in text, "Forbidden content in %s: %r" % (filename, s) |
| 325 |
| 326 |
| 327 def clean(cleandir): |
| 328 """Clean `cleandir` by removing it and all its children completely.""" |
| 329 # rmtree gives mysterious failures on Win7, so retry a "few" times. |
| 330 # I've seen it take over 100 tries, so, 1000! This is probably the |
| 331 # most unpleasant hack I've written in a long time... |
| 332 tries = 1000 |
| 333 while tries: # pragma: part covered |
| 334 if os.path.exists(cleandir): |
| 335 try: |
| 336 shutil.rmtree(cleandir) |
| 337 except OSError: # pragma: not covered |
| 338 if tries == 1: |
| 339 raise |
| 340 else: |
| 341 tries -= 1 |
| 342 continue |
| 343 break |
| 344 |
| 345 |
| 346 def skip(msg=None): |
| 347 """Skip the current test.""" |
| 348 raise SkipTest(msg) |
| 349 |
| 350 |
| 351 # Helpers |
| 352 |
| 353 def fnmatch_list(files, file_pattern): |
| 354 """Filter the list of `files` to only those that match `file_pattern`. |
| 355 |
| 356 If `file_pattern` is None, then return the entire list of files. |
| 357 |
| 358 Returns a list of the filtered files. |
| 359 |
| 360 """ |
| 361 if file_pattern: |
| 362 files = [f for f in files if fnmatch.fnmatch(f, file_pattern)] |
| 363 return files |
| 364 |
| 365 |
| 366 def scrub(strdata, scrubs): |
| 367 """Scrub uninteresting data from the payload in `strdata`. |
| 368 |
| 369 `scrubs` is a list of (find, replace) pairs of regexes that are used on |
| 370 `strdata`. A string is returned. |
| 371 |
| 372 """ |
| 373 for rgx_find, rgx_replace in scrubs: |
| 374 strdata = re.sub(rgx_find, re.escape(rgx_replace), strdata) |
| 375 return strdata |
| 376 |
| 377 |
| 378 def main(): # pragma: not covered |
| 379 """Command-line access to test_farm. |
| 380 |
| 381 Commands: |
| 382 |
| 383 run testcase ... - Run specific test case(s) |
| 384 out testcase ... - Run test cases, but don't clean up, leaving output. |
| 385 clean - Clean all the output for all tests. |
| 386 |
| 387 """ |
| 388 try: |
| 389 op = sys.argv[1] |
| 390 except IndexError: |
| 391 op = 'help' |
| 392 |
| 393 if op == 'run': |
| 394 # Run the test for real. |
| 395 for test_case in sys.argv[2:]: |
| 396 case = FarmTestCase(test_case) |
| 397 case.run_fully() |
| 398 elif op == 'out': |
| 399 # Run the test, but don't clean up, so we can examine the output. |
| 400 for test_case in sys.argv[2:]: |
| 401 case = FarmTestCase(test_case, dont_clean=True) |
| 402 case.run_fully() |
| 403 elif op == 'clean': |
| 404 # Run all the tests, but just clean. |
| 405 for test in test_farm(clean_only=True): |
| 406 test[0].run_fully() |
| 407 else: |
| 408 print(main.__doc__) |
| 409 |
| 410 # So that we can run just one farm run.py at a time. |
| 411 if __name__ == '__main__': |
| 412 main() |
OLD | NEW |