Commit 436831db authored by Senthil Kumaran's avatar Senthil Kumaran

Issue22642 - Convert trace module's option handling mechanism from getopt to argparse.

Patch contributed by SilentGhost.
parent 121edbf7
import os import os
import sys import sys
from test.support import TESTFN, rmtree, unlink, captured_stdout from test.support import TESTFN, rmtree, unlink, captured_stdout
from test.support.script_helper import assert_python_ok, assert_python_failure
import unittest import unittest
import trace import trace
...@@ -364,6 +365,27 @@ class Test_Ignore(unittest.TestCase): ...@@ -364,6 +365,27 @@ class Test_Ignore(unittest.TestCase):
# Matched before. # Matched before.
self.assertTrue(ignore.names(jn('bar', 'baz.py'), 'baz')) 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__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -48,6 +48,7 @@ Sample use, programmatically ...@@ -48,6 +48,7 @@ Sample use, programmatically
r.write_results(show_missing=True, coverdir="/tmp") r.write_results(show_missing=True, coverdir="/tmp")
""" """
__all__ = ['Trace', 'CoverageResults'] __all__ = ['Trace', 'CoverageResults']
import argparse
import linecache import linecache
import os import os
import re import re
...@@ -76,51 +77,6 @@ else: ...@@ -76,51 +77,6 @@ else:
sys.settrace(None) sys.settrace(None)
threading.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" PRAGMA_NOCOVER = "#pragma NO COVER"
# Simple rx to find lines with no code. # Simple rx to find lines with no code.
...@@ -264,7 +220,13 @@ class CoverageResults: ...@@ -264,7 +220,13 @@ class CoverageResults:
def write_results(self, show_missing=True, summary=False, coverdir=None): 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: if self.calledfuncs:
print() print()
...@@ -646,168 +608,135 @@ class Trace: ...@@ -646,168 +608,135 @@ class Trace:
calledfuncs=self._calledfuncs, calledfuncs=self._calledfuncs,
callers=self._callers) callers=self._callers)
def _err_exit(msg): def main():
sys.stderr.write("%s: %s\n" % (sys.argv[0], msg))
sys.exit(1) parser = argparse.ArgumentParser()
parser.add_argument('--version', action='version', version='trace 2.0')
def main(argv=None):
import getopt grp = parser.add_argument_group('Main options',
'One of these (or --report) must be given')
if argv is None:
argv = sys.argv grp.add_argument('-c', '--count', action='store_true',
try: help='Count the number of times each line is executed and write '
opts, prog_argv = getopt.getopt(argv[1:], "tcrRf:d:msC:lTg", 'the counts to <module>.cover for each module executed, in '
["help", "version", "trace", "count", 'the module\'s directory. See also --coverdir, --file, '
"report", "no-report", "summary", '--no-report below.')
"file=", "missing", grp.add_argument('-t', '--trace', action='store_true',
"ignore-module=", "ignore-dir=", help='Print each line to sys.stdout before it is executed')
"coverdir=", "listfuncs", grp.add_argument('-l', '--listfuncs', action='store_true',
"trackcalls", "timing"]) help='Keep track of which functions are executed at least once '
'and write the results to sys.stdout after the program exits. '
except getopt.error as msg: 'Cannot be specified alongside --trace or --count.')
sys.stderr.write("%s: %s\n" % (sys.argv[0], msg)) grp.add_argument('-T', '--trackcalls', action='store_true',
sys.stderr.write("Try `%s --help' for more information\n" help='Keep track of caller/called pairs and write the results to '
% sys.argv[0]) 'sys.stdout after the program exits.')
sys.exit(1)
grp = parser.add_argument_group('Modifiers')
trace = 0
count = 0 _grp = grp.add_mutually_exclusive_group()
report = 0 _grp.add_argument('-r', '--report', action='store_true',
no_report = 0 help='Generate a report from a counts file; does not execute any '
counts_file = None 'code. --file must specify the results file to read, which '
missing = 0 'must have been created in a previous run with --count '
ignore_modules = [] '--file=FILE')
ignore_dirs = [] _grp.add_argument('-R', '--no-report', action='store_true',
coverdir = None help='Do not generate the coverage report files. '
summary = 0 'Useful if you want to accumulate over several runs.')
listfuncs = False
countcallers = False grp.add_argument('-f', '--file',
timing = False help='File to accumulate counts over several runs')
grp.add_argument('-C', '--coverdir',
for opt, val in opts: help='Directory where the report files go. The coverage report '
if opt == "--help": 'for <package>.<module> will be written to file '
_usage(sys.stdout) '<dir>/<package>/<module>.cover')
sys.exit(0) grp.add_argument('-m', '--missing', action='store_true',
help='Annotate executable lines that were not executed with '
if opt == "--version": '">>>>>> "')
sys.stdout.write("trace 2.0\n") grp.add_argument('-s', '--summary', action='store_true',
sys.exit(0) help='Write a brief summary for each file to sys.stdout. '
'Can only be used with --count or --report')
if opt == "-T" or opt == "--trackcalls": grp.add_argument('-g', '--timing', action='store_true',
countcallers = True help='Prefix each line with the time since the program started. '
continue 'Only used while tracing')
if opt == "-l" or opt == "--listfuncs": grp = parser.add_argument_group('Filters',
listfuncs = True 'Can be specified multiple times')
continue grp.add_argument('--ignore-module', action='append', default=[],
help='Ignore the given module(s) and its submodules'
if opt == "-g" or opt == "--timing": '(if it is a package). Accepts comma separated list of '
timing = True 'module names.')
continue grp.add_argument('--ignore-dir', action='append', default=[],
help='Ignore files in the given directory '
if opt == "-t" or opt == "--trace": '(multiple directories can be joined by os.pathsep).')
trace = 1
continue parser.add_argument('filename', nargs='?',
help='file to run as main program')
if opt == "-c" or opt == "--count": parser.add_argument('arguments', nargs=argparse.REMAINDER,
count = 1 help='arguments to the program')
continue
opts = parser.parse_args()
if opt == "-r" or opt == "--report":
report = 1 if opts.ignore_dir:
continue rel_path = 'lib', 'python{0.major}.{0.minor}'.format(sys.version_info)
_prefix = os.path.join(sys.base_prefix, *rel_path)
if opt == "-R" or opt == "--no-report": _exec_prefix = os.path.join(sys.base_exec_prefix, *rel_path)
no_report = 1
continue def parse_ignore_dir(s):
s = os.path.expanduser(os.path.expandvars(s))
if opt == "-f" or opt == "--file": s = s.replace('$prefix', _prefix).replace('$exec_prefix', _exec_prefix)
counts_file = val return os.path.normpath(s)
continue
opts.ignore_module = [mod.strip()
if opt == "-m" or opt == "--missing": for i in opts.ignore_module for mod in i.split(',')]
missing = 1 opts.ignore_dir = [parse_ignore_dir(s)
continue for i in opts.ignore_dir for s in i.split(os.pathsep)]
if opt == "-C" or opt == "--coverdir": if opts.report:
coverdir = val if not opts.file:
continue parser.error('-r/--report requires -f/--file')
results = CoverageResults(infile=opts.file, outfile=opts.file)
if opt == "-s" or opt == "--summary": return results.write_results(opts.missing, opts.summary, opts.coverdir)
summary = 1
continue if not any([opts.trace, opts.count, opts.listfuncs, opts.trackcalls]):
parser.error('must specify one of --trace, --count, --report, '
if opt == "--ignore-module": '--listfuncs, or --trackcalls')
for mod in val.split(","):
ignore_modules.append(mod.strip()) if opts.listfuncs and (opts.count or opts.trace):
continue parser.error('cannot specify both --listfuncs and (--trace or --count)')
if opt == "--ignore-dir": if opts.summary and not opts.count:
for s in val.split(os.pathsep): parser.error('--summary can only be used with --count or --report')
s = os.path.expandvars(s)
# should I also call expanduser? (after all, could use $HOME) if opts.filename is None:
parser.error('filename is missing: required with the main options')
s = s.replace("$prefix",
os.path.join(sys.base_prefix, "lib", sys.argv = opts.filename, *opts.arguments
"python" + sys.version[:3])) sys.path[0] = os.path.dirname(opts.filename)
s = s.replace("$exec_prefix",
os.path.join(sys.base_exec_prefix, "lib", t = Trace(opts.count, opts.trace, countfuncs=opts.listfuncs,
"python" + sys.version[:3])) countcallers=opts.trackcalls, ignoremods=opts.ignore_module,
s = os.path.normpath(s) ignoredirs=opts.ignore_dir, infile=opts.file,
ignore_dirs.append(s) outfile=opts.file, timing=opts.timing)
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: try:
with open(progname) as fp: with open(opts.filename) as fp:
code = compile(fp.read(), progname, 'exec') code = compile(fp.read(), opts.filename, 'exec')
# try to emulate __main__ namespace as much as possible # try to emulate __main__ namespace as much as possible
globs = { globs = {
'__file__': progname, '__file__': opts.filename,
'__name__': '__main__', '__name__': '__main__',
'__package__': None, '__package__': None,
'__cached__': None, '__cached__': None,
} }
t.runctx(code, globs, globs) t.runctx(code, globs, globs)
except OSError as err: except OSError as err:
_err_exit("Cannot run file %r because: %s" % (sys.argv[0], err)) sys.exit("Cannot run file %r because: %s" % (sys.argv[0], err))
except SystemExit: except SystemExit:
pass pass
results = t.results() results = t.results()
if not no_report: if not opts.no_report:
results.write_results(missing, summary=summary, coverdir=coverdir) results.write_results(opts.missing, opts.summary, opts.coverdir)
if __name__=='__main__': if __name__=='__main__':
main() main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment