1"""Raw data collector for Coverage.""" 2 3import os, sys, threading 4 5try: 6 # Use the C extension code when we can, for speed. 7 from coverage.tracer import CTracer # pylint: disable=F0401,E0611 8except ImportError: 9 # Couldn't import the C extension, maybe it isn't built. 10 if os.getenv('COVERAGE_TEST_TRACER') == 'c': 11 # During testing, we use the COVERAGE_TEST_TRACER env var to indicate 12 # that we've fiddled with the environment to test this fallback code. 13 # If we thought we had a C tracer, but couldn't import it, then exit 14 # quickly and clearly instead of dribbling confusing errors. I'm using 15 # sys.exit here instead of an exception because an exception here 16 # causes all sorts of other noise in unittest. 17 sys.stderr.write( 18 "*** COVERAGE_TEST_TRACER is 'c' but can't import CTracer!\n" 19 ) 20 sys.exit(1) 21 CTracer = None 22 23 24class PyTracer(object): 25 """Python implementation of the raw data tracer.""" 26 27 # Because of poor implementations of trace-function-manipulating tools, 28 # the Python trace function must be kept very simple. In particular, there 29 # must be only one function ever set as the trace function, both through 30 # sys.settrace, and as the return value from the trace function. Put 31 # another way, the trace function must always return itself. It cannot 32 # swap in other functions, or return None to avoid tracing a particular 33 # frame. 34 # 35 # The trace manipulator that introduced this restriction is DecoratorTools, 36 # which sets a trace function, and then later restores the pre-existing one 37 # by calling sys.settrace with a function it found in the current frame. 38 # 39 # Systems that use DecoratorTools (or similar trace manipulations) must use 40 # PyTracer to get accurate results. The command-line --timid argument is 41 # used to force the use of this tracer. 42 43 def __init__(self): 44 self.data = None 45 self.should_trace = None 46 self.should_trace_cache = None 47 self.warn = None 48 self.cur_file_data = None 49 self.last_line = 0 50 self.data_stack = [] 51 self.last_exc_back = None 52 self.last_exc_firstlineno = 0 53 self.arcs = False 54 self.thread = None 55 self.stopped = False 56 57 def _trace(self, frame, event, arg_unused): 58 """The trace function passed to sys.settrace.""" 59 60 if self.stopped: 61 return 62 63 if 0: 64 sys.stderr.write("trace event: %s %r @%d\n" % ( 65 event, frame.f_code.co_filename, frame.f_lineno 66 )) 67 68 if self.last_exc_back: 69 if frame == self.last_exc_back: 70 # Someone forgot a return event. 71 if self.arcs and self.cur_file_data: 72 pair = (self.last_line, -self.last_exc_firstlineno) 73 self.cur_file_data[pair] = None 74 self.cur_file_data, self.last_line = self.data_stack.pop() 75 self.last_exc_back = None 76 77 if event == 'call': 78 # Entering a new function context. Decide if we should trace 79 # in this file. 80 self.data_stack.append((self.cur_file_data, self.last_line)) 81 filename = frame.f_code.co_filename 82 if filename not in self.should_trace_cache: 83 tracename = self.should_trace(filename, frame) 84 self.should_trace_cache[filename] = tracename 85 else: 86 tracename = self.should_trace_cache[filename] 87 #print("called, stack is %d deep, tracename is %r" % ( 88 # len(self.data_stack), tracename)) 89 if tracename: 90 if tracename not in self.data: 91 self.data[tracename] = {} 92 self.cur_file_data = self.data[tracename] 93 else: 94 self.cur_file_data = None 95 # Set the last_line to -1 because the next arc will be entering a 96 # code block, indicated by (-1, n). 97 self.last_line = -1 98 elif event == 'line': 99 # Record an executed line. 100 if self.cur_file_data is not None: 101 if self.arcs: 102 #print("lin", self.last_line, frame.f_lineno) 103 self.cur_file_data[(self.last_line, frame.f_lineno)] = None 104 else: 105 #print("lin", frame.f_lineno) 106 self.cur_file_data[frame.f_lineno] = None 107 self.last_line = frame.f_lineno 108 elif event == 'return': 109 if self.arcs and self.cur_file_data: 110 first = frame.f_code.co_firstlineno 111 self.cur_file_data[(self.last_line, -first)] = None 112 # Leaving this function, pop the filename stack. 113 self.cur_file_data, self.last_line = self.data_stack.pop() 114 #print("returned, stack is %d deep" % (len(self.data_stack))) 115 elif event == 'exception': 116 #print("exc", self.last_line, frame.f_lineno) 117 self.last_exc_back = frame.f_back 118 self.last_exc_firstlineno = frame.f_code.co_firstlineno 119 return self._trace 120 121 def start(self): 122 """Start this Tracer. 123 124 Return a Python function suitable for use with sys.settrace(). 125 126 """ 127 self.thread = threading.currentThread() 128 sys.settrace(self._trace) 129 return self._trace 130 131 def stop(self): 132 """Stop this Tracer.""" 133 self.stopped = True 134 if self.thread != threading.currentThread(): 135 # Called on a different thread than started us: we can't unhook 136 # ourseves, but we've set the flag that we should stop, so we won't 137 # do any more tracing. 138 return 139 140 if hasattr(sys, "gettrace") and self.warn: 141 if sys.gettrace() != self._trace: 142 msg = "Trace function changed, measurement is likely wrong: %r" 143 self.warn(msg % (sys.gettrace(),)) 144 #print("Stopping tracer on %s" % threading.current_thread().ident) 145 sys.settrace(None) 146 147 def get_stats(self): 148 """Return a dictionary of statistics, or None.""" 149 return None 150 151 152class Collector(object): 153 """Collects trace data. 154 155 Creates a Tracer object for each thread, since they track stack 156 information. Each Tracer points to the same shared data, contributing 157 traced data points. 158 159 When the Collector is started, it creates a Tracer for the current thread, 160 and installs a function to create Tracers for each new thread started. 161 When the Collector is stopped, all active Tracers are stopped. 162 163 Threads started while the Collector is stopped will never have Tracers 164 associated with them. 165 166 """ 167 168 # The stack of active Collectors. Collectors are added here when started, 169 # and popped when stopped. Collectors on the stack are paused when not 170 # the top, and resumed when they become the top again. 171 _collectors = [] 172 173 def __init__(self, should_trace, timid, branch, warn): 174 """Create a collector. 175 176 `should_trace` is a function, taking a filename, and returning a 177 canonicalized filename, or None depending on whether the file should 178 be traced or not. 179 180 If `timid` is true, then a slower simpler trace function will be 181 used. This is important for some environments where manipulation of 182 tracing functions make the faster more sophisticated trace function not 183 operate properly. 184 185 If `branch` is true, then branches will be measured. This involves 186 collecting data on which statements followed each other (arcs). Use 187 `get_arc_data` to get the arc data. 188 189 `warn` is a warning function, taking a single string message argument, 190 to be used if a warning needs to be issued. 191 192 """ 193 self.should_trace = should_trace 194 self.warn = warn 195 self.branch = branch 196 self.reset() 197 198 if timid: 199 # Being timid: use the simple Python trace function. 200 self._trace_class = PyTracer 201 else: 202 # Being fast: use the C Tracer if it is available, else the Python 203 # trace function. 204 self._trace_class = CTracer or PyTracer 205 206 def __repr__(self): 207 return "<Collector at 0x%x>" % id(self) 208 209 def tracer_name(self): 210 """Return the class name of the tracer we're using.""" 211 return self._trace_class.__name__ 212 213 def reset(self): 214 """Clear collected data, and prepare to collect more.""" 215 # A dictionary mapping filenames to dicts with linenumber keys, 216 # or mapping filenames to dicts with linenumber pairs as keys. 217 self.data = {} 218 219 # A cache of the results from should_trace, the decision about whether 220 # to trace execution in a file. A dict of filename to (filename or 221 # None). 222 self.should_trace_cache = {} 223 224 # Our active Tracers. 225 self.tracers = [] 226 227 def _start_tracer(self): 228 """Start a new Tracer object, and store it in self.tracers.""" 229 tracer = self._trace_class() 230 tracer.data = self.data 231 tracer.arcs = self.branch 232 tracer.should_trace = self.should_trace 233 tracer.should_trace_cache = self.should_trace_cache 234 tracer.warn = self.warn 235 fn = tracer.start() 236 self.tracers.append(tracer) 237 return fn 238 239 # The trace function has to be set individually on each thread before 240 # execution begins. Ironically, the only support the threading module has 241 # for running code before the thread main is the tracing function. So we 242 # install this as a trace function, and the first time it's called, it does 243 # the real trace installation. 244 245 def _installation_trace(self, frame_unused, event_unused, arg_unused): 246 """Called on new threads, installs the real tracer.""" 247 # Remove ourselves as the trace function 248 sys.settrace(None) 249 # Install the real tracer. 250 fn = self._start_tracer() 251 # Invoke the real trace function with the current event, to be sure 252 # not to lose an event. 253 if fn: 254 fn = fn(frame_unused, event_unused, arg_unused) 255 # Return the new trace function to continue tracing in this scope. 256 return fn 257 258 def start(self): 259 """Start collecting trace information.""" 260 if self._collectors: 261 self._collectors[-1].pause() 262 self._collectors.append(self) 263 #print("Started: %r" % self._collectors, file=sys.stderr) 264 265 # Check to see whether we had a fullcoverage tracer installed. 266 traces0 = [] 267 if hasattr(sys, "gettrace"): 268 fn0 = sys.gettrace() 269 if fn0: 270 tracer0 = getattr(fn0, '__self__', None) 271 if tracer0: 272 traces0 = getattr(tracer0, 'traces', []) 273 274 # Install the tracer on this thread. 275 fn = self._start_tracer() 276 277 for args in traces0: 278 (frame, event, arg), lineno = args 279 try: 280 fn(frame, event, arg, lineno=lineno) 281 except TypeError: 282 raise Exception( 283 "fullcoverage must be run with the C trace function." 284 ) 285 286 # Install our installation tracer in threading, to jump start other 287 # threads. 288 threading.settrace(self._installation_trace) 289 290 def stop(self): 291 """Stop collecting trace information.""" 292 #print >>sys.stderr, "Stopping: %r" % self._collectors 293 assert self._collectors 294 assert self._collectors[-1] is self 295 296 self.pause() 297 self.tracers = [] 298 299 # Remove this Collector from the stack, and resume the one underneath 300 # (if any). 301 self._collectors.pop() 302 if self._collectors: 303 self._collectors[-1].resume() 304 305 def pause(self): 306 """Pause tracing, but be prepared to `resume`.""" 307 for tracer in self.tracers: 308 tracer.stop() 309 stats = tracer.get_stats() 310 if stats: 311 print("\nCoverage.py tracer stats:") 312 for k in sorted(stats.keys()): 313 print("%16s: %s" % (k, stats[k])) 314 threading.settrace(None) 315 316 def resume(self): 317 """Resume tracing after a `pause`.""" 318 for tracer in self.tracers: 319 tracer.start() 320 threading.settrace(self._installation_trace) 321 322 def get_line_data(self): 323 """Return the line data collected. 324 325 Data is { filename: { lineno: None, ...}, ...} 326 327 """ 328 if self.branch: 329 # If we were measuring branches, then we have to re-build the dict 330 # to show line data. 331 line_data = {} 332 for f, arcs in self.data.items(): 333 line_data[f] = ldf = {} 334 for l1, _ in list(arcs.keys()): 335 if l1: 336 ldf[l1] = None 337 return line_data 338 else: 339 return self.data 340 341 def get_arc_data(self): 342 """Return the arc data collected. 343 344 Data is { filename: { (l1, l2): None, ...}, ...} 345 346 Note that no data is collected or returned if the Collector wasn't 347 created with `branch` true. 348 349 """ 350 if self.branch: 351 return self.data 352 else: 353 return {} 354