From 369f33586a860ae03b01c938bd02b22881b46384 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski <janne@kuschku.de> Date: Sun, 21 Feb 2021 00:25:24 +0100 Subject: [PATCH] Extract coverage handling to separate library --- .gitlab-ci.yml | 2 +- buildSrc/build.gradle.kts | 3 +- .../CoverageConverterAction.kt | 56 +++--- .../CoverageConverterPlugin.kt | 1 + .../coverageconverter/cover2cover.py | 186 ------------------ .../coverageconverter/source2filename.py | 50 ----- 6 files changed, 32 insertions(+), 266 deletions(-) delete mode 100644 buildSrc/src/main/resources/coverageconverter/cover2cover.py delete mode 100644 buildSrc/src/main/resources/coverageconverter/source2filename.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c8df057..d32ef21 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "k8r.eu/justjanne/quasseldroid-build-env:latest" +image: "openjdk:8" cache: key: "$CI_PROJECT_NAME" diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 6bee059..6681901 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -22,7 +22,8 @@ repositories { } 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(localGroovy()) } diff --git a/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterAction.kt b/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterAction.kt index 33d4910..157d833 100644 --- a/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterAction.kt +++ b/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterAction.kt @@ -10,6 +10,13 @@ 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.Task import org.gradle.testing.jacoco.tasks.JacocoReport @@ -29,44 +36,37 @@ internal class CoverageConverterAction( 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) { - val cover2coverScript = createPythonScript("cover2cover.py", task.temporaryDir) - val source2filenameScript = createPythonScript("source2filename.py", task.temporaryDir) - - fun cover2cover(reportFile: File, outputFile: File, sourceDirectories: Iterable<File>) { - task.project.exec { - commandLine("python3") - args(cover2coverScript.absolutePath) - args(reportFile.absolutePath) - args(sourceDirectories.map(File::getAbsolutePath)) - standardOutput = outputFile.outputStream() - } + fun printTotal(data: ReportDto) { + val instructionRate = convertCounter(data.counters, CounterTypeDto.INSTRUCTION).rate + val instructionMissed = convertCounter(data.counters, CounterTypeDto.INSTRUCTION).missed + val branchRate = convertCounter(data.counters, CounterTypeDto.BRANCH).rate + val branchMissed = convertCounter(data.counters, CounterTypeDto.BRANCH).missed + println("[JacocoPrinter] Instructions $instructionRate (Missed $instructionMissed)") + println("[JacocoPrinter] Branches $branchRate (Missed $branchMissed)") } - fun source2filename(reportFile: File) { - task.project.exec { - commandLine("python3") - args(source2filenameScript.absolutePath) - args(reportFile.absolutePath) + fun convertFile(input: File, output: File) { + val mapper = XmlMapper( + JacksonXmlModule().apply { + setDefaultUseWrapper(false) + } + ).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 { if (it.isEnabled && it.destination.extension == "xml") { val outputFile = findOutputFile(it.destination) if (outputFile != null) { - cover2cover(it.destination, outputFile, jacocoReportTask.sourceDirectories) - source2filename(outputFile) + convertFile(it.destination, outputFile) } } } diff --git a/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterPlugin.kt b/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterPlugin.kt index 4541f93..db06c80 100644 --- a/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterPlugin.kt +++ b/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterPlugin.kt @@ -10,6 +10,7 @@ package de.justjanne.coverageconverter +import groovy.util.XmlSlurper import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.testing.jacoco.plugins.JacocoPluginExtension diff --git a/buildSrc/src/main/resources/coverageconverter/cover2cover.py b/buildSrc/src/main/resources/coverageconverter/cover2cover.py deleted file mode 100644 index 9564b14..0000000 --- a/buildSrc/src/main/resources/coverageconverter/cover2cover.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/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) diff --git a/buildSrc/src/main/resources/coverageconverter/source2filename.py b/buildSrc/src/main/resources/coverageconverter/source2filename.py deleted file mode 100644 index d7330a2..0000000 --- a/buildSrc/src/main/resources/coverageconverter/source2filename.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/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) -- GitLab