blob: a3d21622a9a732651fddf19b6b19ae79e63aac8c [file] [log] [blame]
"""
Coverage tracking internals.
"""
import sys
import threading
err = sys.stderr
import types, symbol
# use builtin sets if in >= 2.4, otherwise use 'sets' module.
try:
set()
except NameError:
from sets import Set as set
def get_interesting_lines(code):
"""
Count 'interesting' lines of Python in a code object, where
'interesting' is defined as 'lines that could possibly be
executed'.
This is done by dissassembling the code objecte and returning
line numbers.
"""
# clean up weird end-of-file issues
lines = set([ l for (o, l) in findlinestarts(code) ])
for const in code.co_consts:
if type(const) == types.CodeType:
lines.update(get_interesting_lines(const))
return lines
def findlinestarts(code):
"""Find the offsets in a byte code which are start of lines in the source.
Generate pairs (offset, lineno) as described in Python/compile.c.
CTB -- swiped from Python 2.5, module 'dis', so that earlier versions
of Python could use the function, too.
"""
byte_increments = [ord(c) for c in code.co_lnotab[0::2]]
line_increments = [ord(c) for c in code.co_lnotab[1::2]]
lastlineno = None
lineno = code.co_firstlineno
addr = 0
for byte_incr, line_incr in zip(byte_increments, line_increments):
if byte_incr:
if lineno != lastlineno:
yield (addr, lineno)
lastlineno = lineno
addr += byte_incr
lineno += line_incr
if lineno != lastlineno:
yield (addr, lineno)
class CodeTracer:
"""
Basic mechanisms for code coverage tracking, using sys.settrace.
"""
def __init__(self, exclude_prefix, include_only_prefix):
self.common = self.c = set()
self.section_name = None
self.sections = {}
self.started = False
assert not (exclude_prefix and include_only_prefix), \
"mutually exclusive"
self.excl = exclude_prefix
self.incl = include_only_prefix
def start(self):
"""
Start recording.
"""
if not self.started:
self.started = True
if self.excl and not self.incl:
global_trace_fn = self.g1
elif self.incl and not self.excl:
global_trace_fn = self.g2
else:
global_trace_fn = self.g0
sys.settrace(global_trace_fn)
if hasattr(threading, 'settrace'):
threading.settrace(global_trace_fn)
def stop(self):
if self.started:
sys.settrace(None)
if hasattr(threading, 'settrace'):
threading.settrace(None)
self.started = False
self.stop_section()
def g0(self, f, e, a):
"""
global trace function, no exclude/include info.
f == frame, e == event, a == arg .
"""
if e == 'call':
return self.t
def g1(self, f, e, a):
"""
global trace function like g0, but ignores files starting with
'self.excl'.
"""
if e == 'call':
excl = self.excl
path = f.f_globals.get('__file__')
if path is None:
path = f.f_code.co_filename
if excl and path.startswith(excl):
return
return self.t
def g2(self, f, e, a):
"""
global trace function like g0, but only records files starting with
'self.incl'.
"""
if e == 'call':
incl = self.incl
if incl and f.f_code.co_filename.startswith(incl):
return self.t
def t(self, f, e, a):
"""
local trace function.
"""
if e is 'line':
self.c.add((f.f_code.co_filename, f.f_lineno))
return self.t
def clear(self):
"""
wipe out coverage info
"""
self.c = {}
def start_section(self, name):
self.stop_section()
self.section_name = name
self.c = self.sections.get(name, set())
def stop_section(self):
if self.section_name:
self.sections[self.section_name] = self.c
self.section_name = None
self.c = self.common
class CoverageData:
"""
A class to manipulate and combine data from the CodeTracer object.
In general, do not pickle this object; it's simpler and more
straightforward to just pass the basic Python objects around
(e.g. CoverageData.common, a set, and CoverageData.sections, a
dictionary of sets).
"""
def __init__(self, trace_obj=None):
self.common = set()
self.sections = {}
if trace_obj:
self.update(trace_obj)
def update(self, trace_obj):
# transfer common-block code coverage -- if no sections are set,
# this will be all of the code coverage info.
self.common.update(trace_obj.common)
# update our internal section dictionary with the (filename, line_no)
# pairs from the section coverage as well.
for section_name, section_d in trace_obj.sections.items():
section_set = self.sections.get(section_name, set())
section_set.update(section_d)
self.sections[section_name] = section_set
def gather_files(self, name=None):
"""
Return the dictionary of lines of executed code; the dict
keys are filenames and values are sets containing individual
(integer) line numbers.
'name', if set, is the desired section name from which to gather
coverage info.
"""
cov = set()
cov.update(self.common)
if name is None:
for section_name, coverage_set in self.sections.items():
cov.update(coverage_set)
else:
coverage_set = self.sections.get(name, set())
cov.update(coverage_set)
# cov = list(cov)
# cov.sort()
files = {}
for (filename, line) in cov: # @CTB could optimize
d = files.get(filename, set())
d.add(line)
files[filename] = d
return files
def gather_sections(self, file):
"""
Return a dictionary of sets containing section coverage information for
a specific file. Dict keys are sections, and the dict values are
sets containing (integer) line numbers.
"""
sections = {}
for k, c in self.sections.items():
s = set()
for (filename, line) in c.keys():
if filename == file:
s.add(line)
sections[k] = s
return sections