123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- # -*- coding: utf-8 -*-
- '''
- tests.support.parser.cover
- ~~~~~~~~~~~~~~~~~~~~~~~~~~
- Code coverage aware testing parser
- :codeauthor: Pedro Algarvio (pedro@algarvio.me)
- :copyright: Copyright 2013 by the SaltStack Team, see AUTHORS for more details.
- :license: Apache 2.0, see LICENSE for more details.
- '''
- # pylint: disable=repr-flag-used-in-string
- # Import python libs
- from __future__ import absolute_import, print_function
- import os
- import re
- import sys
- import shutil
- import warnings
- # Import Salt libs
- import salt.utils.json
- # Import salt testing libs
- from tests.support.parser import SaltTestingParser
- # Import coverage libs
- try:
- import coverage
- COVERAGE_AVAILABLE = True
- except ImportError:
- COVERAGE_AVAILABLE = False
- try:
- import multiprocessing.util
- # Force forked multiprocessing processes to be measured as well
- def multiprocessing_stop(coverage_object):
- '''
- Save the multiprocessing process coverage object
- '''
- coverage_object.stop()
- coverage_object.save()
- def multiprocessing_start(obj):
- coverage_options = salt.utils.json.loads(os.environ.get('COVERAGE_OPTIONS', '{}'))
- if not coverage_options:
- return
- if coverage_options.get('data_suffix', False) is False:
- return
- coverage_object = coverage.coverage(**coverage_options)
- coverage_object.start()
- multiprocessing.util.Finalize(
- None,
- multiprocessing_stop,
- args=(coverage_object,),
- exitpriority=1000
- )
- if COVERAGE_AVAILABLE:
- multiprocessing.util.register_after_fork(
- multiprocessing_start,
- multiprocessing_start
- )
- except ImportError:
- pass
- if COVERAGE_AVAILABLE:
- # Cover any processes if the environ variables are present
- coverage.process_startup()
- class SaltCoverageTestingParser(SaltTestingParser):
- '''
- Code coverage aware testing option parser
- '''
- def __init__(self, *args, **kwargs):
- if kwargs.pop('html_output_from_env', None) is not None or \
- kwargs.pop('html_output_dir', None) is not None:
- warnings.warn(
- 'The unit tests HTML support was removed from {0}. Please '
- 'stop passing \'html_output_dir\' or \'html_output_from_env\' '
- 'as arguments to {0}'.format(self.__class__.__name__),
- category=DeprecationWarning,
- stacklevel=2
- )
- SaltTestingParser.__init__(self, *args, **kwargs)
- self.code_coverage = None
- # Add the coverage related options
- self.output_options_group.add_option(
- '--coverage',
- default=False,
- action='store_true',
- help='Run tests and report code coverage'
- )
- self.output_options_group.add_option(
- '--no-processes-coverage',
- default=False,
- action='store_true',
- help='Do not track subprocess and/or multiprocessing processes'
- )
- self.output_options_group.add_option(
- '--coverage-xml',
- default=None,
- help='If provided, the path to where a XML report of the code '
- 'coverage will be written to'
- )
- self.output_options_group.add_option(
- '--coverage-html',
- default=None,
- help=('The directory where the generated HTML coverage report '
- 'will be saved to. The directory, if existing, will be '
- 'deleted before the report is generated.')
- )
- def _validate_options(self):
- if (self.options.coverage_xml or self.options.coverage_html) and \
- not self.options.coverage:
- self.options.coverage = True
- if self.options.coverage is True and COVERAGE_AVAILABLE is False:
- self.error(
- 'Cannot run tests with coverage report. '
- 'Please install coverage>=3.5.3'
- )
- if self.options.coverage is True:
- coverage_version = tuple([
- int(part) for part in re.search(
- r'([0-9.]+)', coverage.__version__).group(0).split('.')
- ])
- if coverage_version < (3, 5, 3):
- # Should we just print the error instead of exiting?
- self.error(
- 'Versions lower than 3.5.3 of the coverage library are '
- 'know to produce incorrect results. Please consider '
- 'upgrading...'
- )
- SaltTestingParser._validate_options(self)
- def pre_execution_cleanup(self):
- if self.options.coverage_html is not None:
- if os.path.isdir(self.options.coverage_html):
- shutil.rmtree(self.options.coverage_html)
- if self.options.coverage_xml is not None:
- if os.path.isfile(self.options.coverage_xml):
- os.unlink(self.options.coverage_xml)
- SaltTestingParser.pre_execution_cleanup(self)
- def start_coverage(self, **coverage_options):
- '''
- Start code coverage.
- You can pass any coverage options as keyword arguments. For the
- available options please see:
- http://nedbatchelder.com/code/coverage/api.html
- '''
- if self.options.coverage is False:
- return
- if coverage_options.pop('track_processes', None) is not None:
- raise RuntimeWarning(
- 'Please stop passing \'track_processes\' to '
- '\'start_coverage()\'. It\'s now the default and '
- '\'--no-processes-coverage\' was added to the parser to '
- 'disable it.'
- )
- print(' * Starting Coverage')
- if self.options.no_processes_coverage is False:
- # Update environ so that any subprocess started on tests are also
- # included in the report
- coverage_options['data_suffix'] = True
- os.environ['COVERAGE_PROCESS_START'] = ''
- os.environ['COVERAGE_OPTIONS'] = salt.utils.json.dumps(coverage_options)
- # Setup coverage
- self.code_coverage = coverage.coverage(**coverage_options)
- self.code_coverage.start()
- def stop_coverage(self, save_coverage=True):
- '''
- Stop code coverage.
- '''
- if self.options.coverage is False:
- return
- # Clean up environment
- os.environ.pop('COVERAGE_OPTIONS', None)
- os.environ.pop('COVERAGE_PROCESS_START', None)
- print(' * Stopping coverage')
- self.code_coverage.stop()
- if save_coverage:
- print(' * Saving coverage info')
- self.code_coverage.save()
- if self.options.no_processes_coverage is False:
- # Combine any multiprocessing coverage data files
- sys.stdout.write(' * Combining multiple coverage info files ... ')
- sys.stdout.flush()
- self.code_coverage.combine()
- print('Done.')
- if self.options.coverage_xml is not None:
- sys.stdout.write(
- ' * Generating Coverage XML Report At {0!r} ... '.format(
- self.options.coverage_xml
- )
- )
- sys.stdout.flush()
- self.code_coverage.xml_report(
- outfile=self.options.coverage_xml
- )
- print('Done.')
- if self.options.coverage_html is not None:
- sys.stdout.write(
- ' * Generating Coverage HTML Report Under {0!r} ... '.format(
- self.options.coverage_html
- )
- )
- sys.stdout.flush()
- self.code_coverage.html_report(
- directory=self.options.coverage_html
- )
- print('Done.')
- def finalize(self, exit_code=0):
- if self.options.coverage is True:
- self.stop_coverage(save_coverage=True)
- SaltTestingParser.finalize(self, exit_code)
|