test_oddball.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  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. """Oddball cases for testing coverage.py"""
  4. import sys
  5. import coverage
  6. from coverage.files import abs_file
  7. from tests.coveragetest import CoverageTest
  8. from tests import osinfo
  9. class ThreadingTest(CoverageTest):
  10. """Tests of the threading support."""
  11. def test_threading(self):
  12. self.check_coverage("""\
  13. import threading
  14. def fromMainThread():
  15. return "called from main thread"
  16. def fromOtherThread():
  17. return "called from other thread"
  18. def neverCalled():
  19. return "no one calls me"
  20. other = threading.Thread(target=fromOtherThread)
  21. other.start()
  22. fromMainThread()
  23. other.join()
  24. """,
  25. [1, 3, 4, 6, 7, 9, 10, 12, 13, 14, 15], "10")
  26. def test_thread_run(self):
  27. self.check_coverage("""\
  28. import threading
  29. class TestThread(threading.Thread):
  30. def run(self):
  31. self.a = 5
  32. self.do_work()
  33. self.a = 7
  34. def do_work(self):
  35. self.a = 10
  36. thd = TestThread()
  37. thd.start()
  38. thd.join()
  39. """,
  40. [1, 3, 4, 5, 6, 7, 9, 10, 12, 13, 14], "")
  41. class RecursionTest(CoverageTest):
  42. """Check what happens when recursive code gets near limits."""
  43. def test_short_recursion(self):
  44. # We can definitely get close to 500 stack frames.
  45. self.check_coverage("""\
  46. def recur(n):
  47. if n == 0:
  48. return 0
  49. else:
  50. return recur(n-1)+1
  51. recur(495) # We can get at least this many stack frames.
  52. i = 8 # and this line will be traced
  53. """,
  54. [1, 2, 3, 5, 7, 8], "")
  55. def test_long_recursion(self):
  56. # We can't finish a very deep recursion, but we don't crash.
  57. with self.assertRaises(RuntimeError):
  58. self.check_coverage("""\
  59. def recur(n):
  60. if n == 0:
  61. return 0
  62. else:
  63. return recur(n-1)+1
  64. recur(100000) # This is definitely too many frames.
  65. """,
  66. [1, 2, 3, 5, 7], ""
  67. )
  68. def test_long_recursion_recovery(self):
  69. # Test the core of bug 93: http://bitbucket.org/ned/coveragepy/issue/93
  70. # When recovering from a stack overflow, the Python trace function is
  71. # disabled, but the C trace function is not. So if we're using a
  72. # Python trace function, we won't trace anything after the stack
  73. # overflow, and there should be a warning about it. If we're using
  74. # the C trace function, only line 3 will be missing, and all else
  75. # will be traced.
  76. self.make_file("recur.py", """\
  77. def recur(n):
  78. if n == 0:
  79. return 0 # never hit
  80. else:
  81. return recur(n-1)+1
  82. try:
  83. recur(100000) # This is definitely too many frames.
  84. except RuntimeError:
  85. i = 10
  86. i = 11
  87. """)
  88. cov = coverage.Coverage()
  89. self.start_import_stop(cov, "recur")
  90. pytrace = (cov.collector.tracer_name() == "PyTracer")
  91. expected_missing = [3]
  92. if pytrace:
  93. expected_missing += [9, 10, 11]
  94. _, statements, missing, _ = cov.analysis("recur.py")
  95. self.assertEqual(statements, [1, 2, 3, 5, 7, 8, 9, 10, 11])
  96. self.assertEqual(missing, expected_missing)
  97. # Get a warning about the stackoverflow effect on the tracing function.
  98. if pytrace:
  99. self.assertEqual(cov._warnings,
  100. ["Trace function changed, measurement is likely wrong: None"]
  101. )
  102. else:
  103. self.assertEqual(cov._warnings, [])
  104. class MemoryLeakTest(CoverageTest):
  105. """Attempt the impossible: test that memory doesn't leak.
  106. Note: this test is truly unusual, and has had a colorful history. See
  107. for example: https://bitbucket.org/ned/coveragepy/issue/186
  108. It may still fail occasionally, especially on PyPy.
  109. """
  110. def test_for_leaks(self):
  111. # Our original bad memory leak only happened on line numbers > 255, so
  112. # make a code object with more lines than that. Ugly string mumbo
  113. # jumbo to get 300 blank lines at the beginning..
  114. code = """\
  115. # blank line\n""" * 300 + """\
  116. def once(x): # line 301
  117. if x % 100 == 0:
  118. raise Exception("100!")
  119. elif x % 2:
  120. return 10
  121. else: # line 306
  122. return 11
  123. i = 0 # Portable loop without alloc'ing memory.
  124. while i < ITERS:
  125. try:
  126. once(i)
  127. except:
  128. pass
  129. i += 1 # line 315
  130. """
  131. lines = list(range(301, 315))
  132. lines.remove(306) # Line 306 is the "else".
  133. # This is a non-deterministic test, so try it a few times, and fail it
  134. # only if it predominantly fails.
  135. fails = 0
  136. for _ in range(10):
  137. ram_0 = osinfo.process_ram()
  138. self.check_coverage(code.replace("ITERS", "10"), lines, "")
  139. ram_10 = osinfo.process_ram()
  140. self.check_coverage(code.replace("ITERS", "10000"), lines, "")
  141. ram_10k = osinfo.process_ram()
  142. # Running the code 10k times shouldn't grow the ram much more than
  143. # running it 10 times.
  144. ram_growth = (ram_10k - ram_10) - (ram_10 - ram_0)
  145. if ram_growth > 100000: # pragma: only failure
  146. fails += 1
  147. if fails > 8: # pragma: only failure
  148. self.fail("RAM grew by %d" % (ram_growth))
  149. class PyexpatTest(CoverageTest):
  150. """Pyexpat screws up tracing. Make sure we've counter-defended properly."""
  151. def test_pyexpat(self):
  152. # pyexpat calls the trace function explicitly (inexplicably), and does
  153. # it wrong for exceptions. Parsing a DOCTYPE for some reason throws
  154. # an exception internally, and triggers its wrong behavior. This test
  155. # checks that our fake PyTrace_RETURN hack in tracer.c works. It will
  156. # also detect if the pyexpat bug is fixed unbeknownst to us, meaning
  157. # we'd see two RETURNs where there should only be one.
  158. self.make_file("trydom.py", """\
  159. import xml.dom.minidom
  160. XML = '''\\
  161. <!DOCTYPE fooey SYSTEM "http://www.example.com/example.dtd">
  162. <root><child/><child/></root>
  163. '''
  164. def foo():
  165. dom = xml.dom.minidom.parseString(XML)
  166. assert len(dom.getElementsByTagName('child')) == 2
  167. a = 11
  168. foo()
  169. """)
  170. self.make_file("outer.py", "\n"*100 + "import trydom\na = 102\n")
  171. cov = coverage.Coverage()
  172. cov.erase()
  173. # Import the Python file, executing it.
  174. self.start_import_stop(cov, "outer")
  175. _, statements, missing, _ = cov.analysis("trydom.py")
  176. self.assertEqual(statements, [1, 3, 8, 9, 10, 11, 13])
  177. self.assertEqual(missing, [])
  178. _, statements, missing, _ = cov.analysis("outer.py")
  179. self.assertEqual(statements, [101, 102])
  180. self.assertEqual(missing, [])
  181. # Make sure pyexpat isn't recorded as a source file.
  182. # https://bitbucket.org/ned/coveragepy/issues/419/nosource-no-source-for-code-path-to-c
  183. files = cov.get_data().measured_files()
  184. self.assertFalse(
  185. any(f.endswith("pyexpat.c") for f in files),
  186. "Pyexpat.c is in the measured files!: %r:" % (files,)
  187. )
  188. class ExceptionTest(CoverageTest):
  189. """I suspect different versions of Python deal with exceptions differently
  190. in the trace function.
  191. """
  192. def test_exception(self):
  193. # Python 2.3's trace function doesn't get called with "return" if the
  194. # scope is exiting due to an exception. This confounds our trace
  195. # function which relies on scope announcements to track which files to
  196. # trace.
  197. #
  198. # This test is designed to sniff this out. Each function in the call
  199. # stack is in a different file, to try to trip up the tracer. Each
  200. # file has active lines in a different range so we'll see if the lines
  201. # get attributed to the wrong file.
  202. self.make_file("oops.py", """\
  203. def oops(args):
  204. a = 2
  205. raise Exception("oops")
  206. a = 4
  207. """)
  208. self.make_file("fly.py", "\n"*100 + """\
  209. def fly(calls):
  210. a = 2
  211. calls[0](calls[1:])
  212. a = 4
  213. """)
  214. self.make_file("catch.py", "\n"*200 + """\
  215. def catch(calls):
  216. try:
  217. a = 3
  218. calls[0](calls[1:])
  219. a = 5
  220. except:
  221. a = 7
  222. """)
  223. self.make_file("doit.py", "\n"*300 + """\
  224. def doit(calls):
  225. try:
  226. calls[0](calls[1:])
  227. except:
  228. a = 5
  229. """)
  230. # Import all the modules before starting coverage, so the def lines
  231. # won't be in all the results.
  232. for mod in "oops fly catch doit".split():
  233. self.import_local_file(mod)
  234. # Each run nests the functions differently to get different
  235. # combinations of catching exceptions and letting them fly.
  236. runs = [
  237. ("doit fly oops", {
  238. 'doit.py': [302, 303, 304, 305],
  239. 'fly.py': [102, 103],
  240. 'oops.py': [2, 3],
  241. }),
  242. ("doit catch oops", {
  243. 'doit.py': [302, 303],
  244. 'catch.py': [202, 203, 204, 206, 207],
  245. 'oops.py': [2, 3],
  246. }),
  247. ("doit fly catch oops", {
  248. 'doit.py': [302, 303],
  249. 'fly.py': [102, 103, 104],
  250. 'catch.py': [202, 203, 204, 206, 207],
  251. 'oops.py': [2, 3],
  252. }),
  253. ("doit catch fly oops", {
  254. 'doit.py': [302, 303],
  255. 'catch.py': [202, 203, 204, 206, 207],
  256. 'fly.py': [102, 103],
  257. 'oops.py': [2, 3],
  258. }),
  259. ]
  260. for callnames, lines_expected in runs:
  261. # Make the list of functions we'll call for this test.
  262. callnames = callnames.split()
  263. calls = [getattr(sys.modules[cn], cn) for cn in callnames]
  264. cov = coverage.Coverage()
  265. cov.start()
  266. # Call our list of functions: invoke the first, with the rest as
  267. # an argument.
  268. calls[0](calls[1:]) # pragma: nested
  269. cov.stop() # pragma: nested
  270. # Clean the line data and compare to expected results.
  271. # The file names are absolute, so keep just the base.
  272. clean_lines = {}
  273. data = cov.get_data()
  274. for callname in callnames:
  275. filename = callname + ".py"
  276. lines = data.lines(abs_file(filename))
  277. clean_lines[filename] = sorted(lines)
  278. self.assertEqual(clean_lines, lines_expected)
  279. class DoctestTest(CoverageTest):
  280. """Tests invoked with doctest should measure properly."""
  281. def setUp(self):
  282. super(DoctestTest, self).setUp()
  283. # Oh, the irony! This test case exists because Python 2.4's
  284. # doctest module doesn't play well with coverage. But nose fixes
  285. # the problem by monkeypatching doctest. I want to undo the
  286. # monkeypatch to be sure I'm getting the doctest module that users
  287. # of coverage will get. Deleting the imported module here is
  288. # enough: when the test imports doctest again, it will get a fresh
  289. # copy without the monkeypatch.
  290. del sys.modules['doctest']
  291. def test_doctest(self):
  292. self.check_coverage('''\
  293. def return_arg_or_void(arg):
  294. """If <arg> is None, return "Void"; otherwise return <arg>
  295. >>> return_arg_or_void(None)
  296. 'Void'
  297. >>> return_arg_or_void("arg")
  298. 'arg'
  299. >>> return_arg_or_void("None")
  300. 'None'
  301. """
  302. if arg is None:
  303. return "Void"
  304. else:
  305. return arg
  306. import doctest, sys
  307. doctest.testmod(sys.modules[__name__]) # we're not __main__ :(
  308. ''',
  309. [1, 11, 12, 14, 16, 17], "")
  310. class GettraceTest(CoverageTest):
  311. """Tests that we work properly with `sys.gettrace()`."""
  312. def test_round_trip(self):
  313. self.check_coverage('''\
  314. import sys
  315. def foo(n):
  316. return 3*n
  317. def bar(n):
  318. return 5*n
  319. a = foo(6)
  320. sys.settrace(sys.gettrace())
  321. a = bar(8)
  322. ''',
  323. [1, 2, 3, 4, 5, 6, 7, 8], "")
  324. def test_multi_layers(self):
  325. self.check_coverage('''\
  326. import sys
  327. def level1():
  328. a = 3
  329. level2()
  330. b = 5
  331. def level2():
  332. c = 7
  333. sys.settrace(sys.gettrace())
  334. d = 9
  335. e = 10
  336. level1()
  337. f = 12
  338. ''',
  339. [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "")
  340. def test_setting_new_trace_function(self):
  341. # https://bitbucket.org/ned/coveragepy/issues/436/disabled-coverage-ctracer-may-rise-from
  342. self.check_coverage('''\
  343. import sys
  344. def tracer(frame, event, arg):
  345. print("%s: %s @ %d" % (event, frame.f_code.co_filename, frame.f_lineno))
  346. return tracer
  347. def begin():
  348. sys.settrace(tracer)
  349. def collect():
  350. t = sys.gettrace()
  351. assert t is tracer, t
  352. def test_unsets_trace():
  353. begin()
  354. collect()
  355. old = sys.gettrace()
  356. test_unsets_trace()
  357. sys.settrace(old)
  358. ''',
  359. lines=[1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20],
  360. missing="4-5, 11-12",
  361. )
  362. out = self.stdout().replace(self.last_module_name, "coverage_test")
  363. self.assertEqual(
  364. out,
  365. (
  366. "call: coverage_test.py @ 10\n"
  367. "line: coverage_test.py @ 11\n"
  368. "line: coverage_test.py @ 12\n"
  369. "return: coverage_test.py @ 12\n"
  370. ),
  371. )
  372. class ExecTest(CoverageTest):
  373. """Tests of exec."""
  374. def test_correct_filename(self):
  375. # https://bitbucket.org/ned/coveragepy/issues/380/code-executed-by-exec-excluded-from
  376. # Bug was that exec'd files would have their lines attributed to the
  377. # calling file. Make two files, both with ~30 lines, but no lines in
  378. # common. Line 30 in to_exec.py was recorded as line 30 in main.py,
  379. # but now it's fixed. :)
  380. self.make_file("to_exec.py", """\
  381. \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
  382. print("var is {0}".format(var)) # line 31
  383. """)
  384. self.make_file("main.py", """\
  385. namespace = {'var': 17}
  386. with open("to_exec.py") as to_exec_py:
  387. code = compile(to_exec_py.read(), 'to_exec.py', 'exec')
  388. exec(code, globals(), namespace)
  389. \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
  390. print("done") # line 35
  391. """)
  392. cov = coverage.Coverage()
  393. self.start_import_stop(cov, "main")
  394. _, statements, missing, _ = cov.analysis("main.py")
  395. self.assertEqual(statements, [1, 2, 3, 4, 35])
  396. self.assertEqual(missing, [])
  397. _, statements, missing, _ = cov.analysis("to_exec.py")
  398. self.assertEqual(statements, [31])
  399. self.assertEqual(missing, [])
  400. class MockingProtectionTest(CoverageTest):
  401. """Tests about protecting ourselves from aggressive mocking.
  402. https://bitbucket.org/ned/coveragepy/issues/416/coverage-40-is-causing-existing-unit-tests
  403. """
  404. def test_os_path_exists(self):
  405. # To see if this test still detects the problem, change isolate_module
  406. # in misc.py to simply return its argument. It should fail with a
  407. # StopIteration error.
  408. self.make_file("bug416.py", """\
  409. import os.path
  410. import mock
  411. @mock.patch('os.path.exists')
  412. def test_path_exists(mock_exists):
  413. mock_exists.side_effect = [17]
  414. print("in test")
  415. import bug416a
  416. print(bug416a.foo)
  417. print(os.path.exists("."))
  418. test_path_exists()
  419. """)
  420. self.make_file("bug416a.py", """\
  421. print("bug416a.py")
  422. foo = 23
  423. """)
  424. import py_compile
  425. py_compile.compile("bug416a.py")
  426. out = self.run_command("coverage run bug416.py")
  427. self.assertEqual(out, "in test\nbug416a.py\n23\n17\n")