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,
])