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 """Control of and utilities for debugging.""" |
4 """Control of and utilities for debugging.""" |
5 |
5 |
6 import contextlib |
6 import contextlib |
|
7 import functools |
7 import inspect |
8 import inspect |
|
9 import itertools |
8 import os |
10 import os |
9 import re |
11 import pprint |
10 import sys |
12 import sys |
11 try: |
13 try: |
12 import _thread |
14 import _thread |
13 except ImportError: |
15 except ImportError: |
14 import thread as _thread |
16 import thread as _thread |
15 |
17 |
16 from coverage.backward import StringIO |
18 from coverage.backward import reprlib, StringIO |
17 from coverage.misc import isolate_module |
19 from coverage.misc import isolate_module |
18 |
20 |
19 os = isolate_module(os) |
21 os = isolate_module(os) |
20 |
22 |
21 |
23 |
22 # When debugging, it can be helpful to force some options, especially when |
24 # When debugging, it can be helpful to force some options, especially when |
23 # debugging the configuration mechanisms you usually use to control debugging! |
25 # debugging the configuration mechanisms you usually use to control debugging! |
24 # This is a list of forced debugging options. |
26 # This is a list of forced debugging options. |
25 FORCED_DEBUG = [] |
27 FORCED_DEBUG = [] |
26 |
28 FORCED_DEBUG_FILE = None |
27 # A hack for debugging testing in sub-processes. |
|
28 _TEST_NAME_FILE = "" # "/tmp/covtest.txt" |
|
29 |
29 |
30 |
30 |
31 class DebugControl(object): |
31 class DebugControl(object): |
32 """Control and output for debugging.""" |
32 """Control and output for debugging.""" |
|
33 |
|
34 show_repr_attr = False # For SimpleReprMixin |
33 |
35 |
34 def __init__(self, options, output): |
36 def __init__(self, options, output): |
35 """Configure the options and output file for debugging.""" |
37 """Configure the options and output file for debugging.""" |
36 self.options = list(options) + FORCED_DEBUG |
38 self.options = list(options) + FORCED_DEBUG |
37 self.raw_output = output |
|
38 self.suppress_callers = False |
39 self.suppress_callers = False |
39 |
40 |
40 filters = [] |
41 filters = [] |
41 if self.should('pid'): |
42 if self.should('pid'): |
42 filters.append(add_pid_and_tid) |
43 filters.append(add_pid_and_tid) |
43 self.output = DebugOutputFile( |
44 self.output = DebugOutputFile.get_one( |
44 self.raw_output, |
45 output, |
45 show_process=self.should('process'), |
46 show_process=self.should('process'), |
46 filters=filters, |
47 filters=filters, |
47 ) |
48 ) |
|
49 self.raw_output = self.output.outfile |
48 |
50 |
49 def __repr__(self): |
51 def __repr__(self): |
50 return "<DebugControl options=%r raw_output=%r>" % (self.options, self.raw_output) |
52 return "<DebugControl options=%r raw_output=%r>" % (self.options, self.raw_output) |
51 |
53 |
52 def should(self, option): |
54 def should(self, option): |
139 `skip` is the number of frames to skip, so that debugging functions can |
153 `skip` is the number of frames to skip, so that debugging functions can |
140 call this and not be included in the result. |
154 call this and not be included in the result. |
141 |
155 |
142 """ |
156 """ |
143 stack = inspect.stack()[limit:skip:-1] |
157 stack = inspect.stack()[limit:skip:-1] |
144 return "\n".join("%30s : %s @%d" % (t[3], t[1], t[2]) for t in stack) |
158 return "\n".join("%30s : %s:%d" % (t[3], t[1], t[2]) for t in stack) |
145 |
159 |
146 |
160 |
147 def dump_stack_frames(limit=None, out=None, skip=0): |
161 def dump_stack_frames(limit=None, out=None, skip=0): |
148 """Print a summary of the stack to stdout, or someplace else.""" |
162 """Print a summary of the stack to stdout, or someplace else.""" |
149 out = out or sys.stdout |
163 out = out or sys.stdout |
150 out.write(short_stack(limit=limit, skip=skip+1)) |
164 out.write(short_stack(limit=limit, skip=skip+1)) |
151 out.write("\n") |
165 out.write("\n") |
|
166 |
|
167 |
|
168 def clipped_repr(text, numchars=50): |
|
169 """`repr(text)`, but limited to `numchars`.""" |
|
170 r = reprlib.Repr() |
|
171 r.maxstring = numchars |
|
172 return r.repr(text) |
152 |
173 |
153 |
174 |
154 def short_id(id64): |
175 def short_id(id64): |
155 """Given a 64-bit id, make a shorter 16-bit one.""" |
176 """Given a 64-bit id, make a shorter 16-bit one.""" |
156 id16 = 0 |
177 id16 = 0 |
160 |
181 |
161 |
182 |
162 def add_pid_and_tid(text): |
183 def add_pid_and_tid(text): |
163 """A filter to add pid and tid to debug messages.""" |
184 """A filter to add pid and tid to debug messages.""" |
164 # Thread ids are useful, but too long. Make a shorter one. |
185 # Thread ids are useful, but too long. Make a shorter one. |
165 tid = "{0:04x}".format(short_id(_thread.get_ident())) |
186 tid = "{:04x}".format(short_id(_thread.get_ident())) |
166 text = "{0:5d}.{1}: {2}".format(os.getpid(), tid, text) |
187 text = "{:5d}.{}: {}".format(os.getpid(), tid, text) |
167 return text |
188 return text |
|
189 |
|
190 |
|
191 class SimpleReprMixin(object): |
|
192 """A mixin implementing a simple __repr__.""" |
|
193 simple_repr_ignore = ['simple_repr_ignore', '$coverage.object_id'] |
|
194 |
|
195 def __repr__(self): |
|
196 show_attrs = ( |
|
197 (k, v) for k, v in self.__dict__.items() |
|
198 if getattr(v, "show_repr_attr", True) |
|
199 and not callable(v) |
|
200 and k not in self.simple_repr_ignore |
|
201 ) |
|
202 return "<{klass} @0x{id:x} {attrs}>".format( |
|
203 klass=self.__class__.__name__, |
|
204 id=id(self), |
|
205 attrs=" ".join("{}={!r}".format(k, v) for k, v in show_attrs), |
|
206 ) |
|
207 |
|
208 |
|
209 def simplify(v): # pragma: debugging |
|
210 """Turn things which are nearly dict/list/etc into dict/list/etc.""" |
|
211 if isinstance(v, dict): |
|
212 return {k:simplify(vv) for k, vv in v.items()} |
|
213 elif isinstance(v, (list, tuple)): |
|
214 return type(v)(simplify(vv) for vv in v) |
|
215 elif hasattr(v, "__dict__"): |
|
216 return simplify({'.'+k: v for k, v in v.__dict__.items()}) |
|
217 else: |
|
218 return v |
|
219 |
|
220 |
|
221 def pp(v): # pragma: debugging |
|
222 """Debug helper to pretty-print data, including SimpleNamespace objects.""" |
|
223 # Might not be needed in 3.9+ |
|
224 pprint.pprint(simplify(v)) |
168 |
225 |
169 |
226 |
170 def filter_text(text, filters): |
227 def filter_text(text, filters): |
171 """Run `text` through a series of filters. |
228 """Run `text` through a series of filters. |
172 |
229 |
208 self.outfile = outfile |
265 self.outfile = outfile |
209 self.show_process = show_process |
266 self.show_process = show_process |
210 self.filters = list(filters) |
267 self.filters = list(filters) |
211 |
268 |
212 if self.show_process: |
269 if self.show_process: |
213 self.filters.append(CwdTracker().filter) |
270 self.filters.insert(0, CwdTracker().filter) |
214 cmd = " ".join(getattr(sys, 'argv', ['???'])) |
271 self.write("New process: executable: %r\n" % (sys.executable,)) |
215 self.write("New process: executable: %s\n" % (sys.executable,)) |
272 self.write("New process: cmd: %r\n" % (getattr(sys, 'argv', None),)) |
216 self.write("New process: cmd: %s\n" % (cmd,)) |
|
217 if hasattr(os, 'getppid'): |
273 if hasattr(os, 'getppid'): |
218 self.write("New process: parent pid: %s\n" % (os.getppid(),)) |
274 self.write("New process: pid: %r, parent pid: %r\n" % (os.getpid(), os.getppid())) |
219 |
275 |
220 SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one' |
276 SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one' |
221 |
277 |
222 @classmethod |
278 @classmethod |
223 def the_one(cls, fileobj=None, show_process=True, filters=()): |
279 def get_one(cls, fileobj=None, show_process=True, filters=(), interim=False): |
224 """Get the process-wide singleton DebugOutputFile. |
280 """Get a DebugOutputFile. |
225 |
281 |
226 If it doesn't exist yet, then create it as a wrapper around the file |
282 If `fileobj` is provided, then a new DebugOutputFile is made with it. |
227 object `fileobj`. `show_process` controls whether the debug file adds |
283 |
228 process-level information. |
284 If `fileobj` isn't provided, then a file is chosen |
|
285 (COVERAGE_DEBUG_FILE, or stderr), and a process-wide singleton |
|
286 DebugOutputFile is made. |
|
287 |
|
288 `show_process` controls whether the debug file adds process-level |
|
289 information, and filters is a list of other message filters to apply. |
|
290 |
|
291 `filters` are the text filters to apply to the stream to annotate with |
|
292 pids, etc. |
|
293 |
|
294 If `interim` is true, then a future `get_one` can replace this one. |
229 |
295 |
230 """ |
296 """ |
|
297 if fileobj is not None: |
|
298 # Make DebugOutputFile around the fileobj passed. |
|
299 return cls(fileobj, show_process, filters) |
|
300 |
231 # Because of the way igor.py deletes and re-imports modules, |
301 # Because of the way igor.py deletes and re-imports modules, |
232 # this class can be defined more than once. But we really want |
302 # this class can be defined more than once. But we really want |
233 # a process-wide singleton. So stash it in sys.modules instead of |
303 # a process-wide singleton. So stash it in sys.modules instead of |
234 # on a class attribute. Yes, this is aggressively gross. |
304 # on a class attribute. Yes, this is aggressively gross. |
235 the_one = sys.modules.get(cls.SYS_MOD_NAME) |
305 the_one, is_interim = sys.modules.get(cls.SYS_MOD_NAME, (None, True)) |
236 if the_one is None: |
306 if the_one is None or is_interim: |
237 assert fileobj is not None |
307 if fileobj is None: |
238 sys.modules[cls.SYS_MOD_NAME] = the_one = cls(fileobj, show_process, filters) |
308 debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE) |
|
309 if debug_file_name: |
|
310 fileobj = open(debug_file_name, "a") |
|
311 else: |
|
312 fileobj = sys.stderr |
|
313 the_one = cls(fileobj, show_process, filters) |
|
314 sys.modules[cls.SYS_MOD_NAME] = (the_one, interim) |
239 return the_one |
315 return the_one |
240 |
316 |
241 def write(self, text): |
317 def write(self, text): |
242 """Just like file.write, but filter through all our filters.""" |
318 """Just like file.write, but filter through all our filters.""" |
243 self.outfile.write(filter_text(text, self.filters)) |
319 self.outfile.write(filter_text(text, self.filters)) |
248 self.outfile.flush() |
324 self.outfile.flush() |
249 |
325 |
250 |
326 |
251 def log(msg, stack=False): # pragma: debugging |
327 def log(msg, stack=False): # pragma: debugging |
252 """Write a log message as forcefully as possible.""" |
328 """Write a log message as forcefully as possible.""" |
253 out = DebugOutputFile.the_one() |
329 out = DebugOutputFile.get_one(interim=True) |
254 out.write(msg+"\n") |
330 out.write(msg+"\n") |
255 if stack: |
331 if stack: |
256 dump_stack_frames(out=out, skip=1) |
332 dump_stack_frames(out=out, skip=1) |
257 |
333 |
258 |
334 |
259 def filter_aspectlib_frames(text): # pragma: debugging |
335 def decorate_methods(decorator, butnot=(), private=False): # pragma: debugging |
260 """Aspectlib prints stack traces, but includes its own frames. Scrub those out.""" |
336 """A class decorator to apply a decorator to methods.""" |
261 # <<< aspectlib/__init__.py:257:function_wrapper < igor.py:143:run_tests < ... |
337 def _decorator(cls): |
262 text = re.sub(r"(?<= )aspectlib/[^.]+\.py:\d+:\w+ < ", "", text) |
338 for name, meth in inspect.getmembers(cls, inspect.isroutine): |
263 return text |
339 if name not in cls.__dict__: |
264 |
340 continue |
265 |
341 if name != "__init__": |
266 def enable_aspectlib_maybe(): # pragma: debugging |
342 if not private and name.startswith("_"): |
267 """For debugging, we can use aspectlib to trace execution. |
343 continue |
268 |
344 if name in butnot: |
269 Define COVERAGE_ASPECTLIB to enable and configure aspectlib to trace |
345 continue |
270 execution:: |
346 setattr(cls, name, decorator(meth)) |
271 |
347 return cls |
272 $ export COVERAGE_LOG=covaspect.txt |
348 return _decorator |
273 $ export COVERAGE_ASPECTLIB=coverage.Coverage:coverage.data.CoverageData |
349 |
274 $ coverage run blah.py ... |
350 |
275 |
351 def break_in_pudb(func): # pragma: debugging |
276 This will trace all the public methods on Coverage and CoverageData, |
352 """A function decorator to stop in the debugger for each call.""" |
277 writing the information to covaspect.txt. |
353 @functools.wraps(func) |
278 |
354 def _wrapper(*args, **kwargs): |
279 """ |
355 import pudb |
280 aspects = os.environ.get("COVERAGE_ASPECTLIB", "") |
356 sys.stdout = sys.__stdout__ |
281 if not aspects: |
357 pudb.set_trace() |
282 return |
358 return func(*args, **kwargs) |
283 |
359 return _wrapper |
284 import aspectlib # pylint: disable=import-error |
360 |
285 import aspectlib.debug # pylint: disable=import-error |
361 |
286 |
362 OBJ_IDS = itertools.count() |
287 filename = os.environ.get("COVERAGE_LOG", "/tmp/covlog.txt") |
363 CALLS = itertools.count() |
288 filters = [add_pid_and_tid, filter_aspectlib_frames] |
364 OBJ_ID_ATTR = "$coverage.object_id" |
289 aspects_file = DebugOutputFile.the_one(open(filename, "a"), show_process=True, filters=filters) |
365 |
290 aspect_log = aspectlib.debug.log( |
366 def show_calls(show_args=True, show_stack=False, show_return=False): # pragma: debugging |
291 print_to=aspects_file, attributes=['id'], stacktrace=30, use_logging=False |
367 """A method decorator to debug-log each call to the function.""" |
292 ) |
368 def _decorator(func): |
293 public_methods = re.compile(r'^(__init__|[a-zA-Z].*)$') |
369 @functools.wraps(func) |
294 for aspect in aspects.split(':'): |
370 def _wrapper(self, *args, **kwargs): |
295 aspectlib.weave(aspect, aspect_log, methods=public_methods) |
371 oid = getattr(self, OBJ_ID_ATTR, None) |
|
372 if oid is None: |
|
373 oid = "{:08d} {:04d}".format(os.getpid(), next(OBJ_IDS)) |
|
374 setattr(self, OBJ_ID_ATTR, oid) |
|
375 extra = "" |
|
376 if show_args: |
|
377 eargs = ", ".join(map(repr, args)) |
|
378 ekwargs = ", ".join("{}={!r}".format(*item) for item in kwargs.items()) |
|
379 extra += "(" |
|
380 extra += eargs |
|
381 if eargs and ekwargs: |
|
382 extra += ", " |
|
383 extra += ekwargs |
|
384 extra += ")" |
|
385 if show_stack: |
|
386 extra += " @ " |
|
387 extra += "; ".join(_clean_stack_line(l) for l in short_stack().splitlines()) |
|
388 callid = next(CALLS) |
|
389 msg = "{} {:04d} {}{}\n".format(oid, callid, func.__name__, extra) |
|
390 DebugOutputFile.get_one(interim=True).write(msg) |
|
391 ret = func(self, *args, **kwargs) |
|
392 if show_return: |
|
393 msg = "{} {:04d} {} return {!r}\n".format(oid, callid, func.__name__, ret) |
|
394 DebugOutputFile.get_one(interim=True).write(msg) |
|
395 return ret |
|
396 return _wrapper |
|
397 return _decorator |
|
398 |
|
399 |
|
400 def _clean_stack_line(s): # pragma: debugging |
|
401 """Simplify some paths in a stack trace, for compactness.""" |
|
402 s = s.strip() |
|
403 s = s.replace(os.path.dirname(__file__) + '/', '') |
|
404 s = s.replace(os.path.dirname(os.__file__) + '/', '') |
|
405 s = s.replace(sys.prefix + '/', '') |
|
406 return s |