diff options
-rw-r--r-- | coverage/cmdline.py | 252 | ||||
-rw-r--r-- | test/test_cmdline.py | 58 |
2 files changed, 218 insertions, 92 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py index f7f92ee9..6481f229 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -4,65 +4,63 @@ import optparse, sys from coverage.execfile import run_python_file -USAGE = r""" -Coverage version %(__version__)s -Measure, collect, and report on code coverage in Python programs. - -Usage: - -coverage -x [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...] - Execute the module, passing the given command-line arguments, collecting - coverage data. With the -p option, include the machine name and process - id in the .coverage file name. With -L, measure coverage even inside the - Python installed library, which isn't done by default. With --timid, use a - simpler but slower trace method. - -coverage -e - Erase collected coverage data. - -coverage -c - Combine data from multiple coverage files (as created by -p option above) - and store it into a single file representing the union of the coverage. - -coverage -r [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] - Report on the statement coverage for the given files. With the -m - option, show line numbers of the statements that weren't executed. - -coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...] - Create an HTML report of the coverage of the given files. Each file gets - its own page, with the file listing decorated to show executed, excluded, - and missed lines. - -coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...] - Make annotated copies of the given files, marking statements that - are executed with > and statements that are missed with !. --d DIR - Write output files for -b or -a to this directory. - --i Ignore errors while reporting or annotating. - --o DIR,... - Omit reporting or annotating files when their filename path starts with - a directory listed in the omit list. - e.g. coverage -i -r -o c:\python25,lib\enthought\traits - --h Print this help. - -Coverage data is saved in the file .coverage by default. Set the -COVERAGE_FILE environment variable to save it somewhere else. -""".strip() +class opts: + directory = optparse.Option( + '-d', '--directory', action='store', dest='directory', + ) + help = optparse.Option( + '-h', '--help', action='store_true', dest='help', + ) + ignore_errors = optparse.Option( + '-i', '--ignore-errors', action='store_true', + ) + pylib = optparse.Option( + '-L', '--pylib', action='store_true', + help="Measure coverage even inside the Python installed library, which isn't done by default." + ) + show_missing = optparse.Option( + '-m', '--show-missing', action='store_true', + ) + omit = optparse.Option( + '-o', '--omit', action='store', + ) + parallel_mode = optparse.Option( + '-p', '--parallel-mode', action='store_true', + help="Include the machine name and process id in the .coverage data file name." + ) + timid = optparse.Option( + '', '--timid', action='store_true', + help="Use a simpler but slower trace method. Use this if you get seemingly impossible results!" + ) + +class CoverageOptionParser(optparse.OptionParser, object): + """Base OptionParser for coverage. + + Problems don't exit the program. + Defaults are initialized for all options. + + """ -class OptionParser(optparse.OptionParser, object): - """A better OptionParser: Problems don't exit the program.""" - - def __init__(self, help_fn, *args, **kwargs): - super(OptionParser, self).__init__( + def __init__(self, *args, **kwargs): + super(CoverageOptionParser, self).__init__( add_help_option=False, *args, **kwargs ) + self.set_defaults( + actions=[], + directory=None, + help=None, + ignore_errors=None, + omit=None, + parallel_mode=None, + pylib=None, + show_missing=None, + timid=None, + ) + self.disable_interspersed_args() - self.help_fn = help_fn + self.help_fn = None class OptionParserError(Exception): """Used to stop the optparse error handler ending the process.""" @@ -75,7 +73,7 @@ class OptionParser(optparse.OptionParser, object): """ try: - options, args = super(OptionParser, self).parse_args(args, options) + options, args = super(CoverageOptionParser, self).parse_args(args, options) except self.OptionParserError: return False, None, None return True, options, args @@ -86,43 +84,67 @@ class OptionParser(optparse.OptionParser, object): raise self.OptionParserError -class ClassicOptionParser(OptionParser): +class ClassicOptionParser(CoverageOptionParser): """Command-line parser for coverage.py classic arguments.""" - def __init__(self, help_fn, *args, **kwargs): - super(ClassicOptionParser, self).__init__(help_fn, *args, **kwargs) - - self.set_defaults(actions=[]) - - self.help_fn = help_fn + def __init__(self): + super(ClassicOptionParser, self).__init__() self.add_action('-a', '--annotate', 'annotate') self.add_action('-b', '--html', 'html') self.add_action('-c', '--combine', 'combine') - self.add_option('-d', '--directory', action='store', dest='directory') self.add_action('-e', '--erase', 'erase') - self.add_option('-h', '--help', action='store_true', dest='help') - self.add_option('-i', '--ignore-errors', action='store_true') - self.add_option('-L', '--pylib', action='store_true') - self.add_option('-m', '--show-missing', action='store_true') - self.add_option('-p', '--parallel-mode', action='store_true') self.add_action('-r', '--report', 'report') self.add_action('-x', '--execute', 'execute') - self.add_option('-o', '--omit', action='store') - self.add_option('', '--timid', action='store_true') - def add_action(self, dash, dashdash, action): + self.add_options([ + opts.directory, + opts.help, + opts.ignore_errors, + opts.pylib, + opts.show_missing, + opts.omit, + opts.parallel_mode, + opts.timid, + ]) + + def add_action(self, dash, dashdash, action_code): """Add a specialized option that is the action to execute.""" option = self.add_option(dash, dashdash, action='callback', callback=self._append_action ) - option.action_code = action + option.action_code = action_code def _append_action(self, option, opt_unused, value_unused, parser): """Callback for an option that adds to the `actions` list.""" parser.values.actions.append(option.action_code) +class NewOptionParser(CoverageOptionParser): + """Parse one of the new-style commands for coverage.py.""" + + def __init__(self, action): + super(NewOptionParser, self).__init__( + usage="coverage %s [blah]" % action + ) + self.set_defaults(actions=[action]) + + +class RunOptionParser(NewOptionParser): + def __init__(self): + super(RunOptionParser, self).__init__("execute") + self.add_options([ + opts.pylib, + opts.parallel_mode, + opts.timid, + ]) + + +CMDS = { + 'run': RunOptionParser(), +} + + class CoverageScript: """The command-line interface to Coverage.""" @@ -142,30 +164,50 @@ class CoverageScript: self.coverage = None - def help(self, error=None): - """Display an error message, or the usage for Coverage.""" + def help(self, error=None, topic=None): + """Display an error message, or the named topic.""" + assert error or topic if error: print error print "Use -h for help." else: - print USAGE % self.covpkg.__dict__ + print HELP_TOPICS[topic].strip() % self.covpkg.__dict__ def command_line(self, argv): """The bulk of the command line interface to Coverage. `argv` is the argument list to process. - + + Returns 0 if all is well, 1 if something went wrong. + """ # Collect the command-line options. OK, ERR = 0, 1 - parser = ClassicOptionParser(self.help_fn) + if not argv: + self.help_fn( + "Code coverage for Python. Use -h for help." + ) + return OK + + # The command syntax we parse depends on the first argument. Classic + # syntax always starts with an option. + if argv[0].startswith('-'): + parser = ClassicOptionParser() + else: + parser = CMDS.get(argv[0]) + if not parser: + self.help_fn("Unknown command: '%s'" % argv[0]) + return ERR + argv = argv[1:] + + parser.help_fn = self.help_fn ok, options, args = parser.parse_args(argv) if not ok: return ERR if options.help: - self.help_fn() + self.help_fn(topic='usage') return OK # Check for conflicts and problems in the options. @@ -242,7 +284,61 @@ class CoverageScript: directory=options.directory, **report_args) return OK - + + +HELP_TOPICS = { + +'usage': r""" +Coverage version %(__version__)s +Measure, collect, and report on code coverage in Python programs. + +Usage: + +coverage -x [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...] + Execute the module, passing the given command-line arguments, collecting + coverage data. With the -p option, include the machine name and process + id in the .coverage file name. With -L, measure coverage even inside the + Python installed library, which isn't done by default. With --timid, use a + simpler but slower trace method. + +coverage -e + Erase collected coverage data. + +coverage -c + Combine data from multiple coverage files (as created by -p option above) + and store it into a single file representing the union of the coverage. + +coverage -r [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] + Report on the statement coverage for the given files. With the -m + option, show line numbers of the statements that weren't executed. + +coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...] + Create an HTML report of the coverage of the given files. Each file gets + its own page, with the file listing decorated to show executed, excluded, + and missed lines. + +coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...] + Make annotated copies of the given files, marking statements that + are executed with > and statements that are missed with !. + +-d DIR + Write output files for -b or -a to this directory. + +-i Ignore errors while reporting or annotating. + +-o DIR,... + Omit reporting or annotating files when their filename path starts with + a directory listed in the omit list. + e.g. coverage -i -r -o c:\python25,lib\enthought\traits + +-h Print this help. + +Coverage data is saved in the file .coverage by default. Set the +COVERAGE_FILE environment variable to save it somewhere else. +""", + +} + def main(): """The main entrypoint to Coverage. diff --git a/test/test_cmdline.py b/test/test_cmdline.py index a53e3734..875692cd 100644 --- a/test/test_cmdline.py +++ b/test/test_cmdline.py @@ -13,9 +13,10 @@ class CmdLineParserTest(CoverageTest): super(CmdLineParserTest, self).setUp() self.help_out = None - def help_fn(self, error=None): + def help_fn(self, error=None, topic=None): """A mock help_fn to capture the error messages for tests.""" - self.help_out = error or "*usage*" + assert error or topic + self.help_out = error or ("topic:"+topic) def command_line(self, args, ret=0, help_out=""): """Run a Coverage command line, with `args` as arguments. @@ -26,13 +27,22 @@ class CmdLineParserTest(CoverageTest): """ self.help_out = "" argv = shlex.split(args) - ret_code = coverage.CoverageScript(_help_fn=self.help_fn).command_line(argv) + script = coverage.CoverageScript(_help_fn=self.help_fn) + ret_code = script.command_line(argv) self.assertEqual(ret_code, ret) self.assertEqual(self.help_out, help_out) + +class ClassicCmdLineParserTest(CmdLineParserTest): + + def testNoArgumentsAtAll(self): + self.command_line('', + help_out="Code coverage for Python. Use -h for help." + ) + def testHelp(self): - self.command_line('-h', help_out="*usage*") - self.command_line('--help', help_out="*usage*") + self.command_line('-h', help_out="topic:usage") + self.command_line('--help', help_out="topic:usage") def testUnknownOption(self): self.command_line('-z', ret=1, @@ -102,17 +112,20 @@ class CmdLineActionTest(CoverageTest): mk.coverage.return_value = mk return mk - def run_command_line(self, args): + def run_command_line(self, args, ret): """Run `args` through command_line, returning the Mock it used.""" m = self.model_object() - coverage.CoverageScript( - _covpkg=m, _run_python_file=m.run_python_file + ret_actual = coverage.CoverageScript( + _covpkg=m, _run_python_file=m.run_python_file, _help_fn=m.help_fn ).command_line(shlex.split(args)) + self.assertEqual(ret_actual, ret, + "Wrong status: got %s, wanted %s" % (ret_actual, ret) + ) return m - def cmd_executes(self, args, code): + def cmd_executes(self, args, code, ret=0): """Assert that the `args` end up executing the sequence in `code`.""" - m1 = self.run_command_line(args) + m1 = self.run_command_line(args, ret) code = textwrap.dedent(code) code = re.sub(r"(?m)^\.", "m2.", code) @@ -123,10 +136,21 @@ class CmdLineActionTest(CoverageTest): def cmd_executes_same(self, args1, args2): """Assert that the `args1` executes the same as `args2`.""" - m1 = self.run_command_line(args1) - m2 = self.run_command_line(args2) + m1 = self.run_command_line(args1, ret=0) + m2 = self.run_command_line(args2, ret=0) self.assertEqual(m1.method_calls, m2.method_calls) - + + def cmd_help(self, args, help): + """Run a command line, and check that it prints the right help.""" + m = self.run_command_line(args, ret=1) + self.assertEqual(m.method_calls, + [('help_fn', (help,), {})] + ) + + +class ClassicCmdLineActionTest(CmdLineActionTest): + """Tests of the classic coverage.py command line.""" + def testErase(self): # coverage -e self.cmd_executes("-e", """\ @@ -193,7 +217,6 @@ class CmdLineActionTest(CoverageTest): def testReport(self): # coverage -r [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] - init_load = """\ .coverage(cover_pylib=None, data_suffix=False, timid=None) .load()\n""" @@ -326,5 +349,12 @@ class CmdLineActionTest(CoverageTest): self.cmd_executes_same("-b -of,b", "-b --omit=f,b") +class NewCmdLineActionTest(CmdLineActionTest): + """Tests of the coverage.py command line.""" + + def testBadCommand(self): + self.cmd_help("xyzzy", "Unknown command: 'xyzzy'") + + if __name__ == '__main__': unittest.main() |