├── LICENSE ├── README.md └── cover2cover.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Rico Huijbers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cover2cover 2 | 3 | A script for converting JaCoCo XML coverage reports into Cobertura XML coverage 4 | reports. 5 | 6 | ## Motivation 7 | 8 | I created this script because I wanted code coverage reports in Jenkins[[1]]. 9 | Since Cobertura[[2]] doesn't support Java 1.7 or higher (the project seems 10 | abandoned), we had to use JaCoCo[[3]], which is great coverage tool and very easy 11 | to use. 12 | 13 | However, the Jenkins JaCoCo plugin[[4]] leaves a lot to be desired, while Cobertura's 14 | Jenkins plugin[[5]] is a lot better. To be precise, it supports: 15 | 16 | * Trend graphs for packages, classes, etc. instead of just lines 17 | * Trend graphs in percentages instead of absolute numbers 18 | * High and low "water marks" that cause the build to become unstable in Jenkins 19 | 20 | Until the JaCoCo plugin is up to speed, this script can be used to convert 21 | JaCoCo XML reports into Cobertura XML reports, so we can continue to use the 22 | Cobertura Jenkins plugin to track coverage. 23 | 24 | Not every feature is supported, but close enough. 25 | 26 | ## Usage 27 | 28 | Add the following "post step" to your Jenkins build: 29 | 30 | mkdir -p target/site/cobertura && cover2cover.py target/site/jacoco/jacoco.xml src/main/java > target/site/cobertura/coverage.xml 31 | 32 | And add the Cobertura plugin with the following path: 33 | 34 | **/target/site/cobertura/coverage.xml 35 | 36 | > (Note: The above assumes a Maven project) 37 | 38 | [1]: http://jenkins-ci.org/ "Jenkins" 39 | [2]: http://cobertura.sourceforge.net/ "Cobertura" 40 | [3]: http://www.eclemma.org/jacoco/ "JaCoCo" 41 | [4]: https://wiki.jenkins-ci.org/display/JENKINS/JaCoCo+Plugin "Jenkins JaCoCo plugin" 42 | [5]: https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin "Jenkins Cobertura plugin" 43 | -------------------------------------------------------------------------------- /cover2cover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import xml.etree.ElementTree as ET 4 | import re 5 | import os.path 6 | 7 | # branch-rate="0.0" complexity="0.0" line-rate="1.0" 8 | # branch="true" hits="1" number="86" 9 | 10 | def find_lines(j_package, filename): 11 | """Return all elements for a given source file in a package.""" 12 | lines = list() 13 | sourcefiles = j_package.findall("sourcefile") 14 | for sourcefile in sourcefiles: 15 | if sourcefile.attrib.get("name") == os.path.basename(filename): 16 | lines = lines + sourcefile.findall("line") 17 | return lines 18 | 19 | def line_is_after(jm, start_line): 20 | return int(jm.attrib.get('line', 0)) > start_line 21 | 22 | def method_lines(jmethod, jmethods, jlines): 23 | """Filter the lines from the given set of jlines that apply to the given jmethod.""" 24 | start_line = int(jmethod.attrib.get('line', 0)) 25 | larger = list(int(jm.attrib.get('line', 0)) for jm in jmethods if line_is_after(jm, start_line)) 26 | end_line = min(larger) if len(larger) else 99999999 27 | 28 | for jline in jlines: 29 | if start_line <= int(jline.attrib['nr']) < end_line: 30 | yield jline 31 | 32 | def convert_lines(j_lines, into): 33 | """Convert the JaCoCo elements into Cobertura elements, add them under the given element.""" 34 | c_lines = ET.SubElement(into, 'lines') 35 | for jline in j_lines: 36 | mb = int(jline.attrib['mb']) 37 | cb = int(jline.attrib['cb']) 38 | ci = int(jline.attrib['ci']) 39 | 40 | cline = ET.SubElement(c_lines, 'line') 41 | cline.set('number', jline.attrib['nr']) 42 | cline.set('hits', '1' if ci > 0 else '0') # Probably not true but no way to know from JaCoCo XML file 43 | 44 | if mb + cb > 0: 45 | percentage = str(int(100 * (float(cb) / (float(cb) + float(mb))))) + '%' 46 | cline.set('branch', 'true') 47 | cline.set('condition-coverage', percentage + ' (' + str(cb) + '/' + str(cb + mb) + ')') 48 | 49 | cond = ET.SubElement(ET.SubElement(cline, 'conditions'), 'condition') 50 | cond.set('number', '0') 51 | cond.set('type', 'jump') 52 | cond.set('coverage', percentage) 53 | else: 54 | cline.set('branch', 'false') 55 | 56 | def guess_filename(path_to_class): 57 | m = re.match('([^$]*)', path_to_class) 58 | return (m.group(1) if m else path_to_class) + '.java' 59 | 60 | def add_counters(source, target): 61 | target.set('line-rate', counter(source, 'LINE')) 62 | target.set('branch-rate', counter(source, 'BRANCH')) 63 | target.set('complexity', counter(source, 'COMPLEXITY', sum)) 64 | 65 | def fraction(covered, missed): 66 | return covered / (covered + missed) 67 | 68 | def sum(covered, missed): 69 | return covered + missed 70 | 71 | def counter(source, type, operation=fraction): 72 | cs = source.findall('counter') 73 | c = next((ct for ct in cs if ct.attrib.get('type') == type), None) 74 | 75 | if c is not None: 76 | covered = float(c.attrib['covered']) 77 | missed = float(c.attrib['missed']) 78 | 79 | return str(operation(covered, missed)) 80 | else: 81 | return '0.0' 82 | 83 | def convert_method(j_method, j_lines): 84 | c_method = ET.Element('method') 85 | c_method.set('name', j_method.attrib['name']) 86 | c_method.set('signature', j_method.attrib['desc']) 87 | 88 | add_counters(j_method, c_method) 89 | convert_lines(j_lines, c_method) 90 | 91 | return c_method 92 | 93 | def convert_class(j_class, j_package): 94 | c_class = ET.Element('class') 95 | c_class.set('name', j_class.attrib['name'].replace('/', '.')) 96 | c_class.set('filename', guess_filename(j_class.attrib['name'])) 97 | 98 | all_j_lines = list(find_lines(j_package, c_class.attrib['filename'])) 99 | 100 | c_methods = ET.SubElement(c_class, 'methods') 101 | all_j_methods = list(j_class.findall('method')) 102 | for j_method in all_j_methods: 103 | j_method_lines = method_lines(j_method, all_j_methods, all_j_lines) 104 | c_methods.append(convert_method(j_method, j_method_lines)) 105 | 106 | add_counters(j_class, c_class) 107 | convert_lines(all_j_lines, c_class) 108 | 109 | return c_class 110 | 111 | def convert_package(j_package): 112 | c_package = ET.Element('package') 113 | c_package.attrib['name'] = j_package.attrib['name'].replace('/', '.') 114 | 115 | c_classes = ET.SubElement(c_package, 'classes') 116 | for j_class in j_package.findall('class'): 117 | c_classes.append(convert_class(j_class, j_package)) 118 | 119 | add_counters(j_package, c_package) 120 | 121 | return c_package 122 | 123 | def convert_root(source, target, source_roots): 124 | target.set('timestamp', str(int(source.find('sessioninfo').attrib['start']) / 1000)) 125 | 126 | sources = ET.SubElement(target, 'sources') 127 | for s in source_roots: 128 | ET.SubElement(sources, 'source').text = s 129 | 130 | packages = ET.SubElement(target, 'packages') 131 | for package in source.findall('package'): 132 | packages.append(convert_package(package)) 133 | 134 | add_counters(source, target) 135 | 136 | def jacoco2cobertura(filename, source_roots): 137 | if filename == '-': 138 | root = ET.fromstring(sys.stdin.read()) 139 | else: 140 | tree = ET.parse(filename) 141 | root = tree.getroot() 142 | 143 | into = ET.Element('coverage') 144 | convert_root(root, into, source_roots) 145 | print('') 146 | print(ET.tostring(into)) 147 | 148 | if __name__ == '__main__': 149 | if len(sys.argv) < 2: 150 | print("Usage: cover2cover.py FILENAME [SOURCE_ROOTS]") 151 | sys.exit(1) 152 | 153 | filename = sys.argv[1] 154 | source_roots = sys.argv[2:] if 2 < len(sys.argv) else '.' 155 | 156 | jacoco2cobertura(filename, source_roots) 157 | --------------------------------------------------------------------------------