cover.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. # -*- coding: utf-8 -*-
  2. """
  3. tests.support.parser.cover
  4. ~~~~~~~~~~~~~~~~~~~~~~~~~~
  5. Code coverage aware testing parser
  6. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  7. :copyright: Copyright 2013 by the SaltStack Team, see AUTHORS for more details.
  8. :license: Apache 2.0, see LICENSE for more details.
  9. """
  10. # pylint: disable=repr-flag-used-in-string
  11. # Import python libs
  12. from __future__ import absolute_import, print_function
  13. import os
  14. import re
  15. import shutil
  16. import sys
  17. import warnings
  18. # Import Salt libs
  19. import salt.utils.json
  20. # Import salt testing libs
  21. from tests.support.parser import SaltTestingParser
  22. # Import coverage libs
  23. try:
  24. import coverage
  25. COVERAGE_AVAILABLE = True
  26. except ImportError:
  27. COVERAGE_AVAILABLE = False
  28. try:
  29. import multiprocessing.util
  30. # Force forked multiprocessing processes to be measured as well
  31. def multiprocessing_stop(coverage_object):
  32. """
  33. Save the multiprocessing process coverage object
  34. """
  35. coverage_object.stop()
  36. coverage_object.save()
  37. def multiprocessing_start(obj):
  38. coverage_options = salt.utils.json.loads(
  39. os.environ.get("COVERAGE_OPTIONS", "{}")
  40. )
  41. if not coverage_options:
  42. return
  43. if coverage_options.get("data_suffix", False) is False:
  44. return
  45. coverage_object = coverage.coverage(**coverage_options)
  46. coverage_object.start()
  47. multiprocessing.util.Finalize(
  48. None, multiprocessing_stop, args=(coverage_object,), exitpriority=1000
  49. )
  50. if COVERAGE_AVAILABLE:
  51. multiprocessing.util.register_after_fork(
  52. multiprocessing_start, multiprocessing_start
  53. )
  54. except ImportError:
  55. pass
  56. if COVERAGE_AVAILABLE:
  57. # Cover any processes if the environ variables are present
  58. coverage.process_startup()
  59. class SaltCoverageTestingParser(SaltTestingParser):
  60. """
  61. Code coverage aware testing option parser
  62. """
  63. def __init__(self, *args, **kwargs):
  64. if (
  65. kwargs.pop("html_output_from_env", None) is not None
  66. or kwargs.pop("html_output_dir", None) is not None
  67. ):
  68. warnings.warn(
  69. "The unit tests HTML support was removed from {0}. Please "
  70. "stop passing 'html_output_dir' or 'html_output_from_env' "
  71. "as arguments to {0}".format(self.__class__.__name__),
  72. category=DeprecationWarning,
  73. stacklevel=2,
  74. )
  75. SaltTestingParser.__init__(self, *args, **kwargs)
  76. self.code_coverage = None
  77. # Add the coverage related options
  78. self.output_options_group.add_option(
  79. "--coverage",
  80. default=False,
  81. action="store_true",
  82. help="Run tests and report code coverage",
  83. )
  84. self.output_options_group.add_option(
  85. "--no-processes-coverage",
  86. default=False,
  87. action="store_true",
  88. help="Do not track subprocess and/or multiprocessing processes",
  89. )
  90. self.output_options_group.add_option(
  91. "--coverage-xml",
  92. default=None,
  93. help="If provided, the path to where a XML report of the code "
  94. "coverage will be written to",
  95. )
  96. self.output_options_group.add_option(
  97. "--coverage-html",
  98. default=None,
  99. help=(
  100. "The directory where the generated HTML coverage report "
  101. "will be saved to. The directory, if existing, will be "
  102. "deleted before the report is generated."
  103. ),
  104. )
  105. def _validate_options(self):
  106. if (
  107. self.options.coverage_xml or self.options.coverage_html
  108. ) and not self.options.coverage:
  109. self.options.coverage = True
  110. if self.options.coverage is True and COVERAGE_AVAILABLE is False:
  111. self.error(
  112. "Cannot run tests with coverage report. "
  113. "Please install coverage>=3.5.3"
  114. )
  115. if self.options.coverage is True:
  116. coverage_version = tuple(
  117. [
  118. int(part)
  119. for part in re.search(r"([0-9.]+)", coverage.__version__)
  120. .group(0)
  121. .split(".")
  122. ]
  123. )
  124. if coverage_version < (3, 5, 3):
  125. # Should we just print the error instead of exiting?
  126. self.error(
  127. "Versions lower than 3.5.3 of the coverage library are "
  128. "know to produce incorrect results. Please consider "
  129. "upgrading..."
  130. )
  131. SaltTestingParser._validate_options(self)
  132. def pre_execution_cleanup(self):
  133. if self.options.coverage_html is not None:
  134. if os.path.isdir(self.options.coverage_html):
  135. shutil.rmtree(self.options.coverage_html)
  136. if self.options.coverage_xml is not None:
  137. if os.path.isfile(self.options.coverage_xml):
  138. os.unlink(self.options.coverage_xml)
  139. SaltTestingParser.pre_execution_cleanup(self)
  140. def start_coverage(self, **coverage_options):
  141. """
  142. Start code coverage.
  143. You can pass any coverage options as keyword arguments. For the
  144. available options please see:
  145. http://nedbatchelder.com/code/coverage/api.html
  146. """
  147. if self.options.coverage is False:
  148. return
  149. if coverage_options.pop("track_processes", None) is not None:
  150. raise RuntimeWarning(
  151. "Please stop passing 'track_processes' to "
  152. "'start_coverage()'. It's now the default and "
  153. "'--no-processes-coverage' was added to the parser to "
  154. "disable it."
  155. )
  156. print(" * Starting Coverage")
  157. if self.options.no_processes_coverage is False:
  158. # Update environ so that any subprocess started on tests are also
  159. # included in the report
  160. coverage_options["data_suffix"] = True
  161. os.environ["COVERAGE_PROCESS_START"] = ""
  162. os.environ["COVERAGE_OPTIONS"] = salt.utils.json.dumps(coverage_options)
  163. # Setup coverage
  164. self.code_coverage = coverage.coverage(**coverage_options)
  165. self.code_coverage.start()
  166. def stop_coverage(self, save_coverage=True):
  167. """
  168. Stop code coverage.
  169. """
  170. if self.options.coverage is False:
  171. return
  172. # Clean up environment
  173. os.environ.pop("COVERAGE_OPTIONS", None)
  174. os.environ.pop("COVERAGE_PROCESS_START", None)
  175. print(" * Stopping coverage")
  176. self.code_coverage.stop()
  177. if save_coverage:
  178. print(" * Saving coverage info")
  179. self.code_coverage.save()
  180. if self.options.no_processes_coverage is False:
  181. # Combine any multiprocessing coverage data files
  182. sys.stdout.write(" * Combining multiple coverage info files ... ")
  183. sys.stdout.flush()
  184. self.code_coverage.combine()
  185. print("Done.")
  186. if self.options.coverage_xml is not None:
  187. sys.stdout.write(
  188. " * Generating Coverage XML Report At {0!r} ... ".format(
  189. self.options.coverage_xml
  190. )
  191. )
  192. sys.stdout.flush()
  193. self.code_coverage.xml_report(outfile=self.options.coverage_xml)
  194. print("Done.")
  195. if self.options.coverage_html is not None:
  196. sys.stdout.write(
  197. " * Generating Coverage HTML Report Under {0!r} ... ".format(
  198. self.options.coverage_html
  199. )
  200. )
  201. sys.stdout.flush()
  202. self.code_coverage.html_report(directory=self.options.coverage_html)
  203. print("Done.")
  204. def finalize(self, exit_code=0):
  205. if self.options.coverage is True:
  206. self.stop_coverage(save_coverage=True)
  207. SaltTestingParser.finalize(self, exit_code)