Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DO-NOT-MERGE: Patched-on LCOV file format support for coveragepy v4.x #863

Closed
wants to merge 1 commit into from
Closed
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
106 changes: 91 additions & 15 deletions coverage/annotate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import os
import re

from coverage import files

from coverage.files import flat_rootname
from coverage.misc import isolate_module
from coverage.report import Reporter
Expand Down Expand Up @@ -36,31 +38,87 @@ class AnnotateReporter(Reporter):

"""

def __init__(self, coverage, config):
def __init__(self, coverage, config, lcov_file=None):
super(AnnotateReporter, self).__init__(coverage, config)
self.directory = None

blank_re = re.compile(r"\s*(#|$)")
else_re = re.compile(r"\s*else\s*:\s*(#|$)")

def report(self, morfs, directory=None):
def report(self, morfs, directory=None, lcov_file=None):
"""Run the report.

See `coverage.report()` for arguments.

"""
self.report_files(self.annotate_file, morfs, directory)

def annotate_file(self, fr, analysis):
self.report_files(self.annotate_file, morfs, directory,
lcov_file=lcov_file)

def print_ba_lines(self, lcov_file, analysis):
# This function takes advantage of the fact that the functions
# in Analysis returns arcs in sorted order of their line numbers
# and mark the branch numbers in the same order of the
# destination line numbers of the arcs. For example:
#
# Line
# 10 if (something):
# 11 do_something
# 12 else:
# 13 do_something_else
#
# In the coverage analysis result, the tool returns arc list [
# ... (10,11), (10, 13) ...]. We will then regard (10,11) as
# the first branch at line 10 and (10, 13) as the second branch
# at line 10. This is important as in lcov file the branch
# coverage info must appear in order, e.g., suppose the test
# code executes the 'if' branch, the results in lcov format will
# be
#
# BA: 10, 2 (the first branch is taken)
# BA: 10, 1 (the second branch is executed but not taken)
#
# Note that in other languages the branch ordering might be
# treated differently.

all_arcs = analysis.arc_possibilities()
branch_lines = set(analysis.branch_lines())
missing_branch_arcs = analysis.missing_branch_arcs()
missing = analysis.missing

for source_line,target_line in all_arcs:
if source_line in branch_lines:
if source_line in missing:
# Not executed
lcov_file.write('BA:%d,0\n' % source_line)
else:
if (source_line in missing_branch_arcs) and (
target_line in missing_branch_arcs[source_line]):
# Executed and not taken
lcov_file.write('BA:%d,1\n' % source_line)
else:
# Executed and taken
lcov_file.write('BA:%d,2\n' % source_line)

(MISSED, COVERED, BLANK, EXCLUDED) = ('!', '>', ' ', '-')
def annotate_file(self, fr, analysis, lcov_file=None):
"""Annotate a single file.

`fr` is the FileReporter for the file to annotate.

"""
reverse_mapping = files.get_filename_from_cf(fr.filename)

filename = fr.filename
if reverse_mapping:
filename = reverse_mapping

statements = sorted(analysis.statements)
missing = sorted(analysis.missing)
excluded = sorted(analysis.excluded)

if lcov_file:
lcov_file.write("SF:%s\n" % filename)

if self.directory:
dest_file = os.path.join(self.directory, flat_rootname(fr.relative_filename()))
if dest_file.endswith("_py"):
Expand All @@ -69,7 +127,11 @@ def annotate_file(self, fr, analysis):
else:
dest_file = fr.filename + ",cover"

with io.open(dest_file, 'w', encoding='utf8') as dest:
if not lcov_file:
dest = io.open(dest_file, 'w', encoding='utf8')

if True: # GOOGLE: force indent for easy comparison; original:
# with io.open(dest_file, 'w', encoding='utf8') as dest:
i = 0
j = 0
covered = True
Expand All @@ -82,22 +144,36 @@ def annotate_file(self, fr, analysis):
if i < len(statements) and statements[i] == lineno:
covered = j >= len(missing) or missing[j] > lineno
if self.blank_re.match(line):
dest.write(u' ')
line_type = self.BLANK
elif self.else_re.match(line):
# Special logic for lines containing only 'else:'.
if i >= len(statements) and j >= len(missing):
dest.write(u'! ')
line_type = self.MISSED
elif i >= len(statements) or j >= len(missing):
dest.write(u'> ')
line_type = self.COVERED
elif statements[i] == missing[j]:
dest.write(u'! ')
line_type = self.MISSED
else:
dest.write(u'> ')
line_type = self.COVERED
elif lineno in excluded:
dest.write(u'- ')
line_type = self.EXCLUDED
elif covered:
dest.write(u'> ')
line_type = self.COVERED
else:
dest.write(u'! ')
line_type = self.MISSED

dest.write(line)
if not lcov_file:
dest.write("%s %s" % (line_type, line))
else:
# Omit BLANK & EXCLUDED line types from this lcov output type.
if line_type == self.COVERED:
lcov_file.write("DA:%d,1\n" % lineno)
elif line_type == self.MISSED:
lcov_file.write("DA:%d,0\n" % lineno)
# Write branch coverage results
if lcov_file and analysis.has_arcs():
self.print_ba_lines(lcov_file, analysis)
if lcov_file:
lcov_file.write("end_of_record\n")
else:
dest.close() # XXX try: finally: more "appropriate" than "if True"
14 changes: 14 additions & 0 deletions coverage/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ class Opts(object):
"Accepts shell-style wildcards, which must be quoted."
),
)
lcov = optparse.make_option(
'-l', '--lcov', action='store', dest="lcov_file",
metavar="OUTFILE",
help="report in lcov format"
)
pylib = optparse.make_option(
'-L', '--pylib', action='store_true',
help=(
Expand Down Expand Up @@ -271,6 +276,7 @@ def get_prog_name(self):
Opts.ignore_errors,
Opts.include,
Opts.omit,
Opts.lcov,
] + GLOBAL_ARGS,
usage="[options] [modules]",
description=(
Expand Down Expand Up @@ -363,6 +369,7 @@ def get_prog_name(self):
Opts.parallel_mode,
Opts.source,
Opts.timid,
Opts.lcov,
] + GLOBAL_ARGS,
usage="[options] <pyfile> [program options]",
description="Run a Python program, measuring code execution."
Expand Down Expand Up @@ -499,11 +506,18 @@ def command_line(self, argv):
return OK

# Remaining actions are reporting, with some common options.
if options.lcov_file:
lcovfile = open(options.lcov_file, "w")
else:
print("No Report generated: missing --lcov=<file> argument")
return ERR

report_args = dict(
morfs=unglob_args(args),
ignore_errors=options.ignore_errors,
omit=omit,
include=include,
lcov_file = lcovfile,
)

self.coverage.load()
Expand Down
39 changes: 31 additions & 8 deletions coverage/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from coverage.debug import DebugControl, write_formatted_info
from coverage.files import TreeMatcher, FnmatchMatcher
from coverage.files import PathAliases, find_python_files, prep_patterns
from coverage.files import canonical_filename, set_relative_directory
from coverage.files import canonical_filename, set_relative_directory, unicode_filename
from coverage.files import ModuleMatcher, abs_file
from coverage.html import HtmlReporter
from coverage.misc import CoverageException, bool_or_none, join_regex
Expand Down Expand Up @@ -59,6 +59,17 @@
except ImportError:
pass

# Note
# This log might be lost when called in atexit functions. In some
# tests sys.stderr is closed when the atexit handler is called. At that
# point we have to discard the message because otherwise it causes an
# immediate exit with error code 1.
def coverage_log(str):
"""Write to stderr the provided string, if stderr is still open."""
if sys.stderr.closed:
return
sys.stderr.write(str + '\n')


class Coverage(object):
"""Programmatic access to coverage.py.
Expand Down Expand Up @@ -623,7 +634,7 @@ def _warn(self, msg, slug=None):
msg = "%s (%s)" % (msg, slug)
if self.debug.should('pid'):
msg = "[%d] %s" % (os.getpid(), msg)
sys.stderr.write("Coverage.py warning: %s\n" % msg)
coverage_log("Coverage.py warning: %s" % msg)

def get_option(self, option_name):
"""Get an option from the configuration.
Expand Down Expand Up @@ -715,6 +726,18 @@ def _atexit(self):
self.stop()
if self._auto_save:
self.save()
lcov_filename = os.environ.get('PYTHON_LCOV_FILE')
if lcov_filename:
try:
with open(lcov_filename, 'w') as lcov_file:
try:
self.annotate(lcov_file=lcov_file,
ignore_errors=True)
except CoverageException as e:
coverage_log('Coverage error %s'% e)
except IOError as e:
coverage_log('Error (%s) creating lcov file %s' %
(e.strerror, lcov_filename))

def erase(self):
"""Erase previously-collected coverage data.
Expand Down Expand Up @@ -1016,7 +1039,7 @@ def _get_file_reporters(self, morfs=None):
def report(
self, morfs=None, show_missing=None, ignore_errors=None,
file=None, # pylint: disable=redefined-builtin
omit=None, include=None, skip_covered=None,
omit=None, include=None, skip_covered=None, lcov_file=None
):
"""Write a summary report to `file`.

Expand All @@ -1038,11 +1061,11 @@ def report(
show_missing=show_missing, skip_covered=skip_covered,
)
reporter = SummaryReporter(self, self.config)
return reporter.report(morfs, outfile=file)
return reporter.report(morfs, outfile=file, lcov_file=lcov_file)

def annotate(
self, morfs=None, directory=None, ignore_errors=None,
omit=None, include=None,
omit=None, include=None, lcov_file=None,
):
"""Annotate a list of modules.

Expand All @@ -1059,11 +1082,11 @@ def annotate(
ignore_errors=ignore_errors, report_omit=omit, report_include=include
)
reporter = AnnotateReporter(self, self.config)
reporter.report(morfs, directory=directory)
reporter.report(morfs, directory=directory, lcov_file=lcov_file)

def html_report(self, morfs=None, directory=None, ignore_errors=None,
omit=None, include=None, extra_css=None, title=None,
skip_covered=None):
skip_covered=None, lcov_file=None):
"""Generate an HTML report.

The HTML is written to `directory`. The file "index.html" is the
Expand Down Expand Up @@ -1092,7 +1115,7 @@ def html_report(self, morfs=None, directory=None, ignore_errors=None,

def xml_report(
self, morfs=None, outfile=None, ignore_errors=None,
omit=None, include=None,
omit=None, include=None, lcov_file=None,
):
"""Generate an XML report of coverage results.

Expand Down
8 changes: 8 additions & 0 deletions coverage/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ def relative_filename(filename):
return unicode_filename(filename)


def get_filename_from_cf(cf):
"""Return the reverse mapping of canonical_filename."""
for f in CANONICAL_FILENAME_CACHE.keys():
if CANONICAL_FILENAME_CACHE[f] == cf and not f == cf:
return f
return None


@contract(returns='unicode')
def canonical_filename(filename):
"""Return a canonical file name for `filename`.
Expand Down
4 changes: 2 additions & 2 deletions coverage/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def find_file_reporters(self, morfs):
self._file_reporters = sorted(reporters)
return self._file_reporters

def report_files(self, report_fn, morfs, directory=None):
def report_files(self, report_fn, morfs, directory=None, lcov_file=None):
"""Run a reporting function on a number of morfs.

`report_fn` is called for each relative morf in `morfs`. It is called
Expand All @@ -88,7 +88,7 @@ def report_files(self, report_fn, morfs, directory=None):

for fr in file_reporters:
try:
report_fn(fr, self.coverage._analyze(fr))
report_fn(fr, self.coverage._analyze(fr), lcov_file=lcov_file)
except NoSource:
if not self.config.ignore_errors:
raise
Expand Down
2 changes: 1 addition & 1 deletion coverage/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, coverage, config):
super(SummaryReporter, self).__init__(coverage, config)
self.branches = coverage.data.has_arcs()

def report(self, morfs, outfile=None):
def report(self, morfs, outfile=None, lcov_file=None):
"""Writes a report summarizing coverage statistics per module.

`outfile` is a file object to write the summary to. It must be opened
Expand Down