Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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)
------------------

Expand Down
9 changes: 9 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
12 changes: 8 additions & 4 deletions src/pytest_cov/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
46 changes: 39 additions & 7 deletions src/pytest_cov/plugin.py
Original file line number Diff line number Diff line change
@@ -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."""

Expand All @@ -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')
Expand All @@ -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)
Expand Down
92 changes: 92 additions & 0 deletions tests/test_pytest_cov.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])

Expand Down Expand Up @@ -170,6 +172,96 @@ 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)

# 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


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)

# 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,
])
assert result.ret != 0


def test_cov_min_100(testdir):
script = testdir.makepyfile(SCRIPT)

Expand Down