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 |
|
11 from coverage.debug import short_stack |
10 from coverage.debug import short_stack |
12 from coverage.disposition import FileDisposition |
11 from coverage.disposition import FileDisposition |
13 from coverage.misc import CoverageException, isolate_module |
12 from coverage.exceptions import CoverageException |
|
13 from coverage.misc import human_sorted, 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 |
18 |
18 |
19 try: |
19 try: |
20 # Use the C extension code when we can, for speed. |
20 # Use the C extension code when we can, for speed. |
21 from coverage.tracer import CTracer, CFileDisposition |
21 from coverage.tracer import CTracer, CFileDisposition |
22 except ImportError: |
22 except ImportError: |
23 # Couldn't import the C extension, maybe it isn't built. |
23 # Couldn't import the C extension, maybe it isn't built. |
24 if os.getenv('COVERAGE_TEST_TRACER') == 'c': |
24 if os.getenv('COVERAGE_TEST_TRACER') == 'c': # pragma: part covered |
25 # During testing, we use the COVERAGE_TEST_TRACER environment variable |
25 # During testing, we use the COVERAGE_TEST_TRACER environment variable |
26 # to indicate that we've fiddled with the environment to test this |
26 # to indicate that we've fiddled with the environment to test this |
27 # fallback code. If we thought we had a C tracer, but couldn't import |
27 # fallback code. If we thought we had a C tracer, but couldn't import |
28 # it, then exit quickly and clearly instead of dribbling confusing |
28 # it, then exit quickly and clearly instead of dribbling confusing |
29 # errors. I'm using sys.exit here instead of an exception because an |
29 # errors. I'm using sys.exit here instead of an exception because an |
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 Collector(object): |
36 class Collector: |
37 """Collects trace data. |
37 """Collects trace data. |
38 |
38 |
39 Creates a Tracer object for each thread, since they track stack |
39 Creates a Tracer object for each thread, since they track stack |
40 information. Each Tracer points to the same shared data, contributing |
40 information. Each Tracer points to the same shared data, contributing |
41 traced data points. |
41 traced data points. |
114 self.mapped_file_cache = {} |
114 self.mapped_file_cache = {} |
115 |
115 |
116 # 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. |
117 these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency) |
117 these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency) |
118 if len(these_concurrencies) > 1: |
118 if len(these_concurrencies) > 1: |
119 raise CoverageException("Conflicting concurrency settings: %s" % concurrency) |
119 raise CoverageException(f"Conflicting concurrency settings: {concurrency}") |
120 self.concurrency = these_concurrencies.pop() if these_concurrencies else '' |
120 self.concurrency = these_concurrencies.pop() if these_concurrencies else '' |
121 |
121 |
122 try: |
122 try: |
123 if self.concurrency == "greenlet": |
123 if self.concurrency == "greenlet": |
124 import greenlet |
124 import greenlet |
134 # it's imported early, and the program being measured uses |
134 # it's imported early, and the program being measured uses |
135 # gevent, then gevent's monkey-patching won't work properly. |
135 # gevent, then gevent's monkey-patching won't work properly. |
136 import threading |
136 import threading |
137 self.threading = threading |
137 self.threading = threading |
138 else: |
138 else: |
139 raise CoverageException("Don't understand concurrency=%s" % concurrency) |
139 raise CoverageException(f"Don't understand concurrency={concurrency}") |
140 except ImportError: |
140 except ImportError as ex: |
141 raise CoverageException( |
141 raise CoverageException( |
142 "Couldn't trace with concurrency=%s, the module isn't installed." % ( |
142 "Couldn't trace with concurrency={}, the module isn't installed.".format( |
143 self.concurrency, |
143 self.concurrency, |
144 ) |
144 ) |
145 ) |
145 ) from ex |
146 |
146 |
147 self.reset() |
147 self.reset() |
148 |
148 |
149 if timid: |
149 if timid: |
150 # Being timid: use the simple Python trace function. |
150 # Being timid: use the simple Python trace function. |
155 self._trace_class = CTracer or PyTracer |
155 self._trace_class = CTracer or PyTracer |
156 |
156 |
157 if self._trace_class is CTracer: |
157 if self._trace_class is CTracer: |
158 self.file_disposition_class = CFileDisposition |
158 self.file_disposition_class = CFileDisposition |
159 self.supports_plugins = True |
159 self.supports_plugins = True |
|
160 self.packed_arcs = True |
160 else: |
161 else: |
161 self.file_disposition_class = FileDisposition |
162 self.file_disposition_class = FileDisposition |
162 self.supports_plugins = False |
163 self.supports_plugins = False |
|
164 self.packed_arcs = False |
163 |
165 |
164 def __repr__(self): |
166 def __repr__(self): |
165 return "<Collector at 0x%x: %s>" % (id(self), self.tracer_name()) |
167 return f"<Collector at 0x{id(self):x}: {self.tracer_name()}>" |
166 |
168 |
167 def use_data(self, covdata, context): |
169 def use_data(self, covdata, context): |
168 """Use `covdata` for recording data.""" |
170 """Use `covdata` for recording data.""" |
169 self.covdata = covdata |
171 self.covdata = covdata |
170 self.static_context = context |
172 self.static_context = context |
242 |
244 |
243 if hasattr(tracer, 'concur_id_func'): |
245 if hasattr(tracer, 'concur_id_func'): |
244 tracer.concur_id_func = self.concur_id_func |
246 tracer.concur_id_func = self.concur_id_func |
245 elif self.concur_id_func: |
247 elif self.concur_id_func: |
246 raise CoverageException( |
248 raise CoverageException( |
247 "Can't support concurrency=%s with %s, only threads are supported" % ( |
249 "Can't support concurrency={} with {}, only threads are supported".format( |
248 self.concurrency, self.tracer_name(), |
250 self.concurrency, self.tracer_name(), |
249 ) |
251 ) |
250 ) |
252 ) |
251 |
253 |
252 if hasattr(tracer, 'file_tracers'): |
254 if hasattr(tracer, 'file_tracers'): |
316 # Replay all the events from fullcoverage into the new trace function. |
318 # Replay all the events from fullcoverage into the new trace function. |
317 for args in traces0: |
319 for args in traces0: |
318 (frame, event, arg), lineno = args |
320 (frame, event, arg), lineno = args |
319 try: |
321 try: |
320 fn(frame, event, arg, lineno=lineno) |
322 fn(frame, event, arg, lineno=lineno) |
321 except TypeError: |
323 except TypeError as ex: |
322 raise Exception("fullcoverage must be run with the C trace function.") |
324 raise Exception("fullcoverage must be run with the C trace function.") from ex |
323 |
325 |
324 # Install our installation tracer in threading, to jump-start other |
326 # Install our installation tracer in threading, to jump-start other |
325 # threads. |
327 # threads. |
326 if self.threading: |
328 if self.threading: |
327 self.threading.settrace(self._installation_trace) |
329 self.threading.settrace(self._installation_trace) |
330 """Stop collecting trace information.""" |
332 """Stop collecting trace information.""" |
331 assert self._collectors |
333 assert self._collectors |
332 if self._collectors[-1] is not self: |
334 if self._collectors[-1] is not self: |
333 print("self._collectors:") |
335 print("self._collectors:") |
334 for c in self._collectors: |
336 for c in self._collectors: |
335 print(" {!r}\n{}".format(c, c.origin)) |
337 print(f" {c!r}\n{c.origin}") |
336 assert self._collectors[-1] is self, ( |
338 assert self._collectors[-1] is self, ( |
337 "Expected current collector to be %r, but it's %r" % (self, self._collectors[-1]) |
339 f"Expected current collector to be {self!r}, but it's {self._collectors[-1]!r}" |
338 ) |
340 ) |
339 |
341 |
340 self.pause() |
342 self.pause() |
341 |
343 |
342 # Remove this Collector from the stack, and resume the one underneath |
344 # Remove this Collector from the stack, and resume the one underneath |
350 for tracer in self.tracers: |
352 for tracer in self.tracers: |
351 tracer.stop() |
353 tracer.stop() |
352 stats = tracer.get_stats() |
354 stats = tracer.get_stats() |
353 if stats: |
355 if stats: |
354 print("\nCoverage.py tracer stats:") |
356 print("\nCoverage.py tracer stats:") |
355 for k in sorted(stats.keys()): |
357 for k in human_sorted(stats.keys()): |
356 print("%20s: %s" % (k, stats[k])) |
358 print(f"{k:>20}: {stats[k]}") |
357 if self.threading: |
359 if self.threading: |
358 self.threading.settrace(None) |
360 self.threading.settrace(None) |
359 |
361 |
360 def resume(self): |
362 def resume(self): |
361 """Resume tracing after a `pause`.""" |
363 """Resume tracing after a `pause`.""" |
388 def disable_plugin(self, disposition): |
390 def disable_plugin(self, disposition): |
389 """Disable the plugin mentioned in `disposition`.""" |
391 """Disable the plugin mentioned in `disposition`.""" |
390 file_tracer = disposition.file_tracer |
392 file_tracer = disposition.file_tracer |
391 plugin = file_tracer._coverage_plugin |
393 plugin = file_tracer._coverage_plugin |
392 plugin_name = plugin._coverage_plugin_name |
394 plugin_name = plugin._coverage_plugin_name |
393 self.warn("Disabling plug-in {!r} due to previous exception".format(plugin_name)) |
395 self.warn(f"Disabling plug-in {plugin_name!r} due to previous exception") |
394 plugin._coverage_enabled = False |
396 plugin._coverage_enabled = False |
395 disposition.trace = False |
397 disposition.trace = False |
396 |
398 |
397 def cached_mapped_file(self, filename): |
399 def cached_mapped_file(self, filename): |
398 """A locally cached version of file names mapped through file_mapper.""" |
400 """A locally cached version of file names mapped through file_mapper.""" |
402 except KeyError: |
404 except KeyError: |
403 return self.mapped_file_cache.setdefault(key, self.file_mapper(filename)) |
405 return self.mapped_file_cache.setdefault(key, self.file_mapper(filename)) |
404 |
406 |
405 def mapped_file_dict(self, d): |
407 def mapped_file_dict(self, d): |
406 """Return a dict like d, but with keys modified by file_mapper.""" |
408 """Return a dict like d, but with keys modified by file_mapper.""" |
407 # The call to litems() ensures that the GIL protects the dictionary |
409 # The call to list(items()) ensures that the GIL protects the dictionary |
408 # iterator against concurrent modifications by tracers running |
410 # iterator against concurrent modifications by tracers running |
409 # in other threads. We try three times in case of concurrent |
411 # in other threads. We try three times in case of concurrent |
410 # access, hoping to get a clean copy. |
412 # access, hoping to get a clean copy. |
411 runtime_err = None |
413 runtime_err = None |
412 for _ in range(3): |
414 for _ in range(3): # pragma: part covered |
413 try: |
415 try: |
414 items = litems(d) |
416 items = list(d.items()) |
415 except RuntimeError as ex: |
417 except RuntimeError as ex: # pragma: cant happen |
416 runtime_err = ex |
418 runtime_err = ex |
417 else: |
419 else: |
418 break |
420 break |
419 else: |
421 else: |
420 raise runtime_err |
422 raise runtime_err # pragma: cant happen |
421 |
423 |
422 return dict((self.cached_mapped_file(k), v) for k, v in items if v) |
424 return {self.cached_mapped_file(k): v for k, v in items if v} |
423 |
425 |
424 def plugin_was_disabled(self, plugin): |
426 def plugin_was_disabled(self, plugin): |
425 """Record that `plugin` was disabled during the run.""" |
427 """Record that `plugin` was disabled during the run.""" |
426 self.disabled_plugins.add(plugin._coverage_plugin_name) |
428 self.disabled_plugins.add(plugin._coverage_plugin_name) |
427 |
429 |
435 """ |
437 """ |
436 if not self._activity(): |
438 if not self._activity(): |
437 return False |
439 return False |
438 |
440 |
439 if self.branch: |
441 if self.branch: |
440 self.covdata.add_arcs(self.mapped_file_dict(self.data)) |
442 if self.packed_arcs: |
|
443 # Unpack the line number pairs packed into integers. See |
|
444 # tracer.c:CTracer_record_pair for the C code that creates |
|
445 # these packed ints. |
|
446 data = {} |
|
447 for fname, packeds in self.data.items(): |
|
448 tuples = [] |
|
449 for packed in packeds: |
|
450 l1 = packed & 0xFFFFF |
|
451 l2 = (packed & (0xFFFFF << 20)) >> 20 |
|
452 if packed & (1 << 40): |
|
453 l1 *= -1 |
|
454 if packed & (1 << 41): |
|
455 l2 *= -1 |
|
456 tuples.append((l1, l2)) |
|
457 data[fname] = tuples |
|
458 else: |
|
459 data = self.data |
|
460 self.covdata.add_arcs(self.mapped_file_dict(data)) |
441 else: |
461 else: |
442 self.covdata.add_lines(self.mapped_file_dict(self.data)) |
462 self.covdata.add_lines(self.mapped_file_dict(self.data)) |
443 |
463 |
444 file_tracers = { |
464 file_tracers = { |
445 k: v for k, v in self.file_tracers.items() |
465 k: v for k, v in self.file_tracers.items() |