summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/cmdline.py26
-rw-r--r--coverage/config.py3
-rw-r--r--coverage/control.py20
-rw-r--r--coverage/lcovreport.py106
-rw-r--r--coverage/results.py15
-rw-r--r--doc/cmd.rst42
-rw-r--r--doc/dict.txt1
-rw-r--r--doc/index.rst3
-rw-r--r--doc/python-coverage.1.txt28
-rw-r--r--doc/source.rst3
-rw-r--r--tests/test_cmdline.py91
-rw-r--r--tests/test_lcov.py308
12 files changed, 624 insertions, 22 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index efed9040..9f5c9ea8 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -127,6 +127,11 @@ class Opts:
'', '--pretty-print', action='store_true',
help="Format the JSON for human readers.",
)
+ lcov = optparse.make_option(
+ '-o', '', action='store', dest='outfile',
+ metavar="OUTFILE",
+ help="Write the LCOV report to this file. Defaults to 'coverage.lcov'"
+ )
parallel_mode = optparse.make_option(
'-p', '--parallel-mode', action='store_true',
help=(
@@ -473,6 +478,20 @@ CMDS = {
usage="[options] [modules]",
description="Generate an XML report of coverage results."
),
+
+ 'lcov': CmdOptionParser(
+ "lcov",
+ [
+ Opts.fail_under,
+ Opts.ignore_errors,
+ Opts.include,
+ Opts.lcov,
+ Opts.omit,
+ Opts.quiet,
+ ] + GLOBAL_ARGS,
+ usage="[options] [modules]",
+ description="Generate an LCOV report of coverage results."
+ )
}
@@ -657,6 +676,12 @@ class CoverageScript:
show_contexts=options.show_contexts,
**report_args
)
+ elif options.action == "lcov":
+ total = self.coverage.lcov_report(
+ outfile=options.outfile,
+ **report_args
+ )
+
else:
# There are no other possible actions.
raise AssertionError
@@ -854,6 +879,7 @@ HELP_TOPICS = {
report Report coverage stats on modules.
run Run a Python program and measure code execution.
xml Create an XML report of coverage results.
+ lcov Create an LCOV report of coverage results.
Use "{program_name} help <command>" for detailed help on any command.
""",
diff --git a/coverage/config.py b/coverage/config.py
index 9835e341..75217def 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -227,6 +227,9 @@ class CoverageConfig:
self.json_pretty_print = False
self.json_show_contexts = False
+ # Default output filename for lcov_reporter
+ self.lcov_output = "coverage.lcov"
+
# Defaults for [paths]
self.paths = collections.OrderedDict()
diff --git a/coverage/control.py b/coverage/control.py
index 99319c05..bd51ffc5 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -26,6 +26,7 @@ from coverage.files import PathAliases, abs_file, relative_filename, set_relativ
from coverage.html import HtmlReporter
from coverage.inorout import InOrOut
from coverage.jsonreport import JsonReporter
+from coverage.lcovreport import LcovReporter
from coverage.misc import bool_or_none, join_regex, human_sorted, human_sorted_items
from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module
from coverage.plugin import FileReporter
@@ -1049,6 +1050,25 @@ class Coverage:
):
return render_report(self.config.json_output, JsonReporter(self), morfs, self._message)
+ def lcov_report(
+ self, morfs=None, outfile=None, ignore_errors=None,
+ omit=None, include=None, contexts=None,
+ ):
+ """Generate an LCOV report of coverage results.
+
+ Each module in 'morfs' is included in the report. 'outfile' is the
+ path to write the file to, "-" will write to stdout.
+
+ See :meth 'report' for other arguments.
+
+ .. versionadded:: 6.3
+ """
+ with override_config(self,
+ ignore_errors=ignore_errors, report_omit=omit, report_include=include,
+ lcov_output=outfile, report_contexts=contexts,
+ ):
+ return render_report(self.config.lcov_output, LcovReporter(self), morfs, self._message)
+
def sys_info(self):
"""Return a list of (key, value) pairs showing internal information."""
diff --git a/coverage/lcovreport.py b/coverage/lcovreport.py
new file mode 100644
index 00000000..5a49ac4a
--- /dev/null
+++ b/coverage/lcovreport.py
@@ -0,0 +1,106 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""LCOV reporting for coverage.py."""
+
+import sys
+import base64
+from hashlib import md5
+
+from coverage.report import get_analysis_to_report
+
+
+class LcovReporter:
+ """A reporter for writing LCOV coverage reports."""
+
+ report_type = "LCOV report"
+
+ def __init__(self, coverage):
+ self.coverage = coverage
+ self.config = self.coverage.config
+
+ def report(self, morfs, outfile=None):
+ """Renders the full lcov report
+
+ 'morfs' is a list of modules or filenames
+
+ outfile is the file object to write the file into.
+ """
+
+ self.coverage.get_data()
+ outfile = outfile or sys.stdout
+
+ for fr, analysis in get_analysis_to_report(self.coverage, morfs):
+ self.get_lcov(fr, analysis, outfile)
+
+ def get_lcov(self, fr, analysis, outfile=None):
+ """Produces the lcov data for a single file
+
+ get_lcov currently supports both line and branch coverage,
+ however function coverage is not supported.
+
+ """
+
+ outfile.write("TN:\n")
+ outfile.write(f"SF:{fr.relative_filename()}\n")
+ source_lines = fr.source().splitlines()
+ for covered in sorted(analysis.executed):
+ # Note: Coveragepy currently only supports checking *if* a line has
+ # been executed, not how many times, so we set this to 1 for nice
+ # output even if it's technically incorrect
+
+ # The lines below calculate a 64 bit encoded md5 hash of the line
+ # corresponding to the DA lines in the lcov file,
+ # for either case of the line being covered or missed in Coveragepy
+ # The final two characters of the encoding ("==") are removed from
+ # the hash to allow genhtml to run on the resulting lcov file
+ if source_lines:
+ line = source_lines[covered - 1].encode("utf-8")
+ else:
+ line = b""
+ hashed = str(base64.b64encode(md5(line).digest())[:-2], encoding="utf-8")
+ outfile.write(f"DA:{covered},1,{hashed}\n")
+ for missed in sorted(analysis.missing):
+ if source_lines:
+ line = source_lines[missed - 1].encode("utf-8")
+ else:
+ line = b""
+ hashed = str(base64.b64encode(md5(line).digest())[:-2], encoding="utf-8")
+ outfile.write(f"DA:{missed},0,{hashed}\n")
+ outfile.write(f"LF:{len(analysis.statements)}\n")
+ outfile.write(f"LH:{len(analysis.executed)}\n")
+
+ # More information dense branch coverage data
+ missing_arcs = analysis.missing_branch_arcs()
+ executed_arcs = analysis.executed_branch_arcs()
+ for block_number, block_line_number in enumerate(
+ sorted(analysis.branch_stats().keys())
+ ):
+ for branch_number, line_number in enumerate(
+ sorted(missing_arcs[block_line_number])
+ ):
+ # The exit branches have a negative line number,
+ # this will not produce valid lcov, and so setting
+ # the line number of the exit branch to 0 will allow
+ # for valid lcov, while preserving the data
+ line_number = max(line_number, 0)
+ outfile.write(f"BRDA:{line_number},{block_number},{branch_number},-\n")
+ # The start value below allows for the block number to be
+ # preserved between these two for loops (stopping the loop from
+ # resetting the value of the block number to 0)
+ for branch_number, line_number in enumerate(
+ sorted(executed_arcs[block_line_number]),
+ start=len(missing_arcs[block_line_number]),
+ ):
+ line_number = max(line_number, 0)
+ outfile.write(f"BRDA:{line_number},{block_number},{branch_number},1\n")
+
+ # Summary of the branch coverage
+ if analysis.has_arcs():
+ branch_stats = analysis.branch_stats()
+ brf = sum(t for t, k in branch_stats.values())
+ brh = brf - sum(t - k for t, k in branch_stats.values())
+ outfile.write(f"BRF:{brf}\n")
+ outfile.write(f"BRH:{brh}\n")
+
+ outfile.write("end_of_record\n")
diff --git a/coverage/results.py b/coverage/results.py
index 7bb4781c..9675bff9 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -136,6 +136,21 @@ class Analysis:
mba[l1].append(l2)
return mba
+ @contract(returns='dict(int: list(int))')
+ def executed_branch_arcs(self):
+ """Return arcs that were executed from branch lines.
+
+ Returns {l1:[l2a,l2b,...], ...}
+
+ """
+ executed = self.arcs_executed()
+ branch_lines = set(self._branch_lines())
+ eba = collections.defaultdict(list)
+ for l1, l2 in executed:
+ if l1 in branch_lines:
+ eba[l1].append(l2)
+ return eba
+
@contract(returns='dict(int: tuple(int, int))')
def branch_stats(self):
"""Get stats about branches.
diff --git a/doc/cmd.rst b/doc/cmd.rst
index a26aa8ac..f4cbff34 100644
--- a/doc/cmd.rst
+++ b/doc/cmd.rst
@@ -58,6 +58,8 @@ Coverage.py has a number of commands:
* **json** -- :ref:`Produce a JSON report with coverage results <cmd_json>`.
+* **lcov** -- :ref:`Produce an LCOV report with coverage results <cmd_lcov>`.
+
* **annotate** --
:ref:`Annotate source files with coverage results <cmd_annotate>`.
@@ -430,8 +432,8 @@ Reporting
---------
Coverage.py provides a few styles of reporting, with the **report**, **html**,
-**annotate**, **json**, and **xml** commands. They share a number of common
-options.
+**annotate**, **json**, **lcov**, and **xml** commands. They share a number
+of common options.
The command-line arguments are module or file names to report on, if you'd like
to report on a subset of the data collected.
@@ -785,6 +787,42 @@ The **json** command writes coverage data to a "coverage.json" file.
You can specify the name of the output file with the ``-o`` switch. The JSON
can be nicely formatted by specifying the ``--pretty-print`` switch.
+.. _cmd_lcov:
+
+LCOV reporting: ``coverage lcov``
+---------------------------------
+
+The **json** command writes coverage data to a "coverage.lcov" file.
+
+.. [[[cog show_help("lcov") ]]]
+.. code::
+
+ $ coverage lcov --help
+ Usage: coverage lcov [options] [modules]
+
+ Generate an LCOV report of coverage results.
+
+ Options:
+ --fail-under=MIN Exit with a status of 2 if the total coverage is less
+ than MIN.
+ -i, --ignore-errors Ignore errors while reading source files.
+ --include=PAT1,PAT2,...
+ Include only files whose paths match one of these
+ patterns. Accepts shell-style wildcards, which must be
+ quoted.
+ -o OUTFILE Write the LCOV report to this file. Defaults to
+ 'coverage.lcov'
+ --omit=PAT1,PAT2,... Omit files whose paths match one of these patterns.
+ Accepts shell-style wildcards, which must be quoted.
+ -q, --quiet Don't print messages about what is happening.
+ --debug=OPTS Debug options, separated by commas. [env:
+ COVERAGE_DEBUG]
+ -h, --help Get help on this command.
+ --rcfile=RCFILE Specify configuration file. By default '.coveragerc',
+ 'setup.cfg', 'tox.ini', and 'pyproject.toml' are
+ tried. [env: COVERAGE_RCFILE]
+.. [[[end]]] (checksum: 4d078e4637e5b507cbb997803a0d4758)
+
Other common reporting options are described above in :ref:`cmd_reporting`.
diff --git a/doc/dict.txt b/doc/dict.txt
index 70c4a890..b9411bb9 100644
--- a/doc/dict.txt
+++ b/doc/dict.txt
@@ -107,6 +107,7 @@ jquery
json
jython
kwargs
+lcov
Mako
matcher
matchers
diff --git a/doc/index.rst b/doc/index.rst
index 9661797b..74193cbb 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -152,7 +152,8 @@ Coverage.py can do a number of things:
- It can tell you :ref:`what tests ran which lines <dynamic_contexts>`.
- It can produce reports in a number of formats: :ref:`text <cmd_report>`,
- :ref:`HTML <cmd_html>`, :ref:`XML <cmd_xml>`, and :ref:`JSON <cmd_json>`.
+ :ref:`HTML <cmd_html>`, :ref:`XML <cmd_xml>`, :ref:`LCOV <cmd_lcov>`,
+ and :ref:`JSON <cmd_json>`.
- For advanced uses, there's an :ref:`API <api>`, and the result data is
available in a :ref:`SQLite database <dbschema>`.
diff --git a/doc/python-coverage.1.txt b/doc/python-coverage.1.txt
index bffc7072..41a3bbea 100644
--- a/doc/python-coverage.1.txt
+++ b/doc/python-coverage.1.txt
@@ -67,6 +67,9 @@ COMMAND OVERVIEW
|command| **xml**
Create an XML report of coverage results.
+|command| **lcov**
+ Create an LCOV report of coverage results.
+
GLOBAL OPTIONS
==============
@@ -229,6 +232,31 @@ COMMAND REFERENCE
\--show-contexts
Include information about the contexts that executed each line.
+**lcov** [ `option` ... ] [ `MODULE` ... ]
+
+ Create an LCOV report of the coverage results.
+
+ Options:
+
+ \--fail-under `MIN`
+ Exit with a status of 2 if the total coverage is less than `MIN`.
+
+ \-i, --ignore-errors
+ Ignore errors while reading source files.
+
+ \-o `OUTFILE`
+ Write the LCOV report to `OUTFILE`. Defaults to ``coverage.lcov``.
+
+ \--include `PATTERN` [ , ... ]
+ Include only files whose paths match one of these
+ PATTERNs. Accepts shell-style wildcards, which must be quoted.
+
+ \--omit `PATTERN` [ , ... ]
+ Omit files when their file name matches one of these PATTERNs.
+ Usually needs quoting on the command line.
+
+ \-q, --quiet
+ Don't print messages about what is happening.
**report** [ `option` ... ] [ `MODULE` ... ]
diff --git a/doc/source.rst b/doc/source.rst
index 85a46a33..34aa611f 100644
--- a/doc/source.rst
+++ b/doc/source.rst
@@ -88,7 +88,8 @@ reported. Usually you want to see all the code that was measured, but if you
are measuring a large project, you may want to get reports for just certain
parts.
-The report commands (``report``, ``html``, ``json``, ``annotate``, and ``xml``)
+The report commands (``report``, ``html``, ``json``, ``lcov``, ``annotate``,
+and ``xml``)
all take optional ``modules`` arguments, and ``--include`` and ``--omit``
switches. The ``modules`` arguments specify particular modules to report on.
The ``include`` and ``omit`` values are lists of file name patterns, just as
diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py
index 42f313f8..ad46ded3 100644
--- a/tests/test_cmdline.py
+++ b/tests/test_cmdline.py
@@ -52,6 +52,10 @@ class BaseCmdLineTest(CoverageTest):
ignore_errors=None, include=None, omit=None, morfs=[], outfile=None,
contexts=None, pretty_print=None, show_contexts=None,
)
+ _defaults.Coverage().lcov_report(
+ ignore_errors=None, include=None, omit=None, morfs=[], outfile=None,
+ contexts=None,
+ )
_defaults.Coverage(
cover_pylib=None, data_suffix=None, timid=None, branch=None,
config_file=True, source=None, include=None, omit=None, debug=None,
@@ -76,6 +80,7 @@ class BaseCmdLineTest(CoverageTest):
cov.html_report.return_value = 50.0
cov.xml_report.return_value = 50.0
cov.json_report.return_value = 50.0
+ cov.lcov_report.return_value = 50.0
return mk
@@ -438,6 +443,49 @@ class CmdLineTest(BaseCmdLineTest):
cov.json_report()
""")
+ def test_lcov(self):
+ # coverage lcov [-i] [--omit DIR,...] [FILE1 FILE2 ...]
+ self.cmd_executes("lcov", """\
+ cov = Coverage()
+ cov.load()
+ cov.lcov_report()
+ """)
+ self.cmd_executes("lcov -i", """\
+ cov = Coverage()
+ cov.load()
+ cov.lcov_report(ignore_errors=True)
+ """)
+ self.cmd_executes("lcov -o mylcov.foo", """\
+ cov = Coverage()
+ cov.load()
+ cov.lcov_report(outfile="mylcov.foo")
+ """)
+ self.cmd_executes("lcov -o -", """\
+ cov = Coverage()
+ cov.load()
+ cov.lcov_report(outfile="-")
+ """)
+ self.cmd_executes("lcov --omit fooey", """\
+ cov = Coverage(omit=["fooey"])
+ cov.load()
+ cov.lcov_report(omit=["fooey"])
+ """)
+ self.cmd_executes("lcov --omit fooey,booey", """\
+ cov = Coverage(omit=["fooey", "booey"])
+ cov.load()
+ cov.lcov_report(omit=["fooey", "booey"])
+ """)
+ self.cmd_executes("lcov -q", """\
+ cov = Coverage(messages=False)
+ cov.load()
+ cov.lcov_report()
+ """)
+ self.cmd_executes("lcov --quiet", """\
+ cov = Coverage(messages=False)
+ cov.load()
+ cov.lcov_report()
+ """)
+
def test_report(self):
# coverage report [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...]
self.cmd_executes("report", """\
@@ -1006,12 +1054,13 @@ class CmdMainTest(CoverageTest):
class CoverageReportingFake:
"""A fake Coverage.coverage test double."""
# pylint: disable=missing-function-docstring
- def __init__(self, report_result, html_result, xml_result, json_report):
+ def __init__(self, report_result, html_result, xml_result, json_report, lcov_result):
self.config = CoverageConfig()
self.report_result = report_result
self.html_result = html_result
self.xml_result = xml_result
self.json_result = json_report
+ self.lcov_result = lcov_result
def set_option(self, optname, optvalue):
self.config.set_option(optname, optvalue)
@@ -1034,28 +1083,34 @@ class CoverageReportingFake:
def json_report(self, *args_unused, **kwargs_unused):
return self.json_result
+ def lcov_report(self, *args_unused, **kwargs_unused):
+ return self.lcov_result
@pytest.mark.parametrize("results, fail_under, cmd, ret", [
# Command-line switch properly checks the result of reporting functions.
- ((20, 30, 40, 50), None, "report --fail-under=19", 0),
- ((20, 30, 40, 50), None, "report --fail-under=21", 2),
- ((20, 30, 40, 50), None, "html --fail-under=29", 0),
- ((20, 30, 40, 50), None, "html --fail-under=31", 2),
- ((20, 30, 40, 50), None, "xml --fail-under=39", 0),
- ((20, 30, 40, 50), None, "xml --fail-under=41", 2),
- ((20, 30, 40, 50), None, "json --fail-under=49", 0),
- ((20, 30, 40, 50), None, "json --fail-under=51", 2),
+ ((20, 30, 40, 50, 60), None, "report --fail-under=19", 0),
+ ((20, 30, 40, 50, 60), None, "report --fail-under=21", 2),
+ ((20, 30, 40, 50, 60), None, "html --fail-under=29", 0),
+ ((20, 30, 40, 50, 60), None, "html --fail-under=31", 2),
+ ((20, 30, 40, 50, 60), None, "xml --fail-under=39", 0),
+ ((20, 30, 40, 50, 60), None, "xml --fail-under=41", 2),
+ ((20, 30, 40, 50, 60), None, "json --fail-under=49", 0),
+ ((20, 30, 40, 50, 60), None, "json --fail-under=51", 2),
+ ((20, 30, 40, 50, 60), None, "lcov --fail-under=59", 0),
+ ((20, 30, 40, 50, 60), None, "lcov --fail-under=61", 2),
# Configuration file setting properly checks the result of reporting.
- ((20, 30, 40, 50), 19, "report", 0),
- ((20, 30, 40, 50), 21, "report", 2),
- ((20, 30, 40, 50), 29, "html", 0),
- ((20, 30, 40, 50), 31, "html", 2),
- ((20, 30, 40, 50), 39, "xml", 0),
- ((20, 30, 40, 50), 41, "xml", 2),
- ((20, 30, 40, 50), 49, "json", 0),
- ((20, 30, 40, 50), 51, "json", 2),
+ ((20, 30, 40, 50, 60), 19, "report", 0),
+ ((20, 30, 40, 50, 60), 21, "report", 2),
+ ((20, 30, 40, 50, 60), 29, "html", 0),
+ ((20, 30, 40, 50, 60), 31, "html", 2),
+ ((20, 30, 40, 50, 60), 39, "xml", 0),
+ ((20, 30, 40, 50, 60), 41, "xml", 2),
+ ((20, 30, 40, 50, 60), 49, "json", 0),
+ ((20, 30, 40, 50, 60), 51, "json", 2),
+ ((20, 30, 40, 50, 60), 59, "lcov", 0),
+ ((20, 30, 40, 50, 60), 61, "lcov", 2),
# Command-line overrides configuration.
- ((20, 30, 40, 50), 19, "report --fail-under=21", 2),
+ ((20, 30, 40, 50, 60), 19, "report --fail-under=21", 2),
])
def test_fail_under(results, fail_under, cmd, ret):
cov = CoverageReportingFake(*results)
diff --git a/tests/test_lcov.py b/tests/test_lcov.py
new file mode 100644
index 00000000..9d2f8ec6
--- /dev/null
+++ b/tests/test_lcov.py
@@ -0,0 +1,308 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""Test LCOV-based summary reporting for coverage.py."""
+
+import textwrap
+import coverage
+
+from tests.coveragetest import CoverageTest
+
+
+class LcovTest(CoverageTest):
+ """Tests of the LCOV reports from coverage.py."""
+
+ def create_initial_files(self):
+ """
+ Helper for tests that handles the common ceremony so the tests can
+ show the consequences of changes in the setup.
+ """
+ self.make_file(
+ "main_file.py",
+ """\
+ #!/usr/bin/env python3
+
+ def cuboid_volume(l):
+ return (l*l*l)
+
+ def IsItTrue():
+ return True
+
+ """,
+ )
+
+ self.make_file(
+ "test_file.py",
+ """\
+ #!/usr/bin/env python3
+
+ from main_file import cuboid_volume
+ import unittest
+
+ class TestCuboid(unittest.TestCase):
+ def test_volume(self):
+ self.assertAlmostEqual(cuboid_volume(2),8)
+ self.assertAlmostEqual(cuboid_volume(1),1)
+ self.assertAlmostEqual(cuboid_volume(0),0)
+ self.assertAlmostEqual(cuboid_volume(5.5),166.375)
+
+ """,
+ )
+
+ def get_lcov_report_content(self):
+ """Return the content of the LCOV report."""
+ filename = "coverage.lcov"
+ with open(filename, "r") as file:
+ file_contents = file.read()
+ return file_contents
+
+ def test_lone_file(self):
+ """For a single file with a couple of functions, the lcov should cover
+ the function definitions themselves, but not the returns."""
+ self.make_file(
+ "main_file.py",
+ """\
+ #!/usr/bin/env python3
+
+ def cuboid_volume(l):
+ return (l*l*l)
+
+ def IsItTrue():
+ return True
+
+ """,
+ )
+ expected_result = """\
+ TN:
+ SF:main_file.py
+ DA:3,1,7URou3io0zReBkk69lEb/Q
+ DA:6,1,ilhb4KUfytxtEuClijZPlQ
+ DA:4,0,Xqj6H1iz/nsARMCAbE90ng
+ DA:7,0,LWILTcvARcydjFFyo9qM0A
+ LF:4
+ LH:2
+ end_of_record
+ """
+ expected_result = textwrap.dedent(expected_result)
+ self.assert_doesnt_exist(".coverage")
+ cov = coverage.Coverage(source=["."])
+ self.start_import_stop(cov, "main_file")
+ cov.lcov_report()
+ actual_result = self.get_lcov_report_content()
+ assert expected_result == actual_result
+
+ def test_simple_line_coverage_two_files(self):
+ """Test that line coverage is created when coverage is run,
+ and matches the output of the file below."""
+ self.create_initial_files()
+ self.assert_doesnt_exist(".coverage")
+ cov = coverage.Coverage(source=".")
+ self.start_import_stop(cov, "test_file")
+ cov.lcov_report()
+ self.assert_exists("coverage.lcov")
+ expected_result = """\
+ TN:
+ SF:main_file.py
+ DA:3,1,7URou3io0zReBkk69lEb/Q
+ DA:6,1,ilhb4KUfytxtEuClijZPlQ
+ DA:4,0,Xqj6H1iz/nsARMCAbE90ng
+ DA:7,0,LWILTcvARcydjFFyo9qM0A
+ LF:4
+ LH:2
+ end_of_record
+ TN:
+ SF:test_file.py
+ DA:3,1,R5Rb4IzmjKRgY/vFFc1TRg
+ DA:4,1,E/tvV9JPVDhEcTCkgrwOFw
+ DA:6,1,GP08LPBYJq8EzYveHJy2qA
+ DA:7,1,MV+jSLi6PFEl+WatEAptog
+ DA:8,0,qyqd1mF289dg6oQAQHA+gQ
+ DA:9,0,nmEYd5F1KrxemgC9iVjlqg
+ DA:10,0,jodMK26WYDizOO1C7ekBbg
+ DA:11,0,LtxfKehkX8o4KvC5GnN52g
+ LF:8
+ LH:4
+ end_of_record
+ """
+ expected_result = textwrap.dedent(expected_result)
+ actual_result = self.get_lcov_report_content()
+ assert expected_result == actual_result
+
+ def test_branch_coverage_one_file(self):
+ """Test that the reporter produces valid branch coverage."""
+ self.make_file(
+ "main_file.py",
+ """\
+ #!/usr/bin/env python3
+
+ def is_it_x(x):
+ if x == 3:
+ return x
+ else:
+ return False
+
+ """,
+ )
+ self.assert_doesnt_exist(".coverage")
+ cov = coverage.Coverage(branch=True, source=".")
+ self.start_import_stop(cov, "main_file")
+ cov.lcov_report()
+ self.assert_exists("coverage.lcov")
+ expected_result = """\
+ TN:
+ SF:main_file.py
+ DA:3,1,4MDXMbvwQ3L7va1tsphVzw
+ DA:4,0,MuERA6EYyZNpKPqoJfzwkA
+ DA:5,0,sAyiiE6iAuPMte9kyd0+3g
+ DA:7,0,W/g8GJDAYJkSSurt59Mzfw
+ LF:4
+ LH:1
+ BRDA:5,0,0,-
+ BRDA:7,0,1,-
+ BRF:2
+ BRH:0
+ end_of_record
+ """
+ expected_result = textwrap.dedent(expected_result)
+ actual_result = self.get_lcov_report_content()
+ assert expected_result == actual_result
+
+ def test_branch_coverage_two_files(self):
+ """Test that valid branch coverage is generated
+ in the case of two files."""
+ self.make_file(
+ "main_file.py",
+ """\
+ #!/usr/bin/env python3
+
+ def is_it_x(x):
+ if x == 3:
+ return x
+ else:
+ return False
+
+ """,
+ )
+
+ self.make_file(
+ "test_file.py",
+ """\
+ #!/usr/bin/env python3
+
+ from main_file import *
+ import unittest
+
+ class TestIsItX(unittest.TestCase):
+ def test_is_it_x(self):
+ self.assertEqual(is_it_x(3), 3)
+ self.assertEqual(is_it_x(4), False)
+
+ """,
+ )
+ self.assert_doesnt_exist(".coverage")
+ cov = coverage.Coverage(branch=True, source=".")
+ self.start_import_stop(cov, "test_file")
+ cov.lcov_report()
+ self.assert_exists("coverage.lcov")
+ expected_result = """\
+ TN:
+ SF:main_file.py
+ DA:3,1,4MDXMbvwQ3L7va1tsphVzw
+ DA:4,0,MuERA6EYyZNpKPqoJfzwkA
+ DA:5,0,sAyiiE6iAuPMte9kyd0+3g
+ DA:7,0,W/g8GJDAYJkSSurt59Mzfw
+ LF:4
+ LH:1
+ BRDA:5,0,0,-
+ BRDA:7,0,1,-
+ BRF:2
+ BRH:0
+ end_of_record
+ TN:
+ SF:test_file.py
+ DA:3,1,9TxKIyoBtmhopmlbDNa8FQ
+ DA:4,1,E/tvV9JPVDhEcTCkgrwOFw
+ DA:6,1,C3s/c8C1Yd/zoNG1GnGexg
+ DA:7,1,9qPyWexYysgeKtB+YvuzAg
+ DA:8,0,LycuNcdqoUhPXeuXUTf5lA
+ DA:9,0,FPTWzd68bDx76HN7VHu1wA
+ LF:6
+ LH:4
+ BRDA:0,0,0,1
+ BRDA:7,0,1,1
+ BRF:2
+ BRH:2
+ end_of_record
+ """
+ expected_result = textwrap.dedent(expected_result)
+ actual_result = self.get_lcov_report_content()
+ assert actual_result == expected_result
+
+ def test_half_covered_branch(self):
+ """Test that for a given branch that is only half covered,
+ the block numbers remain the same, and produces valid lcov.
+ """
+ self.make_file(
+ "main_file.py",
+ """\
+ something = True
+
+ if something:
+ print("Yes, something")
+ else:
+ print("No, nothing")
+
+ """,
+ )
+ self.assert_doesnt_exist(".coverage")
+ cov = coverage.Coverage(branch=True, source=".")
+ self.start_import_stop(cov, "main_file")
+ cov.lcov_report()
+ self.assert_exists("coverage.lcov")
+ expected_result = """\
+ TN:
+ SF:main_file.py
+ DA:1,1,N4kbVOlkNI1rqOfCArBClw
+ DA:3,1,CmlqqPf0/H+R/p7/PLEXZw
+ DA:4,1,rE3mWnpoMq2W2sMETVk/uQ
+ DA:6,0,+Aov7ekIts7C96udNDVIIQ
+ LF:4
+ LH:3
+ BRDA:6,0,0,-
+ BRDA:4,0,1,1
+ BRF:2
+ BRH:1
+ end_of_record
+ """
+ expected_result = textwrap.dedent(expected_result)
+ actual_result = self.get_lcov_report_content()
+ assert actual_result == expected_result
+
+ def test_empty_init_files(self):
+ """Test that in the case of an empty __init__.py file, the lcov
+ reporter will note that the file is there, and will note the empty
+ line. It will also note the lack of branches, and the checksum for
+ the line.
+
+ Although there are no lines found, it will note one line as hit.
+ """
+
+ self.make_file("__init__.py", "")
+ self.assert_doesnt_exist(".coverage")
+ cov = coverage.Coverage(branch=True, source=".")
+ self.start_import_stop(cov, "__init__")
+ cov.lcov_report()
+ self.assert_exists("coverage.lcov")
+ expected_result = """\
+ TN:
+ SF:__init__.py
+ DA:1,1,1B2M2Y8AsgTpgAmY7PhCfg
+ LF:0
+ LH:1
+ BRF:0
+ BRH:0
+ end_of_record
+ """
+ expected_result = textwrap.dedent(expected_result)
+ actual_result = self.get_lcov_report_content()
+ assert actual_result == expected_result