diff options
author | Matt Bachmann <bachmann.matt@gmail.com> | 2019-07-08 23:09:38 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2019-08-31 07:24:08 -0400 |
commit | 9a78a80aaf8f8161b857ebf3cf02dd511181dd07 (patch) | |
tree | d73603d17fc5269a3b8199085bf67c436d99aedf | |
parent | 790f0b30010a3a1f68f4fa7c172ce3b31c7c4b24 (diff) | |
download | python-coveragepy-git-9a78a80aaf8f8161b857ebf3cf02dd511181dd07.tar.gz |
Create a JSON report
-rw-r--r-- | CHANGES.rst | 3 | ||||
-rw-r--r-- | CONTRIBUTORS.txt | 1 | ||||
-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 | ||||
-rw-r--r-- | doc/branch.rst | 5 | ||||
-rw-r--r-- | doc/cmd.rst | 17 | ||||
-rw-r--r-- | doc/config.rst | 19 | ||||
-rw-r--r-- | doc/howitworks.rst | 4 | ||||
-rw-r--r-- | doc/source.rst | 10 | ||||
-rw-r--r-- | tests/test_cmdline.py | 94 | ||||
-rw-r--r-- | tests/test_config.py | 6 | ||||
-rw-r--r-- | tests/test_json.py | 144 |
17 files changed, 496 insertions, 63 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 34a75634..539f8a09 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -33,6 +33,9 @@ Unreleased - `debug=plugin` didn't properly support configuration or dynamic context plugins, but now it does, closing `issue 834`_. +- Added a JSON report `issue 720`_. + +.. _issue 720: https://github.com/nedbat/coveragepy/issues/720 .. _issue 822: https://github.com/nedbat/coveragepy/issues/822 .. _issue 834: https://github.com/nedbat/coveragepy/issues/834 .. _issue 829: https://github.com/nedbat/coveragepy/issues/829 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 06eef4f7..859edcf6 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -79,6 +79,7 @@ Marc Abramowitz Marcus Cobden Mark van der Wal Martin Fuzzey +Matt Bachmann Matthew Boehm Matthew Desmarais Max Linke 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" diff --git a/doc/branch.rst b/doc/branch.rst index 92cab27b..9af8f050 100644 --- a/doc/branch.rst +++ b/doc/branch.rst @@ -55,8 +55,9 @@ The HTML report gives information about which lines had missing branches. Lines that were missing some branches are shown in yellow, with an annotation at the far right showing branch destination line numbers that were not exercised. -The XML report produced by ``coverage xml`` also includes branch information, -including separate statement and branch coverage percentages. +The XML and JSON reports produced by ``coverage xml`` and ``coverage json`` +respectively also include branch information, including separate statement and +branch coverage percentages. How it works diff --git a/doc/cmd.rst b/doc/cmd.rst index 42738493..ad84a6ae 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -22,6 +22,7 @@ Coverage.py command line usage .. :history: 20121117T091000, Added command aliases. .. :history: 20140924T193000, Added --concurrency .. :history: 20150802T174700, Updated for 4.0b1 +.. :history: 20190828T212200, added json report .. highlight:: console @@ -41,6 +42,8 @@ Coverage.py has a number of commands which determine the action performed: * **html** -- Produce annotated HTML listings with coverage results. +* **json** -- Produce a JSON report with coverage results. + * **xml** -- Produce an XML report with coverage results. * **annotate** -- Annotate source files with coverage results. @@ -292,7 +295,8 @@ Reporting --------- Coverage.py provides a few styles of reporting, with the **report**, **html**, -**annotate**, and **xml** commands. They share a number of common options. +**annotate**, **json**, and **xml** commands. They share a number of common +options. The command-line arguments are module or file names to report on, if you'd like to report on a subset of the data collected. @@ -473,6 +477,17 @@ You can specify the name of the output file with the ``-o`` switch. Other common reporting options are described above in :ref:`cmd_reporting`. +.. _cmd_json: + +JSON reporting +------------- + +The **json** command writes coverage data to a "coverage.json" file. + +You can specify the name of the output file with the ``-o`` switch. + +Other common reporting options are described above in :ref:`cmd_reporting`. + .. _cmd_debug: diff --git a/doc/config.rst b/doc/config.rst index 35584f01..84c77131 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -301,3 +301,22 @@ also apply to XML output, where appropriate. identified as packages in the report. Directories deeper than this depth are not reported as packages. The default is that all directories are reported as packages. + +.. _config_json: + +[json] +----- + +Values particular to json reporting. The values in the ``[report]`` section +also apply to JSON output, where appropriate. + +``json_output`` (string, default "coverage.json"): where to write the json +report. + +``json_pretty_print`` (boolean, default false): controls if fields in the json +are outputted with whitespace formatted for human consumption (True) or for +minimum file size (False) + +``json_show_contexts`` (boolean, default false): should the json report include +an indication on each line of which contexts executed the line. +See :ref:`dynamic_contexts` for details. diff --git a/doc/howitworks.rst b/doc/howitworks.rst index 62af42e3..0e11c29e 100644 --- a/doc/howitworks.rst +++ b/doc/howitworks.rst @@ -83,8 +83,8 @@ Reporting Once we have the set of executed lines and missing lines, reporting is just a matter of formatting that information in a useful way. Each reporting method -(text, html, annotated source, xml) has a different output format, but the -process is the same: write out the information in the particular format, +(text, html, json, annotated source, xml) has a different output format, but +the process is the same: write out the information in the particular format, possibly including the source code itself. diff --git a/doc/source.rst b/doc/source.rst index e1bc8038..21a5f612 100644 --- a/doc/source.rst +++ b/doc/source.rst @@ -80,11 +80,11 @@ reported. Usually you want to see all the code that was measured, but if you are measuring a large project, you may want to get reports for just certain parts. -The report commands (``report``, ``html``, ``annotate``, and ``xml``) all take -optional ``modules`` arguments, and ``--include`` and ``--omit`` switches. The -``modules`` arguments specify particular modules to report on. The ``include`` -and ``omit`` values are lists of file name patterns, just as with the ``run`` -command. +The report commands (``report``, ``html``, ``json``, ``annotate``, and ``xml``) +all take optional ``modules`` arguments, and ``--include`` and ``--omit`` +switches. The ``modules`` arguments specify particular modules to report on. +The ``include`` and ``omit`` values are lists of file name patterns, just as +with the ``run`` command. Remember that the reporting commands can only report on the data that has been collected, so the data you're looking for may not be in the data available for diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 387bf61f..e15c5fcb 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -46,6 +46,10 @@ class BaseCmdLineTest(CoverageTest): ignore_errors=None, include=None, omit=None, morfs=[], outfile=None, contexts=None, ) + _defaults.Coverage().json_report( + ignore_errors=None, include=None, omit=None, morfs=[], outfile=None, + contexts=None, pretty_print=None, show_contexts=None + ) _defaults.Coverage( cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None, debug=None, @@ -69,6 +73,7 @@ class BaseCmdLineTest(CoverageTest): cov.report.return_value = 50.0 cov.html_report.return_value = 50.0 cov.xml_report.return_value = 50.0 + cov.json_report.return_value = 50.0 return mk @@ -667,6 +672,59 @@ class CmdLineTest(BaseCmdLineTest): cov.xml_report(morfs=["mod1", "mod2", "mod3"]) """) + def test_json(self): + # coverage json [-i] [--omit DIR,...] [FILE1 FILE2 ...] + self.cmd_executes("json", """\ + cov = Coverage() + cov.load() + cov.json_report() + """) + self.cmd_executes("json --pretty-print", """\ + cov = Coverage() + cov.load() + cov.json_report(pretty_print=True) + """) + self.cmd_executes("json --pretty-print --show-contexts", """\ + cov = Coverage() + cov.load() + cov.json_report(pretty_print=True, show_contexts=True) + """) + self.cmd_executes("json -i", """\ + cov = Coverage() + cov.load() + cov.json_report(ignore_errors=True) + """) + self.cmd_executes("json -o myjson.foo", """\ + cov = Coverage() + cov.load() + cov.json_report(outfile="myjson.foo") + """) + self.cmd_executes("json -o -", """\ + cov = Coverage() + cov.load() + cov.json_report(outfile="-") + """) + self.cmd_executes("json --omit fooey", """\ + cov = Coverage(omit=["fooey"]) + cov.load() + cov.json_report(omit=["fooey"]) + """) + self.cmd_executes("json --omit fooey,booey", """\ + cov = Coverage(omit=["fooey", "booey"]) + cov.load() + cov.json_report(omit=["fooey", "booey"]) + """) + self.cmd_executes("json mod1", """\ + cov = Coverage() + cov.load() + cov.json_report(morfs=["mod1"]) + """) + self.cmd_executes("json mod1 mod2 mod3", """\ + cov = Coverage() + cov.load() + cov.json_report(morfs=["mod1", "mod2", "mod3"]) + """) + def test_no_arguments_at_all(self): self.cmd_help("", topic="minimum_help", ret=OK) @@ -847,11 +905,12 @@ class CmdMainTest(CoverageTest): class CoverageReportingFake(object): """A fake Coverage.coverage test double.""" # pylint: disable=missing-docstring - def __init__(self, report_result, html_result, xml_result): + def __init__(self, report_result, html_result, xml_result, json_report): self.config = CoverageConfig() self.report_result = report_result self.html_result = html_result self.xml_result = xml_result + self.json_result = json_report def set_option(self, optname, optvalue): self.config.set_option(optname, optvalue) @@ -871,24 +930,31 @@ class CoverageReportingFake(object): def xml_report(self, *args_unused, **kwargs_unused): return self.xml_result + def json_report(self, *args_unused, **kwargs_unused): + return self.json_result + @pytest.mark.parametrize("results, fail_under, cmd, ret", [ # Command-line switch properly checks the result of reporting functions. - ((20, 30, 40), None, "report --fail-under=19", 0), - ((20, 30, 40), None, "report --fail-under=21", 2), - ((20, 30, 40), None, "html --fail-under=29", 0), - ((20, 30, 40), None, "html --fail-under=31", 2), - ((20, 30, 40), None, "xml --fail-under=39", 0), - ((20, 30, 40), None, "xml --fail-under=41", 2), + ((20, 30, 40, 50), None, "report --fail-under=19", 0), + ((20, 30, 40, 50), None, "report --fail-under=21", 2), + ((20, 30, 40, 50), None, "html --fail-under=29", 0), + ((20, 30, 40, 50), None, "html --fail-under=31", 2), + ((20, 30, 40, 50), None, "xml --fail-under=39", 0), + ((20, 30, 40, 50), None, "xml --fail-under=41", 2), + ((20, 30, 40, 50), None, "json --fail-under=49", 0), + ((20, 30, 40, 50), None, "json --fail-under=51", 2), # Configuration file setting properly checks the result of reporting. - ((20, 30, 40), 19, "report", 0), - ((20, 30, 40), 21, "report", 2), - ((20, 30, 40), 29, "html", 0), - ((20, 30, 40), 31, "html", 2), - ((20, 30, 40), 39, "xml", 0), - ((20, 30, 40), 41, "xml", 2), + ((20, 30, 40, 50), 19, "report", 0), + ((20, 30, 40, 50), 21, "report", 2), + ((20, 30, 40, 50), 29, "html", 0), + ((20, 30, 40, 50), 31, "html", 2), + ((20, 30, 40, 50), 39, "xml", 0), + ((20, 30, 40, 50), 41, "xml", 2), + ((20, 30, 40, 50), 49, "json", 0), + ((20, 30, 40, 50), 51, "json", 2), # Command-line overrides configuration. - ((20, 30, 40), 19, "report --fail-under=21", 2), + ((20, 30, 40, 50), 19, "report --fail-under=21", 2), ]) def test_fail_under(results, fail_under, cmd, ret): cov = CoverageReportingFake(*results) diff --git a/tests/test_config.py b/tests/test_config.py index 7b019f94..ebea18a7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -332,6 +332,10 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest): hello = world ; comments still work. names = Jane/John/Jenny + + [{section}json] + pretty_print = True + show_contexts = True """ # Just some sample setup.cfg text from the docs. @@ -399,6 +403,8 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest): 'names': 'Jane/John/Jenny', }) self.assertEqual(cov.config.get_plugin_options("plugins.another"), {}) + self.assertEqual(cov.config.json_show_contexts, True) + self.assertEqual(cov.config.json_pretty_print, True) def test_config_file_settings(self): self.make_file(".coveragerc", self.LOTSA_SETTINGS.format(section="")) diff --git a/tests/test_json.py b/tests/test_json.py new file mode 100644 index 00000000..1ae5764e --- /dev/null +++ b/tests/test_json.py @@ -0,0 +1,144 @@ +# 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 + +"""Test json-based summary reporting for coverage.py""" +from datetime import datetime +import json +import os + +import coverage +from tests.coveragetest import UsingModulesMixin, CoverageTest + + +class JsonReportTest(UsingModulesMixin, CoverageTest): + """Tests of the JSON reports from coverage.py.""" + def _assert_expected_json_report(self, cov, expected_result): + """ + Helper for tests that handles the common ceremony so the tests can be clearly show the + consequences of setting various arguments. + """ + self.make_file("a.py", """\ + a = {'b': 1} + if a.get('a'): + b = 1 + """) + a = self.start_import_stop(cov, "a") + output_path = os.path.join(self.temp_dir, "a.json") + cov.json_report(a, outfile=output_path) + with open(output_path) as result_file: + parsed_result = json.load(result_file) + self.assert_recent_datetime( + datetime.strptime(parsed_result['meta']['timestamp'], "%Y-%m-%dT%H:%M:%S.%f") + ) + del (parsed_result['meta']['timestamp']) + assert parsed_result == expected_result + + def test_branch_coverage(self): + cov = coverage.Coverage(branch=True) + expected_result = { + 'meta': { + "version": coverage.__version__, + "branch_coverage": True, + "show_contexts": False, + }, + 'files': { + 'a.py': { + 'executed_lines': [1, 2], + 'missing_lines': [3], + 'excluded_lines': [], + 'summary': { + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'num_branches': 2, + 'excluded_lines': 0, + 'num_partial_branches': 1, + 'percent_covered': 60.0 + } + } + }, + 'totals': { + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'num_branches': 2, + 'excluded_lines': 0, + 'num_partial_branches': 1, + 'percent_covered': 60.0 + } + } + self._assert_expected_json_report(cov, expected_result) + + def test_simple_line_coverage(self): + cov = coverage.Coverage() + expected_result = { + 'meta': { + "version": coverage.__version__, + "branch_coverage": False, + "show_contexts": False, + }, + 'files': { + 'a.py': { + 'executed_lines': [1, 2], + 'missing_lines': [3], + 'excluded_lines': [], + 'summary': { + 'excluded_lines': 0, + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'percent_covered': 66.66666666666667 + } + } + }, + 'totals': { + 'excluded_lines': 0, + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'percent_covered': 66.66666666666667 + } + } + self._assert_expected_json_report(cov, expected_result) + + def test_context(self): + cov = coverage.Coverage(context="cool_test") + cov.config.json_show_contexts = True + expected_result = { + 'meta': { + "version": coverage.__version__, + "branch_coverage": False, + "show_contexts": True, + }, + 'files': { + 'a.py': { + 'executed_lines': [1, 2], + 'missing_lines': [3], + 'excluded_lines': [], + "contexts": { + "1": [ + "cool_test" + ], + "2": [ + "cool_test" + ] + }, + 'summary': { + 'excluded_lines': 0, + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'percent_covered': 66.66666666666667 + } + } + }, + 'totals': { + 'excluded_lines': 0, + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'percent_covered': 66.66666666666667 + } + } + self._assert_expected_json_report(cov, expected_result) |