trace.py 29.9 KB
Newer Older
1
#!/usr/bin/env python3
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

# 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
38 39 40 41 42 43 44 45 46 47 48
  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,
                    count=1)
  # 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
"""

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

63
import pickle
64 65 66 67 68 69 70 71 72 73 74 75 76 77

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.
78
-l, --listfuncs       Keep track of which functions are executed at least
Fred Drake's avatar
Fred Drake committed
79
                      once and write the results to sys.stdout after the
80
                      program exits.
81 82
-T, --trackcalls      Keep track of caller/called pairs and write the
                      results to sys.stdout after the program exits.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
-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.)
Christian Heimes's avatar
Christian Heimes committed
99 100
-g, --timing          Prefix each line with the time since the program started.
                      Only used while tracing.
101 102

Filters, may be repeated multiple times:
103 104 105
--ignore-module=<mod> Ignore the given module(s) and its submodules
                      (if it is a package).  Accepts comma separated
                      list of module names
106 107 108 109
--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
110 111 112 113 114
PRAGMA_NOCOVER = "#pragma NO COVER"

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

115 116 117 118 119
class Ignore:
    def __init__(self, modules = None, dirs = None):
        self._mods = modules or []
        self._dirs = dirs or []

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

    def names(self, filename, modulename):
124
        if modulename in self._ignore:
125 126 127 128 129 130 131 132 133 134 135 136 137 138
            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
139
            # same and the name has not already occurred, then the size
140 141 142 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
            # 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
168 169
def modname(path):
    """Return a plausible module name for the patch."""
170

Jeremy Hylton's avatar
Jeremy Hylton committed
171 172 173 174
    base = os.path.basename(path)
    filename, ext = os.path.splitext(base)
    return filename

175
def fullmodname(path):
176
    """Return a plausible module name for the path."""
177 178 179 180 181 182

    # 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.

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

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

203 204
class CoverageResults:
    def __init__(self, counts=None, calledfuncs=None, infile=None,
205
                 callers=None, outfile=None):
206 207 208 209 210 211 212 213
        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()
214 215 216 217
        self.callers = callers
        if self.callers is None:
            self.callers = {}
        self.callers = self.callers.copy()
218 219 220
        self.infile = infile
        self.outfile = outfile
        if self.infile:
221
            # Try to merge existing counts file.
222
            try:
223 224 225
                counts, calledfuncs, callers = \
                        pickle.load(open(self.infile, 'rb'))
                self.update(self.__class__(counts, calledfuncs, callers))
226
            except (IOError, EOFError, ValueError) as err:
227 228
                print(("Skipping counts file %r: %s"
                                      % (self.infile, err)), file=sys.stderr)
229

230 231 232 233 234 235 236
    def is_ignored_filename(self, filename):
        """Return True if the filename does not refer to a file
        we want to have reported.
        """
        return (filename == "<string>" or
                filename.startswith("<doctest "))

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

        for key in other_counts.keys():
Jeremy Hylton's avatar
Jeremy Hylton committed
247
            counts[key] = counts.get(key, 0) + other_counts[key]
248 249 250 251

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

252 253 254
        for key in other_callers.keys():
            callers[key] = 1

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

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

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

        # accumulate summary info, if needed
        sums = {}

293
        for filename, count in per_file.items():
294
            if self.is_ignored_filename(filename):
295
                continue
296

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

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

            # 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
312
                lnotab = find_executable_linenos(filename)
313
            else:
Jeremy Hylton's avatar
Jeremy Hylton committed
314
                lnotab = {}
315

Jeremy Hylton's avatar
Jeremy Hylton committed
316 317 318 319
            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
320

321 322 323 324 325
            if summary and n_lines:
                percent = int(100 * n_hits / n_lines)
                sums[modulename] = n_lines, percent, modulename, filename

        if summary and sums:
326
            print("lines   cov%   module   (path)")
327
            for m in sorted(sums.keys()):
328
                n_lines, percent, modulename, filename = sums[m]
329
                print("%5d   %3d%%   %s   (%s)" % sums[m])
330 331 332 333

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

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

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

        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):
360
                outfile.write("       ")
Jeremy Hylton's avatar
Jeremy Hylton committed
361 362 363 364 365 366
            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(">>>>>> ")
367
                    n_lines += 1
Jeremy Hylton's avatar
Jeremy Hylton committed
368 369 370 371 372 373 374 375 376
                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."""
377 378
    linenos = {}

379
    line_increments = code.co_lnotab[1::2]
380
    table_length = len(line_increments)
Jeremy Hylton's avatar
Jeremy Hylton committed
381
    docstring = False
382 383 384 385

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

    return linenos

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

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

403
def find_strings(filename, encoding=None):
Jeremy Hylton's avatar
Jeremy Hylton committed
404
    """Return a dict of possible docstring positions.
405

Jeremy Hylton's avatar
Jeremy Hylton committed
406 407 408
    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.
409
    """
Jeremy Hylton's avatar
Jeremy Hylton committed
410 411 412 413
    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
414
    f = open(filename, encoding=encoding)
Jeremy Hylton's avatar
Jeremy Hylton committed
415 416 417 418 419 420 421 422 423 424
    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
425

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

class Trace:
441
    def __init__(self, count=1, trace=1, countfuncs=0, countcallers=0,
Christian Heimes's avatar
Christian Heimes committed
442 443
                 ignoremods=(), ignoredirs=(), infile=None, outfile=None,
                 timing=False):
444 445
        """
        @param count true iff it should count number of times each
Tim Peters's avatar
Tim Peters committed
446
                     line is executed
447
        @param trace true iff it should print out each line that is
Tim Peters's avatar
Tim Peters committed
448
                     being counted
449 450 451
        @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
452
                     `count' and `trace'
453 454
        @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
455
                     all of the (recursive) contents of
456
        @param infile file from which to read stored counts to be
Tim Peters's avatar
Tim Peters committed
457
                     added into the results
458
        @param outfile file in which to write the results
Christian Heimes's avatar
Christian Heimes committed
459
        @param timing true iff timing information be displayed
460 461 462 463 464 465 466 467 468 469
        """
        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 = {}
470
        self._callers = {}
471
        self._caller_cache = {}
Christian Heimes's avatar
Christian Heimes committed
472 473 474
        self.start_time = None
        if timing:
            self.start_time = time.time()
475 476 477
        if countcallers:
            self.globaltrace = self.globaltrace_trackcallers
        elif countfuncs:
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
            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:
496
            threading.settrace(self.globaltrace)
497
            sys.settrace(self.globaltrace)
498
        try:
499
            exec(cmd, dict, dict)
500 501 502
        finally:
            if not self.donothing:
                sys.settrace(None)
503
                threading.settrace(None)
504 505 506 507 508

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

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

529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
    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)
547
                         if hasattr(f, "__doc__")]
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
            # 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

574 575 576 577 578 579 580
    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
581 582
            this_func = self.file_module_function_of(frame)
            parent_func = self.file_module_function_of(frame.f_back)
583 584
            self._callers[(parent_func, this_func)] = 1

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

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

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

        If the code block being entered is to be ignored, returns `None',
        else returns self.localtrace.
599 600
        """
        if why == 'call':
Jeremy Hylton's avatar
Jeremy Hylton committed
601
            code = frame.f_code
602
            filename = frame.f_globals.get('__file__', None)
603
            if filename:
604 605
                # 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
606
                modulename = modname(filename)
607 608 609 610
                if modulename is not None:
                    ignore_it = self.ignore.names(filename, modulename)
                    if not ignore_it:
                        if self.trace:
611 612
                            print((" --- modulename: %s, funcname: %s"
                                   % (modulename, code.co_name)))
613 614 615 616 617
                        return self.localtrace
            else:
                return None

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

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

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

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

    def localtrace_count(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
646
        if why == "line":
647 648 649 650 651 652 653 654 655
            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,
656 657
                               calledfuncs=self._calledfuncs,
                               callers=self._callers)
658 659 660 661 662 663 664 665 666 667 668

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:
Christian Heimes's avatar
Christian Heimes committed
669
        opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:msC:lTg",
670 671 672 673
                                        ["help", "version", "trace", "count",
                                         "report", "no-report", "summary",
                                         "file=", "missing",
                                         "ignore-module=", "ignore-dir=",
674
                                         "coverdir=", "listfuncs",
Christian Heimes's avatar
Christian Heimes committed
675
                                         "trackcalls", "timing"])
676

677
    except getopt.error as msg:
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
        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
694
    countcallers = False
Christian Heimes's avatar
Christian Heimes committed
695
    timing = False
696 697 698 699 700 701 702 703 704 705

    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)

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

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

Christian Heimes's avatar
Christian Heimes committed
714 715 716 717
        if opt == "-g" or opt == "--timing":
            timing = True
            continue

718 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
        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":
751 752
            for mod in val.split(","):
                ignore_modules.append(mod.strip())
753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774
            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)")

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

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

        results = t.results()

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

if __name__=='__main__':
    main()