summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
Diffstat (limited to 'coverage')
-rw-r--r--coverage/cmdline.py34
-rw-r--r--coverage/config.py10
-rw-r--r--coverage/control.py54
-rw-r--r--coverage/jsonreport.py100
-rw-r--r--coverage/report.py38
-rw-r--r--coverage/results.py3
-rw-r--r--coverage/xmlreport.py17
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"