Skip to content
Snippets Groups Projects
Verified Commit 369f3358 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Extract coverage handling to separate library

parent cf258a9e
No related branches found
No related tags found
No related merge requests found
image: "k8r.eu/justjanne/quasseldroid-build-env:latest" image: "openjdk:8"
cache: cache:
key: "$CI_PROJECT_NAME" key: "$CI_PROJECT_NAME"
......
...@@ -22,7 +22,8 @@ repositories { ...@@ -22,7 +22,8 @@ repositories {
} }
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30") implementation("org.jetbrains.kotlin", "kotlin-gradle-plugin", "1.4.30")
implementation("de.justjanne", "jacoco-cobertura-converter", "1.0.0")
implementation(gradleApi()) implementation(gradleApi())
implementation(localGroovy()) implementation(localGroovy())
} }
...@@ -10,6 +10,13 @@ ...@@ -10,6 +10,13 @@
package de.justjanne.coverageconverter package de.justjanne.coverageconverter
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import de.justjanne.coverageconverter.convertCounter
import de.justjanne.coverageconverter.jacoco.CounterTypeDto
import de.justjanne.coverageconverter.jacoco.ReportDto
import org.gradle.api.Action import org.gradle.api.Action
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.testing.jacoco.tasks.JacocoReport import org.gradle.testing.jacoco.tasks.JacocoReport
...@@ -29,44 +36,37 @@ internal class CoverageConverterAction( ...@@ -29,44 +36,37 @@ internal class CoverageConverterAction(
return null return null
} }
private fun createPythonScript(name: String, temporaryDir: File): File {
val file = File(temporaryDir, name)
if (file.exists()) {
file.delete()
}
val source = CoverageConverterPlugin::class.java.getResourceAsStream("/coverageconverter/$name")
file.writeBytes(source.readBytes())
return file
}
override fun execute(task: Task) { override fun execute(task: Task) {
val cover2coverScript = createPythonScript("cover2cover.py", task.temporaryDir) fun printTotal(data: ReportDto) {
val source2filenameScript = createPythonScript("source2filename.py", task.temporaryDir) val instructionRate = convertCounter(data.counters, CounterTypeDto.INSTRUCTION).rate
val instructionMissed = convertCounter(data.counters, CounterTypeDto.INSTRUCTION).missed
fun cover2cover(reportFile: File, outputFile: File, sourceDirectories: Iterable<File>) { val branchRate = convertCounter(data.counters, CounterTypeDto.BRANCH).rate
task.project.exec { val branchMissed = convertCounter(data.counters, CounterTypeDto.BRANCH).missed
commandLine("python3") println("[JacocoPrinter] Instructions $instructionRate (Missed $instructionMissed)")
args(cover2coverScript.absolutePath) println("[JacocoPrinter] Branches $branchRate (Missed $branchMissed)")
args(reportFile.absolutePath)
args(sourceDirectories.map(File::getAbsolutePath))
standardOutput = outputFile.outputStream()
}
} }
fun source2filename(reportFile: File) { fun convertFile(input: File, output: File) {
task.project.exec { val mapper = XmlMapper(
commandLine("python3") JacksonXmlModule().apply {
args(source2filenameScript.absolutePath) setDefaultUseWrapper(false)
args(reportFile.absolutePath) }
).apply {
enable(SerializationFeature.INDENT_OUTPUT)
enable(SerializationFeature.WRAP_ROOT_VALUE)
registerModule(KotlinModule(strictNullChecks = true))
} }
val data = mapper.readValue(input, ReportDto::class.java)
val result = convertReport(data)
printTotal(data)
mapper.writeValue(output, result)
} }
jacocoReportTask.reports.forEach { jacocoReportTask.reports.forEach {
if (it.isEnabled && it.destination.extension == "xml") { if (it.isEnabled && it.destination.extension == "xml") {
val outputFile = findOutputFile(it.destination) val outputFile = findOutputFile(it.destination)
if (outputFile != null) { if (outputFile != null) {
cover2cover(it.destination, outputFile, jacocoReportTask.sourceDirectories) convertFile(it.destination, outputFile)
source2filename(outputFile)
} }
} }
} }
......
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
package de.justjanne.coverageconverter package de.justjanne.coverageconverter
import groovy.util.XmlSlurper
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
......
#!/usr/bin/env python
# libquassel
# Copyright (c) 2021 Janne Mareike Koschinski
# Copyright (c) 2021 The Quassel Project
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
import os.path
import sys
import time
import xml.etree.ElementTree as ET
# branch-rate="0.0" complexity="0.0" line-rate="1.0"
# branch="true" hits="1" number="86"
def find_lines(j_package, filename):
"""Return all <line> elements for a given source file in a package."""
lines = list()
sourcefiles = j_package.findall("sourcefile")
for sourcefile in sourcefiles:
if sourcefile.attrib.get("name") == os.path.basename(filename):
lines = lines + sourcefile.findall("line")
return lines
def line_is_after(jm, start_line):
return int(jm.attrib.get('line', 0)) > start_line
def method_lines(jmethod, jmethods, jlines):
"""Filter the lines from the given set of jlines that apply to the given jmethod."""
start_line = int(jmethod.attrib.get('line', 0))
larger = list(int(jm.attrib.get('line', 0)) for jm in jmethods if line_is_after(jm, start_line))
end_line = min(larger) if len(larger) else 99999999
for jline in jlines:
if start_line <= int(jline.attrib['nr']) < end_line:
yield jline
def convert_lines(j_lines, into):
"""Convert the JaCoCo <line> elements into Cobertura <line> elements, add them under the given element."""
c_lines = ET.SubElement(into, 'lines')
for jline in j_lines:
mb = int(jline.attrib['mb'])
cb = int(jline.attrib['cb'])
ci = int(jline.attrib['ci'])
cline = ET.SubElement(c_lines, 'line')
cline.set('number', jline.attrib['nr'])
cline.set('hits', '1' if ci > 0 else '0') # Probably not true but no way to know from JaCoCo XML file
if mb + cb > 0:
percentage = str(int(100 * (float(cb) / (float(cb) + float(mb))))) + '%'
cline.set('branch', 'true')
cline.set('condition-coverage', percentage + ' (' + str(cb) + '/' + str(cb + mb) + ')')
cond = ET.SubElement(ET.SubElement(cline, 'conditions'), 'condition')
cond.set('number', '0')
cond.set('type', 'jump')
cond.set('coverage', percentage)
else:
cline.set('branch', 'false')
def path_to_filepath(path_to_class, sourcefilename):
return path_to_class[0: path_to_class.rfind("/") + 1] + sourcefilename
def add_counters(source, target):
target.set('line-rate', counter(source, 'LINE'))
target.set('branch-rate', counter(source, 'BRANCH'))
target.set('complexity', counter(source, 'COMPLEXITY', sum))
def fraction(covered, missed):
return covered / (covered + missed)
def sum(covered, missed):
return covered + missed
def counter(source, type, operation=fraction):
cs = source.findall('counter')
c = next((ct for ct in cs if ct.attrib.get('type') == type), None)
if c is not None:
covered = float(c.attrib['covered'])
missed = float(c.attrib['missed'])
return str(operation(covered, missed))
else:
return '0.0'
def convert_method(j_method, j_lines):
c_method = ET.Element('method')
c_method.set('name', j_method.attrib['name'])
c_method.set('signature', j_method.attrib['desc'])
add_counters(j_method, c_method)
convert_lines(j_lines, c_method)
return c_method
def convert_class(j_class, j_package):
c_class = ET.Element('class')
c_class.set('name', j_class.attrib['name'].replace('/', '.'))
c_class.set('filename', path_to_filepath(j_class.attrib['name'], j_class.attrib['sourcefilename']))
all_j_lines = list(find_lines(j_package, c_class.attrib['filename']))
c_methods = ET.SubElement(c_class, 'methods')
all_j_methods = list(j_class.findall('method'))
for j_method in all_j_methods:
j_method_lines = method_lines(j_method, all_j_methods, all_j_lines)
c_methods.append(convert_method(j_method, j_method_lines))
add_counters(j_class, c_class)
convert_lines(all_j_lines, c_class)
return c_class
def convert_package(j_package):
c_package = ET.Element('package')
c_package.attrib['name'] = j_package.attrib['name'].replace('/', '.')
c_classes = ET.SubElement(c_package, 'classes')
for j_class in j_package.findall('class'):
c_classes.append(convert_class(j_class, j_package))
add_counters(j_package, c_package)
return c_package
def convert_root(source, target, source_roots):
try:
target.set('timestamp', str(int(source.find('sessioninfo').attrib['start']) / 1000))
except AttributeError as e:
target.set('timestamp', str(int(time.time() / 1000)))
sources = ET.SubElement(target, 'sources')
for s in source_roots:
ET.SubElement(sources, 'source').text = s
packages = ET.SubElement(target, 'packages')
for group in source.findall('group'):
for package in group.findall('package'):
packages.append(convert_package(package))
for package in source.findall('package'):
packages.append(convert_package(package))
add_counters(source, target)
def jacoco2cobertura(filename, source_roots):
if filename == '-':
root = ET.fromstring(sys.stdin.read())
else:
tree = ET.parse(filename)
root = tree.getroot()
into = ET.Element('coverage')
convert_root(root, into, source_roots)
print('<?xml version="1.0" ?>')
print(ET.tostring(into, encoding='unicode'))
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: cover2cover.py FILENAME [SOURCE_ROOTS]")
sys.exit(1)
filename = sys.argv[1]
source_roots = sys.argv[2:] if 2 < len(sys.argv) else '.'
jacoco2cobertura(filename, source_roots)
#!/usr/bin/env python
# libquassel
# Copyright (c) 2021 Janne Mareike Koschinski
# Copyright (c) 2021 The Quassel Project
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.
import lxml.etree
import os.path
import sys
def convert_source(filename):
# read input file
root = lxml.etree.parse(filename)
sources = root.find('sources')
packages = root.find('packages')
for package in packages:
classes = package.find('classes')
for clazz in classes:
file_not_found = True
for source in sources:
full_filename = source.text + '/' + clazz.attrib['filename']
if os.path.isfile(full_filename):
clazz.attrib['filename'] = full_filename
file_not_found = False
if file_not_found:
print("Warning: File {} not found in all sources; removing from sources.".format(clazz.attrib['filename']))
clazz.getparent().remove(clazz)
data = lxml.etree.tostring(root, pretty_print=True)
# open the input file in write mode
fin = open(filename, "wb")
# overrite the input file with the resulting data
fin.write(data)
# close the file
fin.close()
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: source2filename.py FILENAME")
sys.exit(1)
filename = sys.argv[1]
convert_source(filename)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment