diff options
Diffstat (limited to 'coverage')
-rw-r--r-- | coverage/cmdline.py | 34 | ||||
-rw-r--r-- | coverage/config.py | 10 | ||||
-rw-r--r-- | coverage/control.py | 54 | ||||
-rw-r--r-- | coverage/jsonreport.py | 100 | ||||
-rw-r--r-- | coverage/report.py | 38 | ||||
-rw-r--r-- | coverage/results.py | 3 | ||||
-rw-r--r-- | coverage/xmlreport.py | 17 |
7 files changed, 217 insertions, 39 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py index fdab7d93..78e90d45 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -118,6 +118,15 @@ class Opts(object): metavar="OUTFILE", help="Write the XML report to this file. Defaults to 'coverage.xml'", ) + output_json = optparse.make_option( + '-o', '', action='store', dest="outfile", + metavar="OUTFILE", + help="Write the JSON report to this file. Defaults to 'coverage.json'", + ) + json_pretty_print = optparse.make_option( + '', '--pretty-print', action='store_true', + help="Print the json formatted for human readers", + ) parallel_mode = optparse.make_option( '-p', '--parallel-mode', action='store_true', help=( @@ -402,6 +411,22 @@ CMDS = { usage="[options] [modules]", description="Generate an XML report of coverage results." ), + + 'json': CmdOptionParser( + "json", + [ + Opts.fail_under, + Opts.ignore_errors, + Opts.include, + Opts.omit, + Opts.output_json, + Opts.json_pretty_print, + Opts.show_contexts, + Opts.contexts, + ] + GLOBAL_ARGS, + usage="[options] [modules]", + description="Generate a JSON report of coverage results." + ), } @@ -565,6 +590,14 @@ class CoverageScript(object): elif options.action == "xml": outfile = options.outfile total = self.coverage.xml_report(outfile=outfile, **report_args) + elif options.action == "json": + outfile = options.outfile + total = self.coverage.json_report( + outfile=outfile, + pretty_print=options.pretty_print, + show_contexts=options.show_contexts, + **report_args + ) if total is not None: # Apply the command line fail-under options, and then use the config @@ -752,6 +785,7 @@ HELP_TOPICS = { erase Erase previously collected coverage data. help Get help on using coverage.py. html Create an HTML report. + json Create a JSON report of coverage results. report Report coverage stats on modules. run Run a Python program and measure code execution. xml Create an XML report of coverage results. diff --git a/coverage/config.py b/coverage/config.py index 4bb60eb3..516fe1b9 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -215,6 +215,11 @@ class CoverageConfig(object): self.xml_output = "coverage.xml" self.xml_package_depth = 99 + # Defaults for [JSON] + self.json_output = "coverage.json" + self.json_pretty_print = False + self.json_show_contexts = False + # Defaults for [paths] self.paths = {} @@ -363,6 +368,11 @@ class CoverageConfig(object): # [xml] ('xml_output', 'xml:output'), ('xml_package_depth', 'xml:package_depth', 'int'), + + # [json] + ('json_output', 'json:output'), + ('json_pretty_print', 'json:pretty_print', 'boolean'), + ('json_show_contexts', 'json:show_contexts', 'boolean'), ] def _set_attr_from_config_option(self, cp, attr, where, type_=''): diff --git a/coverage/control.py b/coverage/control.py index 7d9d9e91..8cba6c38 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -22,11 +22,13 @@ from coverage.disposition import disposition_debug_msg from coverage.files import PathAliases, set_relative_directory, abs_file from coverage.html import HtmlReporter from coverage.inorout import InOrOut +from coverage.jsonreport import JsonReporter from coverage.misc import CoverageException, bool_or_none, join_regex -from coverage.misc import ensure_dir_for_file, file_be_gone, isolate_module +from coverage.misc import ensure_dir_for_file, isolate_module from coverage.plugin import FileReporter from coverage.plugin_support import Plugins from coverage.python import PythonFileReporter +from coverage.report import render_report from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -862,33 +864,29 @@ class Coverage(object): ignore_errors=ignore_errors, report_omit=omit, report_include=include, xml_output=outfile, report_contexts=contexts, ) - file_to_close = None - delete_file = False - if self.config.xml_output: - if self.config.xml_output == '-': - outfile = sys.stdout - else: - # Ensure that the output directory is created; done here - # because this report pre-opens the output file. - # HTMLReport does this using the Report plumbing because - # its task is more complex, being multiple files. - ensure_dir_for_file(self.config.xml_output) - open_kwargs = {} - if env.PY3: - open_kwargs['encoding'] = 'utf8' - outfile = open(self.config.xml_output, "w", **open_kwargs) - file_to_close = outfile - try: - reporter = XmlReporter(self) - return reporter.report(morfs, outfile=outfile) - except CoverageException: - delete_file = True - raise - finally: - if file_to_close: - file_to_close.close() - if delete_file: - file_be_gone(self.config.xml_output) + return render_report(self.config.xml_output, XmlReporter(self), morfs) + + def json_report( + self, morfs=None, outfile=None, ignore_errors=None, + omit=None, include=None, contexts=None, pretty_print=None, + show_contexts=None + ): + """Generate a JSON report of coverage results. + + Each module in `morfs` is included in the report. `outfile` is the + path to write the file to, "-" will write to stdout. + + See :meth:`report` for other arguments. + + Returns a float, the total percentage covered. + + """ + self.config.from_args( + ignore_errors=ignore_errors, report_omit=omit, report_include=include, + json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, + json_show_contexts=show_contexts + ) + return render_report(self.config.json_output, JsonReporter(self), morfs) def sys_info(self): """Return a list of (key, value) pairs showing internal information.""" diff --git a/coverage/jsonreport.py b/coverage/jsonreport.py new file mode 100644 index 00000000..e44cbf08 --- /dev/null +++ b/coverage/jsonreport.py @@ -0,0 +1,100 @@ +# coding: utf-8 +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Json reporting for coverage.py""" +import datetime +import json +import sys + +from coverage import __version__ +from coverage.report import get_analysis_to_report +from coverage.results import Numbers + + +class JsonReporter(object): + """A reporter for writing JSON coverage results.""" + + def __init__(self, coverage): + self.coverage = coverage + self.config = self.coverage.config + self.total = Numbers() + self.report_data = {} + + def report(self, morfs, outfile=None): + """Generate a json report for `morfs`. + + `morfs` is a list of modules or file names. + + `outfile` is a file object to write the json to + + """ + outfile = outfile or sys.stdout + coverage_data = self.coverage.get_data() + coverage_data.set_query_contexts(self.config.report_contexts) + self.report_data["meta"] = { + "version": __version__, + "timestamp": datetime.datetime.now().isoformat(), + "branch_coverage": coverage_data.has_arcs(), + "show_contexts": self.config.json_show_contexts, + } + + measured_files = {} + for file_reporter, analysis in get_analysis_to_report(self.coverage, morfs): + measured_files[file_reporter.relative_filename()] = self.report_one_file( + coverage_data, + file_reporter, + analysis + ) + + self.report_data["files"] = measured_files + + self.report_data["totals"] = { + 'covered_lines': self.total.n_executed, + 'num_statements': self.total.n_statements, + 'percent_covered': self.total.pc_covered, + 'missing_lines': self.total.n_missing, + 'excluded_lines': self.total.n_excluded, + } + + if coverage_data.has_arcs(): + self.report_data["totals"].update({ + 'num_branches': self.total.n_branches, + 'num_partial_branches': self.total.n_partial_branches, + }) + + json.dump( + self.report_data, + outfile, + indent=4 if self.config.json_pretty_print else None + ) + + return self.total.n_statements and self.total.pc_covered + + def report_one_file(self, coverage_data, file_reporter, analysis): + """Extract the relevant report data for a single file""" + nums = analysis.numbers + self.total += nums + summary = { + 'covered_lines': nums.n_executed, + 'num_statements': nums.n_statements, + 'percent_covered': nums.pc_covered, + 'missing_lines': nums.n_missing, + 'excluded_lines': nums.n_excluded, + } + reported_file = { + 'executed_lines': sorted(analysis.executed), + 'summary': summary, + 'missing_lines': sorted(analysis.missing), + 'excluded_lines': sorted(analysis.excluded) + } + if self.config.json_show_contexts: + reported_file['contexts'] = analysis.data.contexts_by_lineno( + file_reporter.filename + ) + if coverage_data.has_arcs(): + reported_file['summary'].update({ + 'num_branches': nums.n_branches, + 'num_partial_branches': nums.n_partial_branches, + }) + return reported_file diff --git a/coverage/report.py b/coverage/report.py index 9a740290..d24dddc8 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -2,9 +2,45 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Reporter foundation for coverage.py.""" +import sys +from coverage import env from coverage.files import prep_patterns, FnmatchMatcher -from coverage.misc import CoverageException, NoSource, NotPython +from coverage.misc import CoverageException, NoSource, NotPython, ensure_dir_for_file, file_be_gone + + +def render_report(output_path, reporter, morfs): + """Run the provided reporter ensuring any required setup and cleanup is done + + At a high level this method ensures the output file is ready to be written to. Then writes the + report to it. Then closes the file and deletes any garbage created if necessary. + """ + file_to_close = None + delete_file = False + if output_path: + if output_path == '-': + outfile = sys.stdout + else: + # Ensure that the output directory is created; done here + # because this report pre-opens the output file. + # HTMLReport does this using the Report plumbing because + # its task is more complex, being multiple files. + ensure_dir_for_file(output_path) + open_kwargs = {} + if env.PY3: + open_kwargs['encoding'] = 'utf8' + outfile = open(output_path, "w", **open_kwargs) + file_to_close = outfile + try: + return reporter.report(morfs, outfile=outfile) + except CoverageException: + delete_file = True + raise + finally: + if file_to_close: + file_to_close.close() + if delete_file: + file_be_gone(output_path) def get_analysis_to_report(coverage, morfs): diff --git a/coverage/results.py b/coverage/results.py index ba335209..c88da919 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -23,7 +23,8 @@ class Analysis(object): # Identify missing statements. executed = self.data.lines(self.filename) or [] executed = self.file_reporter.translate_lines(executed) - self.missing = self.statements - executed + self.executed = executed + self.missing = self.statements - self.executed if self.data.has_arcs(): self._arc_possibilities = sorted(self.file_reporter.arcs()) diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 07967719..265bf02c 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -44,8 +44,6 @@ class XmlReporter(object): self.source_paths.add(files.canonical_filename(src)) self.packages = {} self.xml_out = None - self.data = coverage.get_data() - self.has_arcs = self.data.has_arcs() def report(self, morfs, outfile=None): """Generate a Cobertura-compatible XML report for `morfs`. @@ -57,6 +55,7 @@ class XmlReporter(object): """ # Initial setup. outfile = outfile or sys.stdout + has_arcs = self.coverage.get_data().has_arcs() # Create the DOM that will store the data. impl = xml.dom.minidom.getDOMImplementation() @@ -73,7 +72,7 @@ class XmlReporter(object): # Call xml_file for each file in the data. for fr, analysis in get_analysis_to_report(self.coverage, morfs): - self.xml_file(fr, analysis) + self.xml_file(fr, analysis, has_arcs) xsources = self.xml_out.createElement("sources") xcoverage.appendChild(xsources) @@ -102,7 +101,7 @@ class XmlReporter(object): xclasses.appendChild(class_elt) xpackage.setAttribute("name", pkg_name.replace(os.sep, '.')) xpackage.setAttribute("line-rate", rate(lhits, lnum)) - if self.has_arcs: + if has_arcs: branch_rate = rate(bhits, bnum) else: branch_rate = "0" @@ -117,7 +116,7 @@ class XmlReporter(object): xcoverage.setAttribute("lines-valid", str(lnum_tot)) xcoverage.setAttribute("lines-covered", str(lhits_tot)) xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot)) - if self.has_arcs: + if has_arcs: xcoverage.setAttribute("branches-valid", str(bnum_tot)) xcoverage.setAttribute("branches-covered", str(bhits_tot)) xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot)) @@ -138,7 +137,7 @@ class XmlReporter(object): pct = 100.0 * (lhits_tot + bhits_tot) / denom return pct - def xml_file(self, fr, analysis): + def xml_file(self, fr, analysis, has_arcs): """Add to the XML report for a single file.""" # Create the 'lines' and 'package' XML elements, which @@ -182,7 +181,7 @@ class XmlReporter(object): # executed? If so, that should be recorded here. xline.setAttribute("hits", str(int(line not in analysis.missing))) - if self.has_arcs: + if has_arcs: if line in branch_stats: total, taken = branch_stats[line] xline.setAttribute("branch", "true") @@ -198,7 +197,7 @@ class XmlReporter(object): class_lines = len(analysis.statements) class_hits = class_lines - len(analysis.missing) - if self.has_arcs: + if has_arcs: class_branches = sum(t for t, k in branch_stats.values()) missing_branches = sum(t - k for t, k in branch_stats.values()) class_br_hits = class_branches - missing_branches @@ -208,7 +207,7 @@ class XmlReporter(object): # Finalize the statistics that are collected in the XML DOM. xclass.setAttribute("line-rate", rate(class_hits, class_lines)) - if self.has_arcs: + if has_arcs: branch_rate = rate(class_br_hits, class_branches) else: branch_rate = "0" |