pytracer.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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
  3. """Raw data collector for coverage.py."""
  4. import dis
  5. import sys
  6. from coverage import env
  7. # We need the YIELD_VALUE opcode below, in a comparison-friendly form.
  8. YIELD_VALUE = dis.opmap['YIELD_VALUE']
  9. if env.PY2:
  10. YIELD_VALUE = chr(YIELD_VALUE)
  11. class PyTracer(object):
  12. """Python implementation of the raw data tracer."""
  13. # Because of poor implementations of trace-function-manipulating tools,
  14. # the Python trace function must be kept very simple. In particular, there
  15. # must be only one function ever set as the trace function, both through
  16. # sys.settrace, and as the return value from the trace function. Put
  17. # another way, the trace function must always return itself. It cannot
  18. # swap in other functions, or return None to avoid tracing a particular
  19. # frame.
  20. #
  21. # The trace manipulator that introduced this restriction is DecoratorTools,
  22. # which sets a trace function, and then later restores the pre-existing one
  23. # by calling sys.settrace with a function it found in the current frame.
  24. #
  25. # Systems that use DecoratorTools (or similar trace manipulations) must use
  26. # PyTracer to get accurate results. The command-line --timid argument is
  27. # used to force the use of this tracer.
  28. def __init__(self):
  29. # Attributes set from the collector:
  30. self.data = None
  31. self.trace_arcs = False
  32. self.should_trace = None
  33. self.should_trace_cache = None
  34. self.warn = None
  35. # The threading module to use, if any.
  36. self.threading = None
  37. self.cur_file_dict = []
  38. self.last_line = [0]
  39. self.data_stack = []
  40. self.last_exc_back = None
  41. self.last_exc_firstlineno = 0
  42. self.thread = None
  43. self.stopped = False
  44. def __repr__(self):
  45. return "<PyTracer at 0x{0:0x}: {1} lines in {2} files>".format(
  46. id(self),
  47. sum(len(v) for v in self.data.values()),
  48. len(self.data),
  49. )
  50. def _trace(self, frame, event, arg_unused):
  51. """The trace function passed to sys.settrace."""
  52. if self.stopped:
  53. return
  54. if self.last_exc_back:
  55. if frame == self.last_exc_back:
  56. # Someone forgot a return event.
  57. if self.trace_arcs and self.cur_file_dict:
  58. pair = (self.last_line, -self.last_exc_firstlineno)
  59. self.cur_file_dict[pair] = None
  60. self.cur_file_dict, self.last_line = self.data_stack.pop()
  61. self.last_exc_back = None
  62. if event == 'call':
  63. # Entering a new function context. Decide if we should trace
  64. # in this file.
  65. self.data_stack.append((self.cur_file_dict, self.last_line))
  66. filename = frame.f_code.co_filename
  67. disp = self.should_trace_cache.get(filename)
  68. if disp is None:
  69. disp = self.should_trace(filename, frame)
  70. self.should_trace_cache[filename] = disp
  71. self.cur_file_dict = None
  72. if disp.trace:
  73. tracename = disp.source_filename
  74. if tracename not in self.data:
  75. self.data[tracename] = {}
  76. self.cur_file_dict = self.data[tracename]
  77. # The call event is really a "start frame" event, and happens for
  78. # function calls and re-entering generators. The f_lasti field is
  79. # -1 for calls, and a real offset for generators. Use <0 as the
  80. # line number for calls, and the real line number for generators.
  81. if frame.f_lasti < 0:
  82. self.last_line = -frame.f_code.co_firstlineno
  83. else:
  84. self.last_line = frame.f_lineno
  85. elif event == 'line':
  86. # Record an executed line.
  87. if self.cur_file_dict is not None:
  88. lineno = frame.f_lineno
  89. if self.trace_arcs:
  90. self.cur_file_dict[(self.last_line, lineno)] = None
  91. else:
  92. self.cur_file_dict[lineno] = None
  93. self.last_line = lineno
  94. elif event == 'return':
  95. if self.trace_arcs and self.cur_file_dict:
  96. # Record an arc leaving the function, but beware that a
  97. # "return" event might just mean yielding from a generator.
  98. bytecode = frame.f_code.co_code[frame.f_lasti]
  99. if bytecode != YIELD_VALUE:
  100. first = frame.f_code.co_firstlineno
  101. self.cur_file_dict[(self.last_line, -first)] = None
  102. # Leaving this function, pop the filename stack.
  103. self.cur_file_dict, self.last_line = self.data_stack.pop()
  104. elif event == 'exception':
  105. self.last_exc_back = frame.f_back
  106. self.last_exc_firstlineno = frame.f_code.co_firstlineno
  107. return self._trace
  108. def start(self):
  109. """Start this Tracer.
  110. Return a Python function suitable for use with sys.settrace().
  111. """
  112. if self.threading:
  113. self.thread = self.threading.currentThread()
  114. sys.settrace(self._trace)
  115. self.stopped = False
  116. return self._trace
  117. def stop(self):
  118. """Stop this Tracer."""
  119. self.stopped = True
  120. if self.threading and self.thread.ident != self.threading.currentThread().ident:
  121. # Called on a different thread than started us: we can't unhook
  122. # ourselves, but we've set the flag that we should stop, so we
  123. # won't do any more tracing.
  124. return
  125. if self.warn:
  126. if sys.gettrace() != self._trace:
  127. msg = "Trace function changed, measurement is likely wrong: %r"
  128. self.warn(msg % (sys.gettrace(),))
  129. sys.settrace(None)
  130. def get_stats(self):
  131. """Return a dictionary of statistics, or None."""
  132. return None