| Index: tools/telemetry/third_party/coverage/coverage/control.py
|
| diff --git a/tools/telemetry/third_party/coverage/coverage/control.py b/tools/telemetry/third_party/coverage/coverage/control.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..16b09c84c2a93a0373f308219caaa847265f69a7
|
| --- /dev/null
|
| +++ b/tools/telemetry/third_party/coverage/coverage/control.py
|
| @@ -0,0 +1,1193 @@
|
| +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
| +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
|
| +
|
| +"""Core control stuff for coverage.py."""
|
| +
|
| +import atexit
|
| +import inspect
|
| +import os
|
| +import platform
|
| +import sys
|
| +import traceback
|
| +
|
| +from coverage import env, files
|
| +from coverage.annotate import AnnotateReporter
|
| +from coverage.backward import string_class, iitems
|
| +from coverage.collector import Collector
|
| +from coverage.config import CoverageConfig
|
| +from coverage.data import CoverageData, CoverageDataFiles
|
| +from coverage.debug import DebugControl
|
| +from coverage.files import TreeMatcher, FnmatchMatcher
|
| +from coverage.files import PathAliases, find_python_files, prep_patterns
|
| +from coverage.files import ModuleMatcher, abs_file
|
| +from coverage.html import HtmlReporter
|
| +from coverage.misc import CoverageException, bool_or_none, join_regex
|
| +from coverage.misc import file_be_gone
|
| +from coverage.monkey import patch_multiprocessing
|
| +from coverage.plugin import FileReporter
|
| +from coverage.plugin_support import Plugins
|
| +from coverage.python import PythonFileReporter
|
| +from coverage.results import Analysis, Numbers
|
| +from coverage.summary import SummaryReporter
|
| +from coverage.xmlreport import XmlReporter
|
| +
|
| +
|
| +# Pypy has some unusual stuff in the "stdlib". Consider those locations
|
| +# when deciding where the stdlib is.
|
| +try:
|
| + import _structseq
|
| +except ImportError:
|
| + _structseq = None
|
| +
|
| +
|
| +class Coverage(object):
|
| + """Programmatic access to coverage.py.
|
| +
|
| + To use::
|
| +
|
| + from coverage import Coverage
|
| +
|
| + cov = Coverage()
|
| + cov.start()
|
| + #.. call your code ..
|
| + cov.stop()
|
| + cov.html_report(directory='covhtml')
|
| +
|
| + """
|
| + def __init__(
|
| + self, data_file=None, data_suffix=None, cover_pylib=None,
|
| + auto_data=False, timid=None, branch=None, config_file=True,
|
| + source=None, omit=None, include=None, debug=None,
|
| + concurrency=None,
|
| + ):
|
| + """
|
| + `data_file` is the base name of the data file to use, defaulting to
|
| + ".coverage". `data_suffix` is appended (with a dot) to `data_file` to
|
| + create the final file name. If `data_suffix` is simply True, then a
|
| + suffix is created with the machine and process identity included.
|
| +
|
| + `cover_pylib` is a boolean determining whether Python code installed
|
| + with the Python interpreter is measured. This includes the Python
|
| + standard library and any packages installed with the interpreter.
|
| +
|
| + If `auto_data` is true, then any existing data file will be read when
|
| + coverage measurement starts, and data will be saved automatically when
|
| + measurement stops.
|
| +
|
| + If `timid` is true, then a slower and simpler trace function will be
|
| + used. This is important for some environments where manipulation of
|
| + tracing functions breaks the faster trace function.
|
| +
|
| + If `branch` is true, then branch coverage will be measured in addition
|
| + to the usual statement coverage.
|
| +
|
| + `config_file` determines what configuration file to read:
|
| +
|
| + * If it is ".coveragerc", it is interpreted as if it were True,
|
| + for backward compatibility.
|
| +
|
| + * If it is a string, it is the name of the file to read. If the
|
| + file can't be read, it is an error.
|
| +
|
| + * If it is True, then a few standard files names are tried
|
| + (".coveragerc", "setup.cfg"). It is not an error for these files
|
| + to not be found.
|
| +
|
| + * If it is False, then no configuration file is read.
|
| +
|
| + `source` is a list of file paths or package names. Only code located
|
| + in the trees indicated by the file paths or package names will be
|
| + measured.
|
| +
|
| + `include` and `omit` are lists of file name patterns. Files that match
|
| + `include` will be measured, files that match `omit` will not. Each
|
| + will also accept a single string argument.
|
| +
|
| + `debug` is a list of strings indicating what debugging information is
|
| + desired.
|
| +
|
| + `concurrency` is a string indicating the concurrency library being used
|
| + in the measured code. Without this, coverage.py will get incorrect
|
| + results. Valid strings are "greenlet", "eventlet", "gevent", or
|
| + "thread" (the default).
|
| +
|
| + .. versionadded:: 4.0
|
| + The `concurrency` parameter.
|
| +
|
| + """
|
| + # Build our configuration from a number of sources:
|
| + # 1: defaults:
|
| + self.config = CoverageConfig()
|
| +
|
| + # 2: from the rcfile, .coveragerc or setup.cfg file:
|
| + if config_file:
|
| + did_read_rc = False
|
| + # Some API users were specifying ".coveragerc" to mean the same as
|
| + # True, so make it so.
|
| + if config_file == ".coveragerc":
|
| + config_file = True
|
| + specified_file = (config_file is not True)
|
| + if not specified_file:
|
| + config_file = ".coveragerc"
|
| +
|
| + did_read_rc = self.config.from_file(config_file)
|
| +
|
| + if not did_read_rc:
|
| + if specified_file:
|
| + raise CoverageException(
|
| + "Couldn't read '%s' as a config file" % config_file
|
| + )
|
| + self.config.from_file("setup.cfg", section_prefix="coverage:")
|
| +
|
| + # 3: from environment variables:
|
| + env_data_file = os.environ.get('COVERAGE_FILE')
|
| + if env_data_file:
|
| + self.config.data_file = env_data_file
|
| + debugs = os.environ.get('COVERAGE_DEBUG')
|
| + if debugs:
|
| + self.config.debug.extend(debugs.split(","))
|
| +
|
| + # 4: from constructor arguments:
|
| + self.config.from_args(
|
| + data_file=data_file, cover_pylib=cover_pylib, timid=timid,
|
| + branch=branch, parallel=bool_or_none(data_suffix),
|
| + source=source, omit=omit, include=include, debug=debug,
|
| + concurrency=concurrency,
|
| + )
|
| +
|
| + self._debug_file = None
|
| + self._auto_data = auto_data
|
| + self._data_suffix = data_suffix
|
| +
|
| + # The matchers for _should_trace.
|
| + self.source_match = None
|
| + self.source_pkgs_match = None
|
| + self.pylib_match = self.cover_match = None
|
| + self.include_match = self.omit_match = None
|
| +
|
| + # Is it ok for no data to be collected?
|
| + self._warn_no_data = True
|
| + self._warn_unimported_source = True
|
| +
|
| + # A record of all the warnings that have been issued.
|
| + self._warnings = []
|
| +
|
| + # Other instance attributes, set later.
|
| + self.omit = self.include = self.source = None
|
| + self.source_pkgs = None
|
| + self.data = self.data_files = self.collector = None
|
| + self.plugins = None
|
| + self.pylib_dirs = self.cover_dirs = None
|
| + self.data_suffix = self.run_suffix = None
|
| + self._exclude_re = None
|
| + self.debug = None
|
| +
|
| + # State machine variables:
|
| + # Have we initialized everything?
|
| + self._inited = False
|
| + # Have we started collecting and not stopped it?
|
| + self._started = False
|
| + # Have we measured some data and not harvested it?
|
| + self._measured = False
|
| +
|
| + def _init(self):
|
| + """Set all the initial state.
|
| +
|
| + This is called by the public methods to initialize state. This lets us
|
| + construct a :class:`Coverage` object, then tweak its state before this
|
| + function is called.
|
| +
|
| + """
|
| + if self._inited:
|
| + return
|
| +
|
| + # Create and configure the debugging controller. COVERAGE_DEBUG_FILE
|
| + # is an environment variable, the name of a file to append debug logs
|
| + # to.
|
| + if self._debug_file is None:
|
| + debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE")
|
| + if debug_file_name:
|
| + self._debug_file = open(debug_file_name, "a")
|
| + else:
|
| + self._debug_file = sys.stderr
|
| + self.debug = DebugControl(self.config.debug, self._debug_file)
|
| +
|
| + # Load plugins
|
| + self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug)
|
| +
|
| + # _exclude_re is a dict that maps exclusion list names to compiled
|
| + # regexes.
|
| + self._exclude_re = {}
|
| + self._exclude_regex_stale()
|
| +
|
| + files.set_relative_directory()
|
| +
|
| + # The source argument can be directories or package names.
|
| + self.source = []
|
| + self.source_pkgs = []
|
| + for src in self.config.source or []:
|
| + if os.path.exists(src):
|
| + self.source.append(files.canonical_filename(src))
|
| + else:
|
| + self.source_pkgs.append(src)
|
| +
|
| + self.omit = prep_patterns(self.config.omit)
|
| + self.include = prep_patterns(self.config.include)
|
| +
|
| + concurrency = self.config.concurrency
|
| + if concurrency == "multiprocessing":
|
| + patch_multiprocessing()
|
| + concurrency = None
|
| +
|
| + self.collector = Collector(
|
| + should_trace=self._should_trace,
|
| + check_include=self._check_include_omit_etc,
|
| + timid=self.config.timid,
|
| + branch=self.config.branch,
|
| + warn=self._warn,
|
| + concurrency=concurrency,
|
| + )
|
| +
|
| + # Early warning if we aren't going to be able to support plugins.
|
| + if self.plugins.file_tracers and not self.collector.supports_plugins:
|
| + self._warn(
|
| + "Plugin file tracers (%s) aren't supported with %s" % (
|
| + ", ".join(
|
| + plugin._coverage_plugin_name
|
| + for plugin in self.plugins.file_tracers
|
| + ),
|
| + self.collector.tracer_name(),
|
| + )
|
| + )
|
| + for plugin in self.plugins.file_tracers:
|
| + plugin._coverage_enabled = False
|
| +
|
| + # Suffixes are a bit tricky. We want to use the data suffix only when
|
| + # collecting data, not when combining data. So we save it as
|
| + # `self.run_suffix` now, and promote it to `self.data_suffix` if we
|
| + # find that we are collecting data later.
|
| + if self._data_suffix or self.config.parallel:
|
| + if not isinstance(self._data_suffix, string_class):
|
| + # if data_suffix=True, use .machinename.pid.random
|
| + self._data_suffix = True
|
| + else:
|
| + self._data_suffix = None
|
| + self.data_suffix = None
|
| + self.run_suffix = self._data_suffix
|
| +
|
| + # Create the data file. We do this at construction time so that the
|
| + # data file will be written into the directory where the process
|
| + # started rather than wherever the process eventually chdir'd to.
|
| + self.data = CoverageData(debug=self.debug)
|
| + self.data_files = CoverageDataFiles(basename=self.config.data_file)
|
| +
|
| + # The directories for files considered "installed with the interpreter".
|
| + self.pylib_dirs = set()
|
| + if not self.config.cover_pylib:
|
| + # Look at where some standard modules are located. That's the
|
| + # indication for "installed with the interpreter". In some
|
| + # environments (virtualenv, for example), these modules may be
|
| + # spread across a few locations. Look at all the candidate modules
|
| + # we've imported, and take all the different ones.
|
| + for m in (atexit, inspect, os, platform, _structseq, traceback):
|
| + if m is not None and hasattr(m, "__file__"):
|
| + self.pylib_dirs.add(self._canonical_dir(m))
|
| + if _structseq and not hasattr(_structseq, '__file__'):
|
| + # PyPy 2.4 has no __file__ in the builtin modules, but the code
|
| + # objects still have the file names. So dig into one to find
|
| + # the path to exclude.
|
| + structseq_new = _structseq.structseq_new
|
| + try:
|
| + structseq_file = structseq_new.func_code.co_filename
|
| + except AttributeError:
|
| + structseq_file = structseq_new.__code__.co_filename
|
| + self.pylib_dirs.add(self._canonical_dir(structseq_file))
|
| +
|
| + # To avoid tracing the coverage.py code itself, we skip anything
|
| + # located where we are.
|
| + self.cover_dirs = [self._canonical_dir(__file__)]
|
| + if env.TESTING:
|
| + # When testing, we use PyContracts, which should be considered
|
| + # part of coverage.py, and it uses six. Exclude those directories
|
| + # just as we exclude ourselves.
|
| + import contracts, six
|
| + for mod in [contracts, six]:
|
| + self.cover_dirs.append(self._canonical_dir(mod))
|
| +
|
| + # Set the reporting precision.
|
| + Numbers.set_precision(self.config.precision)
|
| +
|
| + atexit.register(self._atexit)
|
| +
|
| + self._inited = True
|
| +
|
| + # Create the matchers we need for _should_trace
|
| + if self.source or self.source_pkgs:
|
| + self.source_match = TreeMatcher(self.source)
|
| + self.source_pkgs_match = ModuleMatcher(self.source_pkgs)
|
| + else:
|
| + if self.cover_dirs:
|
| + self.cover_match = TreeMatcher(self.cover_dirs)
|
| + if self.pylib_dirs:
|
| + self.pylib_match = TreeMatcher(self.pylib_dirs)
|
| + if self.include:
|
| + self.include_match = FnmatchMatcher(self.include)
|
| + if self.omit:
|
| + self.omit_match = FnmatchMatcher(self.omit)
|
| +
|
| + # The user may want to debug things, show info if desired.
|
| + wrote_any = False
|
| + if self.debug.should('config'):
|
| + config_info = sorted(self.config.__dict__.items())
|
| + self.debug.write_formatted_info("config", config_info)
|
| + wrote_any = True
|
| +
|
| + if self.debug.should('sys'):
|
| + self.debug.write_formatted_info("sys", self.sys_info())
|
| + for plugin in self.plugins:
|
| + header = "sys: " + plugin._coverage_plugin_name
|
| + info = plugin.sys_info()
|
| + self.debug.write_formatted_info(header, info)
|
| + wrote_any = True
|
| +
|
| + if wrote_any:
|
| + self.debug.write_formatted_info("end", ())
|
| +
|
| + def _canonical_dir(self, morf):
|
| + """Return the canonical directory of the module or file `morf`."""
|
| + morf_filename = PythonFileReporter(morf, self).filename
|
| + return os.path.split(morf_filename)[0]
|
| +
|
| + def _source_for_file(self, filename):
|
| + """Return the source file for `filename`.
|
| +
|
| + Given a file name being traced, return the best guess as to the source
|
| + file to attribute it to.
|
| +
|
| + """
|
| + if filename.endswith(".py"):
|
| + # .py files are themselves source files.
|
| + return filename
|
| +
|
| + elif filename.endswith((".pyc", ".pyo")):
|
| + # Bytecode files probably have source files near them.
|
| + py_filename = filename[:-1]
|
| + if os.path.exists(py_filename):
|
| + # Found a .py file, use that.
|
| + return py_filename
|
| + if env.WINDOWS:
|
| + # On Windows, it could be a .pyw file.
|
| + pyw_filename = py_filename + "w"
|
| + if os.path.exists(pyw_filename):
|
| + return pyw_filename
|
| + # Didn't find source, but it's probably the .py file we want.
|
| + return py_filename
|
| +
|
| + elif filename.endswith("$py.class"):
|
| + # Jython is easy to guess.
|
| + return filename[:-9] + ".py"
|
| +
|
| + # No idea, just use the file name as-is.
|
| + return filename
|
| +
|
| + def _name_for_module(self, module_globals, filename):
|
| + """Get the name of the module for a set of globals and file name.
|
| +
|
| + For configurability's sake, we allow __main__ modules to be matched by
|
| + their importable name.
|
| +
|
| + If loaded via runpy (aka -m), we can usually recover the "original"
|
| + full dotted module name, otherwise, we resort to interpreting the
|
| + file name to get the module's name. In the case that the module name
|
| + can't be determined, None is returned.
|
| +
|
| + """
|
| + dunder_name = module_globals.get('__name__', None)
|
| +
|
| + if isinstance(dunder_name, str) and dunder_name != '__main__':
|
| + # This is the usual case: an imported module.
|
| + return dunder_name
|
| +
|
| + loader = module_globals.get('__loader__', None)
|
| + for attrname in ('fullname', 'name'): # attribute renamed in py3.2
|
| + if hasattr(loader, attrname):
|
| + fullname = getattr(loader, attrname)
|
| + else:
|
| + continue
|
| +
|
| + if isinstance(fullname, str) and fullname != '__main__':
|
| + # Module loaded via: runpy -m
|
| + return fullname
|
| +
|
| + # Script as first argument to Python command line.
|
| + inspectedname = inspect.getmodulename(filename)
|
| + if inspectedname is not None:
|
| + return inspectedname
|
| + else:
|
| + return dunder_name
|
| +
|
| + def _should_trace_internal(self, filename, frame):
|
| + """Decide whether to trace execution in `filename`, with a reason.
|
| +
|
| + This function is called from the trace function. As each new file name
|
| + is encountered, this function determines whether it is traced or not.
|
| +
|
| + Returns a FileDisposition object.
|
| +
|
| + """
|
| + original_filename = filename
|
| + disp = _disposition_init(self.collector.file_disposition_class, filename)
|
| +
|
| + def nope(disp, reason):
|
| + """Simple helper to make it easy to return NO."""
|
| + disp.trace = False
|
| + disp.reason = reason
|
| + return disp
|
| +
|
| + # Compiled Python files have two file names: frame.f_code.co_filename is
|
| + # the file name at the time the .pyc was compiled. The second name is
|
| + # __file__, which is where the .pyc was actually loaded from. Since
|
| + # .pyc files can be moved after compilation (for example, by being
|
| + # installed), we look for __file__ in the frame and prefer it to the
|
| + # co_filename value.
|
| + dunder_file = frame.f_globals.get('__file__')
|
| + if dunder_file:
|
| + filename = self._source_for_file(dunder_file)
|
| + if original_filename and not original_filename.startswith('<'):
|
| + orig = os.path.basename(original_filename)
|
| + if orig != os.path.basename(filename):
|
| + # Files shouldn't be renamed when moved. This happens when
|
| + # exec'ing code. If it seems like something is wrong with
|
| + # the frame's file name, then just use the original.
|
| + filename = original_filename
|
| +
|
| + if not filename:
|
| + # Empty string is pretty useless.
|
| + return nope(disp, "empty string isn't a file name")
|
| +
|
| + if filename.startswith('memory:'):
|
| + return nope(disp, "memory isn't traceable")
|
| +
|
| + if filename.startswith('<'):
|
| + # Lots of non-file execution is represented with artificial
|
| + # file names like "<string>", "<doctest readme.txt[0]>", or
|
| + # "<exec_function>". Don't ever trace these executions, since we
|
| + # can't do anything with the data later anyway.
|
| + return nope(disp, "not a real file name")
|
| +
|
| + # Jython reports the .class file to the tracer, use the source file.
|
| + if filename.endswith("$py.class"):
|
| + filename = filename[:-9] + ".py"
|
| +
|
| + canonical = files.canonical_filename(filename)
|
| + disp.canonical_filename = canonical
|
| +
|
| + # Try the plugins, see if they have an opinion about the file.
|
| + plugin = None
|
| + for plugin in self.plugins.file_tracers:
|
| + if not plugin._coverage_enabled:
|
| + continue
|
| +
|
| + try:
|
| + file_tracer = plugin.file_tracer(canonical)
|
| + if file_tracer is not None:
|
| + file_tracer._coverage_plugin = plugin
|
| + disp.trace = True
|
| + disp.file_tracer = file_tracer
|
| + if file_tracer.has_dynamic_source_filename():
|
| + disp.has_dynamic_filename = True
|
| + else:
|
| + disp.source_filename = files.canonical_filename(
|
| + file_tracer.source_filename()
|
| + )
|
| + break
|
| + except Exception:
|
| + self._warn(
|
| + "Disabling plugin %r due to an exception:" % (
|
| + plugin._coverage_plugin_name
|
| + )
|
| + )
|
| + traceback.print_exc()
|
| + plugin._coverage_enabled = False
|
| + continue
|
| + else:
|
| + # No plugin wanted it: it's Python.
|
| + disp.trace = True
|
| + disp.source_filename = canonical
|
| +
|
| + if not disp.has_dynamic_filename:
|
| + if not disp.source_filename:
|
| + raise CoverageException(
|
| + "Plugin %r didn't set source_filename for %r" %
|
| + (plugin, disp.original_filename)
|
| + )
|
| + reason = self._check_include_omit_etc_internal(
|
| + disp.source_filename, frame,
|
| + )
|
| + if reason:
|
| + nope(disp, reason)
|
| +
|
| + return disp
|
| +
|
| + def _check_include_omit_etc_internal(self, filename, frame):
|
| + """Check a file name against the include, omit, etc, rules.
|
| +
|
| + Returns a string or None. String means, don't trace, and is the reason
|
| + why. None means no reason found to not trace.
|
| +
|
| + """
|
| + modulename = self._name_for_module(frame.f_globals, filename)
|
| +
|
| + # If the user specified source or include, then that's authoritative
|
| + # about the outer bound of what to measure and we don't have to apply
|
| + # any canned exclusions. If they didn't, then we have to exclude the
|
| + # stdlib and coverage.py directories.
|
| + if self.source_match:
|
| + if self.source_pkgs_match.match(modulename):
|
| + if modulename in self.source_pkgs:
|
| + self.source_pkgs.remove(modulename)
|
| + return None # There's no reason to skip this file.
|
| +
|
| + if not self.source_match.match(filename):
|
| + return "falls outside the --source trees"
|
| + elif self.include_match:
|
| + if not self.include_match.match(filename):
|
| + return "falls outside the --include trees"
|
| + else:
|
| + # If we aren't supposed to trace installed code, then check if this
|
| + # is near the Python standard library and skip it if so.
|
| + if self.pylib_match and self.pylib_match.match(filename):
|
| + return "is in the stdlib"
|
| +
|
| + # We exclude the coverage.py code itself, since a little of it
|
| + # will be measured otherwise.
|
| + if self.cover_match and self.cover_match.match(filename):
|
| + return "is part of coverage.py"
|
| +
|
| + # Check the file against the omit pattern.
|
| + if self.omit_match and self.omit_match.match(filename):
|
| + return "is inside an --omit pattern"
|
| +
|
| + # No reason found to skip this file.
|
| + return None
|
| +
|
| + def _should_trace(self, filename, frame):
|
| + """Decide whether to trace execution in `filename`.
|
| +
|
| + Calls `_should_trace_internal`, and returns the FileDisposition.
|
| +
|
| + """
|
| + disp = self._should_trace_internal(filename, frame)
|
| + if self.debug.should('trace'):
|
| + self.debug.write(_disposition_debug_msg(disp))
|
| + return disp
|
| +
|
| + def _check_include_omit_etc(self, filename, frame):
|
| + """Check a file name against the include/omit/etc, rules, verbosely.
|
| +
|
| + Returns a boolean: True if the file should be traced, False if not.
|
| +
|
| + """
|
| + reason = self._check_include_omit_etc_internal(filename, frame)
|
| + if self.debug.should('trace'):
|
| + if not reason:
|
| + msg = "Including %r" % (filename,)
|
| + else:
|
| + msg = "Not including %r: %s" % (filename, reason)
|
| + self.debug.write(msg)
|
| +
|
| + return not reason
|
| +
|
| + def _warn(self, msg):
|
| + """Use `msg` as a warning."""
|
| + self._warnings.append(msg)
|
| + if self.debug.should('pid'):
|
| + msg = "[%d] %s" % (os.getpid(), msg)
|
| + sys.stderr.write("Coverage.py warning: %s\n" % msg)
|
| +
|
| + def get_option(self, option_name):
|
| + """Get an option from the configuration.
|
| +
|
| + `option_name` is a colon-separated string indicating the section and
|
| + option name. For example, the ``branch`` option in the ``[run]``
|
| + section of the config file would be indicated with `"run:branch"`.
|
| +
|
| + Returns the value of the option.
|
| +
|
| + .. versionadded:: 4.0
|
| +
|
| + """
|
| + return self.config.get_option(option_name)
|
| +
|
| + def set_option(self, option_name, value):
|
| + """Set an option in the configuration.
|
| +
|
| + `option_name` is a colon-separated string indicating the section and
|
| + option name. For example, the ``branch`` option in the ``[run]``
|
| + section of the config file would be indicated with ``"run:branch"``.
|
| +
|
| + `value` is the new value for the option. This should be a Python
|
| + value where appropriate. For example, use True for booleans, not the
|
| + string ``"True"``.
|
| +
|
| + As an example, calling::
|
| +
|
| + cov.set_option("run:branch", True)
|
| +
|
| + has the same effect as this configuration file::
|
| +
|
| + [run]
|
| + branch = True
|
| +
|
| + .. versionadded:: 4.0
|
| +
|
| + """
|
| + self.config.set_option(option_name, value)
|
| +
|
| + def use_cache(self, usecache):
|
| + """Obsolete method."""
|
| + self._init()
|
| + if not usecache:
|
| + self._warn("use_cache(False) is no longer supported.")
|
| +
|
| + def load(self):
|
| + """Load previously-collected coverage data from the data file."""
|
| + self._init()
|
| + self.collector.reset()
|
| + self.data_files.read(self.data)
|
| +
|
| + def start(self):
|
| + """Start measuring code coverage.
|
| +
|
| + Coverage measurement actually occurs in functions called after
|
| + :meth:`start` is invoked. Statements in the same scope as
|
| + :meth:`start` won't be measured.
|
| +
|
| + Once you invoke :meth:`start`, you must also call :meth:`stop`
|
| + eventually, or your process might not shut down cleanly.
|
| +
|
| + """
|
| + self._init()
|
| + if self.run_suffix:
|
| + # Calling start() means we're running code, so use the run_suffix
|
| + # as the data_suffix when we eventually save the data.
|
| + self.data_suffix = self.run_suffix
|
| + if self._auto_data:
|
| + self.load()
|
| +
|
| + self.collector.start()
|
| + self._started = True
|
| + self._measured = True
|
| +
|
| + def stop(self):
|
| + """Stop measuring code coverage."""
|
| + if self._started:
|
| + self.collector.stop()
|
| + self._started = False
|
| +
|
| + def _atexit(self):
|
| + """Clean up on process shutdown."""
|
| + if self._started:
|
| + self.stop()
|
| + if self._auto_data:
|
| + self.save()
|
| +
|
| + def erase(self):
|
| + """Erase previously-collected coverage data.
|
| +
|
| + This removes the in-memory data collected in this session as well as
|
| + discarding the data file.
|
| +
|
| + """
|
| + self._init()
|
| + self.collector.reset()
|
| + self.data.erase()
|
| + self.data_files.erase(parallel=self.config.parallel)
|
| +
|
| + def clear_exclude(self, which='exclude'):
|
| + """Clear the exclude list."""
|
| + self._init()
|
| + setattr(self.config, which + "_list", [])
|
| + self._exclude_regex_stale()
|
| +
|
| + def exclude(self, regex, which='exclude'):
|
| + """Exclude source lines from execution consideration.
|
| +
|
| + A number of lists of regular expressions are maintained. Each list
|
| + selects lines that are treated differently during reporting.
|
| +
|
| + `which` determines which list is modified. The "exclude" list selects
|
| + lines that are not considered executable at all. The "partial" list
|
| + indicates lines with branches that are not taken.
|
| +
|
| + `regex` is a regular expression. The regex is added to the specified
|
| + list. If any of the regexes in the list is found in a line, the line
|
| + is marked for special treatment during reporting.
|
| +
|
| + """
|
| + self._init()
|
| + excl_list = getattr(self.config, which + "_list")
|
| + excl_list.append(regex)
|
| + self._exclude_regex_stale()
|
| +
|
| + def _exclude_regex_stale(self):
|
| + """Drop all the compiled exclusion regexes, a list was modified."""
|
| + self._exclude_re.clear()
|
| +
|
| + def _exclude_regex(self, which):
|
| + """Return a compiled regex for the given exclusion list."""
|
| + if which not in self._exclude_re:
|
| + excl_list = getattr(self.config, which + "_list")
|
| + self._exclude_re[which] = join_regex(excl_list)
|
| + return self._exclude_re[which]
|
| +
|
| + def get_exclude_list(self, which='exclude'):
|
| + """Return a list of excluded regex patterns.
|
| +
|
| + `which` indicates which list is desired. See :meth:`exclude` for the
|
| + lists that are available, and their meaning.
|
| +
|
| + """
|
| + self._init()
|
| + return getattr(self.config, which + "_list")
|
| +
|
| + def save(self):
|
| + """Save the collected coverage data to the data file."""
|
| + self._init()
|
| + self.get_data()
|
| + self.data_files.write(self.data, suffix=self.data_suffix)
|
| +
|
| + def combine(self, data_paths=None):
|
| + """Combine together a number of similarly-named coverage data files.
|
| +
|
| + All coverage data files whose name starts with `data_file` (from the
|
| + coverage() constructor) will be read, and combined together into the
|
| + current measurements.
|
| +
|
| + `data_paths` is a list of files or directories from which data should
|
| + be combined. If no list is passed, then the data files from the
|
| + directory indicated by the current data file (probably the current
|
| + directory) will be combined.
|
| +
|
| + .. versionadded:: 4.0
|
| + The `data_paths` parameter.
|
| +
|
| + """
|
| + self._init()
|
| + self.get_data()
|
| +
|
| + aliases = None
|
| + if self.config.paths:
|
| + aliases = PathAliases()
|
| + for paths in self.config.paths.values():
|
| + result = paths[0]
|
| + for pattern in paths[1:]:
|
| + aliases.add(pattern, result)
|
| +
|
| + self.data_files.combine_parallel_data(self.data, aliases=aliases, data_paths=data_paths)
|
| +
|
| + def get_data(self):
|
| + """Get the collected data and reset the collector.
|
| +
|
| + Also warn about various problems collecting data.
|
| +
|
| + Returns a :class:`coverage.CoverageData`, the collected coverage data.
|
| +
|
| + .. versionadded:: 4.0
|
| +
|
| + """
|
| + self._init()
|
| + if not self._measured:
|
| + return
|
| +
|
| + self.collector.save_data(self.data)
|
| +
|
| + # If there are still entries in the source_pkgs list, then we never
|
| + # encountered those packages.
|
| + if self._warn_unimported_source:
|
| + for pkg in self.source_pkgs:
|
| + if pkg not in sys.modules:
|
| + self._warn("Module %s was never imported." % pkg)
|
| + elif not (
|
| + hasattr(sys.modules[pkg], '__file__') and
|
| + os.path.exists(sys.modules[pkg].__file__)
|
| + ):
|
| + self._warn("Module %s has no Python source." % pkg)
|
| + else:
|
| + self._warn("Module %s was previously imported, but not measured." % pkg)
|
| +
|
| + # Find out if we got any data.
|
| + if not self.data and self._warn_no_data:
|
| + self._warn("No data was collected.")
|
| +
|
| + # Find files that were never executed at all.
|
| + for src in self.source:
|
| + for py_file in find_python_files(src):
|
| + py_file = files.canonical_filename(py_file)
|
| +
|
| + if self.omit_match and self.omit_match.match(py_file):
|
| + # Turns out this file was omitted, so don't pull it back
|
| + # in as unexecuted.
|
| + continue
|
| +
|
| + self.data.touch_file(py_file)
|
| +
|
| + # Add run information.
|
| + self.data.add_run_info(
|
| + brief_sys=" ".join([
|
| + platform.python_implementation(),
|
| + platform.python_version(),
|
| + platform.system(),
|
| + ])
|
| + )
|
| +
|
| + if self.config.note:
|
| + self.data.add_run_info(note=self.config.note)
|
| +
|
| + self._measured = False
|
| + return self.data
|
| +
|
| + # Backward compatibility with version 1.
|
| + def analysis(self, morf):
|
| + """Like `analysis2` but doesn't return excluded line numbers."""
|
| + f, s, _, m, mf = self.analysis2(morf)
|
| + return f, s, m, mf
|
| +
|
| + def analysis2(self, morf):
|
| + """Analyze a module.
|
| +
|
| + `morf` is a module or a file name. It will be analyzed to determine
|
| + its coverage statistics. The return value is a 5-tuple:
|
| +
|
| + * The file name for the module.
|
| + * A list of line numbers of executable statements.
|
| + * A list of line numbers of excluded statements.
|
| + * A list of line numbers of statements not run (missing from
|
| + execution).
|
| + * A readable formatted string of the missing line numbers.
|
| +
|
| + The analysis uses the source file itself and the current measured
|
| + coverage data.
|
| +
|
| + """
|
| + self._init()
|
| + analysis = self._analyze(morf)
|
| + return (
|
| + analysis.filename,
|
| + sorted(analysis.statements),
|
| + sorted(analysis.excluded),
|
| + sorted(analysis.missing),
|
| + analysis.missing_formatted(),
|
| + )
|
| +
|
| + def _analyze(self, it):
|
| + """Analyze a single morf or code unit.
|
| +
|
| + Returns an `Analysis` object.
|
| +
|
| + """
|
| + self.get_data()
|
| + if not isinstance(it, FileReporter):
|
| + it = self._get_file_reporter(it)
|
| +
|
| + return Analysis(self.data, it)
|
| +
|
| + def _get_file_reporter(self, morf):
|
| + """Get a FileReporter for a module or file name."""
|
| + plugin = None
|
| + file_reporter = "python"
|
| +
|
| + if isinstance(morf, string_class):
|
| + abs_morf = abs_file(morf)
|
| + plugin_name = self.data.file_tracer(abs_morf)
|
| + if plugin_name:
|
| + plugin = self.plugins.get(plugin_name)
|
| +
|
| + if plugin:
|
| + file_reporter = plugin.file_reporter(abs_morf)
|
| + if file_reporter is None:
|
| + raise CoverageException(
|
| + "Plugin %r did not provide a file reporter for %r." % (
|
| + plugin._coverage_plugin_name, morf
|
| + )
|
| + )
|
| +
|
| + if file_reporter == "python":
|
| + file_reporter = PythonFileReporter(morf, self)
|
| +
|
| + return file_reporter
|
| +
|
| + def _get_file_reporters(self, morfs=None):
|
| + """Get a list of FileReporters for a list of modules or file names.
|
| +
|
| + For each module or file name in `morfs`, find a FileReporter. Return
|
| + the list of FileReporters.
|
| +
|
| + If `morfs` is a single module or file name, this returns a list of one
|
| + FileReporter. If `morfs` is empty or None, then the list of all files
|
| + measured is used to find the FileReporters.
|
| +
|
| + """
|
| + if not morfs:
|
| + morfs = self.data.measured_files()
|
| +
|
| + # Be sure we have a list.
|
| + if not isinstance(morfs, (list, tuple)):
|
| + morfs = [morfs]
|
| +
|
| + file_reporters = []
|
| + for morf in morfs:
|
| + file_reporter = self._get_file_reporter(morf)
|
| + file_reporters.append(file_reporter)
|
| +
|
| + return file_reporters
|
| +
|
| + def report(
|
| + self, morfs=None, show_missing=True, ignore_errors=None,
|
| + file=None, # pylint: disable=redefined-builtin
|
| + omit=None, include=None, skip_covered=False,
|
| + ):
|
| + """Write a summary report to `file`.
|
| +
|
| + Each module in `morfs` is listed, with counts of statements, executed
|
| + statements, missing statements, and a list of lines missed.
|
| +
|
| + `include` is a list of file name patterns. Files that match will be
|
| + included in the report. Files matching `omit` will not be included in
|
| + the report.
|
| +
|
| + Returns a float, the total percentage covered.
|
| +
|
| + """
|
| + self.get_data()
|
| + self.config.from_args(
|
| + ignore_errors=ignore_errors, omit=omit, include=include,
|
| + show_missing=show_missing, skip_covered=skip_covered,
|
| + )
|
| + reporter = SummaryReporter(self, self.config)
|
| + return reporter.report(morfs, outfile=file)
|
| +
|
| + def annotate(
|
| + self, morfs=None, directory=None, ignore_errors=None,
|
| + omit=None, include=None,
|
| + ):
|
| + """Annotate a list of modules.
|
| +
|
| + Each module in `morfs` is annotated. The source is written to a new
|
| + file, named with a ",cover" suffix, with each line prefixed with a
|
| + marker to indicate the coverage of the line. Covered lines have ">",
|
| + excluded lines have "-", and missing lines have "!".
|
| +
|
| + See :meth:`report` for other arguments.
|
| +
|
| + """
|
| + self.get_data()
|
| + self.config.from_args(
|
| + ignore_errors=ignore_errors, omit=omit, include=include
|
| + )
|
| + reporter = AnnotateReporter(self, self.config)
|
| + reporter.report(morfs, directory=directory)
|
| +
|
| + def html_report(self, morfs=None, directory=None, ignore_errors=None,
|
| + omit=None, include=None, extra_css=None, title=None):
|
| + """Generate an HTML report.
|
| +
|
| + The HTML is written to `directory`. The file "index.html" is the
|
| + overview starting point, with links to more detailed pages for
|
| + individual modules.
|
| +
|
| + `extra_css` is a path to a file of other CSS to apply on the page.
|
| + It will be copied into the HTML directory.
|
| +
|
| + `title` is a text string (not HTML) to use as the title of the HTML
|
| + report.
|
| +
|
| + See :meth:`report` for other arguments.
|
| +
|
| + Returns a float, the total percentage covered.
|
| +
|
| + """
|
| + self.get_data()
|
| + self.config.from_args(
|
| + ignore_errors=ignore_errors, omit=omit, include=include,
|
| + html_dir=directory, extra_css=extra_css, html_title=title,
|
| + )
|
| + reporter = HtmlReporter(self, self.config)
|
| + return reporter.report(morfs)
|
| +
|
| + def xml_report(
|
| + self, morfs=None, outfile=None, ignore_errors=None,
|
| + omit=None, include=None,
|
| + ):
|
| + """Generate an XML report of coverage results.
|
| +
|
| + The report is compatible with Cobertura reports.
|
| +
|
| + Each module in `morfs` is included in the report. `outfile` is the
|
| + path to write the file to, "-" will write to stdout.
|
| +
|
| + See :meth:`report` for other arguments.
|
| +
|
| + Returns a float, the total percentage covered.
|
| +
|
| + """
|
| + self.get_data()
|
| + self.config.from_args(
|
| + ignore_errors=ignore_errors, omit=omit, include=include,
|
| + xml_output=outfile,
|
| + )
|
| + file_to_close = None
|
| + delete_file = False
|
| + if self.config.xml_output:
|
| + if self.config.xml_output == '-':
|
| + outfile = sys.stdout
|
| + else:
|
| + # Ensure that the output directory is created; done here
|
| + # because this report pre-opens the output file.
|
| + # HTMLReport does this using the Report plumbing because
|
| + # its task is more complex, being multiple files.
|
| + output_dir = os.path.dirname(self.config.xml_output)
|
| + if output_dir and not os.path.isdir(output_dir):
|
| + os.makedirs(output_dir)
|
| + outfile = open(self.config.xml_output, "w")
|
| + file_to_close = outfile
|
| + try:
|
| + reporter = XmlReporter(self, self.config)
|
| + return reporter.report(morfs, outfile=outfile)
|
| + except CoverageException:
|
| + delete_file = True
|
| + raise
|
| + finally:
|
| + if file_to_close:
|
| + file_to_close.close()
|
| + if delete_file:
|
| + file_be_gone(self.config.xml_output)
|
| +
|
| + def sys_info(self):
|
| + """Return a list of (key, value) pairs showing internal information."""
|
| +
|
| + import coverage as covmod
|
| +
|
| + self._init()
|
| +
|
| + ft_plugins = []
|
| + for ft in self.plugins.file_tracers:
|
| + ft_name = ft._coverage_plugin_name
|
| + if not ft._coverage_enabled:
|
| + ft_name += " (disabled)"
|
| + ft_plugins.append(ft_name)
|
| +
|
| + info = [
|
| + ('version', covmod.__version__),
|
| + ('coverage', covmod.__file__),
|
| + ('cover_dirs', self.cover_dirs),
|
| + ('pylib_dirs', self.pylib_dirs),
|
| + ('tracer', self.collector.tracer_name()),
|
| + ('plugins.file_tracers', ft_plugins),
|
| + ('config_files', self.config.attempted_config_files),
|
| + ('configs_read', self.config.config_files),
|
| + ('data_path', self.data_files.filename),
|
| + ('python', sys.version.replace('\n', '')),
|
| + ('platform', platform.platform()),
|
| + ('implementation', platform.python_implementation()),
|
| + ('executable', sys.executable),
|
| + ('cwd', os.getcwd()),
|
| + ('path', sys.path),
|
| + ('environment', sorted(
|
| + ("%s = %s" % (k, v))
|
| + for k, v in iitems(os.environ)
|
| + if k.startswith(("COV", "PY"))
|
| + )),
|
| + ('command_line', " ".join(getattr(sys, 'argv', ['???']))),
|
| + ]
|
| +
|
| + matcher_names = [
|
| + 'source_match', 'source_pkgs_match',
|
| + 'include_match', 'omit_match',
|
| + 'cover_match', 'pylib_match',
|
| + ]
|
| +
|
| + for matcher_name in matcher_names:
|
| + matcher = getattr(self, matcher_name)
|
| + if matcher:
|
| + matcher_info = matcher.info()
|
| + else:
|
| + matcher_info = '-none-'
|
| + info.append((matcher_name, matcher_info))
|
| +
|
| + return info
|
| +
|
| +
|
| +# FileDisposition "methods": FileDisposition is a pure value object, so it can
|
| +# be implemented in either C or Python. Acting on them is done with these
|
| +# functions.
|
| +
|
| +def _disposition_init(cls, original_filename):
|
| + """Construct and initialize a new FileDisposition object."""
|
| + disp = cls()
|
| + disp.original_filename = original_filename
|
| + disp.canonical_filename = original_filename
|
| + disp.source_filename = None
|
| + disp.trace = False
|
| + disp.reason = ""
|
| + disp.file_tracer = None
|
| + disp.has_dynamic_filename = False
|
| + return disp
|
| +
|
| +
|
| +def _disposition_debug_msg(disp):
|
| + """Make a nice debug message of what the FileDisposition is doing."""
|
| + if disp.trace:
|
| + msg = "Tracing %r" % (disp.original_filename,)
|
| + if disp.file_tracer:
|
| + msg += ": will be traced by %r" % disp.file_tracer
|
| + else:
|
| + msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason)
|
| + return msg
|
| +
|
| +
|
| +def process_startup():
|
| + """Call this at Python start-up to perhaps measure coverage.
|
| +
|
| + If the environment variable COVERAGE_PROCESS_START is defined, coverage
|
| + measurement is started. The value of the variable is the config file
|
| + to use.
|
| +
|
| + There are two ways to configure your Python installation to invoke this
|
| + function when Python starts:
|
| +
|
| + #. Create or append to sitecustomize.py to add these lines::
|
| +
|
| + import coverage
|
| + coverage.process_startup()
|
| +
|
| + #. Create a .pth file in your Python installation containing::
|
| +
|
| + import coverage; coverage.process_startup()
|
| +
|
| + """
|
| + cps = os.environ.get("COVERAGE_PROCESS_START")
|
| + if not cps:
|
| + # No request for coverage, nothing to do.
|
| + return
|
| +
|
| + # This function can be called more than once in a process. This happens
|
| + # because some virtualenv configurations make the same directory visible
|
| + # twice in sys.path. This means that the .pth file will be found twice,
|
| + # and executed twice, executing this function twice. We set a global
|
| + # flag (an attribute on this function) to indicate that coverage.py has
|
| + # already been started, so we can avoid doing it twice.
|
| + #
|
| + # https://bitbucket.org/ned/coveragepy/issue/340/keyerror-subpy has more
|
| + # details.
|
| +
|
| + if hasattr(process_startup, "done"):
|
| + # We've annotated this function before, so we must have already
|
| + # started coverage.py in this process. Nothing to do.
|
| + return
|
| +
|
| + process_startup.done = True
|
| + cov = Coverage(config_file=cps, auto_data=True)
|
| + cov.start()
|
| + cov._warn_no_data = False
|
| + cov._warn_unimported_source = False
|
|
|