trace.py 29.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
#!/usr/bin/env python

# portions copyright 2001, Autonomous Zones Industries, Inc., all rights...
# err...  reserved and offered to the public under the terms of the
# Python 2.2 license.
# Author: Zooko O'Whielacronx
# http://zooko.com/
# mailto:zooko@zooko.com
#
# Copyright 2000, Mojam Media, Inc., all rights reserved.
# Author: Skip Montanaro
#
# Copyright 1999, Bioreason, Inc., all rights reserved.
# Author: Andrew Dalke
#
# Copyright 1995-1997, Automatrix, Inc., all rights reserved.
# Author: Skip Montanaro
#
# Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved.
#
#
# Permission to use, copy, modify, and distribute this Python software and
# its associated documentation for any purpose without fee is hereby
# granted, provided that the above copyright notice appears in all copies,
# and that both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of neither Automatrix,
# Bioreason or Mojam Media be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior permission.
#
"""program/module to trace Python program or function execution

Sample use, command line:
  trace.py -c -f counts --ignore-dir '$prefix' spam.py eggs
  trace.py -t --ignore-dir '$prefix' spam.py eggs
35
  trace.py --trackcalls spam.py eggs
36 37

Sample use, programmatically
Skip Montanaro's avatar
Skip Montanaro committed
38 39 40 41 42
  import sys

  # create a Trace object, telling it what to ignore, and whether to
  # do tracing or line-counting or both.
  tracer = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix,], trace=0,
Tim Peters's avatar
Tim Peters committed
43
                    count=1)
Skip Montanaro's avatar
Skip Montanaro committed
44 45 46 47 48
  # run the new command using the given tracer
  tracer.run('main()')
  # make a report, placing output in /tmp
  r = tracer.results()
  r.write_results(show_missing=True, coverdir="/tmp")
49 50
"""

Jeremy Hylton's avatar
Jeremy Hylton committed
51 52 53 54
import linecache
import os
import re
import sys
55
import threading
56
import time
Jeremy Hylton's avatar
Jeremy Hylton committed
57 58 59
import token
import tokenize
import types
60
import gc
Jeremy Hylton's avatar
Jeremy Hylton committed
61

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
try:
    import cPickle
    pickle = cPickle
except ImportError:
    import pickle

def usage(outfile):
    outfile.write("""Usage: %s [OPTIONS] <file> [ARGS]

Meta-options:
--help                Display this help then exit.
--version             Output version information then exit.

Otherwise, exactly one of the following three options must be given:
-t, --trace           Print each line to sys.stdout before it is executed.
-c, --count           Count the number of times each line is executed
                      and write the counts to <module>.cover for each
                      module executed, in the module's directory.
                      See also `--coverdir', `--file', `--no-report' below.
81
-l, --listfuncs       Keep track of which functions are executed at least
Fred Drake's avatar
Fred Drake committed
82
                      once and write the results to sys.stdout after the
83
                      program exits.
84 85
-T, --trackcalls      Keep track of caller/called pairs and write the
                      results to sys.stdout after the program exits.
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
-r, --report          Generate a report from a counts file; do not execute
                      any code.  `--file' must specify the results file to
                      read, which must have been created in a previous run
                      with `--count --file=FILE'.

Modifiers:
-f, --file=<file>     File to accumulate counts over several runs.
-R, --no-report       Do not generate the coverage report files.
                      Useful if you want to accumulate over several runs.
-C, --coverdir=<dir>  Directory where the report files.  The coverage
                      report for <package>.<module> is written to file
                      <dir>/<package>/<module>.cover.
-m, --missing         Annotate executable lines that were not executed
                      with '>>>>>> '.
-s, --summary         Write a brief summary on stdout for each file.
                      (Can only be used with --count or --report.)
102 103
-g, --timing          Prefix each line with the time since the program started.
                      Only used while tracing.
104 105

Filters, may be repeated multiple times:
106 107 108
--ignore-module=<mod> Ignore the given module(s) and its submodules
                      (if it is a package).  Accepts comma separated
                      list of module names
109 110 111 112
--ignore-dir=<dir>    Ignore files in the given directory (multiple
                      directories can be joined by os.pathsep).
""" % sys.argv[0])

Jeremy Hylton's avatar
Jeremy Hylton committed
113 114 115 116 117
PRAGMA_NOCOVER = "#pragma NO COVER"

# Simple rx to find lines with no code.
rx_blank = re.compile(r'^\s*(#.*)?$')

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
class Ignore:
    def __init__(self, modules = None, dirs = None):
        self._mods = modules or []
        self._dirs = dirs or []

        self._dirs = map(os.path.normpath, self._dirs)
        self._ignore = { '<string>': 1 }

    def names(self, filename, modulename):
        if self._ignore.has_key(modulename):
            return self._ignore[modulename]

        # haven't seen this one before, so see if the module name is
        # on the ignore list.  Need to take some care since ignoring
        # "cmp" musn't mean ignoring "cmpcache" but ignoring
        # "Spam" must also mean ignoring "Spam.Eggs".
        for mod in self._mods:
            if mod == modulename:  # Identical names, so ignore
                self._ignore[modulename] = 1
                return 1
            # check if the module is a proper submodule of something on
            # the ignore list
            n = len(mod)
            # (will not overflow since if the first n characters are the
142
            # same and the name has not already occurred, then the size
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
            # of "name" is greater than that of "mod")
            if mod == modulename[:n] and modulename[n] == '.':
                self._ignore[modulename] = 1
                return 1

        # Now check that __file__ isn't in one of the directories
        if filename is None:
            # must be a built-in, so we must ignore
            self._ignore[modulename] = 1
            return 1

        # Ignore a file when it contains one of the ignorable paths
        for d in self._dirs:
            # The '+ os.sep' is to ensure that d is a parent directory,
            # as compared to cases like:
            #  d = "/usr/local"
            #  filename = "/usr/local.py"
            # or
            #  d = "/usr/local.py"
            #  filename = "/usr/local.py"
            if filename.startswith(d + os.sep):
                self._ignore[modulename] = 1
                return 1

        # Tried the different ways, so we don't ignore this module
        self._ignore[modulename] = 0
        return 0

Jeremy Hylton's avatar
Jeremy Hylton committed
171 172
def modname(path):
    """Return a plausible module name for the patch."""
173

Jeremy Hylton's avatar
Jeremy Hylton committed
174 175 176 177
    base = os.path.basename(path)
    filename, ext = os.path.splitext(base)
    return filename

178
def fullmodname(path):
179
    """Return a plausible module name for the path."""
180 181 182 183 184 185

    # If the file 'path' is part of a package, then the filename isn't
    # enough to uniquely identify it.  Try to do the right thing by
    # looking in sys.path for the longest matching prefix.  We'll
    # assume that the rest is the package name.

186
    comparepath = os.path.normcase(path)
187 188
    longest = ""
    for dir in sys.path:
189 190
        dir = os.path.normcase(dir)
        if comparepath.startswith(dir) and comparepath[len(dir)] == os.sep:
191 192 193
            if len(dir) > len(longest):
                longest = dir

194 195 196 197
    if longest:
        base = path[len(longest) + 1:]
    else:
        base = path
198 199
    # the drive letter is never part of the module name
    drive, base = os.path.splitdrive(base)
200 201 202
    base = base.replace(os.sep, ".")
    if os.altsep:
        base = base.replace(os.altsep, ".")
203
    filename, ext = os.path.splitext(base)
204
    return filename.lstrip(".")
205

206 207
class CoverageResults:
    def __init__(self, counts=None, calledfuncs=None, infile=None,
208
                 callers=None, outfile=None):
209 210 211 212 213 214 215 216
        self.counts = counts
        if self.counts is None:
            self.counts = {}
        self.counter = self.counts.copy() # map (filename, lineno) to count
        self.calledfuncs = calledfuncs
        if self.calledfuncs is None:
            self.calledfuncs = {}
        self.calledfuncs = self.calledfuncs.copy()
217 218 219 220
        self.callers = callers
        if self.callers is None:
            self.callers = {}
        self.callers = self.callers.copy()
221 222 223
        self.infile = infile
        self.outfile = outfile
        if self.infile:
224
            # Try to merge existing counts file.
225
            try:
226 227 228
                counts, calledfuncs, callers = \
                        pickle.load(open(self.infile, 'rb'))
                self.update(self.__class__(counts, calledfuncs, callers))
229
            except (IOError, EOFError, ValueError), err:
Jeremy Hylton's avatar
Jeremy Hylton committed
230 231
                print >> sys.stderr, ("Skipping counts file %r: %s"
                                      % (self.infile, err))
232 233 234 235 236

    def update(self, other):
        """Merge in the data from another CoverageResults"""
        counts = self.counts
        calledfuncs = self.calledfuncs
237
        callers = self.callers
238 239
        other_counts = other.counts
        other_calledfuncs = other.calledfuncs
240
        other_callers = other.callers
241 242

        for key in other_counts.keys():
Jeremy Hylton's avatar
Jeremy Hylton committed
243
            counts[key] = counts.get(key, 0) + other_counts[key]
244 245 246 247

        for key in other_calledfuncs.keys():
            calledfuncs[key] = 1

248 249 250
        for key in other_callers.keys():
            callers[key] = 1

Jeremy Hylton's avatar
Jeremy Hylton committed
251
    def write_results(self, show_missing=True, summary=False, coverdir=None):
252 253 254
        """
        @param coverdir
        """
255
        if self.calledfuncs:
256
            print
257 258 259 260 261 262 263 264
            print "functions called:"
            calls = self.calledfuncs.keys()
            calls.sort()
            for filename, modulename, funcname in calls:
                print ("filename: %s, modulename: %s, funcname: %s"
                       % (filename, modulename, funcname))

        if self.callers:
265
            print
266 267 268 269 270 271
            print "calling relationships:"
            calls = self.callers.keys()
            calls.sort()
            lastfile = lastcfile = ""
            for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) in calls:
                if pfile != lastfile:
272
                    print
273 274 275 276 277 278 279
                    print "***", pfile, "***"
                    lastfile = pfile
                    lastcfile = ""
                if cfile != pfile and lastcfile != cfile:
                    print "  -->", cfile
                    lastcfile = cfile
                print "    %s.%s -> %s.%s" % (pmod, pfunc, cmod, cfunc)
280 281 282 283

        # turn the counts data ("(filename, lineno) = count") into something
        # accessible on a per-file basis
        per_file = {}
Jeremy Hylton's avatar
Jeremy Hylton committed
284 285 286
        for filename, lineno in self.counts.keys():
            lines_hit = per_file[filename] = per_file.get(filename, {})
            lines_hit[lineno] = self.counts[(filename, lineno)]
287 288 289 290

        # accumulate summary info, if needed
        sums = {}

Jeremy Hylton's avatar
Jeremy Hylton committed
291
        for filename, count in per_file.iteritems():
292 293 294
            # skip some "files" we don't care about...
            if filename == "<string>":
                continue
295 296
            if filename.startswith("<doctest "):
                continue
297

298
            if filename.endswith((".pyc", ".pyo")):
299 300
                filename = filename[:-1]

Jeremy Hylton's avatar
Jeremy Hylton committed
301 302
            if coverdir is None:
                dir = os.path.dirname(os.path.abspath(filename))
303
                modulename = modname(filename)
304
            else:
Jeremy Hylton's avatar
Jeremy Hylton committed
305 306 307
                dir = coverdir
                if not os.path.exists(dir):
                    os.makedirs(dir)
308
                modulename = fullmodname(filename)
309 310 311 312

            # If desired, get a list of the line numbers which represent
            # executable content (returned as a dict for better lookup speed)
            if show_missing:
Jeremy Hylton's avatar
Jeremy Hylton committed
313
                lnotab = find_executable_linenos(filename)
314
            else:
Jeremy Hylton's avatar
Jeremy Hylton committed
315
                lnotab = {}
316

Jeremy Hylton's avatar
Jeremy Hylton committed
317 318 319 320
            source = linecache.getlines(filename)
            coverpath = os.path.join(dir, modulename + ".cover")
            n_hits, n_lines = self.write_results_file(coverpath, source,
                                                      lnotab, count)
Tim Peters's avatar
Tim Peters committed
321

322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
            if summary and n_lines:
                percent = int(100 * n_hits / n_lines)
                sums[modulename] = n_lines, percent, modulename, filename

        if summary and sums:
            mods = sums.keys()
            mods.sort()
            print "lines   cov%   module   (path)"
            for m in mods:
                n_lines, percent, modulename, filename = sums[m]
                print "%5d   %3d%%   %s   (%s)" % sums[m]

        if self.outfile:
            # try and store counts and module info into self.outfile
            try:
337
                pickle.dump((self.counts, self.calledfuncs, self.callers),
338
                            open(self.outfile, 'wb'), 1)
339
            except IOError, err:
Jeremy Hylton's avatar
Jeremy Hylton committed
340 341 342 343
                print >> sys.stderr, "Can't save counts files because %s" % err

    def write_results_file(self, path, lines, lnotab, lines_hit):
        """Return a coverage results file in path."""
344

Jeremy Hylton's avatar
Jeremy Hylton committed
345 346 347 348 349
        try:
            outfile = open(path, "w")
        except IOError, err:
            print >> sys.stderr, ("trace: Could not open %r for writing: %s"
                                  "- skipping" % (path, err))
350
            return 0, 0
Jeremy Hylton's avatar
Jeremy Hylton committed
351 352 353 354 355 356 357 358 359 360 361 362

        n_lines = 0
        n_hits = 0
        for i, line in enumerate(lines):
            lineno = i + 1
            # do the blank/comment match to try to mark more lines
            # (help the reader find stuff that hasn't been covered)
            if lineno in lines_hit:
                outfile.write("%5d: " % lines_hit[lineno])
                n_hits += 1
                n_lines += 1
            elif rx_blank.match(line):
363
                outfile.write("       ")
Jeremy Hylton's avatar
Jeremy Hylton committed
364 365 366 367 368 369
            else:
                # lines preceded by no marks weren't hit
                # Highlight them if so indicated, unless the line contains
                # #pragma: NO COVER
                if lineno in lnotab and not PRAGMA_NOCOVER in lines[i]:
                    outfile.write(">>>>>> ")
370
                    n_lines += 1
Jeremy Hylton's avatar
Jeremy Hylton committed
371 372 373 374 375 376 377 378 379
                else:
                    outfile.write("       ")
            outfile.write(lines[i].expandtabs(8))
        outfile.close()

        return n_hits, n_lines

def find_lines_from_code(code, strs):
    """Return dict where keys are lines in the line number table."""
380 381 382 383
    linenos = {}

    line_increments = [ord(c) for c in code.co_lnotab[1::2]]
    table_length = len(line_increments)
Jeremy Hylton's avatar
Jeremy Hylton committed
384
    docstring = False
385 386 387 388

    lineno = code.co_firstlineno
    for li in line_increments:
        lineno += li
Jeremy Hylton's avatar
Jeremy Hylton committed
389 390
        if lineno not in strs:
            linenos[lineno] = 1
391 392 393

    return linenos

Jeremy Hylton's avatar
Jeremy Hylton committed
394 395
def find_lines(code, strs):
    """Return lineno dict for all code objects reachable from code."""
396
    # get all of the lineno information from the code of this scope level
Jeremy Hylton's avatar
Jeremy Hylton committed
397
    linenos = find_lines_from_code(code, strs)
398 399 400

    # and check the constants for references to other code objects
    for c in code.co_consts:
Jeremy Hylton's avatar
Jeremy Hylton committed
401
        if isinstance(c, types.CodeType):
402
            # find another code object, so recurse into it
Jeremy Hylton's avatar
Jeremy Hylton committed
403
            linenos.update(find_lines(c, strs))
404 405
    return linenos

Jeremy Hylton's avatar
Jeremy Hylton committed
406 407
def find_strings(filename):
    """Return a dict of possible docstring positions.
408

Jeremy Hylton's avatar
Jeremy Hylton committed
409 410 411
    The dict maps line numbers to strings.  There is an entry for
    line that contains only a string or a part of a triple-quoted
    string.
412
    """
Jeremy Hylton's avatar
Jeremy Hylton committed
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
    d = {}
    # If the first token is a string, then it's the module docstring.
    # Add this special case so that the test in the loop passes.
    prev_ttype = token.INDENT
    f = open(filename)
    for ttype, tstr, start, end, line in tokenize.generate_tokens(f.readline):
        if ttype == token.STRING:
            if prev_ttype == token.INDENT:
                sline, scol = start
                eline, ecol = end
                for i in range(sline, eline + 1):
                    d[i] = 1
        prev_ttype = ttype
    f.close()
    return d
428

Jeremy Hylton's avatar
Jeremy Hylton committed
429 430 431
def find_executable_linenos(filename):
    """Return dict where keys are line numbers in the line number table."""
    try:
432
        prog = open(filename, "rU").read()
Jeremy Hylton's avatar
Jeremy Hylton committed
433 434 435 436 437 438 439
    except IOError, err:
        print >> sys.stderr, ("Not printing coverage data for %r: %s"
                              % (filename, err))
        return {}
    code = compile(prog, filename, "exec")
    strs = find_strings(filename)
    return find_lines(code, strs)
440 441

class Trace:
442
    def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0,
443 444
                 ignoremods=(), ignoredirs=(), infile=None, outfile=None,
                 timing=False):
445 446
        """
        @param count true iff it should count number of times each
Tim Peters's avatar
Tim Peters committed
447
                     line is executed
448
        @param trace true iff it should print out each line that is
Tim Peters's avatar
Tim Peters committed
449
                     being counted
450 451 452
        @param countfuncs true iff it should just output a list of
                     (filename, modulename, funcname,) for functions
                     that were called at least once;  This overrides
Tim Peters's avatar
Tim Peters committed
453
                     `count' and `trace'
454 455
        @param ignoremods a list of the names of modules to ignore
        @param ignoredirs a list of the names of directories to ignore
Tim Peters's avatar
Tim Peters committed
456
                     all of the (recursive) contents of
457
        @param infile file from which to read stored counts to be
Tim Peters's avatar
Tim Peters committed
458
                     added into the results
459
        @param outfile file in which to write the results
460
        @param timing true iff timing information be displayed
461 462 463 464 465 466 467 468 469 470
        """
        self.infile = infile
        self.outfile = outfile
        self.ignore = Ignore(ignoremods, ignoredirs)
        self.counts = {}   # keys are (filename, linenumber)
        self.blabbed = {} # for debugging
        self.pathtobasename = {} # for memoizing os.path.basename
        self.donothing = 0
        self.trace = trace
        self._calledfuncs = {}
471
        self._callers = {}
472
        self._caller_cache = {}
473 474 475
        self.start_time = None
        if timing:
            self.start_time = time.time()
476 477 478
        if countcallers:
            self.globaltrace = self.globaltrace_trackcallers
        elif countfuncs:
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497
            self.globaltrace = self.globaltrace_countfuncs
        elif trace and count:
            self.globaltrace = self.globaltrace_lt
            self.localtrace = self.localtrace_trace_and_count
        elif trace:
            self.globaltrace = self.globaltrace_lt
            self.localtrace = self.localtrace_trace
        elif count:
            self.globaltrace = self.globaltrace_lt
            self.localtrace = self.localtrace_count
        else:
            # Ahem -- do nothing?  Okay.
            self.donothing = 1

    def run(self, cmd):
        import __main__
        dict = __main__.__dict__
        if not self.donothing:
            sys.settrace(self.globaltrace)
498
            threading.settrace(self.globaltrace)
499 500 501 502 503
        try:
            exec cmd in dict, dict
        finally:
            if not self.donothing:
                sys.settrace(None)
504
                threading.settrace(None)
505 506 507 508 509 510

    def runctx(self, cmd, globals=None, locals=None):
        if globals is None: globals = {}
        if locals is None: locals = {}
        if not self.donothing:
            sys.settrace(self.globaltrace)
511
            threading.settrace(self.globaltrace)
512 513 514 515 516
        try:
            exec cmd in globals, locals
        finally:
            if not self.donothing:
                sys.settrace(None)
517
                threading.settrace(None)
518 519 520 521 522 523

    def runfunc(self, func, *args, **kw):
        result = None
        if not self.donothing:
            sys.settrace(self.globaltrace)
        try:
524
            result = func(*args, **kw)
525 526 527 528 529
        finally:
            if not self.donothing:
                sys.settrace(None)
        return result

530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
    def file_module_function_of(self, frame):
        code = frame.f_code
        filename = code.co_filename
        if filename:
            modulename = modname(filename)
        else:
            modulename = None

        funcname = code.co_name
        clsname = None
        if code in self._caller_cache:
            if self._caller_cache[code] is not None:
                clsname = self._caller_cache[code]
        else:
            self._caller_cache[code] = None
            ## use of gc.get_referrers() was suggested by Michael Hudson
            # all functions which refer to this code object
            funcs = [f for f in gc.get_referrers(code)
                         if hasattr(f, "func_doc")]
            # require len(func) == 1 to avoid ambiguity caused by calls to
            # new.function(): "In the face of ambiguity, refuse the
            # temptation to guess."
            if len(funcs) == 1:
                dicts = [d for d in gc.get_referrers(funcs[0])
                             if isinstance(d, dict)]
                if len(dicts) == 1:
                    classes = [c for c in gc.get_referrers(dicts[0])
                                   if hasattr(c, "__bases__")]
                    if len(classes) == 1:
                        # ditto for new.classobj()
                        clsname = str(classes[0])
                        # cache the result - assumption is that new.* is
                        # not called later to disturb this relationship
                        # _caller_cache could be flushed if functions in
                        # the new module get called.
                        self._caller_cache[code] = clsname
        if clsname is not None:
            # final hack - module name shows up in str(cls), but we've already
            # computed module name, so remove it
            clsname = clsname.split(".")[1:]
            clsname = ".".join(clsname)
            funcname = "%s.%s" % (clsname, funcname)

        return filename, modulename, funcname

575 576 577 578 579 580 581
    def globaltrace_trackcallers(self, frame, why, arg):
        """Handler for call events.

        Adds information about who called who to the self._callers dict.
        """
        if why == 'call':
            # XXX Should do a better job of identifying methods
582 583
            this_func = self.file_module_function_of(frame)
            parent_func = self.file_module_function_of(frame.f_back)
584 585
            self._callers[(parent_func, this_func)] = 1

586
    def globaltrace_countfuncs(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
587
        """Handler for call events.
Tim Peters's avatar
Tim Peters committed
588

Jeremy Hylton's avatar
Jeremy Hylton committed
589
        Adds (filename, modulename, funcname) to the self._calledfuncs dict.
590 591
        """
        if why == 'call':
592 593
            this_func = self.file_module_function_of(frame)
            self._calledfuncs[this_func] = 1
594 595

    def globaltrace_lt(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
596 597 598 599
        """Handler for call events.

        If the code block being entered is to be ignored, returns `None',
        else returns self.localtrace.
600 601
        """
        if why == 'call':
Jeremy Hylton's avatar
Jeremy Hylton committed
602
            code = frame.f_code
603
            filename = frame.f_globals.get('__file__', None)
604
            if filename:
605 606
                # XXX modname() doesn't work right for packages, so
                # the ignore support won't work right for packages
Jeremy Hylton's avatar
Jeremy Hylton committed
607
                modulename = modname(filename)
608 609 610 611 612
                if modulename is not None:
                    ignore_it = self.ignore.names(filename, modulename)
                    if not ignore_it:
                        if self.trace:
                            print (" --- modulename: %s, funcname: %s"
Jeremy Hylton's avatar
Jeremy Hylton committed
613
                                   % (modulename, code.co_name))
614 615 616 617 618
                        return self.localtrace
            else:
                return None

    def localtrace_trace_and_count(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
619
        if why == "line":
620
            # record the file name and line number of every trace
Jeremy Hylton's avatar
Jeremy Hylton committed
621 622
            filename = frame.f_code.co_filename
            lineno = frame.f_lineno
623 624
            key = filename, lineno
            self.counts[key] = self.counts.get(key, 0) + 1
Tim Peters's avatar
Tim Peters committed
625

626 627
            if self.start_time:
                print '%.2f' % (time.time() - self.start_time),
Jeremy Hylton's avatar
Jeremy Hylton committed
628 629 630
            bname = os.path.basename(filename)
            print "%s(%d): %s" % (bname, lineno,
                                  linecache.getline(filename, lineno)),
631 632 633
        return self.localtrace

    def localtrace_trace(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
634 635 636 637 638
        if why == "line":
            # record the file name and line number of every trace
            filename = frame.f_code.co_filename
            lineno = frame.f_lineno

639 640
            if self.start_time:
                print '%.2f' % (time.time() - self.start_time),
Jeremy Hylton's avatar
Jeremy Hylton committed
641 642 643
            bname = os.path.basename(filename)
            print "%s(%d): %s" % (bname, lineno,
                                  linecache.getline(filename, lineno)),
644 645 646
        return self.localtrace

    def localtrace_count(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
647
        if why == "line":
648 649 650 651 652 653 654 655 656
            filename = frame.f_code.co_filename
            lineno = frame.f_lineno
            key = filename, lineno
            self.counts[key] = self.counts.get(key, 0) + 1
        return self.localtrace

    def results(self):
        return CoverageResults(self.counts, infile=self.infile,
                               outfile=self.outfile,
657 658
                               calledfuncs=self._calledfuncs,
                               callers=self._callers)
659 660 661 662 663 664 665 666 667 668 669

def _err_exit(msg):
    sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
    sys.exit(1)

def main(argv=None):
    import getopt

    if argv is None:
        argv = sys.argv
    try:
670
        opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:msC:lTg",
671 672 673 674
                                        ["help", "version", "trace", "count",
                                         "report", "no-report", "summary",
                                         "file=", "missing",
                                         "ignore-module=", "ignore-dir=",
675
                                         "coverdir=", "listfuncs",
676
                                         "trackcalls", "timing"])
677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694

    except getopt.error, msg:
        sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
        sys.stderr.write("Try `%s --help' for more information\n"
                         % sys.argv[0])
        sys.exit(1)

    trace = 0
    count = 0
    report = 0
    no_report = 0
    counts_file = None
    missing = 0
    ignore_modules = []
    ignore_dirs = []
    coverdir = None
    summary = 0
    listfuncs = False
695
    countcallers = False
696
    timing = False
697 698 699 700 701 702 703 704 705 706

    for opt, val in opts:
        if opt == "--help":
            usage(sys.stdout)
            sys.exit(0)

        if opt == "--version":
            sys.stdout.write("trace 2.0\n")
            sys.exit(0)

707 708 709 710
        if opt == "-T" or opt == "--trackcalls":
            countcallers = True
            continue

711 712 713 714
        if opt == "-l" or opt == "--listfuncs":
            listfuncs = True
            continue

715 716 717 718
        if opt == "-g" or opt == "--timing":
            timing = True
            continue

719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751
        if opt == "-t" or opt == "--trace":
            trace = 1
            continue

        if opt == "-c" or opt == "--count":
            count = 1
            continue

        if opt == "-r" or opt == "--report":
            report = 1
            continue

        if opt == "-R" or opt == "--no-report":
            no_report = 1
            continue

        if opt == "-f" or opt == "--file":
            counts_file = val
            continue

        if opt == "-m" or opt == "--missing":
            missing = 1
            continue

        if opt == "-C" or opt == "--coverdir":
            coverdir = val
            continue

        if opt == "-s" or opt == "--summary":
            summary = 1
            continue

        if opt == "--ignore-module":
752 753
            for mod in val.split(","):
                ignore_modules.append(mod.strip())
754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775
            continue

        if opt == "--ignore-dir":
            for s in val.split(os.pathsep):
                s = os.path.expandvars(s)
                # should I also call expanduser? (after all, could use $HOME)

                s = s.replace("$prefix",
                              os.path.join(sys.prefix, "lib",
                                           "python" + sys.version[:3]))
                s = s.replace("$exec_prefix",
                              os.path.join(sys.exec_prefix, "lib",
                                           "python" + sys.version[:3]))
                s = os.path.normpath(s)
                ignore_dirs.append(s)
            continue

        assert 0, "Should never get here"

    if listfuncs and (count or trace):
        _err_exit("cannot specify both --listfuncs and (--trace or --count)")

776 777 778
    if not (count or trace or report or listfuncs or countcallers):
        _err_exit("must specify one of --trace, --count, --report, "
                  "--listfuncs, or --trackcalls")
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798

    if report and no_report:
        _err_exit("cannot specify both --report and --no-report")

    if report and not counts_file:
        _err_exit("--report requires a --file")

    if no_report and len(prog_argv) == 0:
        _err_exit("missing name of file to run")

    # everything is ready
    if report:
        results = CoverageResults(infile=counts_file, outfile=counts_file)
        results.write_results(missing, summary=summary, coverdir=coverdir)
    else:
        sys.argv = prog_argv
        progname = prog_argv[0]
        sys.path[0] = os.path.split(progname)[0]

        t = Trace(count, trace, countfuncs=listfuncs,
799 800
                  countcallers=countcallers, ignoremods=ignore_modules,
                  ignoredirs=ignore_dirs, infile=counts_file,
801
                  outfile=counts_file, timing=timing)
802
        try:
803 804 805 806 807 808 809 810 811 812
            with open(progname) as fp:
                code = compile(fp.read(), progname, 'exec')
            # try to emulate __main__ namespace as much as possible
            globs = {
                '__file__': progname,
                '__name__': '__main__',
                '__package__': None,
                '__cached__': None,
            }
            t.runctx(code, globs, globs)
813
        except IOError, err:
Jeremy Hylton's avatar
Jeremy Hylton committed
814
            _err_exit("Cannot run file %r because: %s" % (sys.argv[0], err))
815 816 817 818 819 820 821 822 823 824
        except SystemExit:
            pass

        results = t.results()

        if not no_report:
            results.write_results(missing, summary=summary, coverdir=coverdir)

if __name__=='__main__':
    main()