test_parser.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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. """Tests for coverage.py's code parsing."""
  4. import textwrap
  5. from tests.coveragetest import CoverageTest
  6. from coverage import env
  7. from coverage.misc import NotPython
  8. from coverage.parser import PythonParser
  9. class PythonParserTest(CoverageTest):
  10. """Tests for coverage.py's Python code parsing."""
  11. run_in_temp_dir = False
  12. def parse_source(self, text):
  13. """Parse `text` as source, and return the `PythonParser` used."""
  14. if env.PY2:
  15. text = text.decode("ascii")
  16. text = textwrap.dedent(text)
  17. parser = PythonParser(text=text, exclude="nocover")
  18. parser.parse_source()
  19. return parser
  20. def test_exit_counts(self):
  21. parser = self.parse_source("""\
  22. # check some basic branch counting
  23. class Foo:
  24. def foo(self, a):
  25. if a:
  26. return 5
  27. else:
  28. return 7
  29. class Bar:
  30. pass
  31. """)
  32. self.assertEqual(parser.exit_counts(), {
  33. 2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1
  34. })
  35. def test_generator_exit_counts(self):
  36. # https://bitbucket.org/ned/coveragepy/issue/324/yield-in-loop-confuses-branch-coverage
  37. parser = self.parse_source("""\
  38. def gen(input):
  39. for n in inp:
  40. yield (i * 2 for i in range(n))
  41. list(gen([1,2,3]))
  42. """)
  43. self.assertEqual(parser.exit_counts(), {
  44. 1:1, # def -> list
  45. 2:2, # for -> yield; for -> exit
  46. 3:2, # yield -> for; genexp exit
  47. 5:1, # list -> exit
  48. })
  49. def test_try_except(self):
  50. parser = self.parse_source("""\
  51. try:
  52. a = 2
  53. except ValueError:
  54. a = 4
  55. except ZeroDivideError:
  56. a = 6
  57. except:
  58. a = 8
  59. b = 9
  60. """)
  61. self.assertEqual(parser.exit_counts(), {
  62. 1: 1, 2:1, 3:2, 4:1, 5:2, 6:1, 7:1, 8:1, 9:1
  63. })
  64. def test_excluded_classes(self):
  65. parser = self.parse_source("""\
  66. class Foo:
  67. def __init__(self):
  68. pass
  69. if len([]): # nocover
  70. class Bar:
  71. pass
  72. """)
  73. self.assertEqual(parser.exit_counts(), {
  74. 1:0, 2:1, 3:1
  75. })
  76. def test_missing_branch_to_excluded_code(self):
  77. parser = self.parse_source("""\
  78. if fooey:
  79. a = 2
  80. else: # nocover
  81. a = 4
  82. b = 5
  83. """)
  84. self.assertEqual(parser.exit_counts(), { 1:1, 2:1, 5:1 })
  85. parser = self.parse_source("""\
  86. def foo():
  87. if fooey:
  88. a = 3
  89. else:
  90. a = 5
  91. b = 6
  92. """)
  93. self.assertEqual(parser.exit_counts(), { 1:1, 2:2, 3:1, 5:1, 6:1 })
  94. parser = self.parse_source("""\
  95. def foo():
  96. if fooey:
  97. a = 3
  98. else: # nocover
  99. a = 5
  100. b = 6
  101. """)
  102. self.assertEqual(parser.exit_counts(), { 1:1, 2:1, 3:1, 6:1 })
  103. def test_indentation_error(self):
  104. msg = (
  105. "Couldn't parse '<code>' as Python source: "
  106. "'unindent does not match any outer indentation level' at line 3"
  107. )
  108. with self.assertRaisesRegex(NotPython, msg):
  109. _ = self.parse_source("""\
  110. 0 spaces
  111. 2
  112. 1
  113. """)
  114. def test_token_error(self):
  115. msg = "Couldn't parse '<code>' as Python source: 'EOF in multi-line string' at line 1"
  116. with self.assertRaisesRegex(NotPython, msg):
  117. _ = self.parse_source("""\
  118. '''
  119. """)
  120. def test_decorator_pragmas(self):
  121. parser = self.parse_source("""\
  122. # 1
  123. @foo(3) # nocover
  124. @bar
  125. def func(x, y=5):
  126. return 6
  127. class Foo: # this is the only statement.
  128. '''9'''
  129. @foo # nocover
  130. def __init__(self):
  131. '''12'''
  132. return 13
  133. @foo( # nocover
  134. 16,
  135. 17,
  136. )
  137. def meth(self):
  138. return 20
  139. @foo( # nocover
  140. 23
  141. )
  142. def func(x=25):
  143. return 26
  144. """)
  145. self.assertEqual(
  146. parser.raw_statements,
  147. set([3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26])
  148. )
  149. self.assertEqual(parser.statements, set([8]))
  150. def test_class_decorator_pragmas(self):
  151. parser = self.parse_source("""\
  152. class Foo(object):
  153. def __init__(self):
  154. self.x = 3
  155. @foo # nocover
  156. class Bar(object):
  157. def __init__(self):
  158. self.x = 8
  159. """)
  160. self.assertEqual(parser.raw_statements, set([1, 2, 3, 5, 6, 7, 8]))
  161. self.assertEqual(parser.statements, set([1, 2, 3]))
  162. class ParserMissingArcDescriptionTest(CoverageTest):
  163. """Tests for PythonParser.missing_arc_description."""
  164. run_in_temp_dir = False
  165. def parse_text(self, source):
  166. """Parse Python source, and return the parser object."""
  167. parser = PythonParser(textwrap.dedent(source))
  168. parser.parse_source()
  169. return parser
  170. def test_missing_arc_description(self):
  171. # This code is never run, so the actual values don't matter.
  172. parser = self.parse_text(u"""\
  173. if x:
  174. print(2)
  175. print(3)
  176. def func5():
  177. for x in range(6):
  178. if x == 7:
  179. break
  180. def func10():
  181. while something(11):
  182. thing(12)
  183. more_stuff(13)
  184. """)
  185. self.assertEqual(
  186. parser.missing_arc_description(1, 2),
  187. "line 1 didn't jump to line 2, because the condition on line 1 was never true"
  188. )
  189. self.assertEqual(
  190. parser.missing_arc_description(1, 3),
  191. "line 1 didn't jump to line 3, because the condition on line 1 was never false"
  192. )
  193. self.assertEqual(
  194. parser.missing_arc_description(6, -5),
  195. "line 6 didn't return from function 'func5', "
  196. "because the loop on line 6 didn't complete"
  197. )
  198. self.assertEqual(
  199. parser.missing_arc_description(6, 7),
  200. "line 6 didn't jump to line 7, because the loop on line 6 never started"
  201. )
  202. self.assertEqual(
  203. parser.missing_arc_description(11, 12),
  204. "line 11 didn't jump to line 12, because the condition on line 11 was never true"
  205. )
  206. self.assertEqual(
  207. parser.missing_arc_description(11, 13),
  208. "line 11 didn't jump to line 13, because the condition on line 11 was never false"
  209. )
  210. def test_missing_arc_descriptions_for_small_callables(self):
  211. # We use 2.7 features here, so just skip this test on 2.6
  212. if env.PYVERSION < (2, 7):
  213. self.skipTest("No dict or set comps in 2.6")
  214. parser = self.parse_text(u"""\
  215. callables = [
  216. lambda: 2,
  217. (x for x in range(3)),
  218. {x:1 for x in range(4)},
  219. {x for x in range(5)},
  220. ]
  221. x = 7
  222. """)
  223. self.assertEqual(
  224. parser.missing_arc_description(2, -2),
  225. "line 2 didn't finish the lambda on line 2"
  226. )
  227. self.assertEqual(
  228. parser.missing_arc_description(3, -3),
  229. "line 3 didn't finish the generator expression on line 3"
  230. )
  231. self.assertEqual(
  232. parser.missing_arc_description(4, -4),
  233. "line 4 didn't finish the dictionary comprehension on line 4"
  234. )
  235. self.assertEqual(
  236. parser.missing_arc_description(5, -5),
  237. "line 5 didn't finish the set comprehension on line 5"
  238. )
  239. def test_missing_arc_descriptions_for_exceptions(self):
  240. parser = self.parse_text(u"""\
  241. try:
  242. pass
  243. except ZeroDivideError:
  244. print("whoops")
  245. except ValueError:
  246. print("yikes")
  247. """)
  248. self.assertEqual(
  249. parser.missing_arc_description(3, 4),
  250. "line 3 didn't jump to line 4, because the exception caught by line 3 didn't happen"
  251. )
  252. self.assertEqual(
  253. parser.missing_arc_description(5, 6),
  254. "line 5 didn't jump to line 6, because the exception caught by line 5 didn't happen"
  255. )
  256. def test_missing_arc_descriptions_for_finally(self):
  257. parser = self.parse_text(u"""\
  258. def function():
  259. for i in range(2):
  260. try:
  261. if something(4):
  262. break
  263. else:
  264. if something(7):
  265. continue
  266. else:
  267. continue
  268. if also_this(11):
  269. return 12
  270. else:
  271. raise Exception(14)
  272. finally:
  273. this_thing(16)
  274. that_thing(17)
  275. """)
  276. self.assertEqual(
  277. parser.missing_arc_description(16, 17),
  278. "line 16 didn't jump to line 17, because the break on line 5 wasn't executed"
  279. )
  280. self.assertEqual(
  281. parser.missing_arc_description(16, 2),
  282. "line 16 didn't jump to line 2, "
  283. "because the continue on line 8 wasn't executed"
  284. " or "
  285. "the continue on line 10 wasn't executed"
  286. )
  287. self.assertEqual(
  288. parser.missing_arc_description(16, -1),
  289. "line 16 didn't except from function 'function', "
  290. "because the raise on line 14 wasn't executed"
  291. " or "
  292. "line 16 didn't return from function 'function', "
  293. "because the return on line 12 wasn't executed"
  294. )
  295. def test_missing_arc_descriptions_bug460(self):
  296. parser = self.parse_text(u"""\
  297. x = 1
  298. d = {
  299. 3: lambda: [],
  300. 4: lambda: [],
  301. }
  302. x = 6
  303. """)
  304. self.assertEqual(
  305. parser.missing_arc_description(2, -3),
  306. "line 3 didn't finish the lambda on line 3",
  307. )
  308. class ParserFileTest(CoverageTest):
  309. """Tests for coverage.py's code parsing from files."""
  310. def parse_file(self, filename):
  311. """Parse `text` as source, and return the `PythonParser` used."""
  312. parser = PythonParser(filename=filename, exclude="nocover")
  313. parser.parse_source()
  314. return parser
  315. def test_line_endings(self):
  316. text = """\
  317. # check some basic branch counting
  318. class Foo:
  319. def foo(self, a):
  320. if a:
  321. return 5
  322. else:
  323. return 7
  324. class Bar:
  325. pass
  326. """
  327. counts = { 2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1 }
  328. name_endings = (("unix", "\n"), ("dos", "\r\n"), ("mac", "\r"))
  329. for fname, newline in name_endings:
  330. fname = fname + ".py"
  331. self.make_file(fname, text, newline=newline)
  332. parser = self.parse_file(fname)
  333. self.assertEqual(
  334. parser.exit_counts(),
  335. counts,
  336. "Wrong for %r" % fname
  337. )
  338. def test_encoding(self):
  339. self.make_file("encoded.py", """\
  340. coverage = "\xe7\xf6v\xear\xe3g\xe9"
  341. """)
  342. parser = self.parse_file("encoded.py")
  343. self.assertEqual(parser.exit_counts(), {1: 1})
  344. def test_missing_line_ending(self):
  345. # Test that the set of statements is the same even if a final
  346. # multi-line statement has no final newline.
  347. # https://bitbucket.org/ned/coveragepy/issue/293
  348. self.make_file("normal.py", """\
  349. out, err = subprocess.Popen(
  350. [sys.executable, '-c', 'pass'],
  351. stdout=subprocess.PIPE,
  352. stderr=subprocess.PIPE).communicate()
  353. """)
  354. parser = self.parse_file("normal.py")
  355. self.assertEqual(parser.statements, set([1]))
  356. self.make_file("abrupt.py", """\
  357. out, err = subprocess.Popen(
  358. [sys.executable, '-c', 'pass'],
  359. stdout=subprocess.PIPE,
  360. stderr=subprocess.PIPE).communicate()""") # no final newline.
  361. # Double-check that some test helper wasn't being helpful.
  362. with open("abrupt.py") as f:
  363. self.assertEqual(f.read()[-1], ")")
  364. parser = self.parse_file("abrupt.py")
  365. self.assertEqual(parser.statements, set([1]))