source file: /Library/Python/2.3/site-packages/figleaf-0.6-py2.3.egg/figleaf/__init__.py
file stats: 211 lines, 4 executed: 1.9% covered
   1. #! /usr/bin/env python
   2. """
   3. figleaf is another tool to trace code coverage (yes, in Python ;).
   4. 
   5. figleaf uses the sys.settrace hook to record which statements are
   6. executed by the CPython interpreter; this record can then be saved
   7. into a file, or otherwise communicated back to a reporting script.
   8. 
   9. figleaf differs from the gold standard of coverage tools
  10. ('coverage.py') in several ways.  First and foremost, figleaf uses the
  11. same criterion for "interesting" lines of code as the sys.settrace
  12. function, which obviates some of the complexity in coverage.py (but
  13. does mean that your "loc" count goes down).  Second, figleaf does not
  14. record code executed in the Python standard library, which results in
  15. a significant speedup.  And third, the format in which the coverage
  16. format is saved is very simple and easy to work with.
  17. 
  18. You might want to use figleaf if you're recording coverage from
  19. multiple types of tests and need to aggregate the coverage in
  20. interesting ways, and/or control when coverage is recorded.
  21. coverage.py is a better choice for command-line execution, and its
  22. reporting is a fair bit nicer.
  23. 
  24. Command line usage: ::
  25. 
  26.   figleaf <python file to execute> <args to python file>
  27. 
  28. The figleaf output is saved into the file '.figleaf', which is an
  29. *aggregate* of coverage reports from all figleaf runs from this
  30. directory.  '.figleaf' contains a pickled dictionary of sets; the keys
  31. are source code filenames, and the sets contain all line numbers
  32. executed by the Python interpreter. See the docs or command-line
  33. programs in bin/ for more information.
  34. 
  35. High level API: ::
  36. 
  37.  * ``start(ignore_lib=True)`` -- start recording code coverage.
  38.  * ``stop()``                 -- stop recording code coverage.
  39.  * ``get_trace_obj()``        -- return the (singleton) trace object.
  40.  * ``get_info()``             -- get the coverage dictionary
  41. 
  42. Classes & functions worth knowing about, i.e. a lower level API:
  43. 
  44.  * ``get_lines(fp)`` -- return the set of interesting lines in the fp.
  45.  * ``combine_coverage(d1, d2)`` -- combine coverage info from two dicts.
  46.  * ``read_coverage(filename)`` -- load the coverage dictionary
  47.  * ``write_coverage(filename)`` -- write the coverage out.
  48.  * ``annotate_coverage(...)`` -- annotate a Python file with its coverage info.
  49. 
  50. Known problems:
  51. 
  52.  -- module docstrings are *covered* but not found.
  53. 
  54. AUTHOR: C. Titus Brown, titus@idyll.org, with contributions from Iain Lowe.
  55. 
  56. 'figleaf' is Copyright (C) 2006, 2007.  It is under the MIT license.
  57. """
  58. __version__ = "0.6"
  59. 
  60. import sys
  61. import os
  62. import threading
  63. from cPickle import dump, load
  64. from optparse import OptionParser
  65. 
  66. packagedir = os.path.abspath(os.path.dirname(__file__))
  67. 
  68. # import builtin sets if in > 2.4, otherwise use 'sets' module.
  69. if 'set' not in dir(__builtins__):
  70.     from sets import Set as set
  71. 
  72. 
  73. from token import *
  74. import parser, types, symbol
  75. 
  76. def get_token_name(x):
  77.     """
  78.     Utility to help pretty-print AST symbols/Python tokens.
  79.     """
  80.     if symbol.sym_name.has_key(x):
  81.         return symbol.sym_name[x]
  82.     return tok_name.get(x, '-')
  83. 
  84. class LineGrabber:
  85.     """
  86.     Count 'interesting' lines of Python in source files, where
  87.     'interesting' is defined as 'lines that could possibly be
  88.     executed'.
  89. 
  90.     @CTB this badly needs to be refactored... once I have automated
  91.     tests ;)
  92.     """
  93.     def __init__(self, fp):
  94.         """
  95.         Count lines of code in 'fp'.
  96.         """
  97.         self.lines = set()
  98. 
  99.         self.ast = parser.suite(fp.read().strip())
 100.         self.tree = parser.ast2tuple(self.ast, True)
 101. 
 102.         self.find_terminal_nodes(self.tree)
 103. 
 104.     def find_terminal_nodes(self, tup):
 105.         """
 106.         Recursively eat an AST in tuple form, finding the first line
 107.         number for "interesting" code.
 108.         """
 109.         (sym, rest) = tup[0], tup[1:]
 110. 
 111.         line_nos = set()
 112.         if type(rest[0]) == types.TupleType:  ### node
 113. 
 114.             for x in rest:
 115.                 token_line_no = self.find_terminal_nodes(x)
 116.                 if token_line_no is not None:
 117.                     line_nos.add(token_line_no)
 118. 
 119.             if symbol.sym_name[sym] in ('stmt', 'suite', 'lambdef',
 120.                                         'except_clause') and line_nos:
 121.                 # store the line number that this statement started at
 122.                 self.lines.add(min(line_nos))
 123.             elif symbol.sym_name[sym] in ('if_stmt',):
 124.                 # add all lines under this
 125.                 self.lines.update(line_nos)
 126.             elif symbol.sym_name[sym] in ('global_stmt',): # IGNORE
 127.                 return
 128.             else:
 129.                 if line_nos:
 130.                     return min(line_nos)
 131. 
 132.         else:                               ### leaf
 133.             if sym not in (NEWLINE, STRING, INDENT, DEDENT, COLON) and \
 134.                tup[1] != 'else':
 135.                 return tup[2]
 136.             return None
 137. 
 138.     def pretty_print(self, tup=None, indent=0):
 139.         """
 140.         Pretty print the AST.
 141.         """
 142.         if tup is None:
 143.             tup = self.tree
 144. 
 145.         s = tup[1]
 146. 
 147.         if type(s) == types.TupleType:
 148.             print ' '*indent, get_token_name(tup[0])
 149.             for x in tup[1:]:
 150.                 self.pretty_print(x, indent+1)
 151.         else:
 152.             print ' '*indent, get_token_name(tup[0]), tup[1:]
 153. 
 154. def get_lines(fp):
 155.     """
 156.     Return the set of interesting lines in the source code read from
 157.     this file handle.
 158.     """
 159.     l = LineGrabber(fp)
 160.     return l.lines
 161. 
 162. class CodeTracer:
 163.     """
 164.     Basic code coverage tracking, using sys.settrace.
 165.     """
 166.     def __init__(self, ignore_prefix=None):
 167.         self.common = self.c = {}
 168.         self.name = None
 169.         self.sections = {}
 170. 
 171.         self.started = False
 172.         self.ignore_prefix = ignore_prefix
 173. 
 174.     def start(self):
 175.         """
 176.         Start recording.
 177.         """
 178.         if not self.started:
 179.             self.started = True
 180. 
 181.             sys.settrace(self.g)
 182.             if hasattr(threading, 'settrace'):
 183.                 threading.settrace(self.g)
 184. 
 185.     def stop(self):
 186.         if self.started:
 187.             sys.settrace(None)
 188.             if hasattr(threading, 'settrace'):
 189.                 threading.settrace(None)
 190. 
 191.             self.started = False
 192.             self.stop_section()
 193. 
 194.     def g(self, f, e, a):
 195.         """
 196.         global trace function.
 197.         """
 198.         if e is 'call':
 199.             if self.ignore_prefix and \
 200.                    f.f_code.co_filename.startswith(self.ignore_prefix):
 201.                 return
 202. 
 203.             return self.t
 204. 
 205.     def t(self, f, e, a):
 206.         """
 207.         local trace function.
 208.         """
 209. 
 210.         if e is 'line':
 211.             self.c[(f.f_code.co_filename, f.f_lineno)] = 1
 212.         return self.t
 213. 
 214.     def clear(self):
 215.         """
 216.         wipe out coverage info
 217.         """
 218. 
 219.         self.c = {}
 220. 
 221.     def start_section(self, name):
 222.         self.stop_section()
 223. 
 224.         self.name = name
 225.         self.c = self.sections.get(name, {})
 226. 
 227.     def stop_section(self):
 228.         if self.name:
 229.             old_name = self.name
 230.             self.sections[old_name] = self.c
 231.             self.name = None
 232.             self.c = self.common
 233. 
 234.     def gather_files(self, name=None):
 235.         """
 236.         Return the dictionary of lines of executed code; the dict
 237.         contains items (k, v), where 'k' is the filename and 'v'
 238.         is a set of line numbers.
 239.         """
 240.         assert not self.started
 241. 
 242.         cov = {}
 243.         cov.update(self.common)
 244. 
 245.         if name is None:
 246.             for k, c in self.sections.items():
 247.                 cov.update(c)
 248.         else:
 249.             c = self.sections.get(name)
 250.             cov.update(c)
 251. 
 252.         files = {}
 253.         for (filename, line) in cov.keys():
 254.             d = files.get(filename, set())
 255.             d.add(line)
 256.             files[filename] = d
 257. 
 258.         return files
 259. 
 260.     def gather_sections(self, file):
 261.         sections = {}
 262.         for k, c in self.sections.items():
 263.             s = set()
 264.             for (filename, line) in c.keys():
 265.                 if filename == file:
 266.                     s.add(line)
 267.             sections[k] = s
 268.         return sections
 269. 
 270. def combine_coverage(d1, d2):
 271.     """
 272.     Given two coverage dictionaries, combine the recorded coverage
 273.     and return a new dictionary.
 274.     """
 275.     keys = set(d1.keys())
 276.     keys.update(set(d2.keys()))
 277. 
 278.     new_d = {}
 279.     for k in keys:
 280.         v = d1.get(k, set())
 281.         v2 = d2.get(k, set())
 282. 
 283.         s = set(v)
 284.         s.update(v2)
 285.         new_d[k] = s
 286. 
 287.     return new_d
 288. 
 289. def write_coverage(filename, append=True):
 290.     """
 291.     Write the current coverage info out to the given filename.  If
 292.     'append' is false, destroy any previously recorded coverage info.
 293.     """
 294.     if _t is None:
 295.         return
 296. 
 297.     d = _t.gather_files()
 298. 
 299.     # sum existing coverage?
 300.     if append:
 301.         old = {}
 302.         fp = None
 303.         try:
 304.             fp = open(filename)
 305.         except IOError:
 306.             pass
 307. 
 308.         if fp:
 309.             old = load(fp)
 310.             fp.close()
 311.             d = combine_coverage(d, old)
 312. 
 313.     # ok, save.
 314.     outfp = open(filename, 'w')
 315.     try:
 316.         dump(d, outfp)
 317.     finally:
 318.         outfp.close()
 319. 
 320. def read_coverage(filename):
 321.     """
 322.     Read a coverage dictionary in from the given file.
 323.     """
 324.     fp = open(filename)
 325.     try:
 326.         d = load(fp)
 327.     finally:
 328.         fp.close()
 329. 
 330.     return d
 331. 
 332. def dump_pickled_coverage(out_fp):
 333.     """
 334.     Dump coverage information in pickled format into the given file handle.
 335.     """
 336.     dump(_t, out_fp)
 337. 
 338. def load_pickled_coverage(in_fp):
 339.     """
 340.     Replace (overwrite) coverage information from the given file handle.
 341.     """
 342.     global _t
 343.     _t = load(in_fp)
 344. 
 345. def annotate_coverage(in_fp, out_fp, covered, all_lines,
 346.                       mark_possible_lines=False):
 347.     """
 348.     A simple example coverage annotator that outputs text.
 349.     """
 350.     for i, line in enumerate(in_fp):
 351.         i = i + 1
 352. 
 353.         if i in covered:
 354.             symbol = '>'
 355.         elif i in all_lines:
 356.             symbol = '!'
 357.         else:
 358.             symbol = ' '
 359. 
 360.         symbol2 = ''
 361.         if mark_possible_lines:
 362.             symbol2 = ' '
 363.             if i in all_lines:
 364.                 symbol2 = '-'
 365. 
 366.         out_fp.write('%s%s %s' % (symbol, symbol2, line,))
 367. 
 368. #######################
 369. 
 370. #
 371. # singleton functions/top-level API
 372. #
 373. 
 374. _t = None
 375. 
 376. def init(ignore_python_lib=True):
 377.     global _t
 378.     if _t is None:
 379.         ignore_path = None
 380.         if ignore_python_lib:
 381.             ignore_path = os.path.realpath(os.path.dirname(os.__file__))
 382.         _t = CodeTracer(ignore_path)
 383. 
 384. def start(ignore_python_lib=True):
 385.     """
 386.     Start tracing code coverage.  If 'ignore_python_lib' is True,
 387.     ignore all files that live below the same directory as the 'os'
 388.     module.
 389.     """
 390.     init(ignore_python_lib)
 391.     _t.start()
 392. 
 393. def start_section(name):
 394.     global _t
 395.     _t.start_section(name)
 396. 
 397. def stop_section():
 398.     global _t
 399.     _t.stop_section()
 400. 
 401. def stop():
 402.     """
 403.     Stop tracing code coverage.
 404.     """
 405.     global _t
 406.     if _t is not None:
 407.         _t.stop()
 408. 
 409. def get_trace_obj():
 410.     """
 411.     Return the (singleton) trace object, if it exists.
 412.     """
 413.     return _t
 414. 
 415. def get_info(section_name=None):
 416.     """
 417.     Get the coverage dictionary from the trace object.
 418.     """
 419.     if _t:
 420.         return _t.gather_files(section_name)
 421. 
 422. #############
 423. 
 424. def display_ast():
 425.     l = figleaf._LineGrabber(open(sys.argv[1]))
 426.     l.pretty_print()
 427. 
 428. def main():
 429.     """
 430.     Execute the given Python file with coverage, making it look like it is
 431.     __main__.
 432.     """
 433.     ignore_pylibs = False
 434. 
 435.     option_parser = OptionParser()
 436. 
 437.     option_parser.add_option('-i', '--ignore-pylibs', action="store_true",
 438.                              dest="ignore_pylibs", default=False,
 439.                              help="ignore Python library modules")
 440. 
 441.     (options, args) = option_parser.parse_args()
 442. 
 443.     ignore_pylibs = options.ignore_pylibs
 444. 
 445.     ## Reset system args so that the subsequently exec'd file can read from sys.argv
 446.     sys.argv = args
 447. 
 448.     sys.path[0] = os.path.dirname(args[0])
 449. 
 450.     cwd = os.getcwd()
 451. 
 452.     start(ignore_pylibs)        # START code coverage
 453. 
 454.     import __main__
 455.     try:
 456.         execfile(args[0], __main__.__dict__)
 457.     finally:
 458.         stop()                          # STOP code coverage
 459. 
 460.         write_coverage(os.path.join(cwd, '.figleaf'))