9 |
9 |
10 from coverage import env |
10 from coverage import env |
11 |
11 |
12 # We need the YIELD_VALUE opcode below, in a comparison-friendly form. |
12 # We need the YIELD_VALUE opcode below, in a comparison-friendly form. |
13 YIELD_VALUE = dis.opmap['YIELD_VALUE'] |
13 YIELD_VALUE = dis.opmap['YIELD_VALUE'] |
14 if env.PY2: |
|
15 YIELD_VALUE = chr(YIELD_VALUE) |
|
16 |
14 |
17 # When running meta-coverage, this file can try to trace itself, which confuses |
15 # When running meta-coverage, this file can try to trace itself, which confuses |
18 # everything. Don't trace ourselves. |
16 # everything. Don't trace ourselves. |
19 |
17 |
20 THIS_FILE = __file__.rstrip("co") |
18 THIS_FILE = __file__.rstrip("co") |
21 |
19 |
22 |
20 |
23 class PyTracer(object): |
21 class PyTracer: |
24 """Python implementation of the raw data tracer.""" |
22 """Python implementation of the raw data tracer.""" |
25 |
23 |
26 # Because of poor implementations of trace-function-manipulating tools, |
24 # Because of poor implementations of trace-function-manipulating tools, |
27 # the Python trace function must be kept very simple. In particular, there |
25 # the Python trace function must be kept very simple. In particular, there |
28 # must be only one function ever set as the trace function, both through |
26 # must be only one function ever set as the trace function, both through |
48 self.should_start_context = None |
46 self.should_start_context = None |
49 self.warn = None |
47 self.warn = None |
50 # The threading module to use, if any. |
48 # The threading module to use, if any. |
51 self.threading = None |
49 self.threading = None |
52 |
50 |
53 self.cur_file_dict = None |
51 self.cur_file_data = None |
54 self.last_line = 0 # int, but uninitialized. |
52 self.last_line = 0 # int, but uninitialized. |
55 self.cur_file_name = None |
53 self.cur_file_name = None |
56 self.context = None |
54 self.context = None |
57 self.started_context = False |
55 self.started_context = False |
58 |
56 |
59 self.data_stack = [] |
57 self.data_stack = [] |
60 self.last_exc_back = None |
|
61 self.last_exc_firstlineno = 0 |
|
62 self.thread = None |
58 self.thread = None |
63 self.stopped = False |
59 self.stopped = False |
64 self._activity = False |
60 self._activity = False |
65 |
61 |
66 self.in_atexit = False |
62 self.in_atexit = False |
113 f = frame |
109 f = frame |
114 while f: |
110 while f: |
115 self.log(">", f.f_code.co_filename, f.f_lineno, f.f_code.co_name, f.f_trace) |
111 self.log(">", f.f_code.co_filename, f.f_lineno, f.f_code.co_name, f.f_trace) |
116 f = f.f_back |
112 f = f.f_back |
117 sys.settrace(None) |
113 sys.settrace(None) |
118 self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = ( |
114 self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( |
119 self.data_stack.pop() |
115 self.data_stack.pop() |
120 ) |
116 ) |
121 return None |
117 return None |
122 |
|
123 if self.last_exc_back: |
|
124 if frame == self.last_exc_back: |
|
125 # Someone forgot a return event. |
|
126 if self.trace_arcs and self.cur_file_dict: |
|
127 pair = (self.last_line, -self.last_exc_firstlineno) |
|
128 self.cur_file_dict[pair] = None |
|
129 self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = ( |
|
130 self.data_stack.pop() |
|
131 ) |
|
132 self.last_exc_back = None |
|
133 |
118 |
134 # if event != 'call' and frame.f_code.co_filename != self.cur_file_name: |
119 # if event != 'call' and frame.f_code.co_filename != self.cur_file_name: |
135 # self.log("---\n*", frame.f_code.co_filename, self.cur_file_name, frame.f_lineno) |
120 # self.log("---\n*", frame.f_code.co_filename, self.cur_file_name, frame.f_lineno) |
136 |
121 |
137 if event == 'call': |
122 if event == 'call': |
163 disp = self.should_trace_cache.get(filename) |
148 disp = self.should_trace_cache.get(filename) |
164 if disp is None: |
149 if disp is None: |
165 disp = self.should_trace(filename, frame) |
150 disp = self.should_trace(filename, frame) |
166 self.should_trace_cache[filename] = disp |
151 self.should_trace_cache[filename] = disp |
167 |
152 |
168 self.cur_file_dict = None |
153 self.cur_file_data = None |
169 if disp.trace: |
154 if disp.trace: |
170 tracename = disp.source_filename |
155 tracename = disp.source_filename |
171 if tracename not in self.data: |
156 if tracename not in self.data: |
172 self.data[tracename] = {} |
157 self.data[tracename] = set() |
173 self.cur_file_dict = self.data[tracename] |
158 self.cur_file_data = self.data[tracename] |
174 # The call event is really a "start frame" event, and happens for |
159 # The call event is really a "start frame" event, and happens for |
175 # function calls and re-entering generators. The f_lasti field is |
160 # function calls and re-entering generators. The f_lasti field is |
176 # -1 for calls, and a real offset for generators. Use <0 as the |
161 # -1 for calls, and a real offset for generators. Use <0 as the |
177 # line number for calls, and the real line number for generators. |
162 # line number for calls, and the real line number for generators. |
178 if getattr(frame, 'f_lasti', -1) < 0: |
163 if getattr(frame, 'f_lasti', -1) < 0: |
179 self.last_line = -frame.f_code.co_firstlineno |
164 self.last_line = -frame.f_code.co_firstlineno |
180 else: |
165 else: |
181 self.last_line = frame.f_lineno |
166 self.last_line = frame.f_lineno |
182 elif event == 'line': |
167 elif event == 'line': |
183 # Record an executed line. |
168 # Record an executed line. |
184 if self.cur_file_dict is not None: |
169 if self.cur_file_data is not None: |
185 lineno = frame.f_lineno |
170 lineno = frame.f_lineno |
186 |
171 |
187 if self.trace_arcs: |
172 if self.trace_arcs: |
188 self.cur_file_dict[(self.last_line, lineno)] = None |
173 self.cur_file_data.add((self.last_line, lineno)) |
189 else: |
174 else: |
190 self.cur_file_dict[lineno] = None |
175 self.cur_file_data.add(lineno) |
191 self.last_line = lineno |
176 self.last_line = lineno |
192 elif event == 'return': |
177 elif event == 'return': |
193 if self.trace_arcs and self.cur_file_dict: |
178 if self.trace_arcs and self.cur_file_data: |
194 # Record an arc leaving the function, but beware that a |
179 # Record an arc leaving the function, but beware that a |
195 # "return" event might just mean yielding from a generator. |
180 # "return" event might just mean yielding from a generator. |
196 # Jython seems to have an empty co_code, so just assume return. |
181 # Jython seems to have an empty co_code, so just assume return. |
197 code = frame.f_code.co_code |
182 code = frame.f_code.co_code |
198 if (not code) or code[frame.f_lasti] != YIELD_VALUE: |
183 if (not code) or code[frame.f_lasti] != YIELD_VALUE: |
199 first = frame.f_code.co_firstlineno |
184 first = frame.f_code.co_firstlineno |
200 self.cur_file_dict[(self.last_line, -first)] = None |
185 self.cur_file_data.add((self.last_line, -first)) |
201 # Leaving this function, pop the filename stack. |
186 # Leaving this function, pop the filename stack. |
202 self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = ( |
187 self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( |
203 self.data_stack.pop() |
188 self.data_stack.pop() |
204 ) |
189 ) |
205 # Leaving a context? |
190 # Leaving a context? |
206 if self.started_context: |
191 if self.started_context: |
207 self.context = None |
192 self.context = None |
208 self.switch_context(None) |
193 self.switch_context(None) |
209 elif event == 'exception': |
|
210 self.last_exc_back = frame.f_back |
|
211 self.last_exc_firstlineno = frame.f_code.co_firstlineno |
|
212 return self._trace |
194 return self._trace |
213 |
195 |
214 def start(self): |
196 def start(self): |
215 """Start this Tracer. |
197 """Start this Tracer. |
216 |
198 |
241 # Set the stop flag. The actual call to sys.settrace(None) will happen |
223 # Set the stop flag. The actual call to sys.settrace(None) will happen |
242 # in the self._trace callback itself to make sure to call it from the |
224 # in the self._trace callback itself to make sure to call it from the |
243 # right thread. |
225 # right thread. |
244 self.stopped = True |
226 self.stopped = True |
245 |
227 |
246 if self.threading and self.thread.ident != self.threading.currentThread().ident: |
228 if self.threading and self.thread.ident != self.threading.current_thread().ident: |
247 # Called on a different thread than started us: we can't unhook |
229 # Called on a different thread than started us: we can't unhook |
248 # ourselves, but we've set the flag that we should stop, so we |
230 # ourselves, but we've set the flag that we should stop, so we |
249 # won't do any more tracing. |
231 # won't do any more tracing. |
250 #self.log("~", "stopping on different threads") |
232 #self.log("~", "stopping on different threads") |
251 return |
233 return |
254 # PyPy clears the trace function before running atexit functions, |
236 # PyPy clears the trace function before running atexit functions, |
255 # so don't warn if we are in atexit on PyPy and the trace function |
237 # so don't warn if we are in atexit on PyPy and the trace function |
256 # has changed to None. |
238 # has changed to None. |
257 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) |
239 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) |
258 if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable |
240 if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable |
259 self.warn( |
241 msg = f"Trace function changed, measurement is likely wrong: {tf!r}" |
260 "Trace function changed, measurement is likely wrong: %r" % (tf,), |
242 self.warn(msg, slug="trace-changed") |
261 slug="trace-changed", |
|
262 ) |
|
263 |
243 |
264 def activity(self): |
244 def activity(self): |
265 """Has there been any activity?""" |
245 """Has there been any activity?""" |
266 return self._activity |
246 return self._activity |
267 |
247 |