summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AUTHORS.txt1
-rw-r--r--TODO.txt12
-rw-r--r--coverage/codeunit.py1
-rw-r--r--coverage/runners/__init__.py0
-rw-r--r--coverage/runners/noseplugin.py51
-rw-r--r--coverage/runners/plugin.py145
-rw-r--r--coverage/runners/pytestplugin.py34
-rw-r--r--setup.py8
-rw-r--r--test/test_testplugin.py37
9 files changed, 287 insertions, 2 deletions
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 7e3efe5..a86a44d 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -27,3 +27,4 @@ Martin Fuzzey
Greg Rogers
Christoph Zwerschke
Zooko Wilcox-O'Hearn
+David Stanek
diff --git a/TODO.txt b/TODO.txt
index b9af30a..2095116 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -1,5 +1,9 @@
Coverage TODO
+* plugin work
+ - add --cover-include.
+ - add --cover-rcfile, and maybe remove a bunch of options.
+
* 3.3
- Config file
@@ -35,6 +39,14 @@ Coverage TODO
+* Test-runner plugin
+
+- Support distribute/3k installation, register py.test-entrypoint there
+ as well as py-1.1.1 is 2.4-3.1 compatible.
+- Skip registering plugin entrypoints if not running on Jython.
+- Update docs to include a section about the plugin.
+
+
* Speed
+ C extension collector
diff --git a/coverage/codeunit.py b/coverage/codeunit.py
index cc7c226..96cdb02 100644
--- a/coverage/codeunit.py
+++ b/coverage/codeunit.py
@@ -61,7 +61,6 @@ def code_unit_factory(morfs, file_locator, omit=None, include=None):
return code_units
-
class CodeUnit(object):
"""Code unit: a filename or module.
diff --git a/coverage/runners/__init__.py b/coverage/runners/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/coverage/runners/__init__.py
diff --git a/coverage/runners/noseplugin.py b/coverage/runners/noseplugin.py
new file mode 100644
index 0000000..dceb9f7
--- /dev/null
+++ b/coverage/runners/noseplugin.py
@@ -0,0 +1,51 @@
+"""A nose plugin to run coverage.py"""
+
+import logging
+from nose.plugins import Plugin
+
+from coverage.runners.plugin import CoverageTestWrapper, options as coverage_opts
+
+
+log = logging.getLogger(__name__)
+
+
+class Coverage(Plugin):
+ """Nose plugin for coverage reporting."""
+
+ score = 1
+ status = {}
+
+ def options(self, parser, env):
+ """Add command-line options."""
+
+ super(Coverage, self).options(parser, env)
+ for opt in coverage_opts:
+ parser.add_option(opt)
+
+ def configure(self, options, config):
+ """Configure plugin."""
+
+ try:
+ self.status.pop('active')
+ except KeyError:
+ pass
+
+ super(Coverage, self).configure(options, config)
+
+ self.config = config
+ self.status['active'] = True
+ self.options = options
+
+ def begin(self):
+ """Begin recording coverage information."""
+
+ log.debug("Coverage begin")
+ self.coverage = CoverageTestWrapper(self.options)
+ self.coverage.start()
+
+ def report(self, stream):
+ """Output code coverage report."""
+
+ log.debug("Coverage report")
+ stream.write("Processing Coverage...")
+ self.coverage.finish(stream)
diff --git a/coverage/runners/plugin.py b/coverage/runners/plugin.py
new file mode 100644
index 0000000..cc30e86
--- /dev/null
+++ b/coverage/runners/plugin.py
@@ -0,0 +1,145 @@
+"""Code common to test runner plugins."""
+
+import optparse, sys
+import coverage
+
+class CoverageTestWrapper(object):
+ """A coverage test wrapper.
+
+ 1) Setup the with the parsed options
+ 2) Call start()
+ 3) Run your tests
+ 4) Call finish()
+ 5) Improve your code coverage ;)
+ """
+
+ coverPackages = None
+
+ def __init__(self, options, _covpkg=coverage):
+ # _covpkg is for dependency injection, so we can test this code.
+
+ self.options = options
+ self.covpkg = _covpkg
+
+ self.coverage = None
+
+ self.coverTests = options.cover_tests
+ self.coverPackages = options.cover_package
+
+ def start(self):
+ self.coverage = self.covpkg.coverage(
+ data_suffix = bool(self.options.cover_parallel_mode),
+ cover_pylib = self.options.cover_pylib,
+ timid = self.options.cover_timid,
+ branch = self.options.cover_branch,
+ )
+
+ self.skipModules = sys.modules.keys()[:] #TODO: is this necessary??
+
+ self.coverage.start()
+
+ def finish(self, stream=None):
+ self.coverage.stop()
+ self.coverage.save()
+
+ modules = [module for name, module in sys.modules.items()
+ if self._want_module(name, module)]
+
+ # Remaining actions are reporting, with some common self.options.
+ report_args = {
+ 'morfs': modules,
+ 'ignore_errors': self.options.cover_ignore_errors,
+ }
+
+ try: # try looking for an omit file
+ omit_file = open(self.options.cover_omit)
+ omit = [line.strip() for line in omit_file.readlines()]
+ report_args['omit'] = omit
+ except: # assume cover_omit is a ',' separated list if provided
+ omit = self.options.cover_omit.split(',')
+ report_args['omit'] = omit
+
+ if 'report' in self.options.cover_actions:
+ self.coverage.report(
+ show_missing=self.options.cover_show_missing,
+ file=stream, **report_args)
+ if 'annotate' in self.options.cover_actions:
+ self.coverage.annotate(
+ directory=self.options.cover_directory, **report_args)
+ if 'html' in self.options.cover_actions:
+ self.coverage.html_report(
+ directory=self.options.cover_directory, **report_args)
+ if 'xml' in self.options.cover_actions:
+ outfile = self.options.cover_outfile
+ if outfile == '-':
+ outfile = None
+ self.coverage.xml_report(outfile=outfile, **report_args)
+
+ return
+
+ def _want_module(self, name, module):
+ for package in self.coverPackages:
+ if module is not None and name.startswith(package):
+ return True
+
+ return False
+
+
+options = [
+ optparse.Option('--cover-action', action='append', default=['report'],
+ dest='cover_actions', type="choice",
+ choices=['annotate', 'html', 'report', 'xml'],
+ help="""\
+annotate Annotate source files with execution information.
+html Create an HTML report.
+report Report coverage stats on modules.
+xml Create an XML report of coverage results.
+ """.strip()),
+
+ optparse.Option('--cover-package', action='append', default=[],
+ dest="cover_package", metavar="COVER_PACKAGE",
+ help=("Restrict coverage output to selected package "
+ "- can be specified multiple times")),
+
+ optparse.Option("--cover-tests", action="store_true", dest="cover_tests",
+ metavar="[NOSE_COVER_TESTS]", default=False,
+ help="Include test modules in coverage report "),
+
+ optparse.Option('--cover-branch', action='store_true',
+ help="Measure branch execution. HIGHLY EXPERIMENTAL!"),
+
+ optparse.Option('--cover-directory', action='store', metavar="DIR",
+ help="Write the output files to DIR."),
+
+ optparse.Option('--cover-ignore-errors', action='store_true',
+ help="Ignore errors while reading source files."),
+
+ optparse.Option('--cover-pylib', action='store_true',
+ help=("Measure coverage even inside the Python installed "
+ "library, which isn't done by default.")),
+
+ optparse.Option('--cover-show-missing', action='store_true',
+ help=("Show line numbers of statements in each module "
+ "that weren't executed.")),
+
+ optparse.Option('--cover-omit', action='store',
+ metavar="PRE1,PRE2,...", default='',
+ help=("Omit files when their filename path matches one "
+ "of these patterns.")),
+
+ optparse.Option('--cover-outfile', action='store', metavar="OUTFILE",
+ help=("Write the XML report to this file. Defaults to "
+ "'coverage.xml'")),
+
+ optparse.Option('--cover-parallel-mode', action='store_true',
+ help=("Include the machine name and process id in the "
+ ".coverage data file name.")),
+
+ optparse.Option('--cover-timid', action='store_true',
+ help=("Use a simpler but slower trace method. Try this "
+ "if you get seemingly impossible results!")),
+
+ optparse.Option('--cover-append', action='store_false',
+ help=("Append coverage data to .coverage, otherwise it "
+ "is started clean with each run."))
+]
diff --git a/coverage/runners/pytestplugin.py b/coverage/runners/pytestplugin.py
new file mode 100644
index 0000000..af825aa
--- /dev/null
+++ b/coverage/runners/pytestplugin.py
@@ -0,0 +1,34 @@
+"""py.test plugin hooks"""
+
+from coverage.runners.plugin import CoverageTestWrapper, options
+
+def pytest_addoption(parser):
+ """Get all the options from the coverage.runner and import them."""
+ group = parser.getgroup('Coverage options')
+ for opt in options:
+ group._addoption_instance(opt)
+
+def pytest_configure(config):
+ """Load the runner and start it up."""
+ if config.getvalue("cover_actions"):
+ config.pluginmanager.register(CoveragePlugin(config), "do_coverage")
+
+class CoveragePlugin:
+ """The py.test coverage plugin."""
+
+ def __init__(self, config):
+ self.config = config
+
+ def pytest_sessionstart(self):
+ """Called before session.main() is called."""
+ self.coverage = CoverageTestWrapper(self.config.option)
+ # XXX maybe better to start/suspend/resume coverage
+ # for each single test item
+ self.coverage.start()
+
+ def pytest_terminal_summary(self, terminalreporter):
+ """Add an additional section in the terminal summary reporting."""
+ tw = terminalreporter._tw
+ tw.sep('-', 'coverage')
+ tw.line('Processing Coverage...')
+ self.coverage.finish()
diff --git a/setup.py b/setup.py
index 822b1da..f1d91b2 100644
--- a/setup.py
+++ b/setup.py
@@ -80,7 +80,13 @@ setup_args = dict(
entry_points = {
'console_scripts': [
'coverage = coverage:main',
- ]
+ ],
+ 'pytest11': [
+ 'coverage = coverage.runners.pytestplugin',
+ ],
+ 'nose.plugins.0.10': [
+ 'coverage = coverage.runners.noseplugin:Coverage',
+ ],
},
# We need to get HTML assets from our htmlfiles dir.
diff --git a/test/test_testplugin.py b/test/test_testplugin.py
new file mode 100644
index 0000000..2a38b86
--- /dev/null
+++ b/test/test_testplugin.py
@@ -0,0 +1,37 @@
+import py
+import unittest
+from nose.plugins import PluginTester
+from coverage.runners.noseplugin import Coverage
+
+class TestCoverage(PluginTester, unittest.TestCase):
+ activate = '--with-coverage' # enables the plugin
+ plugins = [Coverage()]
+ args = ['--cover-action=report']
+
+ @py.test.mark.skipif(True) # "requires nose test runner"
+ def test_output(self):
+ assert "Processing Coverage..." in self.output, (
+ "got: %s" % self.output)
+ def makeSuite(self):
+ class TC(unittest.TestCase):
+ def runTest(self):
+ raise ValueError("Coverage down")
+ return unittest.TestSuite([TC()])
+
+
+pytest_plugins = ['pytester']
+def test_functional(testdir):
+ testdir.makepyfile("""
+ def f():
+ x = 42
+ def test_whatever():
+ pass
+ """)
+ result = testdir.runpytest("--cover-action=annotate")
+ assert result.ret == 0
+ assert result.stdout.fnmatch_lines([
+ '*Processing Coverage*'
+ ])
+ coveragefile = testdir.tmpdir.join(".coverage")
+ assert coveragefile.check()
+ # XXX try loading it?