diff options
Diffstat (limited to 'coverage/control.py')
-rw-r--r-- | coverage/control.py | 328 |
1 files changed, 136 insertions, 192 deletions
diff --git a/coverage/control.py b/coverage/control.py index 563925ef..4837356d 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -1,29 +1,31 @@ -"""Core control stuff for Coverage.""" +# 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 random -import socket import sys import traceback -from coverage import env +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 +from coverage.data import CoverageData, CoverageDataFiles from coverage.debug import DebugControl -from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher +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, overrides +from coverage.misc import file_be_gone from coverage.monkey import patch_multiprocessing -from coverage.plugin import CoveragePlugin, FileReporter +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 @@ -138,6 +140,9 @@ class Coverage(object): 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( @@ -166,10 +171,10 @@ class Coverage(object): # Other instance attributes, set later. self.omit = self.include = self.source = None - self.source_pkgs = self.file_locator = None - self.data = self.collector = None - self.plugins = self.file_tracing_plugins = None - self.pylib_dirs = self.cover_dir = 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 @@ -190,8 +195,6 @@ class Coverage(object): is called. """ - from coverage import __version__ - if self._inited: return @@ -201,26 +204,21 @@ class Coverage(object): self.debug = DebugControl(self.config.debug, self._debug_file) # Load plugins - self.plugins = Plugins.load_plugins(self.config.plugins, self.config) - - self.file_tracing_plugins = [] - for plugin in self.plugins: - if overrides(plugin, "file_tracer", CoveragePlugin): - self.file_tracing_plugins.append(plugin) + 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() - self.file_locator = FileLocator() + 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(self.file_locator.canonical_filename(src)) + self.source.append(files.canonical_filename(src)) else: self.source_pkgs.append(src) @@ -242,17 +240,17 @@ class Coverage(object): ) # Early warning if we aren't going to be able to support plugins. - if self.file_tracing_plugins and not self.collector.supports_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.file_tracing_plugins + for plugin in self.plugins.file_tracers ), self.collector.tracer_name(), ) ) - for plugin in self.file_tracing_plugins: + 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 @@ -271,11 +269,8 @@ class Coverage(object): # 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=self.config.data_file, - collector="coverage v%s" % __version__, - debug=self.debug, - ) + self.data = CoverageData(debug=self.debug) + self.data_files = CoverageDataFiles(basename=self.config.data_file) # The dirs for files considered "installed with the interpreter". self.pylib_dirs = set() @@ -285,7 +280,7 @@ class Coverage(object): # 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, platform, random, socket, _structseq): + 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__'): @@ -299,9 +294,16 @@ class Coverage(object): structseq_file = structseq_new.__code__.co_filename self.pylib_dirs.add(self._canonical_dir(structseq_file)) - # To avoid tracing the coverage code itself, we skip anything located - # where we are. - self.cover_dir = self._canonical_dir(__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) @@ -315,8 +317,8 @@ class Coverage(object): self.source_match = TreeMatcher(self.source) self.source_pkgs_match = ModuleMatcher(self.source_pkgs) else: - if self.cover_dir: - self.cover_match = TreeMatcher([self.cover_dir]) + 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: @@ -424,7 +426,8 @@ class Coverage(object): Returns a FileDisposition object. """ - disp = FileDisposition(filename) + 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.""" @@ -441,6 +444,13 @@ class Coverage(object): 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 filename, then just use the original. + filename = original_filename if not filename: # Empty string is pretty useless. @@ -460,12 +470,12 @@ class Coverage(object): if filename.endswith("$py.class"): filename = filename[:-9] + ".py" - canonical = self.file_locator.canonical_filename(filename) + 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.file_tracing_plugins: + for plugin in self.plugins.file_tracers: if not plugin._coverage_enabled: continue @@ -478,10 +488,9 @@ class Coverage(object): if file_tracer.has_dynamic_source_filename(): disp.has_dynamic_filename = True else: - disp.source_filename = \ - self.file_locator.canonical_filename( - file_tracer.source_filename() - ) + disp.source_filename = files.canonical_filename( + file_tracer.source_filename() + ) break except Exception: self._warn( @@ -541,8 +550,8 @@ class Coverage(object): if self.pylib_match and self.pylib_match.match(filename): return "is in the stdlib" - # We exclude the coverage code itself, since a little of it will be - # measured otherwise. + # 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" @@ -561,7 +570,7 @@ class Coverage(object): """ disp = self._should_trace_internal(filename, frame) if self.debug.should('trace'): - self.debug.write(disp.debug_message()) + self.debug.write(_disposition_debug_msg(disp)) return disp def _check_include_omit_etc(self, filename, frame): @@ -588,19 +597,16 @@ class Coverage(object): sys.stderr.write("Coverage.py warning: %s\n" % msg) def use_cache(self, usecache): - """Control the use of a data file (incorrectly called a cache). - - `usecache` is true or false, whether to read and write data on disk. - - """ + """Obsolete method.""" self._init() - self.data.usefile(usecache) + 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.read() + self.data_files.read(self.data) def start(self): """Start measuring code coverage. @@ -647,6 +653,7 @@ class Coverage(object): 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.""" @@ -698,58 +705,48 @@ class Coverage(object): def save(self): """Save the collected coverage data to the data file.""" self._init() - 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: # pragma: debugging - with open(_TEST_NAME_FILE) as f: - test_name = f.read() - extra = "." + test_name - data_suffix = "%s%s.%s.%06d" % ( - socket.gethostname(), extra, os.getpid(), - random.randint(0, 999999) - ) + self.get_data() + self.data_files.write(self.data, suffix=self.data_suffix) - self._harvest_data() - self.data.write(suffix=data_suffix) - - def combine(self): + 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. + """ self._init() + self.get_data() + aliases = None if self.config.paths: - aliases = PathAliases(self.file_locator) + aliases = PathAliases() 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): + 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:`CoverageData`, the collected coverage data. + """ self._init() if not self._measured: return - # TODO: seems like this parallel structure is getting kinda old... - self.data.add_line_data(self.collector.get_line_data()) - self.data.add_arc_data(self.collector.get_arc_data()) - self.data.add_plugin_data(self.collector.get_plugin_data()) - self.collector.reset() + self.collector.save_data(self.data) # If there are still entries in the source_pkgs list, then we never # encountered those packages. @@ -769,14 +766,13 @@ class Coverage(object): ) # Find out if we got any data. - summary = self.data.summary() - if not summary and self._warn_no_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 = self.file_locator.canonical_filename(py_file) + 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 @@ -785,7 +781,20 @@ class Coverage(object): 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): @@ -826,19 +835,20 @@ class Coverage(object): Returns an `Analysis` object. """ - self._harvest_data() + self.get_data() if not isinstance(it, FileReporter): it = self._get_file_reporter(it) - return Analysis(self, it) + return Analysis(self.data, it) def _get_file_reporter(self, morf): """Get a FileReporter for a module or filename.""" plugin = None + file_reporter = "python" if isinstance(morf, string_class): abs_morf = abs_file(morf) - plugin_name = self.data.plugin_data().get(abs_morf) + plugin_name = self.data.file_tracer(abs_morf) if plugin_name: plugin = self.plugins.get(plugin_name) @@ -850,15 +860,9 @@ class Coverage(object): plugin._coverage_plugin_name, morf ) ) - else: - file_reporter = PythonFileReporter(morf, self) - # The FileReporter can have a name attribute, but if it doesn't, we'll - # supply it as the relative path to self.filename. - if not hasattr(file_reporter, "name"): - file_reporter.name = self.file_locator.relative_filename( - file_reporter.filename - ) + if file_reporter == "python": + file_reporter = PythonFileReporter(morf, self) return file_reporter @@ -904,7 +908,7 @@ class Coverage(object): Returns a float, the total percentage covered. """ - self._harvest_data() + self.get_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, show_missing=show_missing, skip_covered=skip_covered, @@ -926,7 +930,7 @@ class Coverage(object): See `coverage.report()` for other arguments. """ - self._harvest_data() + self.get_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include ) @@ -952,7 +956,7 @@ class Coverage(object): Returns a float, the total percentage covered. """ - self._harvest_data() + 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, @@ -976,7 +980,7 @@ class Coverage(object): Returns a float, the total percentage covered. """ - self._harvest_data() + self.get_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, xml_output=outfile, @@ -997,7 +1001,7 @@ class Coverage(object): outfile = open(self.config.xml_output, "w") file_to_close = outfile try: - reporter = XmlReporter(self, self.config, self.file_locator) + reporter = XmlReporter(self, self.config) return reporter.report(morfs, outfile=outfile) except CoverageException: delete_file = True @@ -1014,13 +1018,9 @@ class Coverage(object): import coverage as covmod self._init() - try: - implementation = platform.python_implementation() - except AttributeError: - implementation = "unknown" ft_plugins = [] - for ft in self.file_tracing_plugins: + for ft in self.plugins.file_tracers: ft_name = ft._coverage_plugin_name if not ft._coverage_enabled: ft_name += " (disabled)" @@ -1029,16 +1029,16 @@ class Coverage(object): info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), - ('cover_dir', self.cover_dir), + ('cover_dirs', self.cover_dirs), ('pylib_dirs', self.pylib_dirs), ('tracer', self.collector.tracer_name()), - ('file_tracing_plugins', ft_plugins), + ('plugins.file_tracers', ft_plugins), ('config_files', self.config.attempted_config_files), ('configs_read', self.config.config_files), - ('data_path', self.data.filename), + ('data_path', self.data_files.filename), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), - ('implementation', implementation), + ('implementation', platform.python_implementation()), ('executable', sys.executable), ('cwd', os.getcwd()), ('path', sys.path), @@ -1067,36 +1067,32 @@ class Coverage(object): return info -class FileDisposition(object): - """A simple object for noting a number of details of files to trace.""" - def __init__(self, original_filename): - self.original_filename = original_filename - self.canonical_filename = original_filename - self.source_filename = None - self.trace = False - self.reason = "" - self.file_tracer = None - self.has_dynamic_filename = False - - def __repr__(self): - ret = "FileDisposition %r" % (self.original_filename,) - if self.trace: - ret += " trace" - else: - ret += " notrace=%r" % (self.reason,) - if self.file_tracer: - ret += " file_tracer=%r" % (self.file_tracer,) - return "<" + ret + ">" - - def debug_message(self): - """Produce a debugging message explaining the outcome.""" - if self.trace: - msg = "Tracing %r" % (self.original_filename,) - if self.file_tracer: - msg += ": will be traced by %r" % self.file_tracer - else: - msg = "Not tracing %r: %s" % (self.original_filename, self.reason) - return msg +# 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(): @@ -1128,7 +1124,7 @@ def process_startup(): # 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 has + # 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 @@ -1136,7 +1132,7 @@ def process_startup(): if hasattr(process_startup, "done"): # We've annotated this function before, so we must have already - # started coverage in this process. Nothing to do. + # started coverage.py in this process. Nothing to do. return process_startup.done = True @@ -1144,55 +1140,3 @@ def process_startup(): cov.start() cov._warn_no_data = False cov._warn_unimported_source = False - - -# A hack for debugging testing in sub-processes. -_TEST_NAME_FILE = "" # "/tmp/covtest.txt" - - -class Plugins(object): - """The currently loaded collection of coverage.py plugins.""" - - def __init__(self): - self.order = [] - self.names = {} - - @classmethod - def load_plugins(cls, modules, config): - """Load plugins from `modules`. - - Returns a list of loaded and configured plugins. - - """ - plugins = cls() - - for module in modules: - __import__(module) - mod = sys.modules[module] - - plugin_class = getattr(mod, "Plugin", None) - if plugin_class: - options = config.get_plugin_options(module) - plugin = plugin_class(options) - plugin._coverage_plugin_name = module - plugin._coverage_enabled = True - plugins.order.append(plugin) - plugins.names[module] = plugin - else: - raise CoverageException( - "Plugin module %r didn't define a Plugin class" % module - ) - - return plugins - - def __nonzero__(self): - return bool(self.order) - - __bool__ = __nonzero__ - - def __iter__(self): - return iter(self.order) - - def get(self, plugin_name): - """Return a plugin by name.""" - return self.names[plugin_name] |