cover.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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 sys
  16. import shutil
  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(os.environ.get('COVERAGE_OPTIONS', '{}'))
  39. if not coverage_options:
  40. return
  41. if coverage_options.get('data_suffix', False) is False:
  42. return
  43. coverage_object = coverage.coverage(**coverage_options)
  44. coverage_object.start()
  45. multiprocessing.util.Finalize(
  46. None,
  47. multiprocessing_stop,
  48. args=(coverage_object,),
  49. exitpriority=1000
  50. )
  51. if COVERAGE_AVAILABLE:
  52. multiprocessing.util.register_after_fork(
  53. multiprocessing_start,
  54. multiprocessing_start
  55. )
  56. except ImportError:
  57. pass
  58. if COVERAGE_AVAILABLE:
  59. # Cover any processes if the environ variables are present
  60. coverage.process_startup()
  61. class SaltCoverageTestingParser(SaltTestingParser):
  62. '''
  63. Code coverage aware testing option parser
  64. '''
  65. def __init__(self, *args, **kwargs):
  66. if kwargs.pop('html_output_from_env', None) is not None or \
  67. kwargs.pop('html_output_dir', None) is not None:
  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=('The directory where the generated HTML coverage report '
  100. 'will be saved to. The directory, if existing, will be '
  101. 'deleted before the report is generated.')
  102. )
  103. def _validate_options(self):
  104. if (self.options.coverage_xml or self.options.coverage_html) and \
  105. not self.options.coverage:
  106. self.options.coverage = True
  107. if self.options.coverage is True and COVERAGE_AVAILABLE is False:
  108. self.error(
  109. 'Cannot run tests with coverage report. '
  110. 'Please install coverage>=3.5.3'
  111. )
  112. if self.options.coverage is True:
  113. coverage_version = tuple([
  114. int(part) for part in re.search(
  115. r'([0-9.]+)', coverage.__version__).group(0).split('.')
  116. ])
  117. if coverage_version < (3, 5, 3):
  118. # Should we just print the error instead of exiting?
  119. self.error(
  120. 'Versions lower than 3.5.3 of the coverage library are '
  121. 'know to produce incorrect results. Please consider '
  122. 'upgrading...'
  123. )
  124. SaltTestingParser._validate_options(self)
  125. def pre_execution_cleanup(self):
  126. if self.options.coverage_html is not None:
  127. if os.path.isdir(self.options.coverage_html):
  128. shutil.rmtree(self.options.coverage_html)
  129. if self.options.coverage_xml is not None:
  130. if os.path.isfile(self.options.coverage_xml):
  131. os.unlink(self.options.coverage_xml)
  132. SaltTestingParser.pre_execution_cleanup(self)
  133. def start_coverage(self, **coverage_options):
  134. '''
  135. Start code coverage.
  136. You can pass any coverage options as keyword arguments. For the
  137. available options please see:
  138. http://nedbatchelder.com/code/coverage/api.html
  139. '''
  140. if self.options.coverage is False:
  141. return
  142. if coverage_options.pop('track_processes', None) is not None:
  143. raise RuntimeWarning(
  144. 'Please stop passing \'track_processes\' to '
  145. '\'start_coverage()\'. It\'s now the default and '
  146. '\'--no-processes-coverage\' was added to the parser to '
  147. 'disable it.'
  148. )
  149. print(' * Starting Coverage')
  150. if self.options.no_processes_coverage is False:
  151. # Update environ so that any subprocess started on tests are also
  152. # included in the report
  153. coverage_options['data_suffix'] = True
  154. os.environ['COVERAGE_PROCESS_START'] = ''
  155. os.environ['COVERAGE_OPTIONS'] = salt.utils.json.dumps(coverage_options)
  156. # Setup coverage
  157. self.code_coverage = coverage.coverage(**coverage_options)
  158. self.code_coverage.start()
  159. def stop_coverage(self, save_coverage=True):
  160. '''
  161. Stop code coverage.
  162. '''
  163. if self.options.coverage is False:
  164. return
  165. # Clean up environment
  166. os.environ.pop('COVERAGE_OPTIONS', None)
  167. os.environ.pop('COVERAGE_PROCESS_START', None)
  168. print(' * Stopping coverage')
  169. self.code_coverage.stop()
  170. if save_coverage:
  171. print(' * Saving coverage info')
  172. self.code_coverage.save()
  173. if self.options.no_processes_coverage is False:
  174. # Combine any multiprocessing coverage data files
  175. sys.stdout.write(' * Combining multiple coverage info files ... ')
  176. sys.stdout.flush()
  177. self.code_coverage.combine()
  178. print('Done.')
  179. if self.options.coverage_xml is not None:
  180. sys.stdout.write(
  181. ' * Generating Coverage XML Report At {0!r} ... '.format(
  182. self.options.coverage_xml
  183. )
  184. )
  185. sys.stdout.flush()
  186. self.code_coverage.xml_report(
  187. outfile=self.options.coverage_xml
  188. )
  189. print('Done.')
  190. if self.options.coverage_html is not None:
  191. sys.stdout.write(
  192. ' * Generating Coverage HTML Report Under {0!r} ... '.format(
  193. self.options.coverage_html
  194. )
  195. )
  196. sys.stdout.flush()
  197. self.code_coverage.html_report(
  198. directory=self.options.coverage_html
  199. )
  200. print('Done.')
  201. def finalize(self, exit_code=0):
  202. if self.options.coverage is True:
  203. self.stop_coverage(save_coverage=True)
  204. SaltTestingParser.finalize(self, exit_code)