DebugClients/Python/coverage/cmdline.py

changeset 5141
bc64243b7672
parent 5126
d28b92dabc2b
parent 5140
01484c0afbc6
child 5144
1ab536d25072
equal deleted inserted replaced
5126:d28b92dabc2b 5141:bc64243b7672
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3
4 """Command-line support for coverage.py."""
5
6 import glob
7 import optparse
8 import os.path
9 import sys
10 import textwrap
11 import traceback
12
13 from coverage import env
14 from coverage.collector import CTracer
15 from coverage.execfile import run_python_file, run_python_module
16 from coverage.misc import CoverageException, ExceptionDuringRun, NoSource
17 from coverage.debug import info_formatter, info_header
18
19
20 class Opts(object):
21 """A namespace class for individual options we'll build parsers from."""
22
23 append = optparse.make_option(
24 '-a', '--append', action='store_true',
25 help="Append coverage data to .coverage, otherwise it is started clean with each run.",
26 )
27 branch = optparse.make_option(
28 '', '--branch', action='store_true',
29 help="Measure branch coverage in addition to statement coverage.",
30 )
31 CONCURRENCY_CHOICES = [
32 "thread", "gevent", "greenlet", "eventlet", "multiprocessing",
33 ]
34 concurrency = optparse.make_option(
35 '', '--concurrency', action='store', metavar="LIB",
36 choices=CONCURRENCY_CHOICES,
37 help=(
38 "Properly measure code using a concurrency library. "
39 "Valid values are: %s."
40 ) % ", ".join(CONCURRENCY_CHOICES),
41 )
42 debug = optparse.make_option(
43 '', '--debug', action='store', metavar="OPTS",
44 help="Debug options, separated by commas",
45 )
46 directory = optparse.make_option(
47 '-d', '--directory', action='store', metavar="DIR",
48 help="Write the output files to DIR.",
49 )
50 fail_under = optparse.make_option(
51 '', '--fail-under', action='store', metavar="MIN", type="int",
52 help="Exit with a status of 2 if the total coverage is less than MIN.",
53 )
54 help = optparse.make_option(
55 '-h', '--help', action='store_true',
56 help="Get help on this command.",
57 )
58 ignore_errors = optparse.make_option(
59 '-i', '--ignore-errors', action='store_true',
60 help="Ignore errors while reading source files.",
61 )
62 include = optparse.make_option(
63 '', '--include', action='store',
64 metavar="PAT1,PAT2,...",
65 help=(
66 "Include only files whose paths match one of these patterns. "
67 "Accepts shell-style wildcards, which must be quoted."
68 ),
69 )
70 pylib = optparse.make_option(
71 '-L', '--pylib', action='store_true',
72 help=(
73 "Measure coverage even inside the Python installed library, "
74 "which isn't done by default."
75 ),
76 )
77 show_missing = optparse.make_option(
78 '-m', '--show-missing', action='store_true',
79 help="Show line numbers of statements in each module that weren't executed.",
80 )
81 skip_covered = optparse.make_option(
82 '--skip-covered', action='store_true',
83 help="Skip files with 100% coverage.",
84 )
85 omit = optparse.make_option(
86 '', '--omit', action='store',
87 metavar="PAT1,PAT2,...",
88 help=(
89 "Omit files whose paths match one of these patterns. "
90 "Accepts shell-style wildcards, which must be quoted."
91 ),
92 )
93 output_xml = optparse.make_option(
94 '-o', '', action='store', dest="outfile",
95 metavar="OUTFILE",
96 help="Write the XML report to this file. Defaults to 'coverage.xml'",
97 )
98 parallel_mode = optparse.make_option(
99 '-p', '--parallel-mode', action='store_true',
100 help=(
101 "Append the machine name, process id and random number to the "
102 ".coverage data file name to simplify collecting data from "
103 "many processes."
104 ),
105 )
106 module = optparse.make_option(
107 '-m', '--module', action='store_true',
108 help=(
109 "<pyfile> is an importable Python module, not a script path, "
110 "to be run as 'python -m' would run it."
111 ),
112 )
113 rcfile = optparse.make_option(
114 '', '--rcfile', action='store',
115 help="Specify configuration file. Defaults to '.coveragerc'",
116 )
117 source = optparse.make_option(
118 '', '--source', action='store', metavar="SRC1,SRC2,...",
119 help="A list of packages or directories of code to be measured.",
120 )
121 timid = optparse.make_option(
122 '', '--timid', action='store_true',
123 help=(
124 "Use a simpler but slower trace method. Try this if you get "
125 "seemingly impossible results!"
126 ),
127 )
128 title = optparse.make_option(
129 '', '--title', action='store', metavar="TITLE",
130 help="A text string to use as the title on the HTML.",
131 )
132 version = optparse.make_option(
133 '', '--version', action='store_true',
134 help="Display version information and exit.",
135 )
136
137
138 class CoverageOptionParser(optparse.OptionParser, object):
139 """Base OptionParser for coverage.py.
140
141 Problems don't exit the program.
142 Defaults are initialized for all options.
143
144 """
145
146 def __init__(self, *args, **kwargs):
147 super(CoverageOptionParser, self).__init__(
148 add_help_option=False, *args, **kwargs
149 )
150 self.set_defaults(
151 action=None,
152 append=None,
153 branch=None,
154 concurrency=None,
155 debug=None,
156 directory=None,
157 fail_under=None,
158 help=None,
159 ignore_errors=None,
160 include=None,
161 module=None,
162 omit=None,
163 parallel_mode=None,
164 pylib=None,
165 rcfile=True,
166 show_missing=None,
167 skip_covered=None,
168 source=None,
169 timid=None,
170 title=None,
171 version=None,
172 )
173
174 self.disable_interspersed_args()
175 self.help_fn = self.help_noop
176
177 def help_noop(self, error=None, topic=None, parser=None):
178 """No-op help function."""
179 pass
180
181 class OptionParserError(Exception):
182 """Used to stop the optparse error handler ending the process."""
183 pass
184
185 def parse_args_ok(self, args=None, options=None):
186 """Call optparse.parse_args, but return a triple:
187
188 (ok, options, args)
189
190 """
191 try:
192 options, args = \
193 super(CoverageOptionParser, self).parse_args(args, options)
194 except self.OptionParserError:
195 return False, None, None
196 return True, options, args
197
198 def error(self, msg):
199 """Override optparse.error so sys.exit doesn't get called."""
200 self.help_fn(msg)
201 raise self.OptionParserError
202
203
204 class GlobalOptionParser(CoverageOptionParser):
205 """Command-line parser for coverage.py global option arguments."""
206
207 def __init__(self):
208 super(GlobalOptionParser, self).__init__()
209
210 self.add_options([
211 Opts.help,
212 Opts.version,
213 ])
214
215
216 class CmdOptionParser(CoverageOptionParser):
217 """Parse one of the new-style commands for coverage.py."""
218
219 def __init__(self, action, options=None, defaults=None, usage=None, description=None):
220 """Create an OptionParser for a coverage.py command.
221
222 `action` is the slug to put into `options.action`.
223 `options` is a list of Option's for the command.
224 `defaults` is a dict of default value for options.
225 `usage` is the usage string to display in help.
226 `description` is the description of the command, for the help text.
227
228 """
229 if usage:
230 usage = "%prog " + usage
231 super(CmdOptionParser, self).__init__(
232 usage=usage,
233 description=description,
234 )
235 self.set_defaults(action=action, **(defaults or {}))
236 if options:
237 self.add_options(options)
238 self.cmd = action
239
240 def __eq__(self, other):
241 # A convenience equality, so that I can put strings in unit test
242 # results, and they will compare equal to objects.
243 return (other == "<CmdOptionParser:%s>" % self.cmd)
244
245 def get_prog_name(self):
246 """Override of an undocumented function in optparse.OptionParser."""
247 program_name = super(CmdOptionParser, self).get_prog_name()
248
249 # Include the sub-command for this parser as part of the command.
250 return "%(command)s %(subcommand)s" % {'command': program_name, 'subcommand': self.cmd}
251
252
253 GLOBAL_ARGS = [
254 Opts.debug,
255 Opts.help,
256 Opts.rcfile,
257 ]
258
259 CMDS = {
260 'annotate': CmdOptionParser(
261 "annotate",
262 [
263 Opts.directory,
264 Opts.ignore_errors,
265 Opts.include,
266 Opts.omit,
267 ] + GLOBAL_ARGS,
268 usage="[options] [modules]",
269 description=(
270 "Make annotated copies of the given files, marking statements that are executed "
271 "with > and statements that are missed with !."
272 ),
273 ),
274
275 'combine': CmdOptionParser(
276 "combine",
277 GLOBAL_ARGS,
278 usage="<path1> <path2> ... <pathN>",
279 description=(
280 "Combine data from multiple coverage files collected "
281 "with 'run -p'. The combined results are written to a single "
282 "file representing the union of the data. The positional "
283 "arguments are data files or directories containing data files. "
284 "If no paths are provided, data files in the default data file's "
285 "directory are combined."
286 ),
287 ),
288
289 'debug': CmdOptionParser(
290 "debug", GLOBAL_ARGS,
291 usage="<topic>",
292 description=(
293 "Display information on the internals of coverage.py, "
294 "for diagnosing problems. "
295 "Topics are 'data' to show a summary of the collected data, "
296 "or 'sys' to show installation information."
297 ),
298 ),
299
300 'erase': CmdOptionParser(
301 "erase", GLOBAL_ARGS,
302 usage=" ",
303 description="Erase previously collected coverage data.",
304 ),
305
306 'help': CmdOptionParser(
307 "help", GLOBAL_ARGS,
308 usage="[command]",
309 description="Describe how to use coverage.py",
310 ),
311
312 'html': CmdOptionParser(
313 "html",
314 [
315 Opts.directory,
316 Opts.fail_under,
317 Opts.ignore_errors,
318 Opts.include,
319 Opts.omit,
320 Opts.title,
321 ] + GLOBAL_ARGS,
322 usage="[options] [modules]",
323 description=(
324 "Create an HTML report of the coverage of the files. "
325 "Each file gets its own page, with the source decorated to show "
326 "executed, excluded, and missed lines."
327 ),
328 ),
329
330 'report': CmdOptionParser(
331 "report",
332 [
333 Opts.fail_under,
334 Opts.ignore_errors,
335 Opts.include,
336 Opts.omit,
337 Opts.show_missing,
338 Opts.skip_covered,
339 ] + GLOBAL_ARGS,
340 usage="[options] [modules]",
341 description="Report coverage statistics on modules."
342 ),
343
344 'run': CmdOptionParser(
345 "run",
346 [
347 Opts.append,
348 Opts.branch,
349 Opts.concurrency,
350 Opts.include,
351 Opts.module,
352 Opts.omit,
353 Opts.pylib,
354 Opts.parallel_mode,
355 Opts.source,
356 Opts.timid,
357 ] + GLOBAL_ARGS,
358 usage="[options] <pyfile> [program options]",
359 description="Run a Python program, measuring code execution."
360 ),
361
362 'xml': CmdOptionParser(
363 "xml",
364 [
365 Opts.fail_under,
366 Opts.ignore_errors,
367 Opts.include,
368 Opts.omit,
369 Opts.output_xml,
370 ] + GLOBAL_ARGS,
371 usage="[options] [modules]",
372 description="Generate an XML report of coverage results."
373 ),
374 }
375
376
377 OK, ERR, FAIL_UNDER = 0, 1, 2
378
379
380 class CoverageScript(object):
381 """The command-line interface to coverage.py."""
382
383 def __init__(self, _covpkg=None, _run_python_file=None,
384 _run_python_module=None, _help_fn=None, _path_exists=None):
385 # _covpkg is for dependency injection, so we can test this code.
386 if _covpkg:
387 self.covpkg = _covpkg
388 else:
389 import coverage
390 self.covpkg = coverage
391
392 # For dependency injection:
393 self.run_python_file = _run_python_file or run_python_file
394 self.run_python_module = _run_python_module or run_python_module
395 self.help_fn = _help_fn or self.help
396 self.path_exists = _path_exists or os.path.exists
397 self.global_option = False
398
399 self.coverage = None
400
401 self.program_name = os.path.basename(sys.argv[0])
402 if env.WINDOWS:
403 # entry_points={'console_scripts':...} on Windows makes files
404 # called coverage.exe, coverage3.exe, and coverage-3.5.exe. These
405 # invoke coverage-script.py, coverage3-script.py, and
406 # coverage-3.5-script.py. argv[0] is the .py file, but we want to
407 # get back to the original form.
408 auto_suffix = "-script.py"
409 if self.program_name.endswith(auto_suffix):
410 self.program_name = self.program_name[:-len(auto_suffix)]
411
412 def command_line(self, argv):
413 """The bulk of the command line interface to coverage.py.
414
415 `argv` is the argument list to process.
416
417 Returns 0 if all is well, 1 if something went wrong.
418
419 """
420 # Collect the command-line options.
421 if not argv:
422 self.help_fn(topic='minimum_help')
423 return OK
424
425 # The command syntax we parse depends on the first argument. Global
426 # switch syntax always starts with an option.
427 self.global_option = argv[0].startswith('-')
428 if self.global_option:
429 parser = GlobalOptionParser()
430 else:
431 parser = CMDS.get(argv[0])
432 if not parser:
433 self.help_fn("Unknown command: '%s'" % argv[0])
434 return ERR
435 argv = argv[1:]
436
437 parser.help_fn = self.help_fn
438 ok, options, args = parser.parse_args_ok(argv)
439 if not ok:
440 return ERR
441
442 # Handle help and version.
443 if self.do_help(options, args, parser):
444 return OK
445
446 # Check for conflicts and problems in the options.
447 if not self.args_ok(options, args):
448 return ERR
449
450 # We need to be able to import from the current directory, because
451 # plugins may try to, for example, to read Django settings.
452 sys.path[0] = ''
453
454 # Listify the list options.
455 source = unshell_list(options.source)
456 omit = unshell_list(options.omit)
457 include = unshell_list(options.include)
458 debug = unshell_list(options.debug)
459
460 # Do something.
461 self.coverage = self.covpkg.coverage(
462 data_suffix=options.parallel_mode,
463 cover_pylib=options.pylib,
464 timid=options.timid,
465 branch=options.branch,
466 config_file=options.rcfile,
467 source=source,
468 omit=omit,
469 include=include,
470 debug=debug,
471 concurrency=options.concurrency,
472 )
473
474 if options.action == "debug":
475 return self.do_debug(args)
476
477 elif options.action == "erase":
478 self.coverage.erase()
479 return OK
480
481 elif options.action == "run":
482 return self.do_run(options, args)
483
484 elif options.action == "combine":
485 self.coverage.load()
486 data_dirs = args or None
487 self.coverage.combine(data_dirs)
488 self.coverage.save()
489 return OK
490
491 # Remaining actions are reporting, with some common options.
492 report_args = dict(
493 morfs=unglob_args(args),
494 ignore_errors=options.ignore_errors,
495 omit=omit,
496 include=include,
497 )
498
499 self.coverage.load()
500
501 total = None
502 if options.action == "report":
503 total = self.coverage.report(
504 show_missing=options.show_missing,
505 skip_covered=options.skip_covered, **report_args)
506 elif options.action == "annotate":
507 self.coverage.annotate(
508 directory=options.directory, **report_args)
509 elif options.action == "html":
510 total = self.coverage.html_report(
511 directory=options.directory, title=options.title,
512 **report_args)
513 elif options.action == "xml":
514 outfile = options.outfile
515 total = self.coverage.xml_report(outfile=outfile, **report_args)
516
517 if total is not None:
518 # Apply the command line fail-under options, and then use the config
519 # value, so we can get fail_under from the config file.
520 if options.fail_under is not None:
521 self.coverage.set_option("report:fail_under", options.fail_under)
522
523 if self.coverage.get_option("report:fail_under"):
524
525 # Total needs to be rounded, but be careful of 0 and 100.
526 if 0 < total < 1:
527 total = 1
528 elif 99 < total < 100:
529 total = 99
530 else:
531 total = round(total)
532
533 if total >= self.coverage.get_option("report:fail_under"):
534 return OK
535 else:
536 return FAIL_UNDER
537
538 return OK
539
540 def help(self, error=None, topic=None, parser=None):
541 """Display an error message, or the named topic."""
542 assert error or topic or parser
543 if error:
544 print(error)
545 print("Use '%s help' for help." % (self.program_name,))
546 elif parser:
547 print(parser.format_help().strip())
548 else:
549 help_params = dict(self.covpkg.__dict__)
550 help_params['program_name'] = self.program_name
551 if CTracer is not None:
552 help_params['extension_modifier'] = 'with C extension'
553 else:
554 help_params['extension_modifier'] = 'without C extension'
555 help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip()
556 if help_msg:
557 print(help_msg.format(**help_params))
558 else:
559 print("Don't know topic %r" % topic)
560
561 def do_help(self, options, args, parser):
562 """Deal with help requests.
563
564 Return True if it handled the request, False if not.
565
566 """
567 # Handle help.
568 if options.help:
569 if self.global_option:
570 self.help_fn(topic='help')
571 else:
572 self.help_fn(parser=parser)
573 return True
574
575 if options.action == "help":
576 if args:
577 for a in args:
578 parser = CMDS.get(a)
579 if parser:
580 self.help_fn(parser=parser)
581 else:
582 self.help_fn(topic=a)
583 else:
584 self.help_fn(topic='help')
585 return True
586
587 # Handle version.
588 if options.version:
589 self.help_fn(topic='version')
590 return True
591
592 return False
593
594 def args_ok(self, options, args):
595 """Check for conflicts and problems in the options.
596
597 Returns True if everything is OK, or False if not.
598
599 """
600 if options.action == "run" and not args:
601 self.help_fn("Nothing to do.")
602 return False
603
604 return True
605
606 def do_run(self, options, args):
607 """Implementation of 'coverage run'."""
608
609 if options.append and self.coverage.get_option("run:parallel"):
610 self.help_fn("Can't append to data files in parallel mode.")
611 return ERR
612
613 if not self.coverage.get_option("run:parallel"):
614 if not options.append:
615 self.coverage.erase()
616
617 # Run the script.
618 self.coverage.start()
619 code_ran = True
620 try:
621 if options.module:
622 self.run_python_module(args[0], args)
623 else:
624 filename = args[0]
625 self.run_python_file(filename, args)
626 except NoSource:
627 code_ran = False
628 raise
629 finally:
630 self.coverage.stop()
631 if code_ran:
632 if options.append:
633 data_file = self.coverage.get_option("run:data_file")
634 if self.path_exists(data_file):
635 self.coverage.combine(data_paths=[data_file])
636 self.coverage.save()
637
638 return OK
639
640 def do_debug(self, args):
641 """Implementation of 'coverage debug'."""
642
643 if not args:
644 self.help_fn("What information would you like: data, sys?")
645 return ERR
646
647 for info in args:
648 if info == 'sys':
649 sys_info = self.coverage.sys_info()
650 print(info_header("sys"))
651 for line in info_formatter(sys_info):
652 print(" %s" % line)
653 elif info == 'data':
654 self.coverage.load()
655 data = self.coverage.data
656 print(info_header("data"))
657 print("path: %s" % self.coverage.data_files.filename)
658 if data:
659 print("has_arcs: %r" % data.has_arcs())
660 summary = data.line_counts(fullpath=True)
661 filenames = sorted(summary.keys())
662 print("\n%d files:" % len(filenames))
663 for f in filenames:
664 line = "%s: %d lines" % (f, summary[f])
665 plugin = data.file_tracer(f)
666 if plugin:
667 line += " [%s]" % plugin
668 print(line)
669 else:
670 print("No data collected")
671 else:
672 self.help_fn("Don't know what you mean by %r" % info)
673 return ERR
674
675 return OK
676
677
678 def unshell_list(s):
679 """Turn a command-line argument into a list."""
680 if not s:
681 return None
682 if env.WINDOWS:
683 # When running coverage.py as coverage.exe, some of the behavior
684 # of the shell is emulated: wildcards are expanded into a list of
685 # file names. So you have to single-quote patterns on the command
686 # line, but (not) helpfully, the single quotes are included in the
687 # argument, so we have to strip them off here.
688 s = s.strip("'")
689 return s.split(',')
690
691
692 def unglob_args(args):
693 """Interpret shell wildcards for platforms that need it."""
694 if env.WINDOWS:
695 globbed = []
696 for arg in args:
697 if '?' in arg or '*' in arg:
698 globbed.extend(glob.glob(arg))
699 else:
700 globbed.append(arg)
701 args = globbed
702 return args
703
704
705 HELP_TOPICS = {
706 'help': """\
707 Coverage.py, version {__version__} {extension_modifier}
708 Measure, collect, and report on code coverage in Python programs.
709
710 usage: {program_name} <command> [options] [args]
711
712 Commands:
713 annotate Annotate source files with execution information.
714 combine Combine a number of data files.
715 erase Erase previously collected coverage data.
716 help Get help on using coverage.py.
717 html Create an HTML report.
718 report Report coverage stats on modules.
719 run Run a Python program and measure code execution.
720 xml Create an XML report of coverage results.
721
722 Use "{program_name} help <command>" for detailed help on any command.
723 For full documentation, see {__url__}
724 """,
725
726 'minimum_help': """\
727 Code coverage for Python. Use '{program_name} help' for help.
728 """,
729
730 'version': """\
731 Coverage.py, version {__version__} {extension_modifier}
732 Documentation at {__url__}
733 """,
734 }
735
736
737 def main(argv=None):
738 """The main entry point to coverage.py.
739
740 This is installed as the script entry point.
741
742 """
743 if argv is None:
744 argv = sys.argv[1:]
745 try:
746 status = CoverageScript().command_line(argv)
747 except ExceptionDuringRun as err:
748 # An exception was caught while running the product code. The
749 # sys.exc_info() return tuple is packed into an ExceptionDuringRun
750 # exception.
751 traceback.print_exception(*err.args)
752 status = ERR
753 except CoverageException as err:
754 # A controlled error inside coverage.py: print the message to the user.
755 print(err)
756 status = ERR
757 except SystemExit as err:
758 # The user called `sys.exit()`. Exit with their argument, if any.
759 if err.args:
760 status = err.args[0]
761 else:
762 status = None
763 return status
764
765 #
766 # eflag: FileType = Python2

eric ide

mercurial