diff options
Diffstat (limited to 'coverage/collector.py')
-rw-r--r-- | coverage/collector.py | 161 |
1 files changed, 83 insertions, 78 deletions
diff --git a/coverage/collector.py b/coverage/collector.py index 948cbbb0..0a43d87c 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -1,14 +1,23 @@ -"""Raw data collector 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 -import os, sys +"""Raw data collector for coverage.py.""" + +import os +import sys from coverage import env -from coverage.misc import CoverageException +from coverage.backward import iitems +from coverage.files import abs_file +from coverage.misc import CoverageException, isolate_module from coverage.pytracer import PyTracer +os = isolate_module(os) + + try: # Use the C extension code when we can, for speed. - from coverage.tracer import CTracer # pylint: disable=no-name-in-module + from coverage.tracer import CTracer, CFileDisposition # pylint: disable=no-name-in-module except ImportError: # Couldn't import the C extension, maybe it isn't built. if os.getenv('COVERAGE_TEST_TRACER') == 'c': @@ -18,13 +27,16 @@ except ImportError: # it, then exit quickly and clearly instead of dribbling confusing # errors. I'm using sys.exit here instead of an exception because an # exception here causes all sorts of other noise in unittest. - sys.stderr.write( - "*** COVERAGE_TEST_TRACER is 'c' but can't import CTracer!\n" - ) + sys.stderr.write("*** COVERAGE_TEST_TRACER is 'c' but can't import CTracer!\n") sys.exit(1) CTracer = None +class FileDisposition(object): + """A simple value type for recording what to do with a file.""" + pass + + class Collector(object): """Collects trace data. @@ -46,16 +58,14 @@ class Collector(object): # the top, and resumed when they become the top again. _collectors = [] - def __init__(self, - should_trace, check_include, timid, branch, warn, concurrency, - ): + def __init__(self, should_trace, check_include, timid, branch, warn, concurrency): """Create a collector. - `should_trace` is a function, taking a filename, and returning a - canonicalized filename, or None depending on whether the file should - be traced or not. + `should_trace` is a function, taking a file name, and returning a + `coverage.FileDisposition object`. - TODO: `check_include` + `check_include` is a function taking a file name and a frame. It returns + a boolean: True if the file should be traced, False if not. If `timid` is true, then a slower simpler trace function will be used. This is important for some environments where manipulation of @@ -85,13 +95,13 @@ class Collector(object): try: if concurrency == "greenlet": - import greenlet # pylint: disable=import-error + import greenlet self.concur_id_func = greenlet.getcurrent elif concurrency == "eventlet": - import eventlet.greenthread # pylint: disable=import-error + import eventlet.greenthread # pylint: disable=import-error,useless-suppression self.concur_id_func = eventlet.greenthread.getcurrent elif concurrency == "gevent": - import gevent # pylint: disable=import-error + import gevent # pylint: disable=import-error,useless-suppression self.concur_id_func = gevent.getcurrent elif concurrency == "thread" or not concurrency: # It's important to import threading only if we need it. If @@ -100,13 +110,10 @@ class Collector(object): import threading self.threading = threading else: - raise CoverageException( - "Don't understand concurrency=%s" % concurrency - ) + raise CoverageException("Don't understand concurrency=%s" % concurrency) except ImportError: raise CoverageException( - "Couldn't trace with concurrency=%s, " - "the module isn't installed." % concurrency + "Couldn't trace with concurrency=%s, the module isn't installed." % concurrency ) self.reset() @@ -119,10 +126,15 @@ class Collector(object): # trace function. self._trace_class = CTracer or PyTracer - self.supports_plugins = self._trace_class is CTracer + if self._trace_class is CTracer: + self.file_disposition_class = CFileDisposition + self.supports_plugins = True + else: + self.file_disposition_class = FileDisposition + self.supports_plugins = False def __repr__(self): - return "<Collector at 0x%x>" % id(self) + return "<Collector at 0x%x: %s>" % (id(self), self.tracer_name()) def tracer_name(self): """Return the class name of the tracer we're using.""" @@ -130,15 +142,23 @@ class Collector(object): def reset(self): """Clear collected data, and prepare to collect more.""" - # A dictionary mapping filenames to dicts with line number keys, - # or mapping filenames to dicts with line number pairs as keys. + # A dictionary mapping file names to dicts with line number keys (if not + # branch coverage), or mapping file names to dicts with line number + # pairs as keys (if branch coverage). self.data = {} - self.plugin_data = {} - - # A cache of the results from should_trace, the decision about whether - # to trace execution in a file. A dict of filename to (filename or - # None). + # A dictionary mapping file names to file tracer plugin names that will + # handle them. + self.file_tracers = {} + + # The .should_trace_cache attribute is a cache from file names to + # coverage.FileDisposition objects, or None. When a file is first + # considered for tracing, a FileDisposition is obtained from + # Coverage.should_trace. Its .trace attribute indicates whether the + # file should be traced or not. If it should be, a plugin with dynamic + # file names can decide not to trace it based on the dynamic file name + # being excluded by the inclusion rules, in which case the + # FileDisposition will be replaced by None in the cache. if env.PYPY: import __pypy__ # pylint: disable=import-error # Alex Gaynor said: @@ -166,7 +186,7 @@ class Collector(object): """Start a new Tracer object, and store it in self.tracers.""" tracer = self._trace_class() tracer.data = self.data - tracer.arcs = self.branch + tracer.trace_arcs = self.branch tracer.should_trace = self.should_trace tracer.should_trace_cache = self.should_trace_cache tracer.warn = self.warn @@ -175,14 +195,13 @@ class Collector(object): tracer.concur_id_func = self.concur_id_func elif self.concur_id_func: raise CoverageException( - "Can't support concurrency=%s with %s, " - "only threads are supported" % ( + "Can't support concurrency=%s with %s, only threads are supported" % ( self.concurrency, self.tracer_name(), ) ) - if hasattr(tracer, 'plugin_data'): - tracer.plugin_data = self.plugin_data + if hasattr(tracer, 'file_tracers'): + tracer.file_tracers = self.file_tracers if hasattr(tracer, 'threading'): tracer.threading = self.threading if hasattr(tracer, 'check_include'): @@ -199,16 +218,16 @@ class Collector(object): # install this as a trace function, and the first time it's called, it does # the real trace installation. - def _installation_trace(self, frame_unused, event_unused, arg_unused): + def _installation_trace(self, frame, event, arg): """Called on new threads, installs the real tracer.""" - # Remove ourselves as the trace function + # Remove ourselves as the trace function. sys.settrace(None) # Install the real tracer. fn = self._start_tracer() # Invoke the real trace function with the current event, to be sure # not to lose an event. if fn: - fn = fn(frame_unused, event_unused, arg_unused) + fn = fn(frame, event, arg) # Return the new trace function to continue tracing in this scope. return fn @@ -216,9 +235,9 @@ class Collector(object): """Start collecting trace information.""" if self._collectors: self._collectors[-1].pause() - self._collectors.append(self) - # Check to see whether we had a fullcoverage tracer installed. + # Check to see whether we had a fullcoverage tracer installed. If so, + # get the stack frames it stashed away for us. traces0 = [] fn0 = sys.gettrace() if fn0: @@ -226,8 +245,17 @@ class Collector(object): if tracer0: traces0 = getattr(tracer0, 'traces', []) - # Install the tracer on this thread. - fn = self._start_tracer() + try: + # Install the tracer on this thread. + fn = self._start_tracer() + except: + if self._collectors: + self._collectors[-1].resume() + raise + + # If _start_tracer succeeded, then we add ourselves to the global + # stack of collectors. + self._collectors.append(self) # Replay all the events from fullcoverage into the new trace function. for args in traces0: @@ -235,9 +263,7 @@ class Collector(object): try: fn(frame, event, arg, lineno=lineno) except TypeError: - raise Exception( - "fullcoverage must be run with the C trace function." - ) + raise Exception("fullcoverage must be run with the C trace function.") # Install our installation tracer in threading, to jump start other # threads. @@ -248,9 +274,7 @@ class Collector(object): """Stop collecting trace information.""" assert self._collectors assert self._collectors[-1] is self, ( - "Expected current collector to be %r, but it's %r" % ( - self, self._collectors[-1], - ) + "Expected current collector to be %r, but it's %r" % (self, self._collectors[-1]) ) self.pause() @@ -283,39 +307,20 @@ class Collector(object): else: self._start_tracer() - def get_line_data(self): - """Return the line data collected. + def save_data(self, covdata): + """Save the collected data to a `CoverageData`. - Data is { filename: { lineno: None, ...}, ...} + Also resets the collector. """ - if self.branch: - # If we were measuring branches, then we have to re-build the dict - # to show line data. We'll use the first lines of all the arcs, - # if they are actual lines. We don't need the second lines, because - # the second lines will also be first lines, sometimes to exits. - line_data = {} - for f, arcs in self.data.items(): - line_data[f] = dict( - (l1, None) for l1, _ in arcs.keys() if l1 > 0 - ) - return line_data - else: - return self.data + def abs_file_dict(d): + """Return a dict like d, but with keys modified by `abs_file`.""" + return dict((abs_file(k), v) for k, v in iitems(d)) - def get_arc_data(self): - """Return the arc data collected. - - Data is { filename: { (l1, l2): None, ...}, ...} - - Note that no data is collected or returned if the Collector wasn't - created with `branch` true. - - """ if self.branch: - return self.data + covdata.add_arcs(abs_file_dict(self.data)) else: - return {} + covdata.add_lines(abs_file_dict(self.data)) + covdata.add_file_tracers(abs_file_dict(self.file_tracers)) - def get_plugin_data(self): - return self.plugin_data + self.reset() |