eric6/DebugClients/Python/coverage/collector.py

changeset 7427
362cd1b6f81a
parent 6942
2602857055c5
child 7702
f8b97639deb5
equal deleted inserted replaced
7426:dc171b1d8261 7427:362cd1b6f81a
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)
137 "Couldn't trace with concurrency=%s, the module isn't installed." % ( 142 "Couldn't trace with concurrency=%s, the module isn't installed." % (
138 self.concurrency, 143 self.concurrency,
139 ) 144 )
140 ) 145 )
141 146
142 # Who-Tests-What is just a hack at the moment, so turn it on with an
143 # environment variable.
144 self.wtw = int(os.getenv('COVERAGE_WTW', 0))
145
146 self.reset() 147 self.reset()
147 148
148 if timid: 149 if timid:
149 # Being timid: use the simple Python trace function. 150 # Being timid: use the simple Python trace function.
150 self._trace_class = PyTracer 151 self._trace_class = PyTracer
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

eric ide

mercurial