diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 6d012ee69..303bd4e96 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -9,12 +9,13 @@ import sys import time import xml.dom.minidom - +import numpy as np 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) @@ -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: @@ -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. @@ -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. @@ -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") @@ -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) @@ -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):