test_farm.py 13 KB

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