--- a/DebugClients/Python3/coverage/control.py Fri Apr 04 22:57:07 2014 +0200 +++ b/DebugClients/Python3/coverage/control.py Thu Apr 10 23:02:20 2014 +0200 @@ -1,20 +1,33 @@ """Core control stuff for Coverage.""" -import atexit, os, socket +import atexit, os, random, socket, sys from .annotate import AnnotateReporter -from .backward import string_class # pylint: disable-msg=W0622 +from .backward import string_class, iitems, sorted # pylint: disable=W0622 from .codeunit import code_unit_factory, CodeUnit from .collector import Collector +from .config import CoverageConfig from .data import CoverageData -from .files import FileLocator -from .html import HtmlReporter -from .results import Analysis +from .debug import DebugControl +from .files import FileLocator, TreeMatcher, FnmatchMatcher +from .files import PathAliases, find_python_files, prep_patterns +#from .html import HtmlReporter # Comment for eric5 +from .misc import CoverageException, bool_or_none, join_regex +from .misc import file_be_gone +from .results import Analysis, Numbers from .summary import SummaryReporter from .xmlreport import XmlReporter +# Pypy has some unusual stuff in the "stdlib". Consider those locations +# when deciding where the stdlib is. +try: + import _structseq # pylint: disable=F0401 +except ImportError: + _structseq = None + + class coverage(object): - """Programmatic access to Coverage. + """Programmatic access to coverage.py. To use:: @@ -22,19 +35,20 @@ cov = coverage() cov.start() - #.. blah blah (run your code) blah blah .. + #.. call your code .. cov.stop() cov.html_report(directory='covhtml') """ - - def __init__(self, data_file=None, data_suffix=False, cover_pylib=False, - auto_data=False, timid=False, branch=False): + 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, + debug_file=None): """ `data_file` is the base name of the data file to use, defaulting to - ".coverage". `data_suffix` is appended 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. + ".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 @@ -51,105 +65,287 @@ If `branch` is true, then branch coverage will be measured in addition to the usual statement coverage. + `config_file` determines what config file to read. If it is a string, + it is the name of the config file to read. If it is True, then a + standard file is read (".coveragerc"). If it is False, then no 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 filename 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. `debug_file` is the file to write debug messages to, + defaulting to stderr. + """ from . import __version__ - self.cover_pylib = cover_pylib + # A record of all the warnings that have been issued. + self._warnings = [] + + # Build our configuration from a number of sources: + # 1: defaults: + self.config = CoverageConfig() + + # 2: from the coveragerc file: + if config_file: + if config_file is True: + config_file = ".coveragerc" + try: + self.config.from_file(config_file) + except ValueError: + _, err, _ = sys.exc_info() + raise CoverageException( + "Couldn't read config file %s: %s" % (config_file, err) + ) + + # 3: from environment variables: + self.config.from_environment('COVERAGE_OPTIONS') + env_data_file = os.environ.get('COVERAGE_FILE') + if env_data_file: + self.config.data_file = env_data_file + + # 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, + ) + + # Create and configure the debugging controller. + self.debug = DebugControl(self.config.debug, debug_file or sys.stderr) + self.auto_data = auto_data - self.atexit_registered = False - self.exclude_re = "" - self.exclude_list = [] + # _exclude_re is a dict mapping exclusion list names to compiled + # regexes. + self._exclude_re = {} + self._exclude_regex_stale() self.file_locator = FileLocator() - # Timidity: for nose users, read an environment variable. This is a - # cheap hack, since the rest of the command line arguments aren't - # recognized, but it solves some users' problems. - timid = timid or ('--timid' in os.environ.get('COVERAGE_OPTIONS', '')) + # 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(self.file_locator.canonical_filename(src)) + else: + self.source_pkgs.append(src) + + self.omit = prep_patterns(self.config.omit) + self.include = prep_patterns(self.config.include) + self.collector = Collector( - self._should_trace, timid=timid, branch=branch + self._should_trace, timid=self.config.timid, + branch=self.config.branch, warn=self._warn ) - # Create the data file. - if data_suffix: + # 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 data_suffix or self.config.parallel: if not isinstance(data_suffix, string_class): - # if data_suffix=True, use .machinename.pid - data_suffix = ".%s.%s" % (socket.gethostname(), os.getpid()) + # if data_suffix=True, use .machinename.pid.random + data_suffix = True else: data_suffix = None + self.data_suffix = None + self.run_suffix = 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( - basename=data_file, suffix=data_suffix, - collector="coverage v%s" % __version__ + basename=self.config.data_file, + collector="coverage v%s" % __version__, + debug=self.debug, ) - # The default exclude pattern. - self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]') - - # The prefix for files considered "installed with the interpreter". - if not self.cover_pylib: - # Look at where the "os" module is located. That's the indication - # for "installed with the interpreter". - os_file = self.file_locator.canonical_filename(os.__file__) - self.pylib_prefix = os.path.split(os_file)[0] + # The dirs for files considered "installed with the interpreter". + self.pylib_dirs = [] + 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, os, random, socket, _structseq): + if m is not None and hasattr(m, "__file__"): + m_dir = self._canonical_dir(m) + if m_dir not in self.pylib_dirs: + self.pylib_dirs.append(m_dir) # To avoid tracing the coverage code itself, we skip anything located # where we are. - here = self.file_locator.canonical_filename(__file__) - self.cover_prefix = os.path.split(here)[0] + self.cover_dir = self._canonical_dir(__file__) + + # The matchers for _should_trace. + self.source_match = None + self.pylib_match = self.cover_match = None + self.include_match = self.omit_match = None + + # Set the reporting precision. + Numbers.set_precision(self.config.precision) + + # Is it ok for no data to be collected? + self._warn_no_data = True + self._warn_unimported_source = True - def _should_trace(self, filename, frame): - """Decide whether to trace execution in `filename` + # State machine variables: + # Have we started collecting and not stopped it? + self._started = False + # Have we measured some data and not harvested it? + self._measured = False + + atexit.register(self._atexit) + + def _canonical_dir(self, morf): + """Return the canonical directory of the module or file `morf`.""" + return os.path.split(CodeUnit(morf, self.file_locator).filename)[0] + + def _source_for_file(self, filename): + """Return the source file for `filename`.""" + if not filename.endswith(".py"): + if filename.endswith((".pyc", ".pyo")): + filename = filename[:-1] + elif filename.endswith("$py.class"): # jython + filename = filename[:-9] + ".py" + return filename + + def _should_trace_with_reason(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 canonicalized filename if it should be traced, False if it - should not. + Returns a pair of values: the first indicates whether the file should + be traced: it's a canonicalized filename if it should be traced, None + if it should not. The second value is a string, the resason for the + decision. """ - if filename == '<string>': - # There's no point in ever tracing string executions, we can't do - # anything with the data later anyway. - return False + if not filename: + # Empty string is pretty useless + return None, "empty string isn't a filename" + + if filename.startswith('<'): + # Lots of non-file execution is represented with artificial + # filenames 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 None, "not a real filename" + + self._check_for_packages() # Compiled Python files have two filenames: frame.f_code.co_filename is - # the filename at the time the .pyc was compiled. The second name - # is __file__, which is where the .pyc was actually loaded from. Since + # the filename 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: - if not dunder_file.endswith(".py"): - if dunder_file[-4:-1] == ".py": - dunder_file = dunder_file[:-1] - filename = dunder_file + filename = self._source_for_file(dunder_file) + + # Jython reports the .class file to the tracer, use the source file. + if filename.endswith("$py.class"): + filename = filename[:-9] + ".py" canonical = self.file_locator.canonical_filename(filename) - # 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 not self.cover_pylib: - if canonical.startswith(self.pylib_prefix): - return False + # 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 not self.source_match.match(canonical): + return None, "falls outside the --source trees" + elif self.include_match: + if not self.include_match.match(canonical): + return None, "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(canonical): + return None, "is in the stdlib" - # We exclude the coverage code itself, since a little of it will be - # measured otherwise. - if canonical.startswith(self.cover_prefix): - return False + # We exclude the coverage code itself, since a little of it will be + # measured otherwise. + if self.cover_match and self.cover_match.match(canonical): + return None, "is part of coverage.py" + + # Check the file against the omit pattern. + if self.omit_match and self.omit_match.match(canonical): + return None, "is inside an --omit pattern" + + return canonical, "because we love you" + def _should_trace(self, filename, frame): + """Decide whether to trace execution in `filename`. + + Calls `_should_trace_with_reason`, and returns just the decision. + + """ + canonical, reason = self._should_trace_with_reason(filename, frame) + if self.debug.should('trace'): + if not canonical: + msg = "Not tracing %r: %s" % (filename, reason) + else: + msg = "Tracing %r" % (filename,) + self.debug.write(msg) return canonical - # To log what should_trace returns, change this to "if 1:" - if 0: - _real_should_trace = _should_trace - def _should_trace(self, filename, frame): # pylint: disable-msg=E0102 - """A logging decorator around the real _should_trace function.""" - ret = self._real_should_trace(filename, frame) - print("should_trace: %r -> %r" % (filename, ret)) - return ret + def _warn(self, msg): + """Use `msg` as a warning.""" + self._warnings.append(msg) + sys.stderr.write("Coverage.py warning: %s\n" % msg) + + def _check_for_packages(self): + """Update the source_match matcher with latest imported packages.""" + # Our self.source_pkgs attribute is a list of package names we want to + # measure. Each time through here, we see if we've imported any of + # them yet. If so, we add its file to source_match, and we don't have + # to look for that package any more. + if self.source_pkgs: + found = [] + for pkg in self.source_pkgs: + try: + mod = sys.modules[pkg] + except KeyError: + continue + + found.append(pkg) + + try: + pkg_file = mod.__file__ + except AttributeError: + pkg_file = None + else: + d, f = os.path.split(pkg_file) + if f.startswith('__init__'): + # This is actually a package, return the directory. + pkg_file = d + else: + pkg_file = self._source_for_file(pkg_file) + pkg_file = self.file_locator.canonical_filename(pkg_file) + if not os.path.exists(pkg_file): + pkg_file = None + + if pkg_file: + self.source.append(pkg_file) + self.source_match.add(pkg_file) + else: + self._warn("Module %s has no Python source." % pkg) + + for pkg in found: + self.source_pkgs.remove(pkg) def use_cache(self, usecache): """Control the use of a data file (incorrectly called a cache). @@ -165,19 +361,60 @@ self.data.read() def start(self): - """Start measuring code coverage.""" + """Start measuring code coverage. + + Coverage measurement actually occurs in functions called after `start` + is invoked. Statements in the same scope as `start` won't be measured. + + Once you invoke `start`, you must also call `stop` eventually, or your + process might not shut down cleanly. + + """ + 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() - # Save coverage data when Python exits. - if not self.atexit_registered: - atexit.register(self.save) - self.atexit_registered = True + + # Create the matchers we need for _should_trace + if self.source or self.source_pkgs: + self.source_match = TreeMatcher(self.source) + else: + if self.cover_dir: + self.cover_match = TreeMatcher([self.cover_dir]) + 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. + if self.debug.should('config'): + self.debug.write("Configuration values:") + config_info = sorted(self.config.__dict__.items()) + self.debug.write_formatted_info(config_info) + + if self.debug.should('sys'): + self.debug.write("Debugging info:") + self.debug.write_formatted_info(self.sysinfo()) + self.collector.start() + self._started = True + self._measured = True def stop(self): """Stop measuring code coverage.""" + self._started = False self.collector.stop() - self._harvest_data() + + 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. @@ -189,31 +426,71 @@ self.collector.reset() self.data.erase() - def clear_exclude(self): + def clear_exclude(self, which='exclude'): """Clear the exclude list.""" - self.exclude_list = [] - self.exclude_re = "" + setattr(self.config, which + "_list", []) + self._exclude_regex_stale() - def exclude(self, regex): + def exclude(self, regex, which='exclude'): """Exclude source lines from execution consideration. - `regex` is a regular expression. Lines matching this expression are - not considered executable when reporting code coverage. A list of - regexes is maintained; this function adds a new regex to the list. - Matching any of the regexes excludes a source line. + 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.exclude_list.append(regex) - self.exclude_re = "(" + ")|(".join(self.exclude_list) + ")" + 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 get_exclude_list(self): - """Return the list of excluded regex patterns.""" - return self.exclude_list + 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 `exclude` for the lists + that are available, and their meaning. + + """ + return getattr(self.config, which + "_list") def save(self): """Save the collected coverage data to the data file.""" + data_suffix = self.data_suffix + if data_suffix is True: + # If data_suffix was a simple true value, then make a suffix with + # plenty of distinguishing information. We do this here in + # `save()` at the last minute so that the pid will be correct even + # if the process forks. + extra = "" + if _TEST_NAME_FILE: + f = open(_TEST_NAME_FILE) + test_name = f.read() + f.close() + extra = "." + test_name + data_suffix = "%s%s.%s.%06d" % ( + socket.gethostname(), extra, os.getpid(), + random.randint(0, 999999) + ) + self._harvest_data() - self.data.write() + self.data.write(suffix=data_suffix) def combine(self): """Combine together a number of similarly-named coverage data files. @@ -223,14 +500,53 @@ current measurements. """ - self.data.combine_parallel_data() + aliases = None + if self.config.paths: + aliases = PathAliases(self.file_locator) + for paths in self.config.paths.values(): + result = paths[0] + for pattern in paths[1:]: + aliases.add(pattern, result) + self.data.combine_parallel_data(aliases=aliases) def _harvest_data(self): - """Get the collected data and reset the collector.""" + """Get the collected data and reset the collector. + + Also warn about various problems collecting data. + + """ + if not self._measured: + return + self.data.add_line_data(self.collector.get_line_data()) self.data.add_arc_data(self.collector.get_arc_data()) self.collector.reset() + # 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: + self._warn("Module %s was never imported." % pkg) + + # Find out if we got any data. + summary = self.data.summary() + if not summary 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 = self.file_locator.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) + + self._measured = False + # Backward compatibility with version 1. def analysis(self, morf): """Like `analysis2` but doesn't return excluded line numbers.""" @@ -256,8 +572,11 @@ """ analysis = self._analyze(morf) return ( - analysis.filename, analysis.statements, analysis.excluded, - analysis.missing, analysis.missing_formatted() + analysis.filename, + sorted(analysis.statements), + sorted(analysis.excluded), + sorted(analysis.missing), + analysis.missing_formatted(), ) def _analyze(self, it): @@ -266,24 +585,38 @@ Returns an `Analysis` object. """ + self._harvest_data() if not isinstance(it, CodeUnit): it = code_unit_factory(it, self.file_locator)[0] return Analysis(self, it) - def report(self, morfs=None, show_missing=True, ignore_errors=False, - file=None, omit_prefixes=None): # pylint: disable-msg=W0622 + def report(self, morfs=None, show_missing=True, ignore_errors=None, + file=None, # pylint: disable=W0622 + omit=None, include=None + ): """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 filename patterns. Modules whose filenames + match those patterns will be included in the report. Modules matching + `omit` will not be included in the report. + + Returns a float, the total percentage covered. + """ - reporter = SummaryReporter(self, show_missing, ignore_errors) - reporter.report(morfs, outfile=file, omit_prefixes=omit_prefixes) + self._harvest_data() + self.config.from_args( + ignore_errors=ignore_errors, omit=omit, include=include, + show_missing=show_missing, + ) + reporter = SummaryReporter(self, self.config) + return reporter.report(morfs, outfile=file) - def annotate(self, morfs=None, directory=None, ignore_errors=False, - omit_prefixes=None): + 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 @@ -291,56 +624,156 @@ marker to indicate the coverage of the line. Covered lines have ">", excluded lines have "-", and missing lines have "!". + See `coverage.report()` for other arguments. + """ - reporter = AnnotateReporter(self, ignore_errors) - reporter.report( - morfs, directory=directory, omit_prefixes=omit_prefixes) + self._harvest_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=False, - omit_prefixes=None): + 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 `coverage.report()` for other arguments. + + Returns a float, the total percentage covered. + """ - reporter = HtmlReporter(self, ignore_errors) - reporter.report( - morfs, directory=directory, omit_prefixes=omit_prefixes) + self._harvest_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=False, - omit_prefixes=None): + 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 `coverage.report()` for other arguments. + + Returns a float, the total percentage covered. + """ - if outfile: - outfile = open(outfile, "w") + self._harvest_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: + outfile = open(self.config.xml_output, "w") + file_to_close = outfile try: - reporter = XmlReporter(self, ignore_errors) - reporter.report( - morfs, omit_prefixes=omit_prefixes, outfile=outfile) + try: + reporter = XmlReporter(self, self.config) + return reporter.report(morfs, outfile=outfile) + except CoverageException: + delete_file = True + raise finally: - outfile.close() + if file_to_close: + file_to_close.close() + if delete_file: + file_be_gone(self.config.xml_output) def sysinfo(self): - """Return a list of key,value pairs showing internal information.""" + """Return a list of (key, value) pairs showing internal information.""" import coverage as covmod - import platform, re, sys + import platform, re + + try: + implementation = platform.python_implementation() + except AttributeError: + implementation = "unknown" info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), - ('cover_prefix', self.cover_prefix), - ('pylib_prefix', self.pylib_prefix), + ('cover_dir', self.cover_dir), + ('pylib_dirs', self.pylib_dirs), ('tracer', self.collector.tracer_name()), + ('config_files', self.config.attempted_config_files), + ('configs_read', self.config.config_files), ('data_path', self.data.filename), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), + ('implementation', implementation), + ('executable', sys.executable), ('cwd', os.getcwd()), ('path', sys.path), - ('environment', [ - ("%s = %s" % (k, v)) for k, v in os.environ.items() - if re.search("^COV|^PY", k) - ]), + ('environment', sorted([ + ("%s = %s" % (k, v)) for k, v in iitems(os.environ) + if re.search(r"^COV|^PY", k) + ])), + ('command_line', " ".join(getattr(sys, 'argv', ['???']))), ] - return info \ No newline at end of file + if self.source_match: + info.append(('source_match', self.source_match.info())) + if self.include_match: + info.append(('include_match', self.include_match.info())) + if self.omit_match: + info.append(('omit_match', self.omit_match.info())) + if self.cover_match: + info.append(('cover_match', self.cover_match.info())) + if self.pylib_match: + info.append(('pylib_match', self.pylib_match.info())) + + return info + + +def process_startup(): + """Call this at Python startup 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 cps: + cov = coverage(config_file=cps, auto_data=True) + cov.start() + cov._warn_no_data = False + cov._warn_unimported_source = False + + +# A hack for debugging testing in subprocesses. +_TEST_NAME_FILE = "" #"/tmp/covtest.txt"