DebugClients/Python/coverage/pytracer.py

branch
maintenance
changeset 6273
0daf79d65080
parent 6219
d6c795b5ce33
--- a/DebugClients/Python/coverage/pytracer.py	Mon Apr 02 12:04:56 2018 +0200
+++ b/DebugClients/Python/coverage/pytracer.py	Tue May 01 12:03:52 2018 +0200
@@ -3,6 +3,7 @@
 
 """Raw data collector for coverage.py."""
 
+import atexit
 import dis
 import sys
 
@@ -43,27 +44,51 @@
         # The threading module to use, if any.
         self.threading = None
 
-        self.cur_file_dict = []
-        self.last_line = [0]
+        self.cur_file_dict = None
+        self.last_line = 0          # int, but uninitialized.
+        self.cur_file_name = None
 
         self.data_stack = []
         self.last_exc_back = None
         self.last_exc_firstlineno = 0
         self.thread = None
         self.stopped = False
+        self._activity = False
+
+        self.in_atexit = False
+        # On exit, self.in_atexit = True
+        atexit.register(setattr, self, 'in_atexit', True)
 
     def __repr__(self):
-        return "<PyTracer at 0x{0:0x}: {1} lines in {2} files>".format(
+        return "<PyTracer at {0}: {1} lines in {2} files>".format(
             id(self),
             sum(len(v) for v in self.data.values()),
             len(self.data),
         )
 
+    def log(self, marker, *args):
+        """For hard-core logging of what this tracer is doing."""
+        with open("/tmp/debug_trace.txt", "a") as f:
+            f.write("{} {:x}.{:x}[{}] {:x} {}\n".format(
+                marker,
+                id(self),
+                self.thread.ident,
+                len(self.data_stack),
+                self.threading.currentThread().ident,
+                " ".join(map(str, args))
+            ))
+
     def _trace(self, frame, event, arg_unused):
         """The trace function passed to sys.settrace."""
 
-        if self.stopped:
-            return
+        #self.log(":", frame.f_code.co_filename, frame.f_lineno, event)
+
+        if (self.stopped and sys.gettrace() == self._trace):
+            # The PyTrace.stop() method has been called, possibly by another
+            # thread, let's deactivate ourselves now.
+            #self.log("X", frame.f_code.co_filename, frame.f_lineno)
+            sys.settrace(None)
+            return None
 
         if self.last_exc_back:
             if frame == self.last_exc_back:
@@ -71,14 +96,16 @@
                 if self.trace_arcs and self.cur_file_dict:
                     pair = (self.last_line, -self.last_exc_firstlineno)
                     self.cur_file_dict[pair] = None
-                self.cur_file_dict, self.last_line = self.data_stack.pop()
+                self.cur_file_dict, self.cur_file_name, self.last_line = self.data_stack.pop()
             self.last_exc_back = None
 
         if event == 'call':
             # Entering a new function context.  Decide if we should trace
             # in this file.
-            self.data_stack.append((self.cur_file_dict, self.last_line))
+            self._activity = True
+            self.data_stack.append((self.cur_file_dict, self.cur_file_name, self.last_line))
             filename = frame.f_code.co_filename
+            self.cur_file_name = filename
             disp = self.should_trace_cache.get(filename)
             if disp is None:
                 disp = self.should_trace(filename, frame)
@@ -94,7 +121,7 @@
             # function calls and re-entering generators.  The f_lasti field is
             # -1 for calls, and a real offset for generators.  Use <0 as the
             # line number for calls, and the real line number for generators.
-            if frame.f_lasti < 0:
+            if getattr(frame, 'f_lasti', -1) < 0:
                 self.last_line = -frame.f_code.co_firstlineno
             else:
                 self.last_line = frame.f_lineno
@@ -102,6 +129,8 @@
             # Record an executed line.
             if self.cur_file_dict is not None:
                 lineno = frame.f_lineno
+                #if frame.f_code.co_filename != self.cur_file_name:
+                #    self.log("*", frame.f_code.co_filename, self.cur_file_name, lineno)
                 if self.trace_arcs:
                     self.cur_file_dict[(self.last_line, lineno)] = None
                 else:
@@ -111,12 +140,13 @@
             if self.trace_arcs and self.cur_file_dict:
                 # Record an arc leaving the function, but beware that a
                 # "return" event might just mean yielding from a generator.
-                bytecode = frame.f_code.co_code[frame.f_lasti]
-                if bytecode != YIELD_VALUE:
+                # Jython seems to have an empty co_code, so just assume return.
+                code = frame.f_code.co_code
+                if (not code) or code[frame.f_lasti] != YIELD_VALUE:
                     first = frame.f_code.co_firstlineno
                     self.cur_file_dict[(self.last_line, -first)] = None
             # Leaving this function, pop the filename stack.
-            self.cur_file_dict, self.last_line = self.data_stack.pop()
+            self.cur_file_dict, self.cur_file_name, self.last_line = self.data_stack.pop()
         elif event == 'exception':
             self.last_exc_back = frame.f_back
             self.last_exc_firstlineno = frame.f_code.co_firstlineno
@@ -128,27 +158,57 @@
         Return a Python function suitable for use with sys.settrace().
 
         """
+        self.stopped = False
         if self.threading:
-            self.thread = self.threading.currentThread()
+            if self.thread is None:
+                self.thread = self.threading.currentThread()
+            else:
+                if self.thread.ident != self.threading.currentThread().ident:
+                    # Re-starting from a different thread!? Don't set the trace
+                    # function, but we are marked as running again, so maybe it
+                    # will be ok?
+                    #self.log("~", "starting on different threads")
+                    return self._trace
+
         sys.settrace(self._trace)
-        self.stopped = False
         return self._trace
 
     def stop(self):
         """Stop this Tracer."""
+        # Get the activate tracer callback before setting the stop flag to be
+        # able to detect if the tracer was changed prior to stopping it.
+        tf = sys.gettrace()
+
+        # Set the stop flag. The actual call to sys.settrace(None) will happen
+        # in the self._trace callback itself to make sure to call it from the
+        # right thread.
         self.stopped = True
+
         if self.threading and self.thread.ident != self.threading.currentThread().ident:
             # Called on a different thread than started us: we can't unhook
             # ourselves, but we've set the flag that we should stop, so we
             # won't do any more tracing.
+            #self.log("~", "stopping on different threads")
             return
 
         if self.warn:
-            if sys.gettrace() != self._trace:
-                msg = "Trace function changed, measurement is likely wrong: %r"
-                self.warn(msg % (sys.gettrace(),))
+            # PyPy clears the trace function before running atexit functions,
+            # so don't warn if we are in atexit on PyPy and the trace function
+            # has changed to None.
+            dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None)
+            if (not dont_warn) and tf != self._trace:
+                self.warn(
+                    "Trace function changed, measurement is likely wrong: %r" % (tf,),
+                    slug="trace-changed",
+                )
 
-        sys.settrace(None)
+    def activity(self):
+        """Has there been any activity?"""
+        return self._activity
+
+    def reset_activity(self):
+        """Reset the activity() flag."""
+        self._activity = False
 
     def get_stats(self):
         """Return a dictionary of statistics, or None."""

eric ide

mercurial