From 75d4d497b7b587fa39669ebde57dc7600a09ec48 Mon Sep 17 00:00:00 2001 From: Patrick Lannigan Date: Sat, 23 Apr 2016 20:57:48 -0400 Subject: [PATCH 1/3] Specify report output form cmd line --- src/pytest_cov/engine.py | 12 ++++-- src/pytest_cov/plugin.py | 46 +++++++++++++++++---- tests/test_pytest_cov.py | 86 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 11 deletions(-) diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index e195e6c9..60113777 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -92,20 +92,24 @@ def summary(self, stream): # Produce annotated source code report if wanted. if 'annotate' in self.cov_report: - self.cov.annotate(ignore_errors=True) + annotate_dir = self.cov_report['annotate'] + self.cov.annotate(ignore_errors=True, directory=annotate_dir) # We need to call Coverage.report here, just to get the total # Coverage.annotate don't return any total and we need it for --cov-fail-under. total = self.cov.report(ignore_errors=True, file=StringIO()) - stream.write('Coverage annotated source written next to source\n') + if annotate_dir: + stream.write('Coverage annotated source written to dir %s\n' % annotate_dir) + else: + stream.write('Coverage annotated source written next to source\n') # Produce html report if wanted. if 'html' in self.cov_report: - total = self.cov.html_report(ignore_errors=True) + total = self.cov.html_report(ignore_errors=True, directory=self.cov_report['html']) stream.write('Coverage HTML written to dir %s\n' % self.cov.config.html_dir) # Produce xml report if wanted. if 'xml' in self.cov_report: - total = self.cov.xml_report(ignore_errors=True) + total = self.cov.xml_report(ignore_errors=True, outfile=self.cov_report['xml']) stream.write('Coverage XML written to file %s\n' % self.cov.config.xml_output) # Report on any failed slaves. diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 4aeb910b..f8434f87 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -1,12 +1,44 @@ """Coverage plugin for pytest.""" import os import pytest +import argparse from coverage.misc import CoverageException from . import embed from . import engine +class CoverageError(Exception): + """Indicates that our coverage is too low""" + + +def validate_report(arg): + file_choices = ['annotate', 'html', 'xml'] + term_choices = ['term', 'term-missing'] + all_choices = term_choices + file_choices + values = arg.split(":", 1) + report_type = values[0] + if report_type not in all_choices + ['']: + msg = 'invalid choice: "{}" (choose from "{}")'.format(arg, all_choices) + raise argparse.ArgumentTypeError(msg) + + if len(values) == 1: + return report_type, None + + if report_type not in file_choices: + msg = 'output specifier not supported for: "{}" (choose from "{}")'.format(arg, + file_choices) + raise argparse.ArgumentTypeError(msg) + + return values + + +class StoreReport(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + report_type, file = values + namespace.cov_report[report_type] = file + + def pytest_addoption(parser): """Add options to control coverage.""" @@ -16,12 +48,12 @@ def pytest_addoption(parser): nargs='?', const=True, dest='cov_source', help='measure coverage for filesystem path ' '(multi-allowed)') - group.addoption('--cov-report', action='append', default=[], - metavar='type', - choices=['term', 'term-missing', 'annotate', 'html', - 'xml', ''], + group.addoption('--cov-report', action=StoreReport, default={}, + metavar='type', type=validate_report, help='type of report to generate: term, term-missing, ' - 'annotate, html, xml (multi-allowed)') + 'annotate, html, xml (multi-allowed). ' + 'annotate, html and xml may be be followed by ":DEST" ' + 'where DEST specifies the output location.') group.addoption('--cov-config', action='store', default='.coveragerc', metavar='path', help='config file for coverage, default: .coveragerc') @@ -45,8 +77,8 @@ def pytest_load_initial_conftests(early_config, parser, args): if not ns.cov_report: ns.cov_report = ['term'] - elif ns.cov_report == ['']: - ns.cov_report = [] + elif len(ns.cov_report) == 1 and '' in ns.cov_report: + ns.cov_report = {} if ns.cov: plugin = CovPlugin(ns, early_config.pluginmanager) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 207ee04c..2b866aee 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -134,6 +134,8 @@ def test_fail(): SCRIPT2_RESULT = '3 * 100%' CHILD_SCRIPT_RESULT = '[56] * 100%' PARENT_SCRIPT_RESULT = '8 * 100%' +DEST_DIR = 'cov_dest' +REPORT_NAME = 'cov.xml' xdist = pytest.mark.parametrize('opts', ['', '-n 1'], ids=['nodist', 'xdist']) @@ -170,6 +172,90 @@ def test_annotate(testdir): assert result.ret == 0 +def test_annotate_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=annotate:' + DEST_DIR, + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage annotated source written to dir ' + DEST_DIR, + '*10 passed*', + ]) + dest_dir = testdir.tmpdir.join(DEST_DIR) + assert dest_dir.check(dir=True) + assert dest_dir.join(script.basename + ",cover").check() + assert result.ret == 0 + + +def test_html_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=html:' + DEST_DIR, + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage HTML written to dir ' + DEST_DIR, + '*10 passed*', + ]) + dest_dir = testdir.tmpdir.join(DEST_DIR) + assert dest_dir.check(dir=True) + assert dest_dir.join("index.html").check() + assert result.ret == 0 + + +def test_xml_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=xml:' + REPORT_NAME, + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage XML written to file ' + REPORT_NAME, + '*10 passed*', + ]) + assert testdir.tmpdir.join(REPORT_NAME).check() + assert result.ret == 0 + + +def test_term_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term:' + DEST_DIR, + script) + + result.stderr.fnmatch_lines([ + '*argument --cov-report: output specifier not supported for: "term:%s"*' % DEST_DIR, + ]) + assert result.ret != 0 + + +def test_term_missing_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=term-missing:' + DEST_DIR, + script) + + result.stderr.fnmatch_lines([ + '*argument --cov-report: output specifier not supported for: ' + '"term-missing:%s"*' % DEST_DIR, + ]) + assert result.ret != 0 + + def test_cov_min_100(testdir): script = testdir.makepyfile(SCRIPT) From 5895d70bdb45adf3ca58c160c8c93169a091f8f8 Mon Sep 17 00:00:00 2001 From: Patrick Lannigan Date: Sun, 24 Apr 2016 09:29:49 -0400 Subject: [PATCH 2/3] Update documentation --- AUTHORS.rst | 1 + CHANGELOG.rst | 5 +++++ README.rst | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 3b1a5191..ad700013 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -13,3 +13,4 @@ Authors * Ionel Cristian Mărieș - http://blog.ionelmc.ro * Christian Ledermann - https://github.com/cleder * Alec Nikolas Reiter - https://github.com/justanr +* Patrick Lannigan - https://github.com/unholysampler diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8a5f4a7a..38518934 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog ========= +2.2.2 (tba) +------------------ + +* Add support for specifying output location for html, xml, and annotate report. + 2.2.1 (2016-01-30) ------------------ diff --git a/README.rst b/README.rst index a8827627..3896abd9 100644 --- a/README.rst +++ b/README.rst @@ -231,6 +231,15 @@ These three report options output to files without showing anything on the termi --cov-report annotate --cov=myproj tests/ +The output location for each of these reports can be specified. The output location for the XML +report is a file. Where as the output location for the HTML and annotated source code reports are +directories:: + + py.test --cov-report html:cov_html + --cov-report xml:cov.xml + --cov-report annotate:cov_annotate + --cov=myproj tests/ + The final report option can also suppress printing to the terminal:: py.test --cov-report= --cov=myproj tests/ From 5e1fcc692a09196780fdcc4ffb61d9fef3260a83 Mon Sep 17 00:00:00 2001 From: Patrick Lannigan Date: Sun, 24 Apr 2016 18:37:42 -0400 Subject: [PATCH 3/3] fixup! Specify report output form cmd line --- tests/test_pytest_cov.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 2b866aee..4d21b11f 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -235,7 +235,10 @@ def test_term_output_dir(testdir): '--cov-report=term:' + DEST_DIR, script) + # backport of argparse to py26 doesn't display ArgumentTypeError message result.stderr.fnmatch_lines([ + '*argument --cov-report: *', + ] if tuple(sys.version_info[:2]) == (2, 6) else [ '*argument --cov-report: output specifier not supported for: "term:%s"*' % DEST_DIR, ]) assert result.ret != 0 @@ -249,7 +252,10 @@ def test_term_missing_output_dir(testdir): '--cov-report=term-missing:' + DEST_DIR, script) + # backport of argparse to py26 doesn't display ArgumentTypeError message result.stderr.fnmatch_lines([ + '*argument --cov-report: *', + ] if tuple(sys.version_info[:2]) == (2, 6) else [ '*argument --cov-report: output specifier not supported for: ' '"term-missing:%s"*' % DEST_DIR, ])