blob: 9508655f3e11c8b02f49b0d94c5e7d988d967f21 [file] [log] [blame]
"""
figleaf is another tool to trace Python code coverage.
figleaf uses the sys.settrace hook to record which statements are
executed by the CPython interpreter; this record can then be saved
into a file, or otherwise communicated back to a reporting script.
figleaf differs from the gold standard of Python coverage tools
('coverage.py') in several ways. First and foremost, figleaf uses the
same criterion for "interesting" lines of code as the sys.settrace
function, which obviates some of the complexity in coverage.py (but
does mean that your "loc" count goes down). Second, figleaf does not
record code executed in the Python standard library, which results in
a significant speedup. And third, the format in which the coverage
format is saved is very simple and easy to work with.
You might want to use figleaf if you're recording coverage from
multiple types of tests and need to aggregate the coverage in
interesting ways, and/or control when coverage is recorded.
coverage.py is a better choice for command-line execution, and its
reporting is a fair bit nicer.
Command line usage: ::
figleaf <python file to execute> <args to python file>
The figleaf output is saved into the file '.figleaf', which is an
*aggregate* of coverage reports from all figleaf runs from this
directory. '.figleaf' contains a pickled dictionary of sets; the keys
are source code filenames, and the sets contain all line numbers
executed by the Python interpreter. See the docs or command-line
programs in bin/ for more information.
High level API: ::
* ``start(ignore_lib=True)`` -- start recording code coverage.
* ``stop()`` -- stop recording code coverage.
* ``get_trace_obj()`` -- return the (singleton) trace object.
* ``get_info()`` -- get the coverage dictionary
Classes & functions worth knowing about (lower level API):
* ``get_lines(fp)`` -- return the set of interesting lines in the fp.
* ``combine_coverage(d1, d2)`` -- combine coverage info from two dicts.
* ``read_coverage(filename)`` -- load the coverage dictionary
* ``write_coverage(filename)`` -- write the coverage out.
* ``annotate_coverage(...)`` -- annotate a Python file with its coverage info.
Known problems:
-- module docstrings are *covered* but not found.
AUTHOR: C. Titus Brown, titus@idyll.org, with contributions from Iain Lowe.
'figleaf' is Copyright (C) 2006, 2007 C. Titus Brown. It is under the
BSD license.
"""
__version__ = "0.6.1"
# __all__ == @CTB
import sys
import os
from cPickle import dump, load
from optparse import OptionParser
import internals
# use builtin sets if in >= 2.4, otherwise use 'sets' module.
try:
set()
except NameError:
from sets import Set as set
def get_lines(fp):
"""
Return the set of interesting lines in the source code read from
this file handle.
"""
# rstrip is a workaround for http://bugs.python.org/issue4262
src = fp.read().rstrip() + "\n"
code = compile(src, "", "exec")
return internals.get_interesting_lines(code)
def combine_coverage(d1, d2):
"""
Given two coverage dictionaries, combine the recorded coverage
and return a new dictionary.
"""
keys = set(d1.keys())
keys.update(set(d2.keys()))
new_d = {}
for k in keys:
v = d1.get(k, set())
v2 = d2.get(k, set())
s = set(v)
s.update(v2)
new_d[k] = s
return new_d
def write_coverage(filename, append=True):
"""
Write the current coverage info out to the given filename. If
'append' is false, destroy any previously recorded coverage info.
"""
if _t is None:
return
data = internals.CoverageData(_t)
d = data.gather_files()
# sum existing coverage?
if append:
old = {}
fp = None
try:
fp = open(filename)
except IOError:
pass
if fp:
old = load(fp)
fp.close()
d = combine_coverage(d, old)
# ok, save.
outfp = open(filename, 'w')
try:
dump(d, outfp)
finally:
outfp.close()
def read_coverage(filename):
"""
Read a coverage dictionary in from the given file.
"""
fp = open(filename)
try:
d = load(fp)
finally:
fp.close()
return d
def dump_pickled_coverage(out_fp):
"""
Dump coverage information in pickled format into the given file handle.
"""
dump(_t, out_fp)
def load_pickled_coverage(in_fp):
"""
Replace (overwrite) coverage information from the given file handle.
"""
global _t
_t = load(in_fp)
def annotate_coverage(in_fp, out_fp, covered, all_lines,
mark_possible_lines=False):
"""
A simple example coverage annotator that outputs text.
"""
for i, line in enumerate(in_fp):
i = i + 1
if i in covered:
symbol = '>'
elif i in all_lines:
symbol = '!'
else:
symbol = ' '
symbol2 = ''
if mark_possible_lines:
symbol2 = ' '
if i in all_lines:
symbol2 = '-'
out_fp.write('%s%s %s' % (symbol, symbol2, line,))
def get_data():
if _t:
return internals.CoverageData(_t)
#######################
#
# singleton functions/top-level API
#
_t = None
def init(exclude_path=None, include_only=None):
from internals import CodeTracer
global _t
if _t is None:
_t = CodeTracer(exclude_path, include_only)
def start(ignore_python_lib=True):
"""
Start tracing code coverage. If 'ignore_python_lib' is True on
initial call, ignore all files that live below the same directory as
the 'os' module.
"""
global _t
if not _t:
exclude_path = None
if ignore_python_lib:
exclude_path = os.path.realpath(os.path.dirname(os.__file__))
init(exclude_path, None)
_t.start()
def start_section(name):
global _t
_t.start_section(name)
def stop_section():
global _t
_t.stop_section()
def stop():
"""
Stop tracing code coverage.
"""
global _t
if _t is not None:
_t.stop()
def get_trace_obj():
"""
Return the (singleton) trace object, if it exists.
"""
return _t
def get_info(section_name=None):
"""
Get the coverage dictionary from the trace object.
"""
if _t:
return get_data().gather_files(section_name)
#############
def display_ast():
l = internals.LineGrabber(open(sys.argv[1]))
l.pretty_print()
print l.lines
def main():
"""
Execute the given Python file with coverage, making it look like it is
__main__.
"""
ignore_pylibs = False
# gather args
n = 1
figleaf_args = []
for n in range(1, len(sys.argv)):
arg = sys.argv[n]
if arg.startswith('-'):
figleaf_args.append(arg)
else:
break
remaining_args = sys.argv[n:]
usage = "usage: %prog [options] [python_script arg1 arg2 ...]"
option_parser = OptionParser(usage=usage)
option_parser.add_option('-i', '--ignore-pylibs', action="store_true",
dest="ignore_pylibs", default=False,
help="ignore Python library modules")
(options, args) = option_parser.parse_args(args=figleaf_args)
assert len(args) == 0
if not remaining_args:
option_parser.error("you must specify a python script to run!")
ignore_pylibs = options.ignore_pylibs
## Reset system args so that the subsequently exec'd file can read
## from sys.argv
sys.argv = remaining_args
sys.path[0] = os.path.dirname(sys.argv[0])
cwd = os.getcwd()
start(ignore_pylibs) # START code coverage
import __main__
try:
execfile(sys.argv[0], __main__.__dict__)
finally:
stop() # STOP code coverage
write_coverage(os.path.join(cwd, '.figleaf'))