123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- # -*- 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 shutil
- import sys
- 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)
|