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