test_xml.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  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 XML reports from coverage.py."""
  4. import os
  5. import os.path
  6. import re
  7. import coverage
  8. from coverage.files import abs_file
  9. from tests.coveragetest import CoverageTest
  10. from tests.goldtest import CoverageGoldTest
  11. from tests.goldtest import change_dir, compare
  12. class XmlTestHelpers(CoverageTest):
  13. """Methods to use from XML tests."""
  14. def run_mycode(self):
  15. """Run mycode.py, so we can report on it."""
  16. self.make_file("mycode.py", "print('hello')\n")
  17. self.run_command("coverage run mycode.py")
  18. def run_doit(self):
  19. """Construct a simple sub-package."""
  20. self.make_file("sub/__init__.py")
  21. self.make_file("sub/doit.py", "print('doit!')")
  22. self.make_file("main.py", "import sub.doit")
  23. cov = coverage.Coverage()
  24. self.start_import_stop(cov, "main")
  25. return cov
  26. def make_tree(self, width, depth, curdir="."):
  27. """Make a tree of packages.
  28. Makes `width` directories, named d0 .. d{width-1}. Each directory has
  29. __init__.py, and `width` files, named f0.py .. f{width-1}.py. Each
  30. directory also has `width` sub-directories, in the same fashion, until
  31. a depth of `depth` is reached.
  32. """
  33. if depth == 0:
  34. return
  35. def here(p):
  36. """A path for `p` in our currently interesting directory."""
  37. return os.path.join(curdir, p)
  38. for i in range(width):
  39. next_dir = here("d{0}".format(i))
  40. self.make_tree(width, depth-1, next_dir)
  41. if curdir != ".":
  42. self.make_file(here("__init__.py"), "")
  43. for i in range(width):
  44. filename = here("f{0}.py".format(i))
  45. self.make_file(filename, "# {0}\n".format(filename))
  46. class XmlReportTest(XmlTestHelpers, CoverageTest):
  47. """Tests of the XML reports from coverage.py."""
  48. def test_default_file_placement(self):
  49. self.run_mycode()
  50. self.run_command("coverage xml")
  51. self.assert_exists("coverage.xml")
  52. def test_argument_affects_xml_placement(self):
  53. self.run_mycode()
  54. self.run_command("coverage xml -o put_it_there.xml")
  55. self.assert_doesnt_exist("coverage.xml")
  56. self.assert_exists("put_it_there.xml")
  57. def test_config_file_directory_does_not_exist(self):
  58. self.run_mycode()
  59. self.run_command("coverage xml -o nonexistent/put_it_there.xml")
  60. self.assert_doesnt_exist("coverage.xml")
  61. self.assert_doesnt_exist("put_it_there.xml")
  62. self.assert_exists("nonexistent/put_it_there.xml")
  63. def test_config_affects_xml_placement(self):
  64. self.run_mycode()
  65. self.make_file(".coveragerc", "[xml]\noutput = xml.out\n")
  66. self.run_command("coverage xml")
  67. self.assert_doesnt_exist("coverage.xml")
  68. self.assert_exists("xml.out")
  69. def test_no_data(self):
  70. # https://bitbucket.org/ned/coveragepy/issue/210
  71. self.run_command("coverage xml")
  72. self.assert_doesnt_exist("coverage.xml")
  73. def test_no_source(self):
  74. # Written while investigating a bug, might as well keep it.
  75. # https://bitbucket.org/ned/coveragepy/issue/208
  76. self.make_file("innocuous.py", "a = 4")
  77. cov = coverage.Coverage()
  78. self.start_import_stop(cov, "innocuous")
  79. os.remove("innocuous.py")
  80. cov.xml_report(ignore_errors=True)
  81. self.assert_exists("coverage.xml")
  82. def test_filename_format_showing_everything(self):
  83. cov = self.run_doit()
  84. cov.xml_report(outfile="-")
  85. xml = self.stdout()
  86. doit_line = re_line(xml, "class.*doit")
  87. self.assertIn('filename="sub/doit.py"', doit_line)
  88. def test_filename_format_including_filename(self):
  89. cov = self.run_doit()
  90. cov.xml_report(["sub/doit.py"], outfile="-")
  91. xml = self.stdout()
  92. doit_line = re_line(xml, "class.*doit")
  93. self.assertIn('filename="sub/doit.py"', doit_line)
  94. def test_filename_format_including_module(self):
  95. cov = self.run_doit()
  96. import sub.doit # pylint: disable=import-error
  97. cov.xml_report([sub.doit], outfile="-")
  98. xml = self.stdout()
  99. doit_line = re_line(xml, "class.*doit")
  100. self.assertIn('filename="sub/doit.py"', doit_line)
  101. def test_reporting_on_nothing(self):
  102. # Used to raise a zero division error:
  103. # https://bitbucket.org/ned/coveragepy/issue/250
  104. self.make_file("empty.py", "")
  105. cov = coverage.Coverage()
  106. empty = self.start_import_stop(cov, "empty")
  107. cov.xml_report([empty], outfile="-")
  108. xml = self.stdout()
  109. empty_line = re_line(xml, "class.*empty")
  110. self.assertIn('filename="empty.py"', empty_line)
  111. self.assertIn('line-rate="1"', empty_line)
  112. def test_empty_file_is_100_not_0(self):
  113. # https://bitbucket.org/ned/coveragepy/issue/345
  114. cov = self.run_doit()
  115. cov.xml_report(outfile="-")
  116. xml = self.stdout()
  117. init_line = re_line(xml, 'filename="sub/__init__.py"')
  118. self.assertIn('line-rate="1"', init_line)
  119. def assert_source(self, xml, src):
  120. """Assert that the XML has a <source> element with `src`."""
  121. src = abs_file(src)
  122. self.assertRegex(xml, r'<source>\s*{0}\s*</source>'.format(re.escape(src)))
  123. def test_curdir_source(self):
  124. # With no source= option, the XML report should explain that the source
  125. # is in the current directory.
  126. cov = self.run_doit()
  127. cov.xml_report(outfile="-")
  128. xml = self.stdout()
  129. self.assert_source(xml, ".")
  130. self.assertEqual(xml.count('<source>'), 1)
  131. def test_deep_source(self):
  132. # When using source=, the XML report needs to mention those directories
  133. # in the <source> elements.
  134. # https://bitbucket.org/ned/coveragepy/issues/439/incorrect-cobertura-file-sources-generated
  135. self.make_file("src/main/foo.py", "a = 1")
  136. self.make_file("also/over/there/bar.py", "b = 2")
  137. cov = coverage.Coverage(source=["src/main", "also/over/there", "not/really"])
  138. cov.start()
  139. mod_foo = self.import_local_file("foo", "src/main/foo.py") # pragma: nested
  140. mod_bar = self.import_local_file("bar", "also/over/there/bar.py") # pragma: nested
  141. cov.stop() # pragma: nested
  142. cov.xml_report([mod_foo, mod_bar], outfile="-")
  143. xml = self.stdout()
  144. self.assert_source(xml, "src/main")
  145. self.assert_source(xml, "also/over/there")
  146. self.assertEqual(xml.count('<source>'), 2)
  147. self.assertIn(
  148. '<class branch-rate="0" complexity="0" filename="foo.py" line-rate="1" name="foo.py">',
  149. xml
  150. )
  151. self.assertIn(
  152. '<class branch-rate="0" complexity="0" filename="bar.py" line-rate="1" name="bar.py">',
  153. xml
  154. )
  155. class XmlPackageStructureTest(XmlTestHelpers, CoverageTest):
  156. """Tests about the package structure reported in the coverage.xml file."""
  157. def package_and_class_tags(self, cov):
  158. """Run an XML report on `cov`, and get the package and class tags."""
  159. self.captured_stdout.truncate(0)
  160. cov.xml_report(outfile="-")
  161. packages_and_classes = re_lines(self.stdout(), r"<package |<class ")
  162. scrubs = r' branch-rate="0"| complexity="0"| line-rate="[\d.]+"'
  163. return clean("".join(packages_and_classes), scrubs)
  164. def assert_package_and_class_tags(self, cov, result):
  165. """Check the XML package and class tags from `cov` match `result`."""
  166. self.assertMultiLineEqual(
  167. self.package_and_class_tags(cov),
  168. clean(result)
  169. )
  170. def test_package_names(self):
  171. self.make_tree(width=1, depth=3)
  172. self.make_file("main.py", """\
  173. from d0.d0 import f0
  174. """)
  175. cov = coverage.Coverage(source=".")
  176. self.start_import_stop(cov, "main")
  177. self.assert_package_and_class_tags(cov, """\
  178. <package name=".">
  179. <class filename="main.py" name="main.py">
  180. <package name="d0">
  181. <class filename="d0/__init__.py" name="__init__.py">
  182. <class filename="d0/f0.py" name="f0.py">
  183. <package name="d0.d0">
  184. <class filename="d0/d0/__init__.py" name="__init__.py">
  185. <class filename="d0/d0/f0.py" name="f0.py">
  186. """)
  187. def test_package_depth(self):
  188. self.make_tree(width=1, depth=4)
  189. self.make_file("main.py", """\
  190. from d0.d0 import f0
  191. """)
  192. cov = coverage.Coverage(source=".")
  193. self.start_import_stop(cov, "main")
  194. cov.set_option("xml:package_depth", 1)
  195. self.assert_package_and_class_tags(cov, """\
  196. <package name=".">
  197. <class filename="main.py" name="main.py">
  198. <package name="d0">
  199. <class filename="d0/__init__.py" name="__init__.py">
  200. <class filename="d0/d0/__init__.py" name="d0/__init__.py">
  201. <class filename="d0/d0/d0/__init__.py" name="d0/d0/__init__.py">
  202. <class filename="d0/d0/d0/f0.py" name="d0/d0/f0.py">
  203. <class filename="d0/d0/f0.py" name="d0/f0.py">
  204. <class filename="d0/f0.py" name="f0.py">
  205. """)
  206. cov.set_option("xml:package_depth", 2)
  207. self.assert_package_and_class_tags(cov, """\
  208. <package name=".">
  209. <class filename="main.py" name="main.py">
  210. <package name="d0">
  211. <class filename="d0/__init__.py" name="__init__.py">
  212. <class filename="d0/f0.py" name="f0.py">
  213. <package name="d0.d0">
  214. <class filename="d0/d0/__init__.py" name="__init__.py">
  215. <class filename="d0/d0/d0/__init__.py" name="d0/__init__.py">
  216. <class filename="d0/d0/d0/f0.py" name="d0/f0.py">
  217. <class filename="d0/d0/f0.py" name="f0.py">
  218. """)
  219. cov.set_option("xml:package_depth", 3)
  220. self.assert_package_and_class_tags(cov, """\
  221. <package name=".">
  222. <class filename="main.py" name="main.py">
  223. <package name="d0">
  224. <class filename="d0/__init__.py" name="__init__.py">
  225. <class filename="d0/f0.py" name="f0.py">
  226. <package name="d0.d0">
  227. <class filename="d0/d0/__init__.py" name="__init__.py">
  228. <class filename="d0/d0/f0.py" name="f0.py">
  229. <package name="d0.d0.d0">
  230. <class filename="d0/d0/d0/__init__.py" name="__init__.py">
  231. <class filename="d0/d0/d0/f0.py" name="f0.py">
  232. """)
  233. def test_source_prefix(self):
  234. # https://bitbucket.org/ned/coveragepy/issues/465
  235. self.make_file("src/mod.py", "print(17)")
  236. cov = coverage.Coverage(source=["src"])
  237. self.start_import_stop(cov, "mod", modfile="src/mod.py")
  238. self.assert_package_and_class_tags(cov, """\
  239. <package name=".">
  240. <class filename="src/mod.py" name="mod.py">
  241. """)
  242. def re_lines(text, pat):
  243. """Return a list of lines that match `pat` in the string `text`."""
  244. lines = [l for l in text.splitlines(True) if re.search(pat, l)]
  245. return lines
  246. def re_line(text, pat):
  247. """Return the one line in `text` that matches regex `pat`."""
  248. lines = re_lines(text, pat)
  249. assert len(lines) == 1
  250. return lines[0]
  251. def clean(text, scrub=None):
  252. """Clean text to prepare it for comparison.
  253. Remove text matching `scrub`, and leading whitespace. Convert backslashes
  254. to forward slashes.
  255. """
  256. if scrub:
  257. text = re.sub(scrub, "", text)
  258. text = re.sub(r"(?m)^\s+", "", text)
  259. text = re.sub(r"\\", "/", text)
  260. return text
  261. class XmlGoldTest(CoverageGoldTest):
  262. """Tests of XML reporting that use gold files."""
  263. # TODO: this should move out of html.
  264. root_dir = 'tests/farm/html'
  265. def test_a_xml_1(self):
  266. self.output_dir("out/xml_1")
  267. with change_dir("src"):
  268. # pylint: disable=import-error
  269. cov = coverage.Coverage()
  270. cov.start()
  271. import a # pragma: nested
  272. cov.stop() # pragma: nested
  273. cov.xml_report(a, outfile="../out/xml_1/coverage.xml")
  274. source_path = coverage.files.relative_directory().rstrip(r"\/")
  275. compare("gold_x_xml", "out/xml_1", scrubs=[
  276. (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'),
  277. (r' version="[-.\w]+"', ' version="VERSION"'),
  278. (r'<source>\s*.*?\s*</source>', '<source>%s</source>' % source_path),
  279. (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'),
  280. ])
  281. def test_a_xml_2(self):
  282. self.output_dir("out/xml_2")
  283. with change_dir("src"):
  284. # pylint: disable=import-error
  285. cov = coverage.Coverage(config_file="run_a_xml_2.ini")
  286. cov.start()
  287. import a # pragma: nested
  288. cov.stop() # pragma: nested
  289. cov.xml_report(a)
  290. source_path = coverage.files.relative_directory().rstrip(r"\/")
  291. compare("gold_x_xml", "out/xml_2", scrubs=[
  292. (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'),
  293. (r' version="[-.\w]+"', ' version="VERSION"'),
  294. (r'<source>\s*.*?\s*</source>', '<source>%s</source>' % source_path),
  295. (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'),
  296. ])
  297. def test_y_xml_branch(self):
  298. self.output_dir("out/y_xml_branch")
  299. with change_dir("src"):
  300. # pylint: disable=import-error
  301. cov = coverage.Coverage(branch=True)
  302. cov.start()
  303. import y # pragma: nested
  304. cov.stop() # pragma: nested
  305. cov.xml_report(y, outfile="../out/y_xml_branch/coverage.xml")
  306. source_path = coverage.files.relative_directory().rstrip(r"\/")
  307. compare("gold_y_xml_branch", "out/y_xml_branch", scrubs=[
  308. (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'),
  309. (r' version="[-.\w]+"', ' version="VERSION"'),
  310. (r'<source>\s*.*?\s*</source>', '<source>%s</source>' % source_path),
  311. (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'),
  312. ])