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

addi support to free functions #1081

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
304 changes: 224 additions & 80 deletions coverage/xmlreport.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
import sys
import time
import xml.dom.minidom

import numpy as np

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this will add a dependency on numpy to coverage. Is that really necessary for improved reporting?

And it seems like the changes break the test environment.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my fault. Excuse-me. I will remove it.

Copy link
Author

@jdiego jdiego Dec 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nedbat I noticed that py coverage doesn't assign the methods to their classes. So, I made some modifications to generate the following output:

			<classes>
				<class name="Department" filename="tested_module/company_model.py" complexity="0" class_lines="23" class_hits="18" line-rate="0.7826">
					<method name="__init__" method_lines="4" method_hits="4" method_misses="0">
						<line number="4" hits="1"/>
						<line number="5" hits="1"/>
						<line number="6" hits="1"/>
						<line number="7" hits="1"/>
					</method>
					<method name="__str__" method_lines="1" method_hits="0" method_misses="1">
						<line number="10" hits="0"/>
					</method>
					<method name="add_member" method_lines="2" method_hits="2" method_misses="0">
						<line number="13" hits="1"/>
						<line number="14" hits="1"/>
					</method>
					<method name="remove_member" method_lines="1" method_hits="0" method_misses="1">
						<line number="17" hits="0"/>
					</method>
					<method name="count" method_lines="1" method_hits="1" method_misses="0">
						<line number="21" hits="1"/>
					</method>
					<method name="__len__" method_lines="1" method_hits="0" method_misses="1">
						<line number="24" hits="0"/>
					</method>
				</class>
				<class name="CompanyModel" filename="tested_module/company_model.py" complexity="0" class_lines="22" class_hits="17" line-rate="0.7727">
					<method name="__init__" method_lines="3" method_hits="3" method_misses="0">
						<line number="29" hits="1"/>
						<line number="30" hits="1"/>
						<line number="31" hits="1"/>
					</method>
					<method name="__str__" method_lines="1" method_hits="0" method_misses="1">
						<line number="34" hits="0"/>
					</method>
					<method name="add_user" method_lines="6" method_hits="5" method_misses="1">
						<line number="37" hits="1"/>
						<line number="38" hits="1"/>
						<line number="39" hits="1"/>
						<line number="40" hits="1"/>
						<line number="41" hits="1"/>
						<line number="42" hits="0"/>
					</method>
					<method name="count_persons_per_department" method_lines="1" method_hits="1" method_misses="0">
						<line number="45" hits="1"/>
					</method>
					<method name="get_headcount_for" method_lines="1" method_hits="1" method_misses="0">
						<line number="48" hits="1"/>
					</method>
				</class>
			</classes>
		</package>
	</packages>

Copy link
Author

@jdiego jdiego Dec 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nedbat You are right. I didn't commit the modification on the test suite. I will make it again, because I had some issues with my computer. I thought that this merge commit was complete, but I was wrong about that. Again, sorry about this noob action.

from coverage import env
from coverage import __url__, __version__, files
from coverage.backward import iitems
from coverage.misc import isolate_module
from coverage.report import get_analysis_to_report
from coverage.backward import SimpleNamespace

os = isolate_module(os)

Expand All @@ -30,13 +31,23 @@ def rate(hit, num):
return "%.4g" % (float(hit) / num)


class XmlReporter(object):
"""A reporter for writing Cobertura-style XML coverage results."""
def convert_to_dict(tup):
di = {}
for a, b in tup:
di.setdefault(a, []).append(b)
return di

def __init__(self, coverage):
class XmlReporter(object):
"""
A reporter for writing Cobertura-style XML coverage results.
"""
EMPTY = "(empty)"
def __init__(self, coverage, report_name=None):
self.coverage = coverage
self.config = self.coverage.config

#
self.report_name = report_name
#
self.source_paths = set()
if self.config.source:
for src in self.config.source:
Expand All @@ -46,13 +57,15 @@ def __init__(self, coverage):
self.source_paths.add(src)
self.packages = {}
self.xml_out = None
self.is_class_level = False

def report(self, morfs, outfile=None):
"""Generate a Cobertura-compatible XML report for `morfs`.
def report(self, morfs=None, outfile=None):
"""
Generate a Cobertura-compatible XML report for `morfs`.

`morfs` is a list of modules or file names.
`morfs` is a list of modules or file names.

`outfile` is a file object to write the XML to.
`outfile` is a file object to write the XML to.

"""
# Initial setup.
Expand All @@ -67,9 +80,7 @@ def report(self, morfs, outfile=None):
xcoverage = self.xml_out.documentElement
xcoverage.setAttribute("version", __version__)
xcoverage.setAttribute("timestamp", str(int(time.time()*1000)))
xcoverage.appendChild(self.xml_out.createComment(
" Generated by coverage.py: %s " % __url__
))
xcoverage.appendChild(self.xml_out.createComment(" Generated by coverage.py: %s " % __url__))
xcoverage.appendChild(self.xml_out.createComment(" Based on %s " % DTD_URL))

# Call xml_file for each file in the data.
Expand All @@ -94,19 +105,22 @@ def report(self, morfs, outfile=None):

# Populate the XML DOM with the package info.
for pkg_name, pkg_data in sorted(iitems(self.packages)):
class_elts, lhits, lnum, bhits, bnum = pkg_data
modules_elts, lhits, lnum, bhits, bnum = pkg_data
xpackage = self.xml_out.createElement("package")
xpackages.appendChild(xpackage)
xclasses = self.xml_out.createElement("classes")
xpackage.appendChild(xclasses)
for _, class_elt in sorted(iitems(class_elts)):
xclasses.appendChild(class_elt)
#
for _, (class_elts, fn_elts) in sorted(iitems(modules_elts)):
for class_elt in class_elts:
xclasses.appendChild(class_elt)
#
for fn in fn_elts:
xpackage.appendChild(fn)

xpackage.setAttribute("name", pkg_name.replace(os.sep, '.'))
xpackage.setAttribute("line-rate", rate(lhits, lnum))
if has_arcs:
branch_rate = rate(bhits, bnum)
else:
branch_rate = "0"
branch_rate = rate(bhits, bnum) if has_arcs else "0"
xpackage.setAttribute("branch-rate", branch_rate)
xpackage.setAttribute("complexity", "0")

Expand All @@ -128,26 +142,48 @@ def report(self, morfs, outfile=None):
xcoverage.setAttribute("branch-rate", "0")
xcoverage.setAttribute("complexity", "0")

#
if self.report_name:
xcoverage.setAttribute("name", self.report_name)
# Write the output file.
outfile.write(serialize_xml(self.xml_out))

# Return the total percentage.
denom = lnum_tot + bnum_tot
if denom == 0:
pct = 0.0
else:
pct = 100.0 * (lhits_tot + bhits_tot) / denom
pct = 0.0 if denom == 0 else 100.0 * (lhits_tot + bhits_tot) / denom
return pct

def xml_file(self, fr, analysis, has_arcs):
"""Add to the XML report for a single file."""

if self.config.skip_empty:
if analysis.numbers.n_statements == 0:
return
def is_property_tag(self, tokens_list):
tokens = convert_to_dict(tokens_list)
key = tokens.get('op', [''])[0]
nam = tokens.get('nam', [''])[0]
#
is_tag = key == '@'
if is_tag:
if nam in ['staticmethod', 'classmethod']:
self.is_class_level = True
#
return is_tag

# Create the 'lines' and 'package' XML elements, which
# are populated later. Note that a package == a directory.

def is_member_fn(self, tokens):
for token, value in tokens:
if token == 'nam' and (value in ['self', 'cls']):
return True
return False


def process_tokens(self, tokens_list, tag):
tokens = convert_to_dict(tokens_list)
key = tokens.get('key', [''])[0]
name = tokens.get('nam', [''])[0]
if key == tag:
return True, name
return False, None


def extract_names(self, fr):
filename = fr.filename.replace("\\", "/")
for source_path in self.source_paths:
source_path = files.canonical_filename(source_path)
Expand All @@ -160,70 +196,178 @@ def xml_file(self, fr, analysis, has_arcs):

dirname = os.path.dirname(rel_name) or u"."
dirname = "/".join(dirname.split("/")[:self.config.xml_package_depth])
package_name = dirname.replace("/", ".")
return dirname, rel_name

package = self.packages.setdefault(package_name, [{}, 0, 0, 0, 0])

def create_class(self, name, rel_name, lineno):
xclass = self.xml_out.createElement("class")
xclass.setAttribute("name", name)
xclass.setAttribute("filename", rel_name.replace("\\", "/"))
xclass.setAttribute("complexity", "0")
xclass.first_line = lineno
return xclass

def set_class_stats(self, xclass, end_line, analysis):
first_line = xclass.first_line
class_lines = end_line - first_line
filtered = [smt for smt in analysis.statements if smt >=first_line and smt <=end_line]
class_hits = len(filtered)
xclass.setAttribute("class_lines", str(class_lines))
xclass.setAttribute("class_hits", str(class_hits))


# if has_arcs:
# class_branches = sum(t for t, k in branch_stats.values())
# missing_branches = sum(t - k for t, k in branch_stats.values())
# class_br_hits = class_branches - missing_branches
# else:
# class_branches = 0.0
# class_br_hits = 0.0

xclass.appendChild(self.xml_out.createElement("methods"))
# Finalize the statistics that are collected in the XML DOM.
xclass.setAttribute("line-rate", rate(class_hits, class_lines))

xlines = self.xml_out.createElement("lines")
xclass.appendChild(xlines)
def set_method_stats(self, xmethod):
method_hits = 0
method_misses = 0
for child in xmethod.childNodes:
if child.getAttribute('hits') == "1":
method_hits += 1
else:
method_misses += 1
#
method_lines = len(xmethod.childNodes)
xmethod.setAttribute("method_lines", str(method_lines))
xmethod.setAttribute("method_hits", str(method_hits))
xmethod.setAttribute("method_misses", str(method_misses))

def process_class(self, rel_name, lineno, tokens, xclass, xmethod, analysis):
found, name = self.process_tokens(tokens, "class")
if found:
#
if xclass:
if xmethod:
xclass.appendChild(xmethod)
#
last_line = lineno
for smt in analysis.statements:
if smt < lineno:
last_line = smt
#
self.set_class_stats(xclass, last_line, analysis)
#
xclass = self.create_class(name, rel_name, lineno)
return True, xclass
#
return False, xclass

def process_method(self, xmethod, tokens, xclass, free_fn):
found, method_name = self.process_tokens(tokens, "def")
if found:
#
if xmethod:
self.set_method_stats(xmethod)
#
xmethod = self.xml_out.createElement("method")
xmethod.setAttribute("name", method_name)

if xclass and (self.is_member_fn(tokens) or self.is_class_level):
xclass.appendChild(xmethod)
self.is_class_level = False
else:
free_fn.append(xmethod)
return True, xmethod
return False, xmethod

xclass.setAttribute("name", os.path.relpath(rel_name, dirname))
xclass.setAttribute("filename", rel_name.replace("\\", "/"))
xclass.setAttribute("complexity", "0")
def mount_package(self, dirname):
package_name = dirname.replace("/", ".")
package = self.packages.setdefault(package_name, [{}, 0, 0, 0, 0])
return package

def process_line(self, line, has_arcs, branch_stats, missing_branch_arcs, analysis):
# Processing Line
xline = self.xml_out.createElement("line")
xline.setAttribute("number", str(line))
# Q: can we get info about the number of times a statement is
# executed? If so, that should be recorded here.
xline.setAttribute("hits", str(int(line not in analysis.missing)))
if has_arcs:
if line in branch_stats:
total, taken = branch_stats[line]
xline.setAttribute("branch", "true")
xline.setAttribute("condition-coverage", "%d%% (%d/%d)" % (100*taken//total, taken, total))
if line in missing_branch_arcs:
annlines = ["exit" if b < 0 else str(b) for b in missing_branch_arcs[line]]
xline.setAttribute("missing-branches", ",".join(annlines))
return xline


def xml_file(self, fr, analysis, has_arcs):
"""Add to the XML report for a single file."""
if self.config.skip_empty and analysis.numbers.n_statements == 0:
return
#
dirname, rel_name = self.extract_names(fr)
package = self.mount_package(dirname)
# Free functions
free_fn = []

#
xclasses =[]
xclass, xmethod = None, None
branch_stats = analysis.branch_stats()
missing_branch_arcs = analysis.missing_branch_arcs()

# For each statement, create an XML 'line' element.
for line in sorted(analysis.statements):
xline = self.xml_out.createElement("line")
xline.setAttribute("number", str(line))

# Q: can we get info about the number of times a statement is
# executed? If so, that should be recorded here.
xline.setAttribute("hits", str(int(line not in analysis.missing)))

if has_arcs:
if line in branch_stats:
total, taken = branch_stats[line]
xline.setAttribute("branch", "true")
xline.setAttribute(
"condition-coverage",
"%d%% (%d/%d)" % (100*taken//total, taken, total)
)
if line in missing_branch_arcs:
annlines = ["exit" if b < 0 else str(b) for b in missing_branch_arcs[line]]
xline.setAttribute("missing-branches", ",".join(annlines))
xlines.appendChild(xline)

class_lines = len(analysis.statements)
class_hits = class_lines - len(analysis.missing)

line = 1
self.is_class_level = False
for line, tokens in enumerate(fr.source_token_lines(), start=1):
if tokens:
is_tag = self.is_property_tag(tokens)
if is_tag:
continue
# We found a new class definition?
created, xclass = self.process_class(rel_name, line, tokens, xclass, xmethod, analysis)
if created:
xclasses.append(xclass)
continue
#
created, xmethod = self.process_method(xmethod, tokens, xclass, free_fn)
if created:
continue
#
# Processing a line
xline = self.process_line(line, has_arcs, branch_stats, missing_branch_arcs, analysis)
if xmethod:
xmethod.appendChild(xline)
elif xclass:
xclass.appendChild(xline)
#
if xclass:
self.set_class_stats(xclass, line, analysis)
if xmethod:
self.set_method_stats(xmethod)

#if xscope.hasChildNodes():
# xclasses.append(xscope)
# Rename
package[0][rel_name] = (xclasses, free_fn)
if has_arcs:
class_branches = sum(t for t, k in branch_stats.values())
classes_branches = sum(t for t, k in branch_stats.values())
missing_branches = sum(t - k for t, k in branch_stats.values())
class_br_hits = class_branches - missing_branches
classes_br_hits = classes_branches - missing_branches
else:
class_branches = 0.0
class_br_hits = 0.0
classes_branches = 0.0
classes_br_hits = 0.0
#
classes_lines = len(analysis.statements)
classes_hits = classes_lines - len(analysis.missing)
#
package[1] += classes_hits
package[2] += classes_lines
package[3] += classes_br_hits
package[4] += classes_branches


# Finalize the statistics that are collected in the XML DOM.
xclass.setAttribute("line-rate", rate(class_hits, class_lines))
if has_arcs:
branch_rate = rate(class_br_hits, class_branches)
else:
branch_rate = "0"
xclass.setAttribute("branch-rate", branch_rate)

package[0][rel_name] = xclass
package[1] += class_hits
package[2] += class_lines
package[3] += class_br_hits
package[4] += class_branches


def serialize_xml(dom):
Expand Down