8 import sys |
8 import sys |
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 RESUME = dis.opmap.get('RESUME') |
|
14 RETURN_VALUE = dis.opmap['RETURN_VALUE'] |
|
15 if RESUME is None: |
|
16 YIELD_VALUE = dis.opmap['YIELD_VALUE'] |
|
17 YIELD_FROM = dis.opmap['YIELD_FROM'] |
|
18 YIELD_FROM_OFFSET = 0 if env.PYPY else 2 |
14 |
19 |
15 # When running meta-coverage, this file can try to trace itself, which confuses |
20 # When running meta-coverage, this file can try to trace itself, which confuses |
16 # everything. Don't trace ourselves. |
21 # everything. Don't trace ourselves. |
17 |
22 |
18 THIS_FILE = __file__.rstrip("co") |
23 THIS_FILE = __file__.rstrip("co") |
19 |
|
20 |
24 |
21 class PyTracer: |
25 class PyTracer: |
22 """Python implementation of the raw data tracer.""" |
26 """Python implementation of the raw data tracer.""" |
23 |
27 |
24 # Because of poor implementations of trace-function-manipulating tools, |
28 # Because of poor implementations of trace-function-manipulating tools, |
62 self.in_atexit = False |
66 self.in_atexit = False |
63 # On exit, self.in_atexit = True |
67 # On exit, self.in_atexit = True |
64 atexit.register(setattr, self, 'in_atexit', True) |
68 atexit.register(setattr, self, 'in_atexit', True) |
65 |
69 |
66 def __repr__(self): |
70 def __repr__(self): |
67 return "<PyTracer at {}: {} lines in {} files>".format( |
71 return "<PyTracer at 0x{:x}: {} lines in {} files>".format( |
68 id(self), |
72 id(self), |
69 sum(len(v) for v in self.data.values()), |
73 sum(len(v) for v in self.data.values()), |
70 len(self.data), |
74 len(self.data), |
71 ) |
75 ) |
72 |
76 |
76 f.write("{} {}[{}]".format( |
80 f.write("{} {}[{}]".format( |
77 marker, |
81 marker, |
78 id(self), |
82 id(self), |
79 len(self.data_stack), |
83 len(self.data_stack), |
80 )) |
84 )) |
81 if 0: |
85 if 0: # if you want thread ids.. |
82 f.write(".{:x}.{:x}".format( |
86 f.write(".{:x}.{:x}".format( |
83 self.thread.ident, |
87 self.thread.ident, |
84 self.threading.current_thread().ident, |
88 self.threading.current_thread().ident, |
85 )) |
89 )) |
86 f.write(" {}".format(" ".join(map(str, args)))) |
90 f.write(" {}".format(" ".join(map(str, args)))) |
87 if 0: |
91 if 0: # if you want callers.. |
88 f.write(" | ") |
92 f.write(" | ") |
89 stack = " / ".join( |
93 stack = " / ".join( |
90 (fname or "???").rpartition("/")[-1] |
94 (fname or "???").rpartition("/")[-1] |
91 for _, fname, _, _ in self.data_stack |
95 for _, fname, _, _ in self.data_stack |
92 ) |
96 ) |
130 else: |
134 else: |
131 self.started_context = False |
135 self.started_context = False |
132 else: |
136 else: |
133 self.started_context = False |
137 self.started_context = False |
134 |
138 |
135 # Entering a new frame. Decide if we should trace |
139 # Entering a new frame. Decide if we should trace in this file. |
136 # in this file. |
|
137 self._activity = True |
140 self._activity = True |
138 self.data_stack.append( |
141 self.data_stack.append( |
139 ( |
142 ( |
140 self.cur_file_data, |
143 self.cur_file_data, |
141 self.cur_file_name, |
144 self.cur_file_name, |
158 self.cur_file_data = self.data[tracename] |
161 self.cur_file_data = self.data[tracename] |
159 # The call event is really a "start frame" event, and happens for |
162 # The call event is really a "start frame" event, and happens for |
160 # function calls and re-entering generators. The f_lasti field is |
163 # function calls and re-entering generators. The f_lasti field is |
161 # -1 for calls, and a real offset for generators. Use <0 as the |
164 # -1 for calls, and a real offset for generators. Use <0 as the |
162 # line number for calls, and the real line number for generators. |
165 # line number for calls, and the real line number for generators. |
163 if getattr(frame, 'f_lasti', -1) < 0: |
166 if RESUME is not None: |
|
167 # The current opcode is guaranteed to be RESUME. The argument |
|
168 # determines what kind of resume it is. |
|
169 oparg = frame.f_code.co_code[frame.f_lasti + 1] |
|
170 real_call = (oparg == 0) |
|
171 else: |
|
172 real_call = (getattr(frame, 'f_lasti', -1) < 0) |
|
173 if real_call: |
164 self.last_line = -frame.f_code.co_firstlineno |
174 self.last_line = -frame.f_code.co_firstlineno |
165 else: |
175 else: |
166 self.last_line = frame.f_lineno |
176 self.last_line = frame.f_lineno |
167 elif event == 'line': |
177 elif event == 'line': |
168 # Record an executed line. |
178 # Record an executed line. |
176 self.last_line = lineno |
186 self.last_line = lineno |
177 elif event == 'return': |
187 elif event == 'return': |
178 if self.trace_arcs and self.cur_file_data: |
188 if self.trace_arcs and self.cur_file_data: |
179 # Record an arc leaving the function, but beware that a |
189 # Record an arc leaving the function, but beware that a |
180 # "return" event might just mean yielding from a generator. |
190 # "return" event might just mean yielding from a generator. |
181 # Jython seems to have an empty co_code, so just assume return. |
|
182 code = frame.f_code.co_code |
191 code = frame.f_code.co_code |
183 if (not code) or code[frame.f_lasti] != YIELD_VALUE: |
192 lasti = frame.f_lasti |
|
193 if RESUME is not None: |
|
194 if len(code) == lasti + 2: |
|
195 # A return from the end of a code object is a real return. |
|
196 real_return = True |
|
197 else: |
|
198 # it's a real return. |
|
199 real_return = (code[lasti + 2] != RESUME) |
|
200 else: |
|
201 if code[lasti] == RETURN_VALUE: |
|
202 real_return = True |
|
203 elif code[lasti] == YIELD_VALUE: |
|
204 real_return = False |
|
205 elif len(code) <= lasti + YIELD_FROM_OFFSET: |
|
206 real_return = True |
|
207 elif code[lasti + YIELD_FROM_OFFSET] == YIELD_FROM: |
|
208 real_return = False |
|
209 else: |
|
210 real_return = True |
|
211 if real_return: |
184 first = frame.f_code.co_firstlineno |
212 first = frame.f_code.co_firstlineno |
185 self.cur_file_data.add((self.last_line, -first)) |
213 self.cur_file_data.add((self.last_line, -first)) |
186 # Leaving this function, pop the filename stack. |
214 # Leaving this function, pop the filename stack. |
187 self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( |
215 self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( |
188 self.data_stack.pop() |
216 self.data_stack.pop() |
236 # PyPy clears the trace function before running atexit functions, |
264 # PyPy clears the trace function before running atexit functions, |
237 # so don't warn if we are in atexit on PyPy and the trace function |
265 # so don't warn if we are in atexit on PyPy and the trace function |
238 # has changed to None. |
266 # has changed to None. |
239 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) |
267 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) |
240 if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable |
268 if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable |
241 msg = f"Trace function changed, measurement is likely wrong: {tf!r}" |
269 self.warn( |
242 self.warn(msg, slug="trace-changed") |
270 f"Trace function changed, data is likely wrong: {tf!r} != {self._trace!r}", |
|
271 slug="trace-changed", |
|
272 ) |
243 |
273 |
244 def activity(self): |
274 def activity(self): |
245 """Has there been any activity?""" |
275 """Has there been any activity?""" |
246 return self._activity |
276 return self._activity |
247 |
277 |