|
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 |
|
6 import atexit |
|
7 import dis |
|
8 import sys |
|
9 |
|
10 from coverage import env |
|
11 |
|
12 # We need the YIELD_VALUE opcode below, in a comparison-friendly form. |
|
13 YIELD_VALUE = dis.opmap['YIELD_VALUE'] |
|
14 if env.PY2: |
|
15 YIELD_VALUE = chr(YIELD_VALUE) |
|
16 |
|
17 |
|
18 class 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 |