|
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
|
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
|
3 |
|
4 """Command-line support for coverage.py.""" |
|
5 |
|
6 import glob |
|
7 import optparse # pylint: disable=deprecated-module |
|
8 import os |
|
9 import os.path |
|
10 import shlex |
|
11 import sys |
|
12 import textwrap |
|
13 import traceback |
|
14 |
|
15 import coverage |
|
16 from coverage import Coverage |
|
17 from coverage import env |
|
18 from coverage.collector import CTracer |
|
19 from coverage.config import CoverageConfig |
|
20 from coverage.control import DEFAULT_DATAFILE |
|
21 from coverage.data import combinable_files, debug_data_file |
|
22 from coverage.debug import info_header, short_stack, write_formatted_info |
|
23 from coverage.exceptions import _BaseCoverageException, _ExceptionDuringRun, NoSource |
|
24 from coverage.execfile import PyRunner |
|
25 from coverage.results import Numbers, should_fail_under |
|
26 |
|
27 # When adding to this file, alphabetization is important. Look for |
|
28 # "alphabetize" comments throughout. |
|
29 |
|
30 class Opts: |
|
31 """A namespace class for individual options we'll build parsers from.""" |
|
32 |
|
33 # Keep these entries alphabetized (roughly) by the option name as it |
|
34 # appears on the command line. |
|
35 |
|
36 append = optparse.make_option( |
|
37 '-a', '--append', action='store_true', |
|
38 help="Append coverage data to .coverage, otherwise it starts clean each time.", |
|
39 ) |
|
40 keep = optparse.make_option( |
|
41 '', '--keep', action='store_true', |
|
42 help="Keep original coverage files, otherwise they are deleted.", |
|
43 ) |
|
44 branch = optparse.make_option( |
|
45 '', '--branch', action='store_true', |
|
46 help="Measure branch coverage in addition to statement coverage.", |
|
47 ) |
|
48 concurrency = optparse.make_option( |
|
49 '', '--concurrency', action='store', metavar="LIBS", |
|
50 help=( |
|
51 "Properly measure code using a concurrency library. " + |
|
52 "Valid values are: {}, or a comma-list of them." |
|
53 ).format(", ".join(sorted(CoverageConfig.CONCURRENCY_CHOICES))), |
|
54 ) |
|
55 context = optparse.make_option( |
|
56 '', '--context', action='store', metavar="LABEL", |
|
57 help="The context label to record for this coverage run.", |
|
58 ) |
|
59 contexts = optparse.make_option( |
|
60 '', '--contexts', action='store', metavar="REGEX1,REGEX2,...", |
|
61 help=( |
|
62 "Only display data from lines covered in the given contexts. " + |
|
63 "Accepts Python regexes, which must be quoted." |
|
64 ), |
|
65 ) |
|
66 combine_datafile = optparse.make_option( |
|
67 '', '--data-file', action='store', metavar="DATAFILE", |
|
68 help=( |
|
69 "Base name of the data files to operate on. " + |
|
70 "Defaults to '.coverage'. [env: COVERAGE_FILE]" |
|
71 ), |
|
72 ) |
|
73 input_datafile = optparse.make_option( |
|
74 '', '--data-file', action='store', metavar="INFILE", |
|
75 help=( |
|
76 "Read coverage data for report generation from this file. " + |
|
77 "Defaults to '.coverage'. [env: COVERAGE_FILE]" |
|
78 ), |
|
79 ) |
|
80 output_datafile = optparse.make_option( |
|
81 '', '--data-file', action='store', metavar="OUTFILE", |
|
82 help=( |
|
83 "Write the recorded coverage data to this file. " + |
|
84 "Defaults to '.coverage'. [env: COVERAGE_FILE]" |
|
85 ), |
|
86 ) |
|
87 debug = optparse.make_option( |
|
88 '', '--debug', action='store', metavar="OPTS", |
|
89 help="Debug options, separated by commas. [env: COVERAGE_DEBUG]", |
|
90 ) |
|
91 directory = optparse.make_option( |
|
92 '-d', '--directory', action='store', metavar="DIR", |
|
93 help="Write the output files to DIR.", |
|
94 ) |
|
95 fail_under = optparse.make_option( |
|
96 '', '--fail-under', action='store', metavar="MIN", type="float", |
|
97 help="Exit with a status of 2 if the total coverage is less than MIN.", |
|
98 ) |
|
99 help = optparse.make_option( |
|
100 '-h', '--help', action='store_true', |
|
101 help="Get help on this command.", |
|
102 ) |
|
103 ignore_errors = optparse.make_option( |
|
104 '-i', '--ignore-errors', action='store_true', |
|
105 help="Ignore errors while reading source files.", |
|
106 ) |
|
107 include = optparse.make_option( |
|
108 '', '--include', action='store', metavar="PAT1,PAT2,...", |
|
109 help=( |
|
110 "Include only files whose paths match one of these patterns. " + |
|
111 "Accepts shell-style wildcards, which must be quoted." |
|
112 ), |
|
113 ) |
|
114 pylib = optparse.make_option( |
|
115 '-L', '--pylib', action='store_true', |
|
116 help=( |
|
117 "Measure coverage even inside the Python installed library, " + |
|
118 "which isn't done by default." |
|
119 ), |
|
120 ) |
|
121 show_missing = optparse.make_option( |
|
122 '-m', '--show-missing', action='store_true', |
|
123 help="Show line numbers of statements in each module that weren't executed.", |
|
124 ) |
|
125 module = optparse.make_option( |
|
126 '-m', '--module', action='store_true', |
|
127 help=( |
|
128 "<pyfile> is an importable Python module, not a script path, " + |
|
129 "to be run as 'python -m' would run it." |
|
130 ), |
|
131 ) |
|
132 omit = optparse.make_option( |
|
133 '', '--omit', action='store', metavar="PAT1,PAT2,...", |
|
134 help=( |
|
135 "Omit files whose paths match one of these patterns. " + |
|
136 "Accepts shell-style wildcards, which must be quoted." |
|
137 ), |
|
138 ) |
|
139 output_xml = optparse.make_option( |
|
140 '-o', '', action='store', dest="outfile", metavar="OUTFILE", |
|
141 help="Write the XML report to this file. Defaults to 'coverage.xml'", |
|
142 ) |
|
143 output_json = optparse.make_option( |
|
144 '-o', '', action='store', dest="outfile", metavar="OUTFILE", |
|
145 help="Write the JSON report to this file. Defaults to 'coverage.json'", |
|
146 ) |
|
147 output_lcov = optparse.make_option( |
|
148 '-o', '', action='store', dest='outfile', metavar="OUTFILE", |
|
149 help="Write the LCOV report to this file. Defaults to 'coverage.lcov'", |
|
150 ) |
|
151 json_pretty_print = optparse.make_option( |
|
152 '', '--pretty-print', action='store_true', |
|
153 help="Format the JSON for human readers.", |
|
154 ) |
|
155 parallel_mode = optparse.make_option( |
|
156 '-p', '--parallel-mode', action='store_true', |
|
157 help=( |
|
158 "Append the machine name, process id and random number to the " + |
|
159 "data file name to simplify collecting data from " + |
|
160 "many processes." |
|
161 ), |
|
162 ) |
|
163 precision = optparse.make_option( |
|
164 '', '--precision', action='store', metavar='N', type=int, |
|
165 help=( |
|
166 "Number of digits after the decimal point to display for " + |
|
167 "reported coverage percentages." |
|
168 ), |
|
169 ) |
|
170 quiet = optparse.make_option( |
|
171 '-q', '--quiet', action='store_true', |
|
172 help="Don't print messages about what is happening.", |
|
173 ) |
|
174 rcfile = optparse.make_option( |
|
175 '', '--rcfile', action='store', |
|
176 help=( |
|
177 "Specify configuration file. " + |
|
178 "By default '.coveragerc', 'setup.cfg', 'tox.ini', and " + |
|
179 "'pyproject.toml' are tried. [env: COVERAGE_RCFILE]" |
|
180 ), |
|
181 ) |
|
182 show_contexts = optparse.make_option( |
|
183 '--show-contexts', action='store_true', |
|
184 help="Show contexts for covered lines.", |
|
185 ) |
|
186 skip_covered = optparse.make_option( |
|
187 '--skip-covered', action='store_true', |
|
188 help="Skip files with 100% coverage.", |
|
189 ) |
|
190 no_skip_covered = optparse.make_option( |
|
191 '--no-skip-covered', action='store_false', dest='skip_covered', |
|
192 help="Disable --skip-covered.", |
|
193 ) |
|
194 skip_empty = optparse.make_option( |
|
195 '--skip-empty', action='store_true', |
|
196 help="Skip files with no code.", |
|
197 ) |
|
198 sort = optparse.make_option( |
|
199 '--sort', action='store', metavar='COLUMN', |
|
200 help=( |
|
201 "Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. " + |
|
202 "Default is name." |
|
203 ), |
|
204 ) |
|
205 source = optparse.make_option( |
|
206 '', '--source', action='store', metavar="SRC1,SRC2,...", |
|
207 help="A list of directories or importable names of code to measure.", |
|
208 ) |
|
209 timid = optparse.make_option( |
|
210 '', '--timid', action='store_true', |
|
211 help=( |
|
212 "Use a simpler but slower trace method. Try this if you get " + |
|
213 "seemingly impossible results!" |
|
214 ), |
|
215 ) |
|
216 title = optparse.make_option( |
|
217 '', '--title', action='store', metavar="TITLE", |
|
218 help="A text string to use as the title on the HTML.", |
|
219 ) |
|
220 version = optparse.make_option( |
|
221 '', '--version', action='store_true', |
|
222 help="Display version information and exit.", |
|
223 ) |
|
224 |
|
225 |
|
226 class CoverageOptionParser(optparse.OptionParser): |
|
227 """Base OptionParser for coverage.py. |
|
228 |
|
229 Problems don't exit the program. |
|
230 Defaults are initialized for all options. |
|
231 |
|
232 """ |
|
233 |
|
234 def __init__(self, *args, **kwargs): |
|
235 super().__init__(add_help_option=False, *args, **kwargs) |
|
236 self.set_defaults( |
|
237 # Keep these arguments alphabetized by their names. |
|
238 action=None, |
|
239 append=None, |
|
240 branch=None, |
|
241 concurrency=None, |
|
242 context=None, |
|
243 contexts=None, |
|
244 data_file=None, |
|
245 debug=None, |
|
246 directory=None, |
|
247 fail_under=None, |
|
248 help=None, |
|
249 ignore_errors=None, |
|
250 include=None, |
|
251 keep=None, |
|
252 module=None, |
|
253 omit=None, |
|
254 parallel_mode=None, |
|
255 precision=None, |
|
256 pylib=None, |
|
257 quiet=None, |
|
258 rcfile=True, |
|
259 show_contexts=None, |
|
260 show_missing=None, |
|
261 skip_covered=None, |
|
262 skip_empty=None, |
|
263 sort=None, |
|
264 source=None, |
|
265 timid=None, |
|
266 title=None, |
|
267 version=None, |
|
268 ) |
|
269 |
|
270 self.disable_interspersed_args() |
|
271 |
|
272 class OptionParserError(Exception): |
|
273 """Used to stop the optparse error handler ending the process.""" |
|
274 pass |
|
275 |
|
276 def parse_args_ok(self, args=None, options=None): |
|
277 """Call optparse.parse_args, but return a triple: |
|
278 |
|
279 (ok, options, args) |
|
280 |
|
281 """ |
|
282 try: |
|
283 options, args = super().parse_args(args, options) |
|
284 except self.OptionParserError: |
|
285 return False, None, None |
|
286 return True, options, args |
|
287 |
|
288 def error(self, msg): |
|
289 """Override optparse.error so sys.exit doesn't get called.""" |
|
290 show_help(msg) |
|
291 raise self.OptionParserError |
|
292 |
|
293 |
|
294 class GlobalOptionParser(CoverageOptionParser): |
|
295 """Command-line parser for coverage.py global option arguments.""" |
|
296 |
|
297 def __init__(self): |
|
298 super().__init__() |
|
299 |
|
300 self.add_options([ |
|
301 Opts.help, |
|
302 Opts.version, |
|
303 ]) |
|
304 |
|
305 |
|
306 class CmdOptionParser(CoverageOptionParser): |
|
307 """Parse one of the new-style commands for coverage.py.""" |
|
308 |
|
309 def __init__(self, action, options, defaults=None, usage=None, description=None): |
|
310 """Create an OptionParser for a coverage.py command. |
|
311 |
|
312 `action` is the slug to put into `options.action`. |
|
313 `options` is a list of Option's for the command. |
|
314 `defaults` is a dict of default value for options. |
|
315 `usage` is the usage string to display in help. |
|
316 `description` is the description of the command, for the help text. |
|
317 |
|
318 """ |
|
319 if usage: |
|
320 usage = "%prog " + usage |
|
321 super().__init__( |
|
322 usage=usage, |
|
323 description=description, |
|
324 ) |
|
325 self.set_defaults(action=action, **(defaults or {})) |
|
326 self.add_options(options) |
|
327 self.cmd = action |
|
328 |
|
329 def __eq__(self, other): |
|
330 # A convenience equality, so that I can put strings in unit test |
|
331 # results, and they will compare equal to objects. |
|
332 return (other == f"<CmdOptionParser:{self.cmd}>") |
|
333 |
|
334 __hash__ = None # This object doesn't need to be hashed. |
|
335 |
|
336 def get_prog_name(self): |
|
337 """Override of an undocumented function in optparse.OptionParser.""" |
|
338 program_name = super().get_prog_name() |
|
339 |
|
340 # Include the sub-command for this parser as part of the command. |
|
341 return f"{program_name} {self.cmd}" |
|
342 |
|
343 # In lists of Opts, keep them alphabetized by the option names as they appear |
|
344 # on the command line, since these lists determine the order of the options in |
|
345 # the help output. |
|
346 # |
|
347 # In COMMANDS, keep the keys (command names) alphabetized. |
|
348 |
|
349 GLOBAL_ARGS = [ |
|
350 Opts.debug, |
|
351 Opts.help, |
|
352 Opts.rcfile, |
|
353 ] |
|
354 |
|
355 COMMANDS = { |
|
356 'annotate': CmdOptionParser( |
|
357 "annotate", |
|
358 [ |
|
359 Opts.directory, |
|
360 Opts.input_datafile, |
|
361 Opts.ignore_errors, |
|
362 Opts.include, |
|
363 Opts.omit, |
|
364 ] + GLOBAL_ARGS, |
|
365 usage="[options] [modules]", |
|
366 description=( |
|
367 "Make annotated copies of the given files, marking statements that are executed " + |
|
368 "with > and statements that are missed with !." |
|
369 ), |
|
370 ), |
|
371 |
|
372 'combine': CmdOptionParser( |
|
373 "combine", |
|
374 [ |
|
375 Opts.append, |
|
376 Opts.combine_datafile, |
|
377 Opts.keep, |
|
378 Opts.quiet, |
|
379 ] + GLOBAL_ARGS, |
|
380 usage="[options] <path1> <path2> ... <pathN>", |
|
381 description=( |
|
382 "Combine data from multiple coverage files collected " + |
|
383 "with 'run -p'. The combined results are written to a single " + |
|
384 "file representing the union of the data. The positional " + |
|
385 "arguments are data files or directories containing data files. " + |
|
386 "If no paths are provided, data files in the default data file's " + |
|
387 "directory are combined." |
|
388 ), |
|
389 ), |
|
390 |
|
391 'debug': CmdOptionParser( |
|
392 "debug", GLOBAL_ARGS, |
|
393 usage="<topic>", |
|
394 description=( |
|
395 "Display information about the internals of coverage.py, " + |
|
396 "for diagnosing problems. " + |
|
397 "Topics are: " + |
|
398 "'data' to show a summary of the collected data; " + |
|
399 "'sys' to show installation information; " + |
|
400 "'config' to show the configuration; " + |
|
401 "'premain' to show what is calling coverage; " + |
|
402 "'pybehave' to show internal flags describing Python behavior." |
|
403 ), |
|
404 ), |
|
405 |
|
406 'erase': CmdOptionParser( |
|
407 "erase", |
|
408 [ |
|
409 Opts.combine_datafile |
|
410 ] + GLOBAL_ARGS, |
|
411 description="Erase previously collected coverage data.", |
|
412 ), |
|
413 |
|
414 'help': CmdOptionParser( |
|
415 "help", GLOBAL_ARGS, |
|
416 usage="[command]", |
|
417 description="Describe how to use coverage.py", |
|
418 ), |
|
419 |
|
420 'html': CmdOptionParser( |
|
421 "html", |
|
422 [ |
|
423 Opts.contexts, |
|
424 Opts.directory, |
|
425 Opts.input_datafile, |
|
426 Opts.fail_under, |
|
427 Opts.ignore_errors, |
|
428 Opts.include, |
|
429 Opts.omit, |
|
430 Opts.precision, |
|
431 Opts.quiet, |
|
432 Opts.show_contexts, |
|
433 Opts.skip_covered, |
|
434 Opts.no_skip_covered, |
|
435 Opts.skip_empty, |
|
436 Opts.title, |
|
437 ] + GLOBAL_ARGS, |
|
438 usage="[options] [modules]", |
|
439 description=( |
|
440 "Create an HTML report of the coverage of the files. " + |
|
441 "Each file gets its own page, with the source decorated to show " + |
|
442 "executed, excluded, and missed lines." |
|
443 ), |
|
444 ), |
|
445 |
|
446 'json': CmdOptionParser( |
|
447 "json", |
|
448 [ |
|
449 Opts.contexts, |
|
450 Opts.input_datafile, |
|
451 Opts.fail_under, |
|
452 Opts.ignore_errors, |
|
453 Opts.include, |
|
454 Opts.omit, |
|
455 Opts.output_json, |
|
456 Opts.json_pretty_print, |
|
457 Opts.quiet, |
|
458 Opts.show_contexts, |
|
459 ] + GLOBAL_ARGS, |
|
460 usage="[options] [modules]", |
|
461 description="Generate a JSON report of coverage results.", |
|
462 ), |
|
463 |
|
464 'lcov': CmdOptionParser( |
|
465 "lcov", |
|
466 [ |
|
467 Opts.input_datafile, |
|
468 Opts.fail_under, |
|
469 Opts.ignore_errors, |
|
470 Opts.include, |
|
471 Opts.output_lcov, |
|
472 Opts.omit, |
|
473 Opts.quiet, |
|
474 ] + GLOBAL_ARGS, |
|
475 usage="[options] [modules]", |
|
476 description="Generate an LCOV report of coverage results.", |
|
477 ), |
|
478 |
|
479 'report': CmdOptionParser( |
|
480 "report", |
|
481 [ |
|
482 Opts.contexts, |
|
483 Opts.input_datafile, |
|
484 Opts.fail_under, |
|
485 Opts.ignore_errors, |
|
486 Opts.include, |
|
487 Opts.omit, |
|
488 Opts.precision, |
|
489 Opts.sort, |
|
490 Opts.show_missing, |
|
491 Opts.skip_covered, |
|
492 Opts.no_skip_covered, |
|
493 Opts.skip_empty, |
|
494 ] + GLOBAL_ARGS, |
|
495 usage="[options] [modules]", |
|
496 description="Report coverage statistics on modules.", |
|
497 ), |
|
498 |
|
499 'run': CmdOptionParser( |
|
500 "run", |
|
501 [ |
|
502 Opts.append, |
|
503 Opts.branch, |
|
504 Opts.concurrency, |
|
505 Opts.context, |
|
506 Opts.output_datafile, |
|
507 Opts.include, |
|
508 Opts.module, |
|
509 Opts.omit, |
|
510 Opts.pylib, |
|
511 Opts.parallel_mode, |
|
512 Opts.source, |
|
513 Opts.timid, |
|
514 ] + GLOBAL_ARGS, |
|
515 usage="[options] <pyfile> [program options]", |
|
516 description="Run a Python program, measuring code execution.", |
|
517 ), |
|
518 |
|
519 'xml': CmdOptionParser( |
|
520 "xml", |
|
521 [ |
|
522 Opts.input_datafile, |
|
523 Opts.fail_under, |
|
524 Opts.ignore_errors, |
|
525 Opts.include, |
|
526 Opts.omit, |
|
527 Opts.output_xml, |
|
528 Opts.quiet, |
|
529 Opts.skip_empty, |
|
530 ] + GLOBAL_ARGS, |
|
531 usage="[options] [modules]", |
|
532 description="Generate an XML report of coverage results.", |
|
533 ), |
|
534 } |
|
535 |
|
536 |
|
537 def show_help(error=None, topic=None, parser=None): |
|
538 """Display an error message, or the named topic.""" |
|
539 assert error or topic or parser |
|
540 |
|
541 program_path = sys.argv[0] |
|
542 if program_path.endswith(os.path.sep + '__main__.py'): |
|
543 # The path is the main module of a package; get that path instead. |
|
544 program_path = os.path.dirname(program_path) |
|
545 program_name = os.path.basename(program_path) |
|
546 if env.WINDOWS: |
|
547 # entry_points={'console_scripts':...} on Windows makes files |
|
548 # called coverage.exe, coverage3.exe, and coverage-3.5.exe. These |
|
549 # invoke coverage-script.py, coverage3-script.py, and |
|
550 # coverage-3.5-script.py. argv[0] is the .py file, but we want to |
|
551 # get back to the original form. |
|
552 auto_suffix = "-script.py" |
|
553 if program_name.endswith(auto_suffix): |
|
554 program_name = program_name[:-len(auto_suffix)] |
|
555 |
|
556 help_params = dict(coverage.__dict__) |
|
557 help_params['program_name'] = program_name |
|
558 if CTracer is not None: |
|
559 help_params['extension_modifier'] = 'with C extension' |
|
560 else: |
|
561 help_params['extension_modifier'] = 'without C extension' |
|
562 |
|
563 if error: |
|
564 print(error, file=sys.stderr) |
|
565 print(f"Use '{program_name} help' for help.", file=sys.stderr) |
|
566 elif parser: |
|
567 print(parser.format_help().strip()) |
|
568 print() |
|
569 else: |
|
570 help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip() |
|
571 if help_msg: |
|
572 print(help_msg.format(**help_params)) |
|
573 else: |
|
574 print(f"Don't know topic {topic!r}") |
|
575 print("Full documentation is at {__url__}".format(**help_params)) |
|
576 |
|
577 |
|
578 OK, ERR, FAIL_UNDER = 0, 1, 2 |
|
579 |
|
580 |
|
581 class CoverageScript: |
|
582 """The command-line interface to coverage.py.""" |
|
583 |
|
584 def __init__(self): |
|
585 self.global_option = False |
|
586 self.coverage = None |
|
587 |
|
588 def command_line(self, argv): |
|
589 """The bulk of the command line interface to coverage.py. |
|
590 |
|
591 `argv` is the argument list to process. |
|
592 |
|
593 Returns 0 if all is well, 1 if something went wrong. |
|
594 |
|
595 """ |
|
596 # Collect the command-line options. |
|
597 if not argv: |
|
598 show_help(topic='minimum_help') |
|
599 return OK |
|
600 |
|
601 # The command syntax we parse depends on the first argument. Global |
|
602 # switch syntax always starts with an option. |
|
603 self.global_option = argv[0].startswith('-') |
|
604 if self.global_option: |
|
605 parser = GlobalOptionParser() |
|
606 else: |
|
607 parser = COMMANDS.get(argv[0]) |
|
608 if not parser: |
|
609 show_help(f"Unknown command: {argv[0]!r}") |
|
610 return ERR |
|
611 argv = argv[1:] |
|
612 |
|
613 ok, options, args = parser.parse_args_ok(argv) |
|
614 if not ok: |
|
615 return ERR |
|
616 |
|
617 # Handle help and version. |
|
618 if self.do_help(options, args, parser): |
|
619 return OK |
|
620 |
|
621 # Listify the list options. |
|
622 source = unshell_list(options.source) |
|
623 omit = unshell_list(options.omit) |
|
624 include = unshell_list(options.include) |
|
625 debug = unshell_list(options.debug) |
|
626 contexts = unshell_list(options.contexts) |
|
627 |
|
628 if options.concurrency is not None: |
|
629 concurrency = options.concurrency.split(",") |
|
630 else: |
|
631 concurrency = None |
|
632 |
|
633 # Do something. |
|
634 self.coverage = Coverage( |
|
635 data_file=options.data_file or DEFAULT_DATAFILE, |
|
636 data_suffix=options.parallel_mode, |
|
637 cover_pylib=options.pylib, |
|
638 timid=options.timid, |
|
639 branch=options.branch, |
|
640 config_file=options.rcfile, |
|
641 source=source, |
|
642 omit=omit, |
|
643 include=include, |
|
644 debug=debug, |
|
645 concurrency=concurrency, |
|
646 check_preimported=True, |
|
647 context=options.context, |
|
648 messages=not options.quiet, |
|
649 ) |
|
650 |
|
651 if options.action == "debug": |
|
652 return self.do_debug(args) |
|
653 |
|
654 elif options.action == "erase": |
|
655 self.coverage.erase() |
|
656 return OK |
|
657 |
|
658 elif options.action == "run": |
|
659 return self.do_run(options, args) |
|
660 |
|
661 elif options.action == "combine": |
|
662 if options.append: |
|
663 self.coverage.load() |
|
664 data_paths = args or None |
|
665 self.coverage.combine(data_paths, strict=True, keep=bool(options.keep)) |
|
666 self.coverage.save() |
|
667 return OK |
|
668 |
|
669 # Remaining actions are reporting, with some common options. |
|
670 report_args = dict( |
|
671 morfs=unglob_args(args), |
|
672 ignore_errors=options.ignore_errors, |
|
673 omit=omit, |
|
674 include=include, |
|
675 contexts=contexts, |
|
676 ) |
|
677 |
|
678 # We need to be able to import from the current directory, because |
|
679 # plugins may try to, for example, to read Django settings. |
|
680 sys.path.insert(0, '') |
|
681 |
|
682 self.coverage.load() |
|
683 |
|
684 total = None |
|
685 if options.action == "report": |
|
686 total = self.coverage.report( |
|
687 precision=options.precision, |
|
688 show_missing=options.show_missing, |
|
689 skip_covered=options.skip_covered, |
|
690 skip_empty=options.skip_empty, |
|
691 sort=options.sort, |
|
692 **report_args |
|
693 ) |
|
694 elif options.action == "annotate": |
|
695 self.coverage.annotate(directory=options.directory, **report_args) |
|
696 elif options.action == "html": |
|
697 total = self.coverage.html_report( |
|
698 directory=options.directory, |
|
699 precision=options.precision, |
|
700 skip_covered=options.skip_covered, |
|
701 skip_empty=options.skip_empty, |
|
702 show_contexts=options.show_contexts, |
|
703 title=options.title, |
|
704 **report_args |
|
705 ) |
|
706 elif options.action == "xml": |
|
707 total = self.coverage.xml_report( |
|
708 outfile=options.outfile, |
|
709 skip_empty=options.skip_empty, |
|
710 **report_args |
|
711 ) |
|
712 elif options.action == "json": |
|
713 total = self.coverage.json_report( |
|
714 outfile=options.outfile, |
|
715 pretty_print=options.pretty_print, |
|
716 show_contexts=options.show_contexts, |
|
717 **report_args |
|
718 ) |
|
719 elif options.action == "lcov": |
|
720 total = self.coverage.lcov_report( |
|
721 outfile=options.outfile, |
|
722 **report_args |
|
723 ) |
|
724 else: |
|
725 # There are no other possible actions. |
|
726 raise AssertionError |
|
727 |
|
728 if total is not None: |
|
729 # Apply the command line fail-under options, and then use the config |
|
730 # value, so we can get fail_under from the config file. |
|
731 if options.fail_under is not None: |
|
732 self.coverage.set_option("report:fail_under", options.fail_under) |
|
733 if options.precision is not None: |
|
734 self.coverage.set_option("report:precision", options.precision) |
|
735 |
|
736 fail_under = self.coverage.get_option("report:fail_under") |
|
737 precision = self.coverage.get_option("report:precision") |
|
738 if should_fail_under(total, fail_under, precision): |
|
739 msg = "total of {total} is less than fail-under={fail_under:.{p}f}".format( |
|
740 total=Numbers(precision=precision).display_covered(total), |
|
741 fail_under=fail_under, |
|
742 p=precision, |
|
743 ) |
|
744 print("Coverage failure:", msg) |
|
745 return FAIL_UNDER |
|
746 |
|
747 return OK |
|
748 |
|
749 def do_help(self, options, args, parser): |
|
750 """Deal with help requests. |
|
751 |
|
752 Return True if it handled the request, False if not. |
|
753 |
|
754 """ |
|
755 # Handle help. |
|
756 if options.help: |
|
757 if self.global_option: |
|
758 show_help(topic='help') |
|
759 else: |
|
760 show_help(parser=parser) |
|
761 return True |
|
762 |
|
763 if options.action == "help": |
|
764 if args: |
|
765 for a in args: |
|
766 parser = COMMANDS.get(a) |
|
767 if parser: |
|
768 show_help(parser=parser) |
|
769 else: |
|
770 show_help(topic=a) |
|
771 else: |
|
772 show_help(topic='help') |
|
773 return True |
|
774 |
|
775 # Handle version. |
|
776 if options.version: |
|
777 show_help(topic='version') |
|
778 return True |
|
779 |
|
780 return False |
|
781 |
|
782 def do_run(self, options, args): |
|
783 """Implementation of 'coverage run'.""" |
|
784 |
|
785 if not args: |
|
786 if options.module: |
|
787 # Specified -m with nothing else. |
|
788 show_help("No module specified for -m") |
|
789 return ERR |
|
790 command_line = self.coverage.get_option("run:command_line") |
|
791 if command_line is not None: |
|
792 args = shlex.split(command_line) |
|
793 if args and args[0] in {"-m", "--module"}: |
|
794 options.module = True |
|
795 args = args[1:] |
|
796 if not args: |
|
797 show_help("Nothing to do.") |
|
798 return ERR |
|
799 |
|
800 if options.append and self.coverage.get_option("run:parallel"): |
|
801 show_help("Can't append to data files in parallel mode.") |
|
802 return ERR |
|
803 |
|
804 if options.concurrency == "multiprocessing": |
|
805 # Can't set other run-affecting command line options with |
|
806 # multiprocessing. |
|
807 for opt_name in ['branch', 'include', 'omit', 'pylib', 'source', 'timid']: |
|
808 # As it happens, all of these options have no default, meaning |
|
809 # they will be None if they have not been specified. |
|
810 if getattr(options, opt_name) is not None: |
|
811 show_help( |
|
812 "Options affecting multiprocessing must only be specified " + |
|
813 "in a configuration file.\n" + |
|
814 f"Remove --{opt_name} from the command line." |
|
815 ) |
|
816 return ERR |
|
817 |
|
818 os.environ["COVERAGE_RUN"] = "true" |
|
819 |
|
820 runner = PyRunner(args, as_module=bool(options.module)) |
|
821 runner.prepare() |
|
822 |
|
823 if options.append: |
|
824 self.coverage.load() |
|
825 |
|
826 # Run the script. |
|
827 self.coverage.start() |
|
828 code_ran = True |
|
829 try: |
|
830 runner.run() |
|
831 except NoSource: |
|
832 code_ran = False |
|
833 raise |
|
834 finally: |
|
835 self.coverage.stop() |
|
836 if code_ran: |
|
837 self.coverage.save() |
|
838 |
|
839 return OK |
|
840 |
|
841 def do_debug(self, args): |
|
842 """Implementation of 'coverage debug'.""" |
|
843 |
|
844 if not args: |
|
845 show_help("What information would you like: config, data, sys, premain, pybehave?") |
|
846 return ERR |
|
847 if args[1:]: |
|
848 show_help("Only one topic at a time, please") |
|
849 return ERR |
|
850 |
|
851 if args[0] == "sys": |
|
852 write_formatted_info(print, "sys", self.coverage.sys_info()) |
|
853 elif args[0] == "data": |
|
854 print(info_header("data")) |
|
855 data_file = self.coverage.config.data_file |
|
856 debug_data_file(data_file) |
|
857 for filename in combinable_files(data_file): |
|
858 print("-----") |
|
859 debug_data_file(filename) |
|
860 elif args[0] == "config": |
|
861 write_formatted_info(print, "config", self.coverage.config.debug_info()) |
|
862 elif args[0] == "premain": |
|
863 print(info_header("premain")) |
|
864 print(short_stack()) |
|
865 elif args[0] == "pybehave": |
|
866 write_formatted_info(print, "pybehave", env.debug_info()) |
|
867 else: |
|
868 show_help(f"Don't know what you mean by {args[0]!r}") |
|
869 return ERR |
|
870 |
|
871 return OK |
|
872 |
|
873 |
|
874 def unshell_list(s): |
|
875 """Turn a command-line argument into a list.""" |
|
876 if not s: |
|
877 return None |
|
878 if env.WINDOWS: |
|
879 # When running coverage.py as coverage.exe, some of the behavior |
|
880 # of the shell is emulated: wildcards are expanded into a list of |
|
881 # file names. So you have to single-quote patterns on the command |
|
882 # line, but (not) helpfully, the single quotes are included in the |
|
883 # argument, so we have to strip them off here. |
|
884 s = s.strip("'") |
|
885 return s.split(',') |
|
886 |
|
887 |
|
888 def unglob_args(args): |
|
889 """Interpret shell wildcards for platforms that need it.""" |
|
890 if env.WINDOWS: |
|
891 globbed = [] |
|
892 for arg in args: |
|
893 if '?' in arg or '*' in arg: |
|
894 globbed.extend(glob.glob(arg)) |
|
895 else: |
|
896 globbed.append(arg) |
|
897 args = globbed |
|
898 return args |
|
899 |
|
900 |
|
901 HELP_TOPICS = { |
|
902 'help': """\ |
|
903 Coverage.py, version {__version__} {extension_modifier} |
|
904 Measure, collect, and report on code coverage in Python programs. |
|
905 |
|
906 usage: {program_name} <command> [options] [args] |
|
907 |
|
908 Commands: |
|
909 annotate Annotate source files with execution information. |
|
910 combine Combine a number of data files. |
|
911 debug Display information about the internals of coverage.py |
|
912 erase Erase previously collected coverage data. |
|
913 help Get help on using coverage.py. |
|
914 html Create an HTML report. |
|
915 json Create a JSON report of coverage results. |
|
916 lcov Create an LCOV report of coverage results. |
|
917 report Report coverage stats on modules. |
|
918 run Run a Python program and measure code execution. |
|
919 xml Create an XML report of coverage results. |
|
920 |
|
921 Use "{program_name} help <command>" for detailed help on any command. |
|
922 """, |
|
923 |
|
924 'minimum_help': """\ |
|
925 Code coverage for Python, version {__version__} {extension_modifier}. Use '{program_name} help' for help. |
|
926 """, |
|
927 |
|
928 'version': """\ |
|
929 Coverage.py, version {__version__} {extension_modifier} |
|
930 """, |
|
931 } |
|
932 |
|
933 |
|
934 def main(argv=None): |
|
935 """The main entry point to coverage.py. |
|
936 |
|
937 This is installed as the script entry point. |
|
938 |
|
939 """ |
|
940 if argv is None: |
|
941 argv = sys.argv[1:] |
|
942 try: |
|
943 status = CoverageScript().command_line(argv) |
|
944 except _ExceptionDuringRun as err: |
|
945 # An exception was caught while running the product code. The |
|
946 # sys.exc_info() return tuple is packed into an _ExceptionDuringRun |
|
947 # exception. |
|
948 traceback.print_exception(*err.args) # pylint: disable=no-value-for-parameter |
|
949 status = ERR |
|
950 except _BaseCoverageException as err: |
|
951 # A controlled error inside coverage.py: print the message to the user. |
|
952 msg = err.args[0] |
|
953 print(msg) |
|
954 status = ERR |
|
955 except SystemExit as err: |
|
956 # The user called `sys.exit()`. Exit with their argument, if any. |
|
957 if err.args: |
|
958 status = err.args[0] |
|
959 else: |
|
960 status = None |
|
961 return status |
|
962 |
|
963 # Profiling using ox_profile. Install it from GitHub: |
|
964 # pip install git+https://github.com/emin63/ox_profile.git |
|
965 # |
|
966 # $set_env.py: COVERAGE_PROFILE - Set to use ox_profile. |
|
967 _profile = os.environ.get("COVERAGE_PROFILE", "") |
|
968 if _profile: # pragma: debugging |
|
969 from ox_profile.core.launchers import SimpleLauncher # pylint: disable=import-error |
|
970 original_main = main |
|
971 |
|
972 def main(argv=None): # pylint: disable=function-redefined |
|
973 """A wrapper around main that profiles.""" |
|
974 profiler = SimpleLauncher.launch() |
|
975 try: |
|
976 return original_main(argv) |
|
977 finally: |
|
978 data, _ = profiler.query(re_filter='coverage', max_records=100) |
|
979 print(profiler.show(query=data, limit=100, sep='', col='')) |
|
980 profiler.cancel() |