123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520 |
- # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
- # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
- """Oddball cases for testing coverage.py"""
- import sys
- import coverage
- from coverage.files import abs_file
- from tests.coveragetest import CoverageTest
- from tests import osinfo
- class ThreadingTest(CoverageTest):
- """Tests of the threading support."""
- def test_threading(self):
- self.check_coverage("""\
- import threading
- def fromMainThread():
- return "called from main thread"
- def fromOtherThread():
- return "called from other thread"
- def neverCalled():
- return "no one calls me"
- other = threading.Thread(target=fromOtherThread)
- other.start()
- fromMainThread()
- other.join()
- """,
- [1, 3, 4, 6, 7, 9, 10, 12, 13, 14, 15], "10")
- def test_thread_run(self):
- self.check_coverage("""\
- import threading
- class TestThread(threading.Thread):
- def run(self):
- self.a = 5
- self.do_work()
- self.a = 7
- def do_work(self):
- self.a = 10
- thd = TestThread()
- thd.start()
- thd.join()
- """,
- [1, 3, 4, 5, 6, 7, 9, 10, 12, 13, 14], "")
- class RecursionTest(CoverageTest):
- """Check what happens when recursive code gets near limits."""
- def test_short_recursion(self):
- # We can definitely get close to 500 stack frames.
- self.check_coverage("""\
- def recur(n):
- if n == 0:
- return 0
- else:
- return recur(n-1)+1
- recur(495) # We can get at least this many stack frames.
- i = 8 # and this line will be traced
- """,
- [1, 2, 3, 5, 7, 8], "")
- def test_long_recursion(self):
- # We can't finish a very deep recursion, but we don't crash.
- with self.assertRaises(RuntimeError):
- self.check_coverage("""\
- def recur(n):
- if n == 0:
- return 0
- else:
- return recur(n-1)+1
- recur(100000) # This is definitely too many frames.
- """,
- [1, 2, 3, 5, 7], ""
- )
- def test_long_recursion_recovery(self):
- # Test the core of bug 93: http://bitbucket.org/ned/coveragepy/issue/93
- # When recovering from a stack overflow, the Python trace function is
- # disabled, but the C trace function is not. So if we're using a
- # Python trace function, we won't trace anything after the stack
- # overflow, and there should be a warning about it. If we're using
- # the C trace function, only line 3 will be missing, and all else
- # will be traced.
- self.make_file("recur.py", """\
- def recur(n):
- if n == 0:
- return 0 # never hit
- else:
- return recur(n-1)+1
- try:
- recur(100000) # This is definitely too many frames.
- except RuntimeError:
- i = 10
- i = 11
- """)
- cov = coverage.Coverage()
- self.start_import_stop(cov, "recur")
- pytrace = (cov.collector.tracer_name() == "PyTracer")
- expected_missing = [3]
- if pytrace:
- expected_missing += [9, 10, 11]
- _, statements, missing, _ = cov.analysis("recur.py")
- self.assertEqual(statements, [1, 2, 3, 5, 7, 8, 9, 10, 11])
- self.assertEqual(missing, expected_missing)
- # Get a warning about the stackoverflow effect on the tracing function.
- if pytrace:
- self.assertEqual(cov._warnings,
- ["Trace function changed, measurement is likely wrong: None"]
- )
- else:
- self.assertEqual(cov._warnings, [])
- class MemoryLeakTest(CoverageTest):
- """Attempt the impossible: test that memory doesn't leak.
- Note: this test is truly unusual, and has had a colorful history. See
- for example: https://bitbucket.org/ned/coveragepy/issue/186
- It may still fail occasionally, especially on PyPy.
- """
- def test_for_leaks(self):
- # Our original bad memory leak only happened on line numbers > 255, so
- # make a code object with more lines than that. Ugly string mumbo
- # jumbo to get 300 blank lines at the beginning..
- code = """\
- # blank line\n""" * 300 + """\
- def once(x): # line 301
- if x % 100 == 0:
- raise Exception("100!")
- elif x % 2:
- return 10
- else: # line 306
- return 11
- i = 0 # Portable loop without alloc'ing memory.
- while i < ITERS:
- try:
- once(i)
- except:
- pass
- i += 1 # line 315
- """
- lines = list(range(301, 315))
- lines.remove(306) # Line 306 is the "else".
- # This is a non-deterministic test, so try it a few times, and fail it
- # only if it predominantly fails.
- fails = 0
- for _ in range(10):
- ram_0 = osinfo.process_ram()
- self.check_coverage(code.replace("ITERS", "10"), lines, "")
- ram_10 = osinfo.process_ram()
- self.check_coverage(code.replace("ITERS", "10000"), lines, "")
- ram_10k = osinfo.process_ram()
- # Running the code 10k times shouldn't grow the ram much more than
- # running it 10 times.
- ram_growth = (ram_10k - ram_10) - (ram_10 - ram_0)
- if ram_growth > 100000: # pragma: only failure
- fails += 1
- if fails > 8: # pragma: only failure
- self.fail("RAM grew by %d" % (ram_growth))
- class PyexpatTest(CoverageTest):
- """Pyexpat screws up tracing. Make sure we've counter-defended properly."""
- def test_pyexpat(self):
- # pyexpat calls the trace function explicitly (inexplicably), and does
- # it wrong for exceptions. Parsing a DOCTYPE for some reason throws
- # an exception internally, and triggers its wrong behavior. This test
- # checks that our fake PyTrace_RETURN hack in tracer.c works. It will
- # also detect if the pyexpat bug is fixed unbeknownst to us, meaning
- # we'd see two RETURNs where there should only be one.
- self.make_file("trydom.py", """\
- import xml.dom.minidom
- XML = '''\\
- <!DOCTYPE fooey SYSTEM "http://www.example.com/example.dtd">
- <root><child/><child/></root>
- '''
- def foo():
- dom = xml.dom.minidom.parseString(XML)
- assert len(dom.getElementsByTagName('child')) == 2
- a = 11
- foo()
- """)
- self.make_file("outer.py", "\n"*100 + "import trydom\na = 102\n")
- cov = coverage.Coverage()
- cov.erase()
- # Import the Python file, executing it.
- self.start_import_stop(cov, "outer")
- _, statements, missing, _ = cov.analysis("trydom.py")
- self.assertEqual(statements, [1, 3, 8, 9, 10, 11, 13])
- self.assertEqual(missing, [])
- _, statements, missing, _ = cov.analysis("outer.py")
- self.assertEqual(statements, [101, 102])
- self.assertEqual(missing, [])
- # Make sure pyexpat isn't recorded as a source file.
- # https://bitbucket.org/ned/coveragepy/issues/419/nosource-no-source-for-code-path-to-c
- files = cov.get_data().measured_files()
- self.assertFalse(
- any(f.endswith("pyexpat.c") for f in files),
- "Pyexpat.c is in the measured files!: %r:" % (files,)
- )
- class ExceptionTest(CoverageTest):
- """I suspect different versions of Python deal with exceptions differently
- in the trace function.
- """
- def test_exception(self):
- # Python 2.3's trace function doesn't get called with "return" if the
- # scope is exiting due to an exception. This confounds our trace
- # function which relies on scope announcements to track which files to
- # trace.
- #
- # This test is designed to sniff this out. Each function in the call
- # stack is in a different file, to try to trip up the tracer. Each
- # file has active lines in a different range so we'll see if the lines
- # get attributed to the wrong file.
- self.make_file("oops.py", """\
- def oops(args):
- a = 2
- raise Exception("oops")
- a = 4
- """)
- self.make_file("fly.py", "\n"*100 + """\
- def fly(calls):
- a = 2
- calls[0](calls[1:])
- a = 4
- """)
- self.make_file("catch.py", "\n"*200 + """\
- def catch(calls):
- try:
- a = 3
- calls[0](calls[1:])
- a = 5
- except:
- a = 7
- """)
- self.make_file("doit.py", "\n"*300 + """\
- def doit(calls):
- try:
- calls[0](calls[1:])
- except:
- a = 5
- """)
- # Import all the modules before starting coverage, so the def lines
- # won't be in all the results.
- for mod in "oops fly catch doit".split():
- self.import_local_file(mod)
- # Each run nests the functions differently to get different
- # combinations of catching exceptions and letting them fly.
- runs = [
- ("doit fly oops", {
- 'doit.py': [302, 303, 304, 305],
- 'fly.py': [102, 103],
- 'oops.py': [2, 3],
- }),
- ("doit catch oops", {
- 'doit.py': [302, 303],
- 'catch.py': [202, 203, 204, 206, 207],
- 'oops.py': [2, 3],
- }),
- ("doit fly catch oops", {
- 'doit.py': [302, 303],
- 'fly.py': [102, 103, 104],
- 'catch.py': [202, 203, 204, 206, 207],
- 'oops.py': [2, 3],
- }),
- ("doit catch fly oops", {
- 'doit.py': [302, 303],
- 'catch.py': [202, 203, 204, 206, 207],
- 'fly.py': [102, 103],
- 'oops.py': [2, 3],
- }),
- ]
- for callnames, lines_expected in runs:
- # Make the list of functions we'll call for this test.
- callnames = callnames.split()
- calls = [getattr(sys.modules[cn], cn) for cn in callnames]
- cov = coverage.Coverage()
- cov.start()
- # Call our list of functions: invoke the first, with the rest as
- # an argument.
- calls[0](calls[1:]) # pragma: nested
- cov.stop() # pragma: nested
- # Clean the line data and compare to expected results.
- # The file names are absolute, so keep just the base.
- clean_lines = {}
- data = cov.get_data()
- for callname in callnames:
- filename = callname + ".py"
- lines = data.lines(abs_file(filename))
- clean_lines[filename] = sorted(lines)
- self.assertEqual(clean_lines, lines_expected)
- class DoctestTest(CoverageTest):
- """Tests invoked with doctest should measure properly."""
- def setUp(self):
- super(DoctestTest, self).setUp()
- # Oh, the irony! This test case exists because Python 2.4's
- # doctest module doesn't play well with coverage. But nose fixes
- # the problem by monkeypatching doctest. I want to undo the
- # monkeypatch to be sure I'm getting the doctest module that users
- # of coverage will get. Deleting the imported module here is
- # enough: when the test imports doctest again, it will get a fresh
- # copy without the monkeypatch.
- del sys.modules['doctest']
- def test_doctest(self):
- self.check_coverage('''\
- def return_arg_or_void(arg):
- """If <arg> is None, return "Void"; otherwise return <arg>
- >>> return_arg_or_void(None)
- 'Void'
- >>> return_arg_or_void("arg")
- 'arg'
- >>> return_arg_or_void("None")
- 'None'
- """
- if arg is None:
- return "Void"
- else:
- return arg
- import doctest, sys
- doctest.testmod(sys.modules[__name__]) # we're not __main__ :(
- ''',
- [1, 11, 12, 14, 16, 17], "")
- class GettraceTest(CoverageTest):
- """Tests that we work properly with `sys.gettrace()`."""
- def test_round_trip(self):
- self.check_coverage('''\
- import sys
- def foo(n):
- return 3*n
- def bar(n):
- return 5*n
- a = foo(6)
- sys.settrace(sys.gettrace())
- a = bar(8)
- ''',
- [1, 2, 3, 4, 5, 6, 7, 8], "")
- def test_multi_layers(self):
- self.check_coverage('''\
- import sys
- def level1():
- a = 3
- level2()
- b = 5
- def level2():
- c = 7
- sys.settrace(sys.gettrace())
- d = 9
- e = 10
- level1()
- f = 12
- ''',
- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "")
- def test_setting_new_trace_function(self):
- # https://bitbucket.org/ned/coveragepy/issues/436/disabled-coverage-ctracer-may-rise-from
- self.check_coverage('''\
- import sys
- def tracer(frame, event, arg):
- print("%s: %s @ %d" % (event, frame.f_code.co_filename, frame.f_lineno))
- return tracer
- def begin():
- sys.settrace(tracer)
- def collect():
- t = sys.gettrace()
- assert t is tracer, t
- def test_unsets_trace():
- begin()
- collect()
- old = sys.gettrace()
- test_unsets_trace()
- sys.settrace(old)
- ''',
- lines=[1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20],
- missing="4-5, 11-12",
- )
- out = self.stdout().replace(self.last_module_name, "coverage_test")
- self.assertEqual(
- out,
- (
- "call: coverage_test.py @ 10\n"
- "line: coverage_test.py @ 11\n"
- "line: coverage_test.py @ 12\n"
- "return: coverage_test.py @ 12\n"
- ),
- )
- class ExecTest(CoverageTest):
- """Tests of exec."""
- def test_correct_filename(self):
- # https://bitbucket.org/ned/coveragepy/issues/380/code-executed-by-exec-excluded-from
- # Bug was that exec'd files would have their lines attributed to the
- # calling file. Make two files, both with ~30 lines, but no lines in
- # common. Line 30 in to_exec.py was recorded as line 30 in main.py,
- # but now it's fixed. :)
- self.make_file("to_exec.py", """\
- \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
- print("var is {0}".format(var)) # line 31
- """)
- self.make_file("main.py", """\
- namespace = {'var': 17}
- with open("to_exec.py") as to_exec_py:
- code = compile(to_exec_py.read(), 'to_exec.py', 'exec')
- exec(code, globals(), namespace)
- \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
- print("done") # line 35
- """)
- cov = coverage.Coverage()
- self.start_import_stop(cov, "main")
- _, statements, missing, _ = cov.analysis("main.py")
- self.assertEqual(statements, [1, 2, 3, 4, 35])
- self.assertEqual(missing, [])
- _, statements, missing, _ = cov.analysis("to_exec.py")
- self.assertEqual(statements, [31])
- self.assertEqual(missing, [])
- class MockingProtectionTest(CoverageTest):
- """Tests about protecting ourselves from aggressive mocking.
- https://bitbucket.org/ned/coveragepy/issues/416/coverage-40-is-causing-existing-unit-tests
- """
- def test_os_path_exists(self):
- # To see if this test still detects the problem, change isolate_module
- # in misc.py to simply return its argument. It should fail with a
- # StopIteration error.
- self.make_file("bug416.py", """\
- import os.path
- import mock
- @mock.patch('os.path.exists')
- def test_path_exists(mock_exists):
- mock_exists.side_effect = [17]
- print("in test")
- import bug416a
- print(bug416a.foo)
- print(os.path.exists("."))
- test_path_exists()
- """)
- self.make_file("bug416a.py", """\
- print("bug416a.py")
- foo = 23
- """)
- import py_compile
- py_compile.compile("bug416a.py")
- out = self.run_command("coverage run bug416.py")
- self.assertEqual(out, "in test\nbug416a.py\n23\n17\n")
|