eric7/DebugClients/Python/coverage/pytracer.py

branch
eric7
changeset 8991
2fc945191992
parent 8775
0802ae193343
equal deleted inserted replaced
8990:ca8e477c590c 8991:2fc945191992
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

eric ide

mercurial