summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2014-09-17 20:17:09 -0400
committerNed Batchelder <ned@nedbatchelder.com>2014-09-17 20:17:09 -0400
commit8b3265d3482baf285fe1061ff285f73f2a818b26 (patch)
tree40e8c4180b01d802bc1d84ab6479ef2a9b307dfd
parentaaa309565ee0cadff04ca844bac3d8e9cf17489d (diff)
downloadpython-coveragepy-git-8b3265d3482baf285fe1061ff285f73f2a818b26.tar.gz
Gevent, etc, support finally works. #149
-rw-r--r--AUTHORS.txt1
-rw-r--r--CHANGES.txt5
-rw-r--r--coverage/collector.py66
-rw-r--r--tests/test_coroutine.py33
4 files changed, 58 insertions, 47 deletions
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 3aa04adf..d374c2ac 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -32,6 +32,7 @@ Noel O'Boyle
Detlev Offenbach
JT Olds
George Paci
+Peter Portante
Catherine Proulx
Brandon Rhodes
Adi Roiban
diff --git a/CHANGES.txt b/CHANGES.txt
index 2f71f63d..b034744f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -8,6 +8,10 @@ Change history for Coverage.py
- Python versions supported are now CPython 2.6, 2.7, 3.2, 3.3, and 3.4, and
PyPy 2.2.
+- Gevent, eventlet, and greenlet are now supported, closing `issue 149`_. Huge
+ thanks to Peter Portante for initial implementation, and to Joe Jevnik for
+ the final insight that completed the work.
+
- Options are now also read from a setup.cfg file, if any. Sections are
prefixed with "coverage:", so the ``[run]`` options will be read from the
``[coverage:run]`` section of setup.cfg. Finishes `issue 304`_.
@@ -41,6 +45,7 @@ Change history for Coverage.py
.. _issue 57: https://bitbucket.org/ned/coveragepy/issue/57/annotate-command-fails-to-annotate-many
.. _issue 94: https://bitbucket.org/ned/coveragepy/issue/94/coverage-xml-doesnt-produce-sources
+.. _issue 149: https://bitbucket.org/ned/coveragepy/issue/149/coverage-gevent-looks-broken
.. _issue 230: https://bitbucket.org/ned/coveragepy/issue/230/show-line-no-for-missing-branches-in
.. _issue 284: https://bitbucket.org/ned/coveragepy/issue/284/fail-under-should-show-more-precision
.. _issue 285: https://bitbucket.org/ned/coveragepy/issue/285/xml-report-fails-if-output-file-directory
diff --git a/coverage/collector.py b/coverage/collector.py
index 26de3727..e44b7410 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -1,6 +1,6 @@
"""Raw data collector for Coverage."""
-import collections, os, sys, threading
+import collections, os, sys
try:
# Use the C extension code when we can, for speed.
@@ -48,6 +48,8 @@ class PyTracer(object):
self.should_trace_cache = None
self.warn = None
self.plugin_data = None
+ # The threading module to use, if any.
+ self.threading = None
self.plugin = []
self.cur_file_dict = []
@@ -62,39 +64,19 @@ class PyTracer(object):
self.coroutine_id_func = None
self.last_coroutine = None
+ def __repr__(self):
+ return "<PyTracer at 0x{0:0x}: {1} lines in {2} files>".format(
+ id(self),
+ sum(len(v) for v in self.data.values()),
+ len(self.data),
+ )
+
def _trace(self, frame, event, arg_unused):
"""The trace function passed to sys.settrace."""
if self.stopped:
return
- if 0:
- # A lot of debugging to try to understand why gevent isn't right.
- import os.path, pprint
- def short_ident(ident):
- return "{}:{:06X}".format(ident.__class__.__name__, id(ident) & 0xFFFFFF)
-
- ident = None
- if self.coroutine_id_func:
- ident = short_ident(self.coroutine_id_func())
- sys.stdout.write("trace event: %s %s %r @%d\n" % (
- event, ident, frame.f_code.co_filename, frame.f_lineno
- ))
- pprint.pprint(
- dict(
- (
- short_ident(ident),
- [
- (os.path.basename(tn or ""), sorted((cfd or {}).keys()), ll)
- for ex, tn, cfd, ll in data_stacks
- ]
- )
- for ident, data_stacks in self.data_stacks.items()
- )
- , width=250)
- pprint.pprint(sorted((self.cur_file_dict or {}).keys()), width=250)
- print("TRYING: {}".format(sorted(next((v for k,v in self.data.items() if k.endswith("try_it.py")), {}).keys())))
-
if self.last_exc_back: # TODO: bring this up to speed
if frame == self.last_exc_back:
# Someone forgot a return event.
@@ -186,14 +168,15 @@ class PyTracer(object):
Return a Python function suitable for use with sys.settrace().
"""
- self.thread = threading.currentThread()
+ if self.threading:
+ self.thread = self.threading.currentThread()
sys.settrace(self._trace)
return self._trace
def stop(self):
"""Stop this Tracer."""
self.stopped = True
- if self.thread != threading.currentThread():
+ if self.threading and self.thread != self.threading.currentThread():
# Called on a different thread than started us: we can't unhook
# ourseves, but we've set the flag that we should stop, so we won't
# do any more tracing.
@@ -203,7 +186,7 @@ class PyTracer(object):
if sys.gettrace() != self._trace:
msg = "Trace function changed, measurement is likely wrong: %r"
self.warn(msg % (sys.gettrace(),))
- #print("Stopping tracer on %s" % threading.current_thread().ident)
+
sys.settrace(None)
def get_stats(self):
@@ -260,6 +243,7 @@ class Collector(object):
self.check_include = check_include
self.warn = warn
self.branch = branch
+ self.threading = None
if coroutine == "greenlet":
import greenlet
@@ -271,7 +255,12 @@ class Collector(object):
import gevent
self.coroutine_id_func = gevent.getcurrent
else:
+ # 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.coroutine_id_func = None
+ self.threading = threading
self.reset()
@@ -318,6 +307,8 @@ class Collector(object):
tracer.coroutine_id_func = self.coroutine_id_func
if hasattr(tracer, 'plugin_data'):
tracer.plugin_data = self.plugin_data
+ if hasattr(tracer, 'threading'):
+ tracer.threading = self.threading
fn = tracer.start()
self.tracers.append(tracer)
return fn
@@ -346,7 +337,6 @@ class Collector(object):
if self._collectors:
self._collectors[-1].pause()
self._collectors.append(self)
- #print("Started: %r" % self._collectors, file=sys.stderr)
# Check to see whether we had a fullcoverage tracer installed.
traces0 = []
@@ -371,11 +361,11 @@ class Collector(object):
# Install our installation tracer in threading, to jump start other
# threads.
- threading.settrace(self._installation_trace)
+ if self.threading:
+ self.threading.settrace(self._installation_trace)
def stop(self):
"""Stop collecting trace information."""
- #print >>sys.stderr, "Stopping: %r" % self._collectors
assert self._collectors
assert self._collectors[-1] is self
@@ -397,13 +387,17 @@ class Collector(object):
print("\nCoverage.py tracer stats:")
for k in sorted(stats.keys()):
print("%16s: %s" % (k, stats[k]))
- threading.settrace(None)
+ if self.threading:
+ self.threading.settrace(None)
def resume(self):
"""Resume tracing after a `pause`."""
for tracer in self.tracers:
tracer.start()
- threading.settrace(self._installation_trace)
+ if self.threading:
+ self.threading.settrace(self._installation_trace)
+ else:
+ self._start_tracer()
def get_line_data(self):
"""Return the line data collected.
diff --git a/tests/test_coroutine.py b/tests/test_coroutine.py
index fe6c8326..5668c110 100644
--- a/tests/test_coroutine.py
+++ b/tests/test_coroutine.py
@@ -2,7 +2,6 @@
import os.path, sys
-from nose.plugins.skip import SkipTest
import coverage
from tests.coveragetest import CoverageTest
@@ -96,6 +95,15 @@ class CoroutineTest(CoverageTest):
import gevent.queue as queue
""" + COMMON
+ # Uncomplicated code that doesn't use any of the coroutining stuff, to test
+ # the simple case under each of the regimes.
+ SIMPLE = """\
+ total = 0
+ for i in range({LIMIT}):
+ total += i
+ print(total)
+ """.format(LIMIT=LIMIT)
+
def try_some_code(self, code, args):
"""Run some coroutine testing code and see that it was all covered."""
@@ -122,22 +130,25 @@ class CoroutineTest(CoverageTest):
def test_threads(self):
self.try_some_code(self.THREAD, "")
- def test_eventlet(self):
- if eventlet is None:
- raise SkipTest("No eventlet available")
+ def test_threads_simple_code(self):
+ self.try_some_code(self.SIMPLE, "")
- self.try_some_code(self.EVENTLET, "--coroutine=eventlet")
+ if eventlet is not None:
+ def test_eventlet(self):
+ self.try_some_code(self.EVENTLET, "--coroutine=eventlet")
- def test_gevent(self):
- raise SkipTest("Still not sure why gevent isn't working...")
+ def test_eventlet_simple_code(self):
+ self.try_some_code(self.SIMPLE, "--coroutine=eventlet")
- if gevent is None:
- raise SkipTest("No gevent available")
+ if gevent is not None:
+ def test_gevent(self):
+ self.try_some_code(self.GEVENT, "--coroutine=gevent")
- self.try_some_code(self.GEVENT, "--coroutine=gevent")
+ def test_gevent_simple_code(self):
+ self.try_some_code(self.SIMPLE, "--coroutine=gevent")
def print_simple_annotation(code, linenos):
"""Print the lines in `code` with X for each line number in `linenos`."""
for lineno, line in enumerate(code.splitlines(), start=1):
- print(" {0:s} {1}".format("X" if lineno in linenos else " ", line))
+ print(" {0} {1}".format("X" if lineno in linenos else " ", line))