DebugClients/Python/coverage/collector.py

changeset 6219
d6c795b5ce33
parent 5178
878ce843ca9f
diff -r bedab77d0fa3 -r d6c795b5ce33 DebugClients/Python/coverage/collector.py
--- a/DebugClients/Python/coverage/collector.py	Sat Apr 07 13:17:06 2018 +0200
+++ b/DebugClients/Python/coverage/collector.py	Sat Apr 07 13:35:10 2018 +0200
@@ -7,7 +7,8 @@
 import sys
 
 from coverage import env
-from coverage.backward import iitems
+from coverage.backward import litems, range     # pylint: disable=redefined-builtin
+from coverage.debug import short_stack
 from coverage.files import abs_file
 from coverage.misc import CoverageException, isolate_module
 from coverage.pytracer import PyTracer
@@ -17,7 +18,7 @@
 
 try:
     # Use the C extension code when we can, for speed.
-    from coverage.tracer import CTracer, CFileDisposition   # 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':
@@ -42,6 +43,7 @@
     fn_name = frame.f_code.co_name
     if fn_name.startswith("test"):
         return fn_name
+    return None
 
 
 class Collector(object):
@@ -65,11 +67,14 @@
     # the top, and resumed when they become the top again.
     _collectors = []
 
+    # 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 file name, and returning a
-        `coverage.FileDisposition object`.
+        `should_trace` is a function, taking a file name and a frame, and
+        returning a `coverage.FileDisposition object`.
 
         `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.
@@ -83,12 +88,14 @@
         collecting data on which statements followed each other (arcs).  Use
         `get_arc_data` to get the arc data.
 
-        `warn` is a warning function, taking a single string message argument,
-        to be used if a warning needs to be issued.
+        `warn` is a warning function, taking a single string message argument
+        and an optional slug argument which will be a string or None, 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
@@ -96,21 +103,28 @@
         self.warn = warn
         self.branch = branch
         self.threading = None
-        self.concurrency = concurrency
+
+        self.origin = short_stack()
 
         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":
+            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.
@@ -120,7 +134,9 @@
                 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
@@ -151,6 +167,13 @@
         """Return the class name of the tracer we're using."""
         return self._trace_class.__name__
 
+    def _clear_data(self):
+        """Clear out existing data, but stay ready for more collection."""
+        self.data.clear()
+
+        for tracer in self.tracers:
+            tracer.reset_activity()
+
     def reset(self):
         """Clear collected data, and prepare to collect more."""
         # A dictionary mapping file names to dicts with line number keys (if not
@@ -197,6 +220,8 @@
         # Our active Tracers.
         self.tracers = []
 
+        self._clear_data()
+
     def _start_tracer(self):
         """Start a new Tracer object, and store it in self.tracers."""
         tracer = self._trace_class()
@@ -256,6 +281,8 @@
         if self._collectors:
             self._collectors[-1].pause()
 
+        self.tracers = []
+
         # Check to see whether we had a fullcoverage tracer installed. If so,
         # get the stack frames it stashed away for us.
         traces0 = []
@@ -285,7 +312,7 @@
             except TypeError:
                 raise Exception("fullcoverage must be run with the C trace function.")
 
-        # Install our installation tracer in threading, to jump start other
+        # Install our installation tracer in threading, to jump-start other
         # threads.
         if self.threading:
             self.threading.settrace(self._installation_trace)
@@ -293,12 +320,15 @@
     def stop(self):
         """Stop collecting trace information."""
         assert self._collectors
+        if self._collectors[-1] is not self:
+            print("self._collectors:")
+            for c in self._collectors:
+                print("  {!r}\n{}".format(c, c.origin))
         assert self._collectors[-1] is self, (
             "Expected current collector to be %r, but it's %r" % (self, self._collectors[-1])
         )
 
         self.pause()
-        self.tracers = []
 
         # Remove this Collector from the stack, and resume the one underneath
         # (if any).
@@ -327,6 +357,14 @@
         else:
             self._start_tracer()
 
+    def _activity(self):
+        """Has any activity been traced?
+
+        Returns a boolean, True if any trace function was invoked.
+
+        """
+        return any(tracer.activity() for tracer in self.tracers)
+
     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
@@ -338,12 +376,29 @@
     def save_data(self, covdata):
         """Save the collected data to a `CoverageData`.
 
-        Also resets the collector.
+        Returns True if there was data to save, False if not.
+        """
+        if not self._activity():
+            return False
 
-        """
         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))
+            # The call to litems() ensures that the GIL protects the dictionary
+            # iterator against concurrent modifications by tracers running
+            # in other threads. We try three times in case of concurrent
+            # access, hoping to get a clean copy.
+            runtime_err = None
+            for _ in range(3):
+                try:
+                    items = litems(d)
+                except RuntimeError as ex:
+                    runtime_err = ex
+                else:
+                    break
+            else:
+                raise runtime_err       # pylint: disable=raising-bad-type
+
+            return dict((abs_file(k), v) for k, v in items)
 
         if self.branch:
             covdata.add_arcs(abs_file_dict(self.data))
@@ -358,4 +413,5 @@
             with open(out_file, "w") as wtw_out:
                 pprint.pprint(self.contexts, wtw_out)
 
-        self.reset()
+        self._clear_data()
+        return True

eric ide

mercurial