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
6import atexit
7import dis
8import sys
9
10from coverage import env
11
12# We need the YIELD_VALUE opcode below, in a comparison-friendly form.
13YIELD_VALUE = dis.opmap['YIELD_VALUE']
14if env.PY2:
15    YIELD_VALUE = chr(YIELD_VALUE)
16
17
18class 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)
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)
136                self.should_trace_cache[filename] = disp
137
138            self.cur_file_dict = None
139            if disp.trace:
140                tracename = disp.source_filename
141                if tracename not in self.data:
142                    self.data[tracename] = {}
143                self.cur_file_dict = self.data[tracename]
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(
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
246