diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2014-09-17 20:17:09 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2014-09-17 20:17:09 -0400 |
commit | 8b3265d3482baf285fe1061ff285f73f2a818b26 (patch) | |
tree | 40e8c4180b01d802bc1d84ab6479ef2a9b307dfd | |
parent | aaa309565ee0cadff04ca844bac3d8e9cf17489d (diff) | |
download | python-coveragepy-git-8b3265d3482baf285fe1061ff285f73f2a818b26.tar.gz |
Gevent, etc, support finally works. #149
-rw-r--r-- | AUTHORS.txt | 1 | ||||
-rw-r--r-- | CHANGES.txt | 5 | ||||
-rw-r--r-- | coverage/collector.py | 66 | ||||
-rw-r--r-- | tests/test_coroutine.py | 33 |
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)) |