Issue22642 - Convert trace module's option handling mechanism from getopt to argparse.
Patch contributed by SilentGhost.
This commit is contained in:
parent
121edbf7e2
commit
436831dbe4
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
import sys
|
||||
from test.support import TESTFN, rmtree, unlink, captured_stdout
|
||||
from test.support.script_helper import assert_python_ok, assert_python_failure
|
||||
import unittest
|
||||
|
||||
import trace
|
||||
|
@ -364,6 +365,27 @@ class Test_Ignore(unittest.TestCase):
|
|||
# Matched before.
|
||||
self.assertTrue(ignore.names(jn('bar', 'baz.py'), 'baz'))
|
||||
|
||||
class TestCommandLine(unittest.TestCase):
|
||||
|
||||
def test_failures(self):
|
||||
_errors = (
|
||||
(b'filename is missing: required with the main options', '-l', '-T'),
|
||||
(b'cannot specify both --listfuncs and (--trace or --count)', '-lc'),
|
||||
(b'argument -R/--no-report: not allowed with argument -r/--report', '-rR'),
|
||||
(b'must specify one of --trace, --count, --report, --listfuncs, or --trackcalls', '-g'),
|
||||
(b'-r/--report requires -f/--file', '-r'),
|
||||
(b'--summary can only be used with --count or --report', '-sT'),
|
||||
(b'unrecognized arguments: -y', '-y'))
|
||||
for message, *args in _errors:
|
||||
*_, stderr = assert_python_failure('-m', 'trace', *args)
|
||||
self.assertIn(message, stderr)
|
||||
|
||||
def test_listfuncs_flag_success(self):
|
||||
with open(TESTFN, 'w') as fd:
|
||||
self.addCleanup(unlink, TESTFN)
|
||||
fd.write("a = 1\n")
|
||||
status, stdout, stderr = assert_python_ok('-m', 'trace', '-l', TESTFN)
|
||||
self.assertIn(b'functions called:', stdout)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
335
Lib/trace.py
335
Lib/trace.py
|
@ -48,6 +48,7 @@ Sample use, programmatically
|
|||
r.write_results(show_missing=True, coverdir="/tmp")
|
||||
"""
|
||||
__all__ = ['Trace', 'CoverageResults']
|
||||
import argparse
|
||||
import linecache
|
||||
import os
|
||||
import re
|
||||
|
@ -76,51 +77,6 @@ else:
|
|||
sys.settrace(None)
|
||||
threading.settrace(None)
|
||||
|
||||
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.
|
||||
-l, --listfuncs Keep track of which functions are executed at least
|
||||
once and write the results to sys.stdout after the
|
||||
program exits.
|
||||
-T, --trackcalls Keep track of caller/called pairs and write the
|
||||
results to sys.stdout after the program exits.
|
||||
-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.)
|
||||
-g, --timing Prefix each line with the time since the program started.
|
||||
Only used while tracing.
|
||||
|
||||
Filters, may be repeated multiple times:
|
||||
--ignore-module=<mod> Ignore the given module(s) and its submodules
|
||||
(if it is a package). Accepts comma separated
|
||||
list of module names
|
||||
--ignore-dir=<dir> Ignore files in the given directory (multiple
|
||||
directories can be joined by os.pathsep).
|
||||
""" % sys.argv[0])
|
||||
|
||||
PRAGMA_NOCOVER = "#pragma NO COVER"
|
||||
|
||||
# Simple rx to find lines with no code.
|
||||
|
@ -264,7 +220,13 @@ class CoverageResults:
|
|||
|
||||
def write_results(self, show_missing=True, summary=False, coverdir=None):
|
||||
"""
|
||||
@param coverdir
|
||||
Write the coverage results.
|
||||
|
||||
:param show_missing: Show lines that had no hits.
|
||||
:param summary: Include coverage summary per module.
|
||||
:param coverdir: If None, the results of each module are placed in it's
|
||||
directory, otherwise it is included in the directory
|
||||
specified.
|
||||
"""
|
||||
if self.calledfuncs:
|
||||
print()
|
||||
|
@ -646,168 +608,135 @@ class Trace:
|
|||
calledfuncs=self._calledfuncs,
|
||||
callers=self._callers)
|
||||
|
||||
def _err_exit(msg):
|
||||
sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
|
||||
sys.exit(1)
|
||||
def main():
|
||||
|
||||
def main(argv=None):
|
||||
import getopt
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--version', action='version', version='trace 2.0')
|
||||
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
grp = parser.add_argument_group('Main options',
|
||||
'One of these (or --report) must be given')
|
||||
|
||||
grp.add_argument('-c', '--count', action='store_true',
|
||||
help='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.')
|
||||
grp.add_argument('-t', '--trace', action='store_true',
|
||||
help='Print each line to sys.stdout before it is executed')
|
||||
grp.add_argument('-l', '--listfuncs', action='store_true',
|
||||
help='Keep track of which functions are executed at least once '
|
||||
'and write the results to sys.stdout after the program exits. '
|
||||
'Cannot be specified alongside --trace or --count.')
|
||||
grp.add_argument('-T', '--trackcalls', action='store_true',
|
||||
help='Keep track of caller/called pairs and write the results to '
|
||||
'sys.stdout after the program exits.')
|
||||
|
||||
grp = parser.add_argument_group('Modifiers')
|
||||
|
||||
_grp = grp.add_mutually_exclusive_group()
|
||||
_grp.add_argument('-r', '--report', action='store_true',
|
||||
help='Generate a report from a counts file; does 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')
|
||||
_grp.add_argument('-R', '--no-report', action='store_true',
|
||||
help='Do not generate the coverage report files. '
|
||||
'Useful if you want to accumulate over several runs.')
|
||||
|
||||
grp.add_argument('-f', '--file',
|
||||
help='File to accumulate counts over several runs')
|
||||
grp.add_argument('-C', '--coverdir',
|
||||
help='Directory where the report files go. The coverage report '
|
||||
'for <package>.<module> will be written to file '
|
||||
'<dir>/<package>/<module>.cover')
|
||||
grp.add_argument('-m', '--missing', action='store_true',
|
||||
help='Annotate executable lines that were not executed with '
|
||||
'">>>>>> "')
|
||||
grp.add_argument('-s', '--summary', action='store_true',
|
||||
help='Write a brief summary for each file to sys.stdout. '
|
||||
'Can only be used with --count or --report')
|
||||
grp.add_argument('-g', '--timing', action='store_true',
|
||||
help='Prefix each line with the time since the program started. '
|
||||
'Only used while tracing')
|
||||
|
||||
grp = parser.add_argument_group('Filters',
|
||||
'Can be specified multiple times')
|
||||
grp.add_argument('--ignore-module', action='append', default=[],
|
||||
help='Ignore the given module(s) and its submodules'
|
||||
'(if it is a package). Accepts comma separated list of '
|
||||
'module names.')
|
||||
grp.add_argument('--ignore-dir', action='append', default=[],
|
||||
help='Ignore files in the given directory '
|
||||
'(multiple directories can be joined by os.pathsep).')
|
||||
|
||||
parser.add_argument('filename', nargs='?',
|
||||
help='file to run as main program')
|
||||
parser.add_argument('arguments', nargs=argparse.REMAINDER,
|
||||
help='arguments to the program')
|
||||
|
||||
opts = parser.parse_args()
|
||||
|
||||
if opts.ignore_dir:
|
||||
rel_path = 'lib', 'python{0.major}.{0.minor}'.format(sys.version_info)
|
||||
_prefix = os.path.join(sys.base_prefix, *rel_path)
|
||||
_exec_prefix = os.path.join(sys.base_exec_prefix, *rel_path)
|
||||
|
||||
def parse_ignore_dir(s):
|
||||
s = os.path.expanduser(os.path.expandvars(s))
|
||||
s = s.replace('$prefix', _prefix).replace('$exec_prefix', _exec_prefix)
|
||||
return os.path.normpath(s)
|
||||
|
||||
opts.ignore_module = [mod.strip()
|
||||
for i in opts.ignore_module for mod in i.split(',')]
|
||||
opts.ignore_dir = [parse_ignore_dir(s)
|
||||
for i in opts.ignore_dir for s in i.split(os.pathsep)]
|
||||
|
||||
if opts.report:
|
||||
if not opts.file:
|
||||
parser.error('-r/--report requires -f/--file')
|
||||
results = CoverageResults(infile=opts.file, outfile=opts.file)
|
||||
return results.write_results(opts.missing, opts.summary, opts.coverdir)
|
||||
|
||||
if not any([opts.trace, opts.count, opts.listfuncs, opts.trackcalls]):
|
||||
parser.error('must specify one of --trace, --count, --report, '
|
||||
'--listfuncs, or --trackcalls')
|
||||
|
||||
if opts.listfuncs and (opts.count or opts.trace):
|
||||
parser.error('cannot specify both --listfuncs and (--trace or --count)')
|
||||
|
||||
if opts.summary and not opts.count:
|
||||
parser.error('--summary can only be used with --count or --report')
|
||||
|
||||
if opts.filename is None:
|
||||
parser.error('filename is missing: required with the main options')
|
||||
|
||||
sys.argv = opts.filename, *opts.arguments
|
||||
sys.path[0] = os.path.dirname(opts.filename)
|
||||
|
||||
t = Trace(opts.count, opts.trace, countfuncs=opts.listfuncs,
|
||||
countcallers=opts.trackcalls, ignoremods=opts.ignore_module,
|
||||
ignoredirs=opts.ignore_dir, infile=opts.file,
|
||||
outfile=opts.file, timing=opts.timing)
|
||||
try:
|
||||
opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:msC:lTg",
|
||||
["help", "version", "trace", "count",
|
||||
"report", "no-report", "summary",
|
||||
"file=", "missing",
|
||||
"ignore-module=", "ignore-dir=",
|
||||
"coverdir=", "listfuncs",
|
||||
"trackcalls", "timing"])
|
||||
with open(opts.filename) as fp:
|
||||
code = compile(fp.read(), opts.filename, 'exec')
|
||||
# try to emulate __main__ namespace as much as possible
|
||||
globs = {
|
||||
'__file__': opts.filename,
|
||||
'__name__': '__main__',
|
||||
'__package__': None,
|
||||
'__cached__': None,
|
||||
}
|
||||
t.runctx(code, globs, globs)
|
||||
except OSError as err:
|
||||
sys.exit("Cannot run file %r because: %s" % (sys.argv[0], err))
|
||||
except SystemExit:
|
||||
pass
|
||||
|
||||
except getopt.error as 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)
|
||||
results = t.results()
|
||||
|
||||
trace = 0
|
||||
count = 0
|
||||
report = 0
|
||||
no_report = 0
|
||||
counts_file = None
|
||||
missing = 0
|
||||
ignore_modules = []
|
||||
ignore_dirs = []
|
||||
coverdir = None
|
||||
summary = 0
|
||||
listfuncs = False
|
||||
countcallers = False
|
||||
timing = False
|
||||
|
||||
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)
|
||||
|
||||
if opt == "-T" or opt == "--trackcalls":
|
||||
countcallers = True
|
||||
continue
|
||||
|
||||
if opt == "-l" or opt == "--listfuncs":
|
||||
listfuncs = True
|
||||
continue
|
||||
|
||||
if opt == "-g" or opt == "--timing":
|
||||
timing = True
|
||||
continue
|
||||
|
||||
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":
|
||||
for mod in val.split(","):
|
||||
ignore_modules.append(mod.strip())
|
||||
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.base_prefix, "lib",
|
||||
"python" + sys.version[:3]))
|
||||
s = s.replace("$exec_prefix",
|
||||
os.path.join(sys.base_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)")
|
||||
|
||||
if not (count or trace or report or listfuncs or countcallers):
|
||||
_err_exit("must specify one of --trace, --count, --report, "
|
||||
"--listfuncs, or --trackcalls")
|
||||
|
||||
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,
|
||||
countcallers=countcallers, ignoremods=ignore_modules,
|
||||
ignoredirs=ignore_dirs, infile=counts_file,
|
||||
outfile=counts_file, timing=timing)
|
||||
try:
|
||||
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)
|
||||
except OSError as err:
|
||||
_err_exit("Cannot run file %r because: %s" % (sys.argv[0], err))
|
||||
except SystemExit:
|
||||
pass
|
||||
|
||||
results = t.results()
|
||||
|
||||
if not no_report:
|
||||
results.write_results(missing, summary=summary, coverdir=coverdir)
|
||||
if not opts.no_report:
|
||||
results.write_results(opts.missing, opts.summary, opts.coverdir)
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
|
|
Loading…
Reference in New Issue