trace.py 30.7 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
__all__ = ['Trace', 'CoverageResults']
51
import io
Jeremy Hylton's avatar
Jeremy Hylton committed
52 53 54 55
import linecache
import os
import re
import sys
Christian Heimes's avatar
Christian Heimes committed
56
import time
Jeremy Hylton's avatar
Jeremy Hylton committed
57 58
import token
import tokenize
59
import inspect
60
import gc
61
import dis
62
import pickle
63
from warnings import warn as _warn
64

65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
try:
    import threading
except ImportError:
    _settrace = sys.settrace

    def _unsettrace():
        sys.settrace(None)
else:
    def _settrace(func):
        threading.settrace(func)
        sys.settrace(func)

    def _unsettrace():
        sys.settrace(None)
        threading.settrace(None)

81
def _usage(outfile):
82 83 84 85 86 87 88 89 90 91 92 93
    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.
94
-l, --listfuncs       Keep track of which functions are executed at least
Fred Drake's avatar
Fred Drake committed
95
                      once and write the results to sys.stdout after the
96
                      program exits.
97 98
-T, --trackcalls      Keep track of caller/called pairs and write the
                      results to sys.stdout after the program exits.
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
-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
115 116
-g, --timing          Prefix each line with the time since the program started.
                      Only used while tracing.
117 118

Filters, may be repeated multiple times:
119 120 121
--ignore-module=<mod> Ignore the given module(s) and its submodules
                      (if it is a package).  Accepts comma separated
                      list of module names
122 123 124 125
--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
126 127 128 129 130
PRAGMA_NOCOVER = "#pragma NO COVER"

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

131
class _Ignore:
132 133 134 135
    def __init__(self, modules=None, dirs=None):
        self._mods = set() if not modules else set(modules)
        self._dirs = [] if not dirs else [os.path.normpath(d)
                                          for d in dirs]
136 137 138
        self._ignore = { '<string>': 1 }

    def names(self, filename, modulename):
139
        if modulename in self._ignore:
140 141 142
            return self._ignore[modulename]

        # haven't seen this one before, so see if the module name is
143 144 145 146 147 148 149
        # on the ignore list.
        if modulename in self._mods:  # Identical names, so ignore
            self._ignore[modulename] = 1
            return 1

        # check if the module is a proper submodule of something on
        # the ignore list
150
        for mod in self._mods:
151 152 153 154
            # Need to take some care since ignoring
            # "cmp" mustn't mean ignoring "cmpcache" but ignoring
            # "Spam" must also mean ignoring "Spam.Eggs".
            if modulename.startswith(mod + '.'):
155 156 157
                self._ignore[modulename] = 1
                return 1

158
        # Now check that filename isn't in one of the directories
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
        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

181
def _modname(path):
Jeremy Hylton's avatar
Jeremy Hylton committed
182
    """Return a plausible module name for the patch."""
183

Jeremy Hylton's avatar
Jeremy Hylton committed
184 185 186 187
    base = os.path.basename(path)
    filename, ext = os.path.splitext(base)
    return filename

188
def _fullmodname(path):
189
    """Return a plausible module name for the path."""
190 191 192 193 194 195

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

196
    comparepath = os.path.normcase(path)
197 198
    longest = ""
    for dir in sys.path:
199 200
        dir = os.path.normcase(dir)
        if comparepath.startswith(dir) and comparepath[len(dir)] == os.sep:
201 202 203
            if len(dir) > len(longest):
                longest = dir

204 205 206 207
    if longest:
        base = path[len(longest) + 1:]
    else:
        base = path
208 209
    # the drive letter is never part of the module name
    drive, base = os.path.splitdrive(base)
210 211 212
    base = base.replace(os.sep, ".")
    if os.altsep:
        base = base.replace(os.altsep, ".")
213
    filename, ext = os.path.splitext(base)
214
    return filename.lstrip(".")
215

216 217
class CoverageResults:
    def __init__(self, counts=None, calledfuncs=None, infile=None,
218
                 callers=None, outfile=None):
219 220 221 222 223 224 225 226
        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()
227 228 229 230
        self.callers = callers
        if self.callers is None:
            self.callers = {}
        self.callers = self.callers.copy()
231 232 233
        self.infile = infile
        self.outfile = outfile
        if self.infile:
234
            # Try to merge existing counts file.
235
            try:
236 237 238
                counts, calledfuncs, callers = \
                        pickle.load(open(self.infile, 'rb'))
                self.update(self.__class__(counts, calledfuncs, callers))
239
            except (IOError, EOFError, ValueError) as err:
240 241
                print(("Skipping counts file %r: %s"
                                      % (self.infile, err)), file=sys.stderr)
242

243 244 245 246 247 248 249
    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 "))

250 251 252 253
    def update(self, other):
        """Merge in the data from another CoverageResults"""
        counts = self.counts
        calledfuncs = self.calledfuncs
254
        callers = self.callers
255 256
        other_counts = other.counts
        other_calledfuncs = other.calledfuncs
257
        other_callers = other.callers
258

259
        for key in other_counts:
Jeremy Hylton's avatar
Jeremy Hylton committed
260
            counts[key] = counts.get(key, 0) + other_counts[key]
261

262
        for key in other_calledfuncs:
263 264
            calledfuncs[key] = 1

265
        for key in other_callers:
266 267
            callers[key] = 1

Jeremy Hylton's avatar
Jeremy Hylton committed
268
    def write_results(self, show_missing=True, summary=False, coverdir=None):
269 270 271
        """
        @param coverdir
        """
272
        if self.calledfuncs:
273 274
            print()
            print("functions called:")
275
            calls = self.calledfuncs
276
            for filename, modulename, funcname in sorted(calls):
277 278
                print(("filename: %s, modulename: %s, funcname: %s"
                       % (filename, modulename, funcname)))
279 280

        if self.callers:
281 282
            print()
            print("calling relationships:")
283
            lastfile = lastcfile = ""
284
            for ((pfile, pmod, pfunc), (cfile, cmod, cfunc)) \
285
                    in sorted(self.callers):
286
                if pfile != lastfile:
287 288
                    print()
                    print("***", pfile, "***")
289 290 291
                    lastfile = pfile
                    lastcfile = ""
                if cfile != pfile and lastcfile != cfile:
292
                    print("  -->", cfile)
293
                    lastcfile = cfile
294
                print("    %s.%s -> %s.%s" % (pmod, pfunc, cmod, cfunc))
295 296 297 298

        # turn the counts data ("(filename, lineno) = count") into something
        # accessible on a per-file basis
        per_file = {}
299
        for filename, lineno in self.counts:
Jeremy Hylton's avatar
Jeremy Hylton committed
300 301
            lines_hit = per_file[filename] = per_file.get(filename, {})
            lines_hit[lineno] = self.counts[(filename, lineno)]
302 303 304 305

        # accumulate summary info, if needed
        sums = {}

306
        for filename, count in per_file.items():
307
            if self.is_ignored_filename(filename):
308
                continue
309

310
            if filename.endswith((".pyc", ".pyo")):
311 312
                filename = filename[:-1]

Jeremy Hylton's avatar
Jeremy Hylton committed
313 314
            if coverdir is None:
                dir = os.path.dirname(os.path.abspath(filename))
315
                modulename = _modname(filename)
316
            else:
Jeremy Hylton's avatar
Jeremy Hylton committed
317 318 319
                dir = coverdir
                if not os.path.exists(dir):
                    os.makedirs(dir)
320
                modulename = _fullmodname(filename)
321 322 323 324

            # If desired, get a list of the line numbers which represent
            # executable content (returned as a dict for better lookup speed)
            if show_missing:
325
                lnotab = _find_executable_linenos(filename)
326
            else:
Jeremy Hylton's avatar
Jeremy Hylton committed
327
                lnotab = {}
328

Jeremy Hylton's avatar
Jeremy Hylton committed
329 330
            source = linecache.getlines(filename)
            coverpath = os.path.join(dir, modulename + ".cover")
331 332
            with open(filename, 'rb') as fp:
                encoding, _ = tokenize.detect_encoding(fp.readline)
Jeremy Hylton's avatar
Jeremy Hylton committed
333
            n_hits, n_lines = self.write_results_file(coverpath, source,
334
                                                      lnotab, count, encoding)
335 336 337 338 339
            if summary and n_lines:
                percent = int(100 * n_hits / n_lines)
                sums[modulename] = n_lines, percent, modulename, filename

        if summary and sums:
340
            print("lines   cov%   module   (path)")
341
            for m in sorted(sums):
342
                n_lines, percent, modulename, filename = sums[m]
343
                print("%5d   %3d%%   %s   (%s)" % sums[m])
344 345 346 347

        if self.outfile:
            # try and store counts and module info into self.outfile
            try:
348
                pickle.dump((self.counts, self.calledfuncs, self.callers),
349
                            open(self.outfile, 'wb'), 1)
350
            except IOError as err:
351
                print("Can't save counts files because %s" % err, file=sys.stderr)
Jeremy Hylton's avatar
Jeremy Hylton committed
352

353
    def write_results_file(self, path, lines, lnotab, lines_hit, encoding=None):
Jeremy Hylton's avatar
Jeremy Hylton committed
354
        """Return a coverage results file in path."""
355

Jeremy Hylton's avatar
Jeremy Hylton committed
356
        try:
357
            outfile = open(path, "w", encoding=encoding)
358
        except IOError as err:
359 360
            print(("trace: Could not open %r for writing: %s"
                                  "- skipping" % (path, err)), file=sys.stderr)
361
            return 0, 0
Jeremy Hylton's avatar
Jeremy Hylton committed
362 363 364

        n_lines = 0
        n_hits = 0
365
        for lineno, line in enumerate(lines, 1):
Jeremy Hylton's avatar
Jeremy Hylton committed
366 367 368 369 370 371 372
            # 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):
373
                outfile.write("       ")
Jeremy Hylton's avatar
Jeremy Hylton committed
374 375 376 377
            else:
                # lines preceded by no marks weren't hit
                # Highlight them if so indicated, unless the line contains
                # #pragma: NO COVER
378
                if lineno in lnotab and not PRAGMA_NOCOVER in line:
Jeremy Hylton's avatar
Jeremy Hylton committed
379
                    outfile.write(">>>>>> ")
380
                    n_lines += 1
Jeremy Hylton's avatar
Jeremy Hylton committed
381 382
                else:
                    outfile.write("       ")
383
            outfile.write(line.expandtabs(8))
Jeremy Hylton's avatar
Jeremy Hylton committed
384 385 386 387
        outfile.close()

        return n_hits, n_lines

388
def _find_lines_from_code(code, strs):
Jeremy Hylton's avatar
Jeremy Hylton committed
389
    """Return dict where keys are lines in the line number table."""
390 391
    linenos = {}

392
    for _, lineno in dis.findlinestarts(code):
Jeremy Hylton's avatar
Jeremy Hylton committed
393 394
        if lineno not in strs:
            linenos[lineno] = 1
395 396 397

    return linenos

398
def _find_lines(code, strs):
Jeremy Hylton's avatar
Jeremy Hylton committed
399
    """Return lineno dict for all code objects reachable from code."""
400
    # get all of the lineno information from the code of this scope level
401
    linenos = _find_lines_from_code(code, strs)
402 403 404

    # and check the constants for references to other code objects
    for c in code.co_consts:
405
        if inspect.iscode(c):
406
            # find another code object, so recurse into it
407
            linenos.update(_find_lines(c, strs))
408 409
    return linenos

410
def _find_strings(filename, encoding=None):
Jeremy Hylton's avatar
Jeremy Hylton committed
411
    """Return a dict of possible docstring positions.
412

Jeremy Hylton's avatar
Jeremy Hylton committed
413 414 415
    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.
416
    """
Jeremy Hylton's avatar
Jeremy Hylton committed
417 418 419 420
    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
421 422 423 424 425 426 427 428 429 430
    with open(filename, encoding=encoding) as f:
        tok = tokenize.generate_tokens(f.readline)
        for ttype, tstr, start, end, line in tok:
            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
Jeremy Hylton's avatar
Jeremy Hylton committed
431
    return d
432

433
def _find_executable_linenos(filename):
Jeremy Hylton's avatar
Jeremy Hylton committed
434 435
    """Return dict where keys are line numbers in the line number table."""
    try:
436
        with tokenize.open(filename) as f:
437
            prog = f.read()
438
            encoding = f.encoding
439
    except IOError as err:
440 441
        print(("Not printing coverage data for %r: %s"
                              % (filename, err)), file=sys.stderr)
Jeremy Hylton's avatar
Jeremy Hylton committed
442 443
        return {}
    code = compile(prog, filename, "exec")
444 445
    strs = _find_strings(filename, encoding)
    return _find_lines(code, strs)
446 447

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

    def runctx(self, cmd, globals=None, locals=None):
        if globals is None: globals = {}
        if locals is None: locals = {}
        if not self.donothing:
507
            _settrace(self.globaltrace)
508
        try:
509
            exec(cmd, globals, locals)
510 511
        finally:
            if not self.donothing:
512
                _unsettrace()
513 514 515 516 517 518

    def runfunc(self, func, *args, **kw):
        result = None
        if not self.donothing:
            sys.settrace(self.globaltrace)
        try:
519
            result = func(*args, **kw)
520 521 522 523 524
        finally:
            if not self.donothing:
                sys.settrace(None)
        return result

525 526 527 528
    def file_module_function_of(self, frame):
        code = frame.f_code
        filename = code.co_filename
        if filename:
529
            modulename = _modname(filename)
530 531 532 533 534 535 536 537 538 539 540 541 542
        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)
543
                         if inspect.isfunction(f)]
544 545 546 547 548 549 550 551 552 553 554
            # 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()
555
                        clsname = classes[0].__name__
556 557 558 559 560 561 562 563 564 565
                        # 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:
            funcname = "%s.%s" % (clsname, funcname)

        return filename, modulename, funcname

566 567 568 569 570 571 572
    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
573 574
            this_func = self.file_module_function_of(frame)
            parent_func = self.file_module_function_of(frame.f_back)
575 576
            self._callers[(parent_func, this_func)] = 1

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

Jeremy Hylton's avatar
Jeremy Hylton committed
580
        Adds (filename, modulename, funcname) to the self._calledfuncs dict.
581 582
        """
        if why == 'call':
583 584
            this_func = self.file_module_function_of(frame)
            self._calledfuncs[this_func] = 1
585 586

    def globaltrace_lt(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
587 588 589 590
        """Handler for call events.

        If the code block being entered is to be ignored, returns `None',
        else returns self.localtrace.
591 592
        """
        if why == 'call':
Jeremy Hylton's avatar
Jeremy Hylton committed
593
            code = frame.f_code
594
            filename = frame.f_globals.get('__file__', None)
595
            if filename:
596
                # XXX _modname() doesn't work right for packages, so
597
                # the ignore support won't work right for packages
598
                modulename = _modname(filename)
599 600 601 602
                if modulename is not None:
                    ignore_it = self.ignore.names(filename, modulename)
                    if not ignore_it:
                        if self.trace:
603 604
                            print((" --- modulename: %s, funcname: %s"
                                   % (modulename, code.co_name)))
605 606 607 608 609
                        return self.localtrace
            else:
                return None

    def localtrace_trace_and_count(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
610
        if why == "line":
611
            # record the file name and line number of every trace
Jeremy Hylton's avatar
Jeremy Hylton committed
612 613
            filename = frame.f_code.co_filename
            lineno = frame.f_lineno
614 615
            key = filename, lineno
            self.counts[key] = self.counts.get(key, 0) + 1
Tim Peters's avatar
Tim Peters committed
616

Christian Heimes's avatar
Christian Heimes committed
617 618
            if self.start_time:
                print('%.2f' % (time.time() - self.start_time), end=' ')
Jeremy Hylton's avatar
Jeremy Hylton committed
619
            bname = os.path.basename(filename)
620
            print("%s(%d): %s" % (bname, lineno,
Georg Brandl's avatar
Georg Brandl committed
621
                                  linecache.getline(filename, lineno)), end='')
622 623 624
        return self.localtrace

    def localtrace_trace(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
625 626 627 628 629
        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
630 631
            if self.start_time:
                print('%.2f' % (time.time() - self.start_time), end=' ')
Jeremy Hylton's avatar
Jeremy Hylton committed
632
            bname = os.path.basename(filename)
633
            print("%s(%d): %s" % (bname, lineno,
Georg Brandl's avatar
Georg Brandl committed
634
                                  linecache.getline(filename, lineno)), end='')
635 636 637
        return self.localtrace

    def localtrace_count(self, frame, why, arg):
Jeremy Hylton's avatar
Jeremy Hylton committed
638
        if why == "line":
639 640 641 642 643 644 645 646 647
            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,
648 649
                               calledfuncs=self._calledfuncs,
                               callers=self._callers)
650 651 652 653 654 655 656 657 658 659 660

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
661
        opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:msC:lTg",
662 663 664 665
                                        ["help", "version", "trace", "count",
                                         "report", "no-report", "summary",
                                         "file=", "missing",
                                         "ignore-module=", "ignore-dir=",
666
                                         "coverdir=", "listfuncs",
Christian Heimes's avatar
Christian Heimes committed
667
                                         "trackcalls", "timing"])
668

669
    except getopt.error as msg:
670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685
        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
686
    countcallers = False
Christian Heimes's avatar
Christian Heimes committed
687
    timing = False
688 689 690 691 692 693 694 695 696 697

    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)

698 699 700 701
        if opt == "-T" or opt == "--trackcalls":
            countcallers = True
            continue

702 703 704 705
        if opt == "-l" or opt == "--listfuncs":
            listfuncs = True
            continue

Christian Heimes's avatar
Christian Heimes committed
706 707 708 709
        if opt == "-g" or opt == "--timing":
            timing = True
            continue

710 711 712 713 714 715 716 717 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
        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":
743 744
            for mod in val.split(","):
                ignore_modules.append(mod.strip())
745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766
            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)")

767 768 769
    if not (count or trace or report or listfuncs or countcallers):
        _err_exit("must specify one of --trace, --count, --report, "
                  "--listfuncs, or --trackcalls")
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789

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

        results = t.results()

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

814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855
#  Deprecated API
def usage(outfile):
    _warn("The trace.usage() function is deprecated",
         DeprecationWarning, 2)
    _usage(outfile)

class Ignore(_Ignore):
    def __init__(self, modules=None, dirs=None):
        _warn("The class trace.Ignore is deprecated",
             DeprecationWarning, 2)
        _Ignore.__init__(self, modules, dirs)

def modname(path):
    _warn("The trace.modname() function is deprecated",
         DeprecationWarning, 2)
    return _modname(path)

def fullmodname(path):
    _warn("The trace.fullmodname() function is deprecated",
         DeprecationWarning, 2)
    return _fullmodname(path)

def find_lines_from_code(code, strs):
    _warn("The trace.find_lines_from_code() function is deprecated",
         DeprecationWarning, 2)
    return _find_lines_from_code(code, strs)

def find_lines(code, strs):
    _warn("The trace.find_lines() function is deprecated",
         DeprecationWarning, 2)
    return _find_lines(code, strs)

def find_strings(filename, encoding=None):
    _warn("The trace.find_strings() function is deprecated",
         DeprecationWarning, 2)
    return _find_strings(filename, encoding=None)

def find_executable_linenos(filename):
    _warn("The trace.find_executable_linenos() function is deprecated",
         DeprecationWarning, 2)
    return _find_executable_linenos(filename)

856 857
if __name__=='__main__':
    main()