diff options
Diffstat (limited to 'coverage/collector.py')
-rw-r--r-- | coverage/collector.py | 213 |
1 files changed, 126 insertions, 87 deletions
diff --git a/coverage/collector.py b/coverage/collector.py index 57c35605..3e28b3b1 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 except ImportError: # Couldn't import the C extension, maybe it isn't built. if os.getenv('COVERAGE_TEST_TRACER') == 'c': @@ -18,13 +27,23 @@ 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 + + +def should_start_context(frame): + """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" + fn_name = frame.f_code.co_name + if fn_name.startswith("test"): + return fn_name + + class Collector(object): """Collects trace data. @@ -46,15 +65,17 @@ 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, - ): + # The concurrency settings we support here. + SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"]) + + 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 + `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 @@ -68,9 +89,10 @@ class Collector(object): `warn` is a warning function, taking a single string message argument, to be used if a warning needs to be issued. - `concurrency` is a string indicating the concurrency library in use. - Valid values are "greenlet", "eventlet", "gevent", or "thread" (the - default). + `concurrency` is a list of strings indicating the concurrency libraries + in use. Valid values are "greenlet", "eventlet", "gevent", or "thread" + (the default). Of these four values, only one can be supplied. Other + values are ignored. """ self.should_trace = should_trace @@ -78,36 +100,44 @@ class Collector(object): self.warn = warn self.branch = branch self.threading = None - self.concurrency = concurrency self.concur_id_func = None + # We can handle a few concurrency options here, but only one at a time. + these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency) + if len(these_concurrencies) > 1: + raise CoverageException("Conflicting concurrency settings: %s" % concurrency) + self.concurrency = these_concurrencies.pop() if these_concurrencies else '' + try: - if concurrency == "greenlet": - import greenlet # pylint: disable=import-error,useless-suppression + if self.concurrency == "greenlet": + import greenlet self.concur_id_func = greenlet.getcurrent - elif concurrency == "eventlet": + elif self.concurrency == "eventlet": import eventlet.greenthread # pylint: disable=import-error,useless-suppression self.concur_id_func = eventlet.greenthread.getcurrent - elif concurrency == "gevent": + elif self.concurrency == "gevent": import gevent # pylint: disable=import-error,useless-suppression self.concur_id_func = gevent.getcurrent - elif concurrency == "thread" or not concurrency: + elif self.concurrency == "thread" or not self.concurrency: # It's important to import threading only if we need it. If # it's imported early, and the program being measured uses # gevent, then gevent's monkey-patching won't work properly. 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." % ( + self.concurrency, + ) ) + # Who-Tests-What is just a hack at the moment, so turn it on with an + # environment variable. + self.wtw = int(os.getenv('COVERAGE_WTW', 0)) + self.reset() if timid: @@ -118,10 +148,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.""" @@ -129,20 +164,25 @@ class Collector(object): def reset(self): """Clear collected data, and prepare to collect more.""" - # A dictionary mapping filenames to dicts with line number keys (if not - # branch coverage), or mapping filenames to dicts with line number + # 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 = {} - # A dictionary mapping filenames to plugin names that will handle them. - self.plugin_data = {} + # A dict mapping contexts to data dictionaries. + self.contexts = {} + self.contexts[None] = self.data + + # 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 filenames to + # 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 - # filenames can decide not to trace it based on the dynamic filename + # 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: @@ -172,7 +212,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 @@ -181,18 +221,22 @@ 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'): tracer.check_include = self.check_include + if self.wtw: + if hasattr(tracer, 'should_start_context'): + tracer.should_start_context = should_start_context + if hasattr(tracer, 'switch_context'): + tracer.switch_context = self.switch_context fn = tracer.start() self.tracers.append(tracer) @@ -205,16 +249,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 @@ -222,9 +266,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: @@ -232,8 +276,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: @@ -241,9 +294,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. @@ -254,9 +305,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() @@ -276,7 +325,7 @@ class Collector(object): if stats: print("\nCoverage.py tracer stats:") for k in sorted(stats.keys()): - print("%16s: %s" % (k, stats[k])) + print("%20s: %s" % (k, stats[k])) if self.threading: self.threading.settrace(None) @@ -289,45 +338,35 @@ class Collector(object): else: self._start_tracer() - def get_line_data(self): - """Return the line data collected. - - Data is { filename: { lineno: None, ...}, ...} - - """ - 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 get_arc_data(self): - """Return the arc data collected. + def switch_context(self, new_context): + """Who-Tests-What hack: switch to a new who-context.""" + # Make a new data dict, or find the existing one, and switch all the + # tracers to use it. + data = self.contexts.setdefault(new_context, {}) + for tracer in self.tracers: + tracer.data = data - Data is { filename: { (l1, l2): None, ...}, ...} + def save_data(self, covdata): + """Save the collected data to a `CoverageData`. - Note that no data is collected or returned if the Collector wasn't - created with `branch` true. + Also resets the collector. """ + 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)) + if self.branch: - return self.data + covdata.add_arcs(abs_file_dict(self.data)) else: - return {} - - def get_plugin_data(self): - """Return the mapping of source files to plugins. + covdata.add_lines(abs_file_dict(self.data)) + covdata.add_file_tracers(abs_file_dict(self.file_tracers)) - Returns: - dict: { filename: plugin_name, ... } + if self.wtw: + # Just a hack, so just hack it. + import pprint + out_file = "coverage_wtw_{:06}.py".format(os.getpid()) + with open(out_file, "w") as wtw_out: + pprint.pprint(self.contexts, wtw_out) - """ - return self.plugin_data + self.reset() |