1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
3 |
3 |
4 """Raw data collector for coverage.py.""" |
4 """Raw data collector for coverage.py.""" |
5 |
5 |
6 import os |
6 import os |
7 import sys |
7 import sys |
8 |
8 |
9 from coverage import env |
9 from coverage import env |
10 from coverage.backward import litems, range # pylint: disable=redefined-builtin |
10 from coverage.backward import litems, range # pylint: disable=redefined-builtin |
11 from coverage.debug import short_stack |
11 from coverage.debug import short_stack |
12 from coverage.files import abs_file |
12 from coverage.disposition import FileDisposition |
13 from coverage.misc import CoverageException, isolate_module |
13 from coverage.misc import CoverageException, isolate_module |
14 from coverage.pytracer import PyTracer |
14 from coverage.pytracer import PyTracer |
15 |
15 |
16 os = isolate_module(os) |
16 os = isolate_module(os) |
17 |
17 |
31 sys.stderr.write("*** COVERAGE_TEST_TRACER is 'c' but can't import CTracer!\n") |
31 sys.stderr.write("*** COVERAGE_TEST_TRACER is 'c' but can't import CTracer!\n") |
32 sys.exit(1) |
32 sys.exit(1) |
33 CTracer = None |
33 CTracer = None |
34 |
34 |
35 |
35 |
36 class FileDisposition(object): |
|
37 """A simple value type for recording what to do with a file.""" |
|
38 pass |
|
39 |
|
40 |
|
41 def should_start_context(frame): |
|
42 """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" |
|
43 fn_name = frame.f_code.co_name |
|
44 if fn_name.startswith("test"): |
|
45 return fn_name |
|
46 return None |
|
47 |
|
48 |
|
49 class Collector(object): |
36 class Collector(object): |
50 """Collects trace data. |
37 """Collects trace data. |
51 |
38 |
52 Creates a Tracer object for each thread, since they track stack |
39 Creates a Tracer object for each thread, since they track stack |
53 information. Each Tracer points to the same shared data, contributing |
40 information. Each Tracer points to the same shared data, contributing |
68 _collectors = [] |
55 _collectors = [] |
69 |
56 |
70 # The concurrency settings we support here. |
57 # The concurrency settings we support here. |
71 SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"]) |
58 SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"]) |
72 |
59 |
73 def __init__(self, should_trace, check_include, timid, branch, warn, concurrency): |
60 def __init__( |
|
61 self, should_trace, check_include, should_start_context, file_mapper, |
|
62 timid, branch, warn, concurrency, |
|
63 ): |
74 """Create a collector. |
64 """Create a collector. |
75 |
65 |
76 `should_trace` is a function, taking a file name and a frame, and |
66 `should_trace` is a function, taking a file name and a frame, and |
77 returning a `coverage.FileDisposition object`. |
67 returning a `coverage.FileDisposition object`. |
78 |
68 |
79 `check_include` is a function taking a file name and a frame. It returns |
69 `check_include` is a function taking a file name and a frame. It returns |
80 a boolean: True if the file should be traced, False if not. |
70 a boolean: True if the file should be traced, False if not. |
|
71 |
|
72 `should_start_context` is a function taking a frame, and returning a |
|
73 string. If the frame should be the start of a new context, the string |
|
74 is the new context. If the frame should not be the start of a new |
|
75 context, return None. |
|
76 |
|
77 `file_mapper` is a function taking a filename, and returning a Unicode |
|
78 filename. The result is the name that will be recorded in the data |
|
79 file. |
81 |
80 |
82 If `timid` is true, then a slower simpler trace function will be |
81 If `timid` is true, then a slower simpler trace function will be |
83 used. This is important for some environments where manipulation of |
82 used. This is important for some environments where manipulation of |
84 tracing functions make the faster more sophisticated trace function not |
83 tracing functions make the faster more sophisticated trace function not |
85 operate properly. |
84 operate properly. |
98 values are ignored. |
97 values are ignored. |
99 |
98 |
100 """ |
99 """ |
101 self.should_trace = should_trace |
100 self.should_trace = should_trace |
102 self.check_include = check_include |
101 self.check_include = check_include |
|
102 self.should_start_context = should_start_context |
|
103 self.file_mapper = file_mapper |
103 self.warn = warn |
104 self.warn = warn |
104 self.branch = branch |
105 self.branch = branch |
105 self.threading = None |
106 self.threading = None |
|
107 self.covdata = None |
|
108 |
|
109 self.static_context = None |
106 |
110 |
107 self.origin = short_stack() |
111 self.origin = short_stack() |
108 |
112 |
109 self.concur_id_func = None |
113 self.concur_id_func = None |
|
114 self.mapped_file_cache = {} |
110 |
115 |
111 # We can handle a few concurrency options here, but only one at a time. |
116 # We can handle a few concurrency options here, but only one at a time. |
112 these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency) |
117 these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency) |
113 if len(these_concurrencies) > 1: |
118 if len(these_concurrencies) > 1: |
114 raise CoverageException("Conflicting concurrency settings: %s" % concurrency) |
119 raise CoverageException("Conflicting concurrency settings: %s" % concurrency) |
161 self.supports_plugins = False |
162 self.supports_plugins = False |
162 |
163 |
163 def __repr__(self): |
164 def __repr__(self): |
164 return "<Collector at 0x%x: %s>" % (id(self), self.tracer_name()) |
165 return "<Collector at 0x%x: %s>" % (id(self), self.tracer_name()) |
165 |
166 |
|
167 def use_data(self, covdata, context): |
|
168 """Use `covdata` for recording data.""" |
|
169 self.covdata = covdata |
|
170 self.static_context = context |
|
171 self.covdata.set_context(self.static_context) |
|
172 |
166 def tracer_name(self): |
173 def tracer_name(self): |
167 """Return the class name of the tracer we're using.""" |
174 """Return the class name of the tracer we're using.""" |
168 return self._trace_class.__name__ |
175 return self._trace_class.__name__ |
169 |
176 |
170 def _clear_data(self): |
177 def _clear_data(self): |
171 """Clear out existing data, but stay ready for more collection.""" |
178 """Clear out existing data, but stay ready for more collection.""" |
172 self.data.clear() |
179 # We used to used self.data.clear(), but that would remove filename |
|
180 # keys and data values that were still in use higher up the stack |
|
181 # when we are called as part of switch_context. |
|
182 for d in self.data.values(): |
|
183 d.clear() |
173 |
184 |
174 for tracer in self.tracers: |
185 for tracer in self.tracers: |
175 tracer.reset_activity() |
186 tracer.reset_activity() |
176 |
187 |
177 def reset(self): |
188 def reset(self): |
178 """Clear collected data, and prepare to collect more.""" |
189 """Clear collected data, and prepare to collect more.""" |
179 # A dictionary mapping file names to dicts with line number keys (if not |
190 # A dictionary mapping file names to dicts with line number keys (if not |
180 # branch coverage), or mapping file names to dicts with line number |
191 # branch coverage), or mapping file names to dicts with line number |
181 # pairs as keys (if branch coverage). |
192 # pairs as keys (if branch coverage). |
182 self.data = {} |
193 self.data = {} |
183 |
|
184 # A dict mapping contexts to data dictionaries. |
|
185 self.contexts = {} |
|
186 self.contexts[None] = self.data |
|
187 |
194 |
188 # A dictionary mapping file names to file tracer plugin names that will |
195 # A dictionary mapping file names to file tracer plugin names that will |
189 # handle them. |
196 # handle them. |
190 self.file_tracers = {} |
197 self.file_tracers = {} |
191 |
198 |
244 tracer.file_tracers = self.file_tracers |
251 tracer.file_tracers = self.file_tracers |
245 if hasattr(tracer, 'threading'): |
252 if hasattr(tracer, 'threading'): |
246 tracer.threading = self.threading |
253 tracer.threading = self.threading |
247 if hasattr(tracer, 'check_include'): |
254 if hasattr(tracer, 'check_include'): |
248 tracer.check_include = self.check_include |
255 tracer.check_include = self.check_include |
249 if self.wtw: |
256 if hasattr(tracer, 'should_start_context'): |
250 if hasattr(tracer, 'should_start_context'): |
257 tracer.should_start_context = self.should_start_context |
251 tracer.should_start_context = should_start_context |
258 tracer.switch_context = self.switch_context |
252 if hasattr(tracer, 'switch_context'): |
|
253 tracer.switch_context = self.switch_context |
|
254 |
259 |
255 fn = tracer.start() |
260 fn = tracer.start() |
256 self.tracers.append(tracer) |
261 self.tracers.append(tracer) |
257 |
262 |
258 return fn |
263 return fn |
364 |
369 |
365 """ |
370 """ |
366 return any(tracer.activity() for tracer in self.tracers) |
371 return any(tracer.activity() for tracer in self.tracers) |
367 |
372 |
368 def switch_context(self, new_context): |
373 def switch_context(self, new_context): |
369 """Who-Tests-What hack: switch to a new who-context.""" |
374 """Switch to a new dynamic context.""" |
370 # Make a new data dict, or find the existing one, and switch all the |
375 self.flush_data() |
371 # tracers to use it. |
376 if self.static_context: |
372 data = self.contexts.setdefault(new_context, {}) |
377 context = self.static_context |
373 for tracer in self.tracers: |
378 if new_context: |
374 tracer.data = data |
379 context += "|" + new_context |
375 |
380 else: |
376 def save_data(self, covdata): |
381 context = new_context |
377 """Save the collected data to a `CoverageData`. |
382 self.covdata.set_context(context) |
|
383 |
|
384 def cached_mapped_file(self, filename): |
|
385 """A locally cached version of file names mapped through file_mapper.""" |
|
386 key = (type(filename), filename) |
|
387 try: |
|
388 return self.mapped_file_cache[key] |
|
389 except KeyError: |
|
390 return self.mapped_file_cache.setdefault(key, self.file_mapper(filename)) |
|
391 |
|
392 def mapped_file_dict(self, d): |
|
393 """Return a dict like d, but with keys modified by file_mapper.""" |
|
394 # The call to litems() ensures that the GIL protects the dictionary |
|
395 # iterator against concurrent modifications by tracers running |
|
396 # in other threads. We try three times in case of concurrent |
|
397 # access, hoping to get a clean copy. |
|
398 runtime_err = None |
|
399 for _ in range(3): |
|
400 try: |
|
401 items = litems(d) |
|
402 except RuntimeError as ex: |
|
403 runtime_err = ex |
|
404 else: |
|
405 break |
|
406 else: |
|
407 raise runtime_err |
|
408 |
|
409 return dict((self.cached_mapped_file(k), v) for k, v in items if v) |
|
410 |
|
411 def flush_data(self): |
|
412 """Save the collected data to our associated `CoverageData`. |
|
413 |
|
414 Data may have also been saved along the way. This forces the |
|
415 last of the data to be saved. |
378 |
416 |
379 Returns True if there was data to save, False if not. |
417 Returns True if there was data to save, False if not. |
380 """ |
418 """ |
381 if not self._activity(): |
419 if not self._activity(): |
382 return False |
420 return False |
383 |
421 |
384 def abs_file_dict(d): |
|
385 """Return a dict like d, but with keys modified by `abs_file`.""" |
|
386 # The call to litems() ensures that the GIL protects the dictionary |
|
387 # iterator against concurrent modifications by tracers running |
|
388 # in other threads. We try three times in case of concurrent |
|
389 # access, hoping to get a clean copy. |
|
390 runtime_err = None |
|
391 for _ in range(3): |
|
392 try: |
|
393 items = litems(d) |
|
394 except RuntimeError as ex: |
|
395 runtime_err = ex |
|
396 else: |
|
397 break |
|
398 else: |
|
399 raise runtime_err # pylint: disable=raising-bad-type |
|
400 |
|
401 return dict((abs_file(k), v) for k, v in items) |
|
402 |
|
403 if self.branch: |
422 if self.branch: |
404 covdata.add_arcs(abs_file_dict(self.data)) |
423 self.covdata.add_arcs(self.mapped_file_dict(self.data)) |
405 else: |
424 else: |
406 covdata.add_lines(abs_file_dict(self.data)) |
425 self.covdata.add_lines(self.mapped_file_dict(self.data)) |
407 covdata.add_file_tracers(abs_file_dict(self.file_tracers)) |
426 self.covdata.add_file_tracers(self.mapped_file_dict(self.file_tracers)) |
408 |
|
409 if self.wtw: |
|
410 # Just a hack, so just hack it. |
|
411 import pprint |
|
412 out_file = "coverage_wtw_{:06}.py".format(os.getpid()) |
|
413 with open(out_file, "w") as wtw_out: |
|
414 pprint.pprint(self.contexts, wtw_out) |
|
415 |
427 |
416 self._clear_data() |
428 self._clear_data() |
417 return True |
429 return True |