diff --git a/AUTHORS.rst b/AUTHORS.rst index 489903c2..d8271272 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -51,3 +51,4 @@ Authors * Danilo Šegan - https://github.com/dsegan * Michał Bielawski - https://github.com/D3X * Zac Hatfield-Dodds - https://github.com/Zac-HD +* Christian Fetzer - https://github.com/fetzerch diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6dbf514b..d7150485 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,9 @@ Changelog concurrency = multiprocessing parallel = true sigterm = true +* Added support for LCOV output format via `--cov-report=lcov`. Only works with coverage 6.3+. + Contributed by Christian Fetzer in + `#536 `_. 3.0.0 (2021-10-04) diff --git a/docs/config.rst b/docs/config.rst index 6712292b..fa257037 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -56,9 +56,9 @@ The complete list of command line options is: --cov=PATH Measure coverage for filesystem path. (multi-allowed) --cov-report=type Type of report to generate: term, term-missing, - annotate, html, xml (multi-allowed). term, term- + annotate, html, xml, lcov (multi-allowed). term, term- missing may be followed by ":skip-covered". annotate, - html and xml may be followed by ":DEST" where DEST + html, xml and lcov may be followed by ":DEST" where DEST specifies the output location. Use --cov-report= to not generate any output. --cov-config=path Config file for coverage. Default: .coveragerc diff --git a/docs/reporting.rst b/docs/reporting.rst index eaa99ad3..69191d48 100644 --- a/docs/reporting.rst +++ b/docs/reporting.rst @@ -3,7 +3,7 @@ Reporting It is possible to generate any combination of the reports for a single test run. -The available reports are terminal (with or without missing line numbers shown), HTML, XML and +The available reports are terminal (with or without missing line numbers shown), HTML, XML, LCOV and annotated source code. The terminal report without line numbers (default):: @@ -49,19 +49,21 @@ The terminal report with skip covered:: You can use ``skip-covered`` with ``term-missing`` as well. e.g. ``--cov-report term-missing:skip-covered`` -These three report options output to files without showing anything on the terminal:: +These four report options output to files without showing anything on the terminal:: pytest --cov-report html --cov-report xml + --cov-report lcov --cov-report annotate --cov=myproj tests/ -The output location for each of these reports can be specified. The output location for the XML +The output location for each of these reports can be specified. The output location for the XML and LCOV report is a file. Where as the output location for the HTML and annotated source code reports are directories:: pytest --cov-report html:cov_html --cov-report xml:cov.xml + --cov-report lcov:cov.info --cov-report annotate:cov_annotate --cov=myproj tests/ diff --git a/src/pytest_cov/engine.py b/src/pytest_cov/engine.py index 0303c2f1..bfede8c7 100644 --- a/src/pytest_cov/engine.py +++ b/src/pytest_cov/engine.py @@ -196,6 +196,18 @@ def summary(self, stream): total = self.cov.xml_report(ignore_errors=True, outfile=output) stream.write('Coverage XML written to file %s\n' % (self.cov.config.xml_output if output is None else output)) + # Produce lcov report if wanted. + if 'lcov' in self.cov_report: + output = self.cov_report['lcov'] + with _backup(self.cov, "config"): + self.cov.lcov_report(ignore_errors=True, outfile=output) + + # We need to call Coverage.report here, just to get the total + # Coverage.lcov_report doesn't return any total and we need it for --cov-fail-under. + total = self.cov.report(ignore_errors=True, file=_NullFile) + + stream.write('Coverage LCOV written to file %s\n' % (self.cov.config.lcov_output if output is None else output)) + return total diff --git a/src/pytest_cov/plugin.py b/src/pytest_cov/plugin.py index 252439ed..a415f5a0 100644 --- a/src/pytest_cov/plugin.py +++ b/src/pytest_cov/plugin.py @@ -29,7 +29,7 @@ class CovReportWarning(PytestCovWarning): def validate_report(arg): - file_choices = ['annotate', 'html', 'xml'] + file_choices = ['annotate', 'html', 'xml', 'lcov'] term_choices = ['term', 'term-missing'] term_modifier_choices = ['skip-covered'] all_choices = term_choices + file_choices @@ -39,6 +39,9 @@ def validate_report(arg): msg = f'invalid choice: "{arg}" (choose from "{all_choices}")' raise argparse.ArgumentTypeError(msg) + if report_type == 'lcov' and coverage.version_info <= (6, 3): + raise argparse.ArgumentTypeError('LCOV output is only supported with coverage.py >= 6.3') + if len(values) == 1: return report_type, None @@ -96,9 +99,9 @@ def pytest_addoption(parser): 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, lcov (multi-allowed). ' 'term, term-missing may be followed by ":skip-covered". ' - 'annotate, html and xml may be followed by ":DEST" ' + 'annotate, html, xml and lcov may be followed by ":DEST" ' 'where DEST specifies the output location. ' 'Use --cov-report= to not generate any output.') group.addoption('--cov-config', action='store', default='.coveragerc', diff --git a/tests/test_pytest_cov.py b/tests/test_pytest_cov.py index 4402fc7b..415049de 100644 --- a/tests/test_pytest_cov.py +++ b/tests/test_pytest_cov.py @@ -150,7 +150,8 @@ def test_foo(cov): CHILD_SCRIPT_RESULT = '[56] * 100%' PARENT_SCRIPT_RESULT = '9 * 100%' DEST_DIR = 'cov_dest' -REPORT_NAME = 'cov.xml' +XML_REPORT_NAME = 'cov.xml' +LCOV_REPORT_NAME = 'cov.info' xdist_params = pytest.mark.parametrize('opts', [ '', @@ -333,18 +334,50 @@ def test_xml_output_dir(testdir): result = testdir.runpytest('-v', '--cov=%s' % script.dirpath(), - '--cov-report=xml:' + REPORT_NAME, + '--cov-report=xml:' + XML_REPORT_NAME, script) result.stdout.fnmatch_lines([ '*- coverage: platform *, python * -*', - 'Coverage XML written to file ' + REPORT_NAME, + 'Coverage XML written to file ' + XML_REPORT_NAME, '*10 passed*', ]) - assert testdir.tmpdir.join(REPORT_NAME).check() + assert testdir.tmpdir.join(XML_REPORT_NAME).check() assert result.ret == 0 +@pytest.mark.skipif("coverage.version_info < (6, 3)") +def test_lcov_output_dir(testdir): + script = testdir.makepyfile(SCRIPT) + + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=lcov:' + LCOV_REPORT_NAME, + script) + + result.stdout.fnmatch_lines([ + '*- coverage: platform *, python * -*', + 'Coverage LCOV written to file ' + LCOV_REPORT_NAME, + '*10 passed*', + ]) + assert testdir.tmpdir.join(LCOV_REPORT_NAME).check() + assert result.ret == 0 + + +@pytest.mark.skipif("coverage.version_info >= (6, 3)") +def test_lcov_not_supported(testdir): + script = testdir.makepyfile("a = 1") + result = testdir.runpytest('-v', + '--cov=%s' % script.dirpath(), + '--cov-report=lcov', + script, + ) + result.stderr.fnmatch_lines([ + '*argument --cov-report: LCOV output is only supported with coverage.py >= 6.3', + ]) + assert result.ret != 0 + + def test_term_output_dir(testdir): script = testdir.makepyfile(SCRIPT)