1"""Raw data collector for Coverage."""
2
3import sys, threading
4
5try:
6    # Use the C extension code when we can, for speed.
7    from coverage.tracer import CTracer
8except ImportError:
9    # Couldn't import the C extension, maybe it isn't built.
10    CTracer = None
11
12
13class PyTracer(object):
14    """Python implementation of the raw data tracer."""
15
16    # Because of poor implementations of trace-function-manipulating tools,
17    # the Python trace function must be kept very simple.  In particular, there
18    # must be only one function ever set as the trace function, both through
19    # sys.settrace, and as the return value from the trace function.  Put
20    # another way, the trace function must always return itself.  It cannot
21    # swap in other functions, or return None to avoid tracing a particular
22    # frame.
23    #
24    # The trace manipulator that introduced this restriction is DecoratorTools,
25    # which sets a trace function, and then later restores the pre-existing one
26    # by calling sys.settrace with a function it found in the current frame.
27    #
28    # Systems that use DecoratorTools (or similar trace manipulations) must use
29    # PyTracer to get accurate results.  The command-line --timid argument is
30    # used to force the use of this tracer.
31
32    def __init__(self):
33        self.data = None
34        self.should_trace = None
35        self.should_trace_cache = None
36        self.warn = None
37        self.cur_file_data = None
38        self.last_line = 0
39        self.data_stack = []
40        self.last_exc_back = None
41        self.last_exc_firstlineno = 0
42        self.arcs = False
43
44    def _trace(self, frame, event, arg_unused):
45        """The trace function passed to sys.settrace."""
46
47        #print("trace event: %s %r @%d" % (
48        #           event, frame.f_code.co_filename, frame.f_lineno))
49
50        if self.last_exc_back:
51            if frame == self.last_exc_back:
52                # Someone forgot a return event.
53                if self.arcs and self.cur_file_data:
54                    pair = (self.last_line, -self.last_exc_firstlineno)
55                    self.cur_file_data[pair] = None
56                self.cur_file_data, self.last_line = self.data_stack.pop()
57            self.last_exc_back = None
58
59        if event == 'call':
60            # Entering a new function context.  Decide if we should trace
61            # in this file.
62            self.data_stack.append((self.cur_file_data, self.last_line))
63            filename = frame.f_code.co_filename
64            tracename = self.should_trace_cache.get(filename)
65            if tracename is None:
66                tracename = self.should_trace(filename, frame)
67                self.should_trace_cache[filename] = tracename
68            #print("called, stack is %d deep, tracename is %r" % (
69            #               len(self.data_stack), tracename))
70            if tracename:
71                if tracename not in self.data:
72                    self.data[tracename] = {}
73                self.cur_file_data = self.data[tracename]
74            else:
75                self.cur_file_data = None
76            # Set the last_line to -1 because the next arc will be entering a
77            # code block, indicated by (-1, n).
78            self.last_line = -1
79        elif event == 'line':
80            # Record an executed line.
81            if self.cur_file_data is not None:
82                if self.arcs:
83                    #print("lin", self.last_line, frame.f_lineno)
84                    self.cur_file_data[(self.last_line, frame.f_lineno)] = None
85                else:
86                    #print("lin", frame.f_lineno)
87                    self.cur_file_data[frame.f_lineno] = None
88            self.last_line = frame.f_lineno
89        elif event == 'return':
90            if self.arcs and self.cur_file_data:
91                first = frame.f_code.co_firstlineno
92                self.cur_file_data[(self.last_line, -first)] = None
93            # Leaving this function, pop the filename stack.
94            self.cur_file_data, self.last_line = self.data_stack.pop()
95            #print("returned, stack is %d deep" % (len(self.data_stack)))
96        elif event == 'exception':
97            #print("exc", self.last_line, frame.f_lineno)
98            self.last_exc_back = frame.f_back
99            self.last_exc_firstlineno = frame.f_code.co_firstlineno
100        return self._trace
101
102    def start(self):
103        """Start this Tracer.
104
105        Return a Python function suitable for use with sys.settrace().
106
107        """
108        sys.settrace(self._trace)
109        return self._trace
110
111    def stop(self):
112        """Stop this Tracer."""
113        if hasattr(sys, "gettrace") and self.warn:
114            if sys.gettrace() != self._trace:
115                msg = "Trace function changed, measurement is likely wrong: %r"
116                self.warn(msg % sys.gettrace())
117        sys.settrace(None)
118
119    def get_stats(self):
120        """Return a dictionary of statistics, or None."""
121        return None
122
123
124class Collector(object):
125    """Collects trace data.
126
127    Creates a Tracer object for each thread, since they track stack
128    information.  Each Tracer points to the same shared data, contributing
129    traced data points.
130
131    When the Collector is started, it creates a Tracer for the current thread,
132    and installs a function to create Tracers for each new thread started.
133    When the Collector is stopped, all active Tracers are stopped.
134
135    Threads started while the Collector is stopped will never have Tracers
136    associated with them.
137
138    """
139
140    # The stack of active Collectors.  Collectors are added here when started,
141    # and popped when stopped.  Collectors on the stack are paused when not
142    # the top, and resumed when they become the top again.
143    _collectors = []
144
145    def __init__(self, should_trace, timid, branch, warn):
146        """Create a collector.
147
148        `should_trace` is a function, taking a filename, and returning a
149        canonicalized filename, or False depending on whether the file should
150        be traced or not.
151
152        If `timid` is true, then a slower simpler trace function will be
153        used.  This is important for some environments where manipulation of
154        tracing functions make the faster more sophisticated trace function not
155        operate properly.
156
157        If `branch` is true, then branches will be measured.  This involves
158        collecting data on which statements followed each other (arcs).  Use
159        `get_arc_data` to get the arc data.
160
161        `warn` is a warning function, taking a single string message argument,
162        to be used if a warning needs to be issued.
163
164        """
165        self.should_trace = should_trace
166        self.warn = warn
167        self.branch = branch
168        self.reset()
169
170        if timid:
171            # Being timid: use the simple Python trace function.
172            self._trace_class = PyTracer
173        else:
174            # Being fast: use the C Tracer if it is available, else the Python
175            # trace function.
176            self._trace_class = CTracer or PyTracer
177
178    def __repr__(self):
179        return "<Collector at 0x%x>" % id(self)
180
181    def tracer_name(self):
182        """Return the class name of the tracer we're using."""
183        return self._trace_class.__name__
184
185    def reset(self):
186        """Clear collected data, and prepare to collect more."""
187        # A dictionary mapping filenames to dicts with linenumber keys,
188        # or mapping filenames to dicts with linenumber pairs as keys.
189        self.data = {}
190
191        # A cache of the results from should_trace, the decision about whether
192        # to trace execution in a file. A dict of filename to (filename or
193        # False).
194        self.should_trace_cache = {}
195
196        # Our active Tracers.
197        self.tracers = []
198
199    def _start_tracer(self):
200        """Start a new Tracer object, and store it in self.tracers."""
201        tracer = self._trace_class()
202        tracer.data = self.data
203        tracer.arcs = self.branch
204        tracer.should_trace = self.should_trace
205        tracer.should_trace_cache = self.should_trace_cache
206        tracer.warn = self.warn
207        fn = tracer.start()
208        self.tracers.append(tracer)
209        return fn
210
211    # The trace function has to be set individually on each thread before
212    # execution begins.  Ironically, the only support the threading module has
213    # for running code before the thread main is the tracing function.  So we
214    # install this as a trace function, and the first time it's called, it does
215    # the real trace installation.
216
217    def _installation_trace(self, frame_unused, event_unused, arg_unused):
218        """Called on new threads, installs the real tracer."""
219        # Remove ourselves as the trace function
220        sys.settrace(None)
221        # Install the real tracer.
222        fn = self._start_tracer()
223        # Invoke the real trace function with the current event, to be sure
224        # not to lose an event.
225        if fn:
226            fn = fn(frame_unused, event_unused, arg_unused)
227        # Return the new trace function to continue tracing in this scope.
228        return fn
229
230    def start(self):
231        """Start collecting trace information."""
232        if self._collectors:
233            self._collectors[-1].pause()
234        self._collectors.append(self)
235        #print >>sys.stderr, "Started: %r" % self._collectors
236
237        # Check to see whether we had a fullcoverage tracer installed.
238        traces0 = None
239        if hasattr(sys, "gettrace"):
240            fn0 = sys.gettrace()
241            if fn0:
242                tracer0 = getattr(fn0, '__self__', None)
243                if tracer0:
244                    traces0 = getattr(tracer0, 'traces', None)
245
246        # Install the tracer on this thread.
247        fn = self._start_tracer()
248
249        if traces0:
250            for args in traces0:
251                (frame, event, arg), lineno = args
252                fn(frame, event, arg, lineno=lineno)
253
254        # Install our installation tracer in threading, to jump start other
255        # threads.
256        threading.settrace(self._installation_trace)
257
258    def stop(self):
259        """Stop collecting trace information."""
260        #print >>sys.stderr, "Stopping: %r" % self._collectors
261        assert self._collectors
262        assert self._collectors[-1] is self
263
264        self.pause()
265        self.tracers = []
266
267        # Remove this Collector from the stack, and resume the one underneath
268        # (if any).
269        self._collectors.pop()
270        if self._collectors:
271            self._collectors[-1].resume()
272
273    def pause(self):
274        """Pause tracing, but be prepared to `resume`."""
275        for tracer in self.tracers:
276            tracer.stop()
277            stats = tracer.get_stats()
278            if stats:
279                print("\nCoverage.py tracer stats:")
280                for k in sorted(stats.keys()):
281                    print("%16s: %s" % (k, stats[k]))
282        threading.settrace(None)
283
284    def resume(self):
285        """Resume tracing after a `pause`."""
286        for tracer in self.tracers:
287            tracer.start()
288        threading.settrace(self._installation_trace)
289
290    def get_line_data(self):
291        """Return the line data collected.
292
293        Data is { filename: { lineno: None, ...}, ...}
294
295        """
296        if self.branch:
297            # If we were measuring branches, then we have to re-build the dict
298            # to show line data.
299            line_data = {}
300            for f, arcs in self.data.items():
301                line_data[f] = ldf = {}
302                for l1, _ in list(arcs.keys()):
303                    if l1:
304                        ldf[l1] = None
305            return line_data
306        else:
307            return self.data
308
309    def get_arc_data(self):
310        """Return the arc data collected.
311
312        Data is { filename: { (l1, l2): None, ...}, ...}
313
314        Note that no data is collected or returned if the Collector wasn't
315        created with `branch` true.
316
317        """
318        if self.branch:
319            return self.data
320        else:
321            return {}
322