|
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 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 |
|
19 |
|
20 # When running meta-coverage, this file can try to trace itself, which confuses |
|
21 # everything. Don't trace ourselves. |
|
22 |
|
23 THIS_FILE = __file__.rstrip("co") |
|
24 |
|
25 class PyTracer: |
|
26 """Python implementation of the raw data tracer.""" |
|
27 |
|
28 # Because of poor implementations of trace-function-manipulating tools, |
|
29 # the Python trace function must be kept very simple. In particular, there |
|
30 # must be only one function ever set as the trace function, both through |
|
31 # sys.settrace, and as the return value from the trace function. Put |
|
32 # another way, the trace function must always return itself. It cannot |
|
33 # swap in other functions, or return None to avoid tracing a particular |
|
34 # frame. |
|
35 # |
|
36 # The trace manipulator that introduced this restriction is DecoratorTools, |
|
37 # which sets a trace function, and then later restores the pre-existing one |
|
38 # by calling sys.settrace with a function it found in the current frame. |
|
39 # |
|
40 # Systems that use DecoratorTools (or similar trace manipulations) must use |
|
41 # PyTracer to get accurate results. The command-line --timid argument is |
|
42 # used to force the use of this tracer. |
|
43 |
|
44 def __init__(self): |
|
45 # Attributes set from the collector: |
|
46 self.data = None |
|
47 self.trace_arcs = False |
|
48 self.should_trace = None |
|
49 self.should_trace_cache = None |
|
50 self.should_start_context = None |
|
51 self.warn = None |
|
52 # The threading module to use, if any. |
|
53 self.threading = None |
|
54 |
|
55 self.cur_file_data = None |
|
56 self.last_line = 0 # int, but uninitialized. |
|
57 self.cur_file_name = None |
|
58 self.context = None |
|
59 self.started_context = False |
|
60 |
|
61 self.data_stack = [] |
|
62 self.thread = None |
|
63 self.stopped = False |
|
64 self._activity = False |
|
65 |
|
66 self.in_atexit = False |
|
67 # On exit, self.in_atexit = True |
|
68 atexit.register(setattr, self, 'in_atexit', True) |
|
69 |
|
70 def __repr__(self): |
|
71 return "<PyTracer at 0x{:x}: {} lines in {} files>".format( |
|
72 id(self), |
|
73 sum(len(v) for v in self.data.values()), |
|
74 len(self.data), |
|
75 ) |
|
76 |
|
77 def log(self, marker, *args): |
|
78 """For hard-core logging of what this tracer is doing.""" |
|
79 with open("/tmp/debug_trace.txt", "a") as f: |
|
80 f.write("{} {}[{}]".format( |
|
81 marker, |
|
82 id(self), |
|
83 len(self.data_stack), |
|
84 )) |
|
85 if 0: # if you want thread ids.. |
|
86 f.write(".{:x}.{:x}".format( |
|
87 self.thread.ident, |
|
88 self.threading.current_thread().ident, |
|
89 )) |
|
90 f.write(" {}".format(" ".join(map(str, args)))) |
|
91 if 0: # if you want callers.. |
|
92 f.write(" | ") |
|
93 stack = " / ".join( |
|
94 (fname or "???").rpartition("/")[-1] |
|
95 for _, fname, _, _ in self.data_stack |
|
96 ) |
|
97 f.write(stack) |
|
98 f.write("\n") |
|
99 |
|
100 def _trace(self, frame, event, arg_unused): |
|
101 """The trace function passed to sys.settrace.""" |
|
102 |
|
103 if THIS_FILE in frame.f_code.co_filename: |
|
104 return None |
|
105 |
|
106 #self.log(":", frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + "()", event) |
|
107 |
|
108 if (self.stopped and sys.gettrace() == self._trace): # pylint: disable=comparison-with-callable |
|
109 # The PyTrace.stop() method has been called, possibly by another |
|
110 # thread, let's deactivate ourselves now. |
|
111 if 0: |
|
112 self.log("---\nX", frame.f_code.co_filename, frame.f_lineno) |
|
113 f = frame |
|
114 while f: |
|
115 self.log(">", f.f_code.co_filename, f.f_lineno, f.f_code.co_name, f.f_trace) |
|
116 f = f.f_back |
|
117 sys.settrace(None) |
|
118 self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( |
|
119 self.data_stack.pop() |
|
120 ) |
|
121 return None |
|
122 |
|
123 # if event != 'call' and frame.f_code.co_filename != self.cur_file_name: |
|
124 # self.log("---\n*", frame.f_code.co_filename, self.cur_file_name, frame.f_lineno) |
|
125 |
|
126 if event == 'call': |
|
127 # Should we start a new context? |
|
128 if self.should_start_context and self.context is None: |
|
129 context_maybe = self.should_start_context(frame) |
|
130 if context_maybe is not None: |
|
131 self.context = context_maybe |
|
132 self.started_context = True |
|
133 self.switch_context(self.context) |
|
134 else: |
|
135 self.started_context = False |
|
136 else: |
|
137 self.started_context = False |
|
138 |
|
139 # Entering a new frame. Decide if we should trace in this file. |
|
140 self._activity = True |
|
141 self.data_stack.append( |
|
142 ( |
|
143 self.cur_file_data, |
|
144 self.cur_file_name, |
|
145 self.last_line, |
|
146 self.started_context, |
|
147 ) |
|
148 ) |
|
149 filename = frame.f_code.co_filename |
|
150 self.cur_file_name = filename |
|
151 disp = self.should_trace_cache.get(filename) |
|
152 if disp is None: |
|
153 disp = self.should_trace(filename, frame) |
|
154 self.should_trace_cache[filename] = disp |
|
155 |
|
156 self.cur_file_data = None |
|
157 if disp.trace: |
|
158 tracename = disp.source_filename |
|
159 if tracename not in self.data: |
|
160 self.data[tracename] = set() |
|
161 self.cur_file_data = self.data[tracename] |
|
162 # The call event is really a "start frame" event, and happens for |
|
163 # function calls and re-entering generators. The f_lasti field is |
|
164 # -1 for calls, and a real offset for generators. Use <0 as the |
|
165 # line number for calls, and the real line number for generators. |
|
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: |
|
174 self.last_line = -frame.f_code.co_firstlineno |
|
175 else: |
|
176 self.last_line = frame.f_lineno |
|
177 elif event == 'line': |
|
178 # Record an executed line. |
|
179 if self.cur_file_data is not None: |
|
180 lineno = frame.f_lineno |
|
181 |
|
182 if self.trace_arcs: |
|
183 self.cur_file_data.add((self.last_line, lineno)) |
|
184 else: |
|
185 self.cur_file_data.add(lineno) |
|
186 self.last_line = lineno |
|
187 elif event == 'return': |
|
188 if self.trace_arcs and self.cur_file_data: |
|
189 # Record an arc leaving the function, but beware that a |
|
190 # "return" event might just mean yielding from a generator. |
|
191 code = frame.f_code.co_code |
|
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: |
|
212 first = frame.f_code.co_firstlineno |
|
213 self.cur_file_data.add((self.last_line, -first)) |
|
214 # Leaving this function, pop the filename stack. |
|
215 self.cur_file_data, self.cur_file_name, self.last_line, self.started_context = ( |
|
216 self.data_stack.pop() |
|
217 ) |
|
218 # Leaving a context? |
|
219 if self.started_context: |
|
220 self.context = None |
|
221 self.switch_context(None) |
|
222 return self._trace |
|
223 |
|
224 def start(self): |
|
225 """Start this Tracer. |
|
226 |
|
227 Return a Python function suitable for use with sys.settrace(). |
|
228 |
|
229 """ |
|
230 self.stopped = False |
|
231 if self.threading: |
|
232 if self.thread is None: |
|
233 self.thread = self.threading.current_thread() |
|
234 else: |
|
235 if self.thread.ident != self.threading.current_thread().ident: |
|
236 # Re-starting from a different thread!? Don't set the trace |
|
237 # function, but we are marked as running again, so maybe it |
|
238 # will be ok? |
|
239 #self.log("~", "starting on different threads") |
|
240 return self._trace |
|
241 |
|
242 sys.settrace(self._trace) |
|
243 return self._trace |
|
244 |
|
245 def stop(self): |
|
246 """Stop this Tracer.""" |
|
247 # Get the active tracer callback before setting the stop flag to be |
|
248 # able to detect if the tracer was changed prior to stopping it. |
|
249 tf = sys.gettrace() |
|
250 |
|
251 # Set the stop flag. The actual call to sys.settrace(None) will happen |
|
252 # in the self._trace callback itself to make sure to call it from the |
|
253 # right thread. |
|
254 self.stopped = True |
|
255 |
|
256 if self.threading and self.thread.ident != self.threading.current_thread().ident: |
|
257 # Called on a different thread than started us: we can't unhook |
|
258 # ourselves, but we've set the flag that we should stop, so we |
|
259 # won't do any more tracing. |
|
260 #self.log("~", "stopping on different threads") |
|
261 return |
|
262 |
|
263 if self.warn: |
|
264 # PyPy clears the trace function before running atexit functions, |
|
265 # so don't warn if we are in atexit on PyPy and the trace function |
|
266 # has changed to None. |
|
267 dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) |
|
268 if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable |
|
269 self.warn( |
|
270 f"Trace function changed, data is likely wrong: {tf!r} != {self._trace!r}", |
|
271 slug="trace-changed", |
|
272 ) |
|
273 |
|
274 def activity(self): |
|
275 """Has there been any activity?""" |
|
276 return self._activity |
|
277 |
|
278 def reset_activity(self): |
|
279 """Reset the activity() flag.""" |
|
280 self._activity = False |
|
281 |
|
282 def get_stats(self): |
|
283 """Return a dictionary of statistics, or None.""" |
|
284 return None |