1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 2# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 3 4"""Raw data collector for coverage.py.""" 5 6import atexit 7import dis 8import sys 9 10from coverage import env 11 12# We need the YIELD_VALUE opcode below, in a comparison-friendly form. 13YIELD_VALUE = dis.opmap['YIELD_VALUE'] 14if env.PY2: 15 YIELD_VALUE = chr(YIELD_VALUE) 16 17 18class PyTracer(object): 19 """Python implementation of the raw data tracer.""" 20 21 # Because of poor implementations of trace-function-manipulating tools, 22 # the Python trace function must be kept very simple. In particular, there 23 # must be only one function ever set as the trace function, both through 24 # sys.settrace, and as the return value from the trace function. Put 25 # another way, the trace function must always return itself. It cannot 26 # swap in other functions, or return None to avoid tracing a particular 27 # frame. 28 # 29 # The trace manipulator that introduced this restriction is DecoratorTools, 30 # which sets a trace function, and then later restores the pre-existing one 31 # by calling sys.settrace with a function it found in the current frame. 32 # 33 # Systems that use DecoratorTools (or similar trace manipulations) must use 34 # PyTracer to get accurate results. The command-line --timid argument is 35 # used to force the use of this tracer. 36 37 def __init__(self): 38 # Attributes set from the collector: 39 self.data = None 40 self.trace_arcs = False 41 self.should_trace = None 42 self.should_trace_cache = None 43 self.should_start_context = None 44 self.warn = None 45 # The threading module to use, if any. 46 self.threading = None 47 48 self.cur_file_dict = None 49 self.last_line = 0 # int, but uninitialized. 50 self.cur_file_name = None 51 self.context = None 52 self.started_context = False 53 54 self.data_stack = [] 55 self.last_exc_back = None 56 self.last_exc_firstlineno = 0 57 self.thread = None 58 self.stopped = False 59 self._activity = False 60 61 self.in_atexit = False 62 # On exit, self.in_atexit = True 63 atexit.register(setattr, self, 'in_atexit', True) 64 65 def __repr__(self): 66 return "<PyTracer at {}: {} lines in {} files>".format( 67 id(self), 68 sum(len(v) for v in self.data.values()), 69 len(self.data), 70 ) 71 72 def log(self, marker, *args): 73 """For hard-core logging of what this tracer is doing.""" 74 with open("/tmp/debug_trace.txt", "a") as f: 75 f.write("{} {:x}.{:x}[{}] {:x} {}\n".format( 76 marker, 77 id(self), 78 self.thread.ident, 79 len(self.data_stack), 80 self.threading.currentThread().ident, 81 " ".join(map(str, args)) 82 )) 83 84 def _trace(self, frame, event, arg_unused): 85 """The trace function passed to sys.settrace.""" 86 87 #self.log(":", frame.f_code.co_filename, frame.f_lineno, event) 88 89 if (self.stopped and sys.gettrace() == self._trace): # pylint: disable=comparison-with-callable 90 # The PyTrace.stop() method has been called, possibly by another 91 # thread, let's deactivate ourselves now. 92 #self.log("X", frame.f_code.co_filename, frame.f_lineno) 93 sys.settrace(None) 94 return None 95 96 if self.last_exc_back: 97 if frame == self.last_exc_back: 98 # Someone forgot a return event. 99 if self.trace_arcs and self.cur_file_dict: 100 pair = (self.last_line, -self.last_exc_firstlineno) 101 self.cur_file_dict[pair] = None 102 self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = ( 103 self.data_stack.pop() 104 ) 105 self.last_exc_back = None 106 107 if event == 'call': 108 # Should we start a new context? 109 if self.should_start_context and self.context is None: 110 context_maybe = self.should_start_context(frame) # pylint: disable=not-callable 111 if context_maybe is not None: 112 self.context = context_maybe 113 self.started_context = True 114 self.switch_context(self.context) 115 else: 116 self.started_context = False 117 else: 118 self.started_context = False 119 120 # Entering a new frame. Decide if we should trace 121 # in this file. 122 self._activity = True 123 self.data_stack.append( 124 ( 125 self.cur_file_dict, 126 self.cur_file_name, 127 self.last_line, 128 self.started_context, 129 ) 130 ) 131 filename = frame.f_code.co_filename 132 self.cur_file_name = filename 133 disp = self.should_trace_cache.get(filename) 134 if disp is None: 135 disp = self.should_trace(filename, frame) # pylint: disable=not-callable 136 self.should_trace_cache[filename] = disp # pylint: disable=unsupported-assignment-operation 137 138 self.cur_file_dict = None 139 if disp.trace: 140 tracename = disp.source_filename 141 if tracename not in self.data: # pylint: disable=unsupported-membership-test 142 self.data[tracename] = {} # pylint: disable=unsupported-assignment-operation 143 self.cur_file_dict = self.data[tracename] # pylint: disable=unsubscriptable-object 144 # The call event is really a "start frame" event, and happens for 145 # function calls and re-entering generators. The f_lasti field is 146 # -1 for calls, and a real offset for generators. Use <0 as the 147 # line number for calls, and the real line number for generators. 148 if getattr(frame, 'f_lasti', -1) < 0: 149 self.last_line = -frame.f_code.co_firstlineno 150 else: 151 self.last_line = frame.f_lineno 152 elif event == 'line': 153 # Record an executed line. 154 if self.cur_file_dict is not None: 155 lineno = frame.f_lineno 156 #if frame.f_code.co_filename != self.cur_file_name: 157 # self.log("*", frame.f_code.co_filename, self.cur_file_name, lineno) 158 if self.trace_arcs: 159 self.cur_file_dict[(self.last_line, lineno)] = None 160 else: 161 self.cur_file_dict[lineno] = None 162 self.last_line = lineno 163 elif event == 'return': 164 if self.trace_arcs and self.cur_file_dict: 165 # Record an arc leaving the function, but beware that a 166 # "return" event might just mean yielding from a generator. 167 # Jython seems to have an empty co_code, so just assume return. 168 code = frame.f_code.co_code 169 if (not code) or code[frame.f_lasti] != YIELD_VALUE: 170 first = frame.f_code.co_firstlineno 171 self.cur_file_dict[(self.last_line, -first)] = None 172 # Leaving this function, pop the filename stack. 173 self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = ( 174 self.data_stack.pop() 175 ) 176 # Leaving a context? 177 if self.started_context: 178 self.context = None 179 self.switch_context(None) 180 elif event == 'exception': 181 self.last_exc_back = frame.f_back 182 self.last_exc_firstlineno = frame.f_code.co_firstlineno 183 return self._trace 184 185 def start(self): 186 """Start this Tracer. 187 188 Return a Python function suitable for use with sys.settrace(). 189 190 """ 191 self.stopped = False 192 if self.threading: 193 if self.thread is None: 194 self.thread = self.threading.currentThread() 195 else: 196 if self.thread.ident != self.threading.currentThread().ident: 197 # Re-starting from a different thread!? Don't set the trace 198 # function, but we are marked as running again, so maybe it 199 # will be ok? 200 #self.log("~", "starting on different threads") 201 return self._trace 202 203 sys.settrace(self._trace) 204 return self._trace 205 206 def stop(self): 207 """Stop this Tracer.""" 208 # Get the active tracer callback before setting the stop flag to be 209 # able to detect if the tracer was changed prior to stopping it. 210 tf = sys.gettrace() 211 212 # Set the stop flag. The actual call to sys.settrace(None) will happen 213 # in the self._trace callback itself to make sure to call it from the 214 # right thread. 215 self.stopped = True 216 217 if self.threading and self.thread.ident != self.threading.currentThread().ident: 218 # Called on a different thread than started us: we can't unhook 219 # ourselves, but we've set the flag that we should stop, so we 220 # won't do any more tracing. 221 #self.log("~", "stopping on different threads") 222 return 223 224 if self.warn: 225 # PyPy clears the trace function before running atexit functions, 226 # so don't warn if we are in atexit on PyPy and the trace function 227 # has changed to None. 228 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) 229 if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable 230 self.warn( # pylint: disable=not-callable 231 "Trace function changed, measurement is likely wrong: %r" % (tf,), 232 slug="trace-changed", 233 ) 234 235 def activity(self): 236 """Has there been any activity?""" 237 return self._activity 238 239 def reset_activity(self): 240 """Reset the activity() flag.""" 241 self._activity = False 242 243 def get_stats(self): 244 """Return a dictionary of statistics, or None.""" 245 return None 246