From 45be705a30002ff49d9ea3116a1a4ed9ca5c6de6 Mon Sep 17 00:00:00 2001 From: Michael Gross Date: Sat, 22 Apr 2023 20:58:04 +0200 Subject: [PATCH 1/4] Add first draft of merge coverage --- pycobertura/merge_coverage.py | 130 ++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 pycobertura/merge_coverage.py diff --git a/pycobertura/merge_coverage.py b/pycobertura/merge_coverage.py new file mode 100644 index 00000000..e7bf0517 --- /dev/null +++ b/pycobertura/merge_coverage.py @@ -0,0 +1,130 @@ +import xml.etree.ElementTree as ET +from typing import List + + +def update_element_stats( + update_data, element, package_name=None, class_name=None, method_name=None +): + element_id = ( + str(element.get("number")) if element.tag == "line" else element.get("name") + ) + + if package_name is not None and class_name is not None and method_name is not None: + dict_ = update_data["packages"][package_name]["classes"][class_name]["methods"][ + method_name + ]["lines"] + if ( + dict_[element_id]["line-rate"] == 0.0 + and int(dict_[element_id].get("hits")) > 0 + ): + dict_[element_id]["line-rate"] = 1.0 + dict_[element_id]["hits"] += int(dict_[element_id].get("hits")) + dict_[element_id]["branch-rate"] = 0 + else: + if package_name is None: + dict_ = update_data["packages"] + elif class_name is None: + dict_ = update_data["packages"][package_name]["classes"] + elif method_name is None: + dict_ = update_data["packages"][package_name]["classes"][class_name][ + "methods" + ] + dict_[element_id]["line-rate"] += float(dict_[element_id].get("line-rate")) + dict_[element_id]["branch-rate"] += float( + dict_[element_id].get("branch-rate", 0) + ) + + return update_data + + +def merge_coverage(files: List[str], output_file: str) -> str: + merged_data = {"packages": {}} + for file in files: + tree = ET.parse(file) + root = tree.getroot() + for package in root.findall("./packages/package"): + package_name = package.get("name") + merged_data["packages"][package_name] = { + "name": package_name, + "line-rate": float(package.get("line-rate")), + "branch-rate": float(package.get("branch-rate")), + "classes": {}, + } + updated_package_data = update_element_stats(merged_data, package) + merged_data.update(updated_package_data) + + for class_ in package.findall("./classes/class"): + class_name = class_.get("name") + merged_data["packages"][package_name]["classes"][class_name] = { + "name": class_name, + "line-rate": 0.0, + "branch-rate": 0, + "methods": {}, + } + updated_class_data = update_element_stats( + merged_data, class_, package_name + ) + merged_data.update(updated_class_data) + + for method in class_.findall("./methods/method"): + method_name = method.get("name") + merged_data["packages"][package_name]["classes"][class_name][ + "methods" + ][method_name] = { + "name": method_name, + "line-rate": 0.0, + "branch-rate": 0, + "lines": {}, + } + updated_method_data = update_element_stats( + merged_data, method, package_name, class_name + ) + merged_data.update(updated_method_data) + + for line in method.findall("./lines/line"): + line_num = line.get("number") + merged_data["packages"][package_name]["classes"][class_name][ + "methods" + ][method_name]["lines"][line_num] = { + "number": line_num, + "line-rate": 0.0, + "hits": 0, + } + updated_line_data = update_element_stats( + merged_data, line, package_name, class_name, method_name + ) + merged_data.update(updated_line_data) + + xml_root = ET.Element("coverage") + total_line_rate = sum( + [package_data["line-rate"] for package_data in merged_data["packages"].values()] + ) / len(merged_data["packages"]) + total_branches_rate = sum( + [ + package_data["branch-rate"] + for package_data in merged_data["packages"].values() + ] + ) / len(merged_data["packages"]) + + xml_root.set("line-rate", str(total_line_rate)) + xml_root.set("branch-rate", str(total_branches_rate)) + + for package_name, package_data in merged_data["packages"].items(): + if package_name: + package_element = ET.SubElement(xml_root, "package") + package_element.set("name", package_name) + package_element.set( + "line-rate", str(package_data["line-rate"] / len(package_data)) + ) + package_element.set( + "branch-rate", str(package_data["branch-rate"] / len(package_data)) + ) + + tree = ET.ElementTree(xml_root) + tree.write(output_file) + + +if __name__ == "__main__": + files = ["coverage1.xml", "coverage2.xml", "coverage3.xml"] + combined_coverage = merge_coverage(files, "overall_coverage.xml") + print(combined_coverage) From ca2fecd59016938a26f97575ff09d0740df38972 Mon Sep 17 00:00:00 2001 From: Michael Gross Date: Tue, 9 May 2023 19:47:04 +0200 Subject: [PATCH 2/4] add error handling to merge_coverage function. --- pycobertura/merge_coverage.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pycobertura/merge_coverage.py b/pycobertura/merge_coverage.py index e7bf0517..94840682 100644 --- a/pycobertura/merge_coverage.py +++ b/pycobertura/merge_coverage.py @@ -1,3 +1,5 @@ +import sys +import os import xml.etree.ElementTree as ET from typing import List @@ -37,7 +39,29 @@ def update_element_stats( return update_data +def check_file_exists(file_): + if not os.path.isfile(file_): + print(f"Error: file {file_} does not exist.") + sys.exit(1) + + +def check_is_valid_xml_file(file_): + try: + ET.parse(file_) + except ET.ParseError: + print(f"Error: Input file {file_} is not a valid XML file.") + sys.exit(1) + + def merge_coverage(files: List[str], output_file: str) -> str: + # Check that the output file does not already exist + check_file_exists(output_file) + + # Check that all input files exist and are valid XML files + for file_ in files: + check_file_exists(file_) + check_is_valid_xml_file(file_) + merged_data = {"packages": {}} for file in files: tree = ET.parse(file) From bad2ed55672292579e53b82f98320f95b59d48d1 Mon Sep 17 00:00:00 2001 From: Michael Gross Date: Mon, 25 Mar 2024 23:21:12 +0100 Subject: [PATCH 3/4] Merge Cobertura XML files --- pycobertura/cobertura1.xml | 29 +++++ pycobertura/cobertura2.xml | 29 +++++ pycobertura/merge_coverage.py | 186 +++++++------------------------ pycobertura/merged_cobertura.xml | 2 + 4 files changed, 100 insertions(+), 146 deletions(-) create mode 100644 pycobertura/cobertura1.xml create mode 100644 pycobertura/cobertura2.xml create mode 100644 pycobertura/merged_cobertura.xml diff --git a/pycobertura/cobertura1.xml b/pycobertura/cobertura1.xml new file mode 100644 index 00000000..59a4b482 --- /dev/null +++ b/pycobertura/cobertura1.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pycobertura/cobertura2.xml b/pycobertura/cobertura2.xml new file mode 100644 index 00000000..9e2a555d --- /dev/null +++ b/pycobertura/cobertura2.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pycobertura/merge_coverage.py b/pycobertura/merge_coverage.py index 94840682..eb22b42b 100644 --- a/pycobertura/merge_coverage.py +++ b/pycobertura/merge_coverage.py @@ -1,154 +1,48 @@ -import sys -import os import xml.etree.ElementTree as ET -from typing import List +from collections import defaultdict +def merge_coverage_data(files): + merged_data = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))) -def update_element_stats( - update_data, element, package_name=None, class_name=None, method_name=None -): - element_id = ( - str(element.get("number")) if element.tag == "line" else element.get("name") - ) - - if package_name is not None and class_name is not None and method_name is not None: - dict_ = update_data["packages"][package_name]["classes"][class_name]["methods"][ - method_name - ]["lines"] - if ( - dict_[element_id]["line-rate"] == 0.0 - and int(dict_[element_id].get("hits")) > 0 - ): - dict_[element_id]["line-rate"] = 1.0 - dict_[element_id]["hits"] += int(dict_[element_id].get("hits")) - dict_[element_id]["branch-rate"] = 0 - else: - if package_name is None: - dict_ = update_data["packages"] - elif class_name is None: - dict_ = update_data["packages"][package_name]["classes"] - elif method_name is None: - dict_ = update_data["packages"][package_name]["classes"][class_name][ - "methods" - ] - dict_[element_id]["line-rate"] += float(dict_[element_id].get("line-rate")) - dict_[element_id]["branch-rate"] += float( - dict_[element_id].get("branch-rate", 0) - ) - - return update_data - - -def check_file_exists(file_): - if not os.path.isfile(file_): - print(f"Error: file {file_} does not exist.") - sys.exit(1) - - -def check_is_valid_xml_file(file_): - try: - ET.parse(file_) - except ET.ParseError: - print(f"Error: Input file {file_} is not a valid XML file.") - sys.exit(1) - - -def merge_coverage(files: List[str], output_file: str) -> str: - # Check that the output file does not already exist - check_file_exists(output_file) - - # Check that all input files exist and are valid XML files - for file_ in files: - check_file_exists(file_) - check_is_valid_xml_file(file_) - - merged_data = {"packages": {}} for file in files: tree = ET.parse(file) root = tree.getroot() - for package in root.findall("./packages/package"): - package_name = package.get("name") - merged_data["packages"][package_name] = { - "name": package_name, - "line-rate": float(package.get("line-rate")), - "branch-rate": float(package.get("branch-rate")), - "classes": {}, - } - updated_package_data = update_element_stats(merged_data, package) - merged_data.update(updated_package_data) - - for class_ in package.findall("./classes/class"): - class_name = class_.get("name") - merged_data["packages"][package_name]["classes"][class_name] = { - "name": class_name, - "line-rate": 0.0, - "branch-rate": 0, - "methods": {}, - } - updated_class_data = update_element_stats( - merged_data, class_, package_name - ) - merged_data.update(updated_class_data) - - for method in class_.findall("./methods/method"): - method_name = method.get("name") - merged_data["packages"][package_name]["classes"][class_name][ - "methods" - ][method_name] = { - "name": method_name, - "line-rate": 0.0, - "branch-rate": 0, - "lines": {}, - } - updated_method_data = update_element_stats( - merged_data, method, package_name, class_name - ) - merged_data.update(updated_method_data) - - for line in method.findall("./lines/line"): - line_num = line.get("number") - merged_data["packages"][package_name]["classes"][class_name][ - "methods" - ][method_name]["lines"][line_num] = { - "number": line_num, - "line-rate": 0.0, - "hits": 0, - } - updated_line_data = update_element_stats( - merged_data, line, package_name, class_name, method_name - ) - merged_data.update(updated_line_data) - - xml_root = ET.Element("coverage") - total_line_rate = sum( - [package_data["line-rate"] for package_data in merged_data["packages"].values()] - ) / len(merged_data["packages"]) - total_branches_rate = sum( - [ - package_data["branch-rate"] - for package_data in merged_data["packages"].values() - ] - ) / len(merged_data["packages"]) - - xml_root.set("line-rate", str(total_line_rate)) - xml_root.set("branch-rate", str(total_branches_rate)) - - for package_name, package_data in merged_data["packages"].items(): - if package_name: - package_element = ET.SubElement(xml_root, "package") - package_element.set("name", package_name) - package_element.set( - "line-rate", str(package_data["line-rate"] / len(package_data)) - ) - package_element.set( - "branch-rate", str(package_data["branch-rate"] / len(package_data)) - ) - - tree = ET.ElementTree(xml_root) - tree.write(output_file) - + + for package in root.findall('./packages/package'): + package_name = package.attrib['name'] + for class_ in package.findall('classes/class'): + class_name = class_.attrib['name'] + for method in class_.findall('methods/method'): + method_name = method.attrib['name'] + for line in method.findall('lines/line'): + line_number = line.attrib['number'] + hits = int(line.attrib['hits']) + merged_data[package_name][class_name][method_name][line_number] += hits + + return merged_data + +def generate_merged_xml(merged_data): + root = ET.Element('coverage') + + for package_name, package_data in merged_data.items(): + package_elem = ET.SubElement(root, 'package', name=package_name) + for class_name, class_data in package_data.items(): + class_elem = ET.SubElement(package_elem, 'class', name=class_name) + for method_name, method_data in class_data.items(): + method_elem = ET.SubElement(class_elem, 'method', name=method_name) + lines_elem = ET.SubElement(method_elem, 'lines') + for line_number, hits in method_data.items(): + ET.SubElement(lines_elem, 'line', number=line_number, hits=str(hits)) + + return ET.ElementTree(root) + +def main(): + files = ['cobertura1.xml', 'cobertura2.xml'] # List of Cobertura XML files to merge + merged_data = merge_coverage_data(files) + + merged_xml_tree = generate_merged_xml(merged_data) + merged_xml_tree.write('merged_cobertura.xml', encoding='utf-8', xml_declaration=True) if __name__ == "__main__": - files = ["coverage1.xml", "coverage2.xml", "coverage3.xml"] - combined_coverage = merge_coverage(files, "overall_coverage.xml") - print(combined_coverage) + main() + diff --git a/pycobertura/merged_cobertura.xml b/pycobertura/merged_cobertura.xml new file mode 100644 index 00000000..5ed03f61 --- /dev/null +++ b/pycobertura/merged_cobertura.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 951e8bd65c85a4769e158c6d655dd841914850a1 Mon Sep 17 00:00:00 2001 From: Michael Gross Date: Mon, 25 Mar 2024 23:25:43 +0100 Subject: [PATCH 4/4] black-formatted merge_coverage.py --- pycobertura/merge_coverage.py | 53 +++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/pycobertura/merge_coverage.py b/pycobertura/merge_coverage.py index eb22b42b..e3563c05 100644 --- a/pycobertura/merge_coverage.py +++ b/pycobertura/merge_coverage.py @@ -1,48 +1,59 @@ import xml.etree.ElementTree as ET from collections import defaultdict + def merge_coverage_data(files): - merged_data = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))) + merged_data = defaultdict( + lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(int))) + ) for file in files: tree = ET.parse(file) root = tree.getroot() - - for package in root.findall('./packages/package'): - package_name = package.attrib['name'] - for class_ in package.findall('classes/class'): - class_name = class_.attrib['name'] - for method in class_.findall('methods/method'): - method_name = method.attrib['name'] - for line in method.findall('lines/line'): - line_number = line.attrib['number'] - hits = int(line.attrib['hits']) - merged_data[package_name][class_name][method_name][line_number] += hits + + for package in root.findall("./packages/package"): + package_name = package.attrib["name"] + for class_ in package.findall("classes/class"): + class_name = class_.attrib["name"] + for method in class_.findall("methods/method"): + method_name = method.attrib["name"] + for line in method.findall("lines/line"): + line_number = line.attrib["number"] + hits = int(line.attrib["hits"]) + merged_data[package_name][class_name][method_name][ + line_number + ] += hits return merged_data + def generate_merged_xml(merged_data): - root = ET.Element('coverage') + root = ET.Element("coverage") for package_name, package_data in merged_data.items(): - package_elem = ET.SubElement(root, 'package', name=package_name) + package_elem = ET.SubElement(root, "package", name=package_name) for class_name, class_data in package_data.items(): - class_elem = ET.SubElement(package_elem, 'class', name=class_name) + class_elem = ET.SubElement(package_elem, "class", name=class_name) for method_name, method_data in class_data.items(): - method_elem = ET.SubElement(class_elem, 'method', name=method_name) - lines_elem = ET.SubElement(method_elem, 'lines') + method_elem = ET.SubElement(class_elem, "method", name=method_name) + lines_elem = ET.SubElement(method_elem, "lines") for line_number, hits in method_data.items(): - ET.SubElement(lines_elem, 'line', number=line_number, hits=str(hits)) + ET.SubElement( + lines_elem, "line", number=line_number, hits=str(hits) + ) return ET.ElementTree(root) + def main(): - files = ['cobertura1.xml', 'cobertura2.xml'] # List of Cobertura XML files to merge + files = ["cobertura1.xml", "cobertura2.xml"] # List of Cobertura XML files to merge merged_data = merge_coverage_data(files) merged_xml_tree = generate_merged_xml(merged_data) - merged_xml_tree.write('merged_cobertura.xml', encoding='utf-8', xml_declaration=True) + merged_xml_tree.write( + "merged_cobertura.xml", encoding="utf-8", xml_declaration=True + ) + if __name__ == "__main__": main() -