diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c1904b2850b6b24e58cd2763b1fa98b1d5803637..38c523583a0559ed85c008ee5ea8d2208bfd92eb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,7 +36,10 @@ test: paths: - "*/build/test-results/**/TEST-*.xml" - "*/build/reports/*.xml" + - "*/build/reports/cobertura/*.xml" reports: + cobertura: + - "*/build/reports/cobertura/*.xml" junit: - "*/build/test-results/**/TEST-*.xml" - "*/build/reports/*.xml" diff --git a/bitflags/build.gradle.kts b/bitflags/build.gradle.kts index 393ac2018b3b0aa91362e120487c84311b5e1dd1..67af7e273f1daffaae4c8a9412dce32a3bee4954 100644 --- a/bitflags/build.gradle.kts +++ b/bitflags/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("jvm") id("jacoco") + id("de.kuschku.coverageconverter") } tasks.withType<Test> { diff --git a/buildSrc/src/main/kotlin/de/kuschku/coverageconverter/ConvertCoverageReportsAction.kt b/buildSrc/src/main/kotlin/de/kuschku/coverageconverter/ConvertCoverageReportsAction.kt new file mode 100644 index 0000000000000000000000000000000000000000..9e2f3aa360ece654c61839686900cd5d206df94f --- /dev/null +++ b/buildSrc/src/main/kotlin/de/kuschku/coverageconverter/ConvertCoverageReportsAction.kt @@ -0,0 +1,84 @@ +/* + * Quasseldroid - Quassel client for Android + * + * Copyright (c) 2021 Janne Mareike Koschinski + * Copyright (c) 2021 The Quassel Project + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.kuschku.coverageconverter + +import org.gradle.api.Action +import org.gradle.api.Task +import org.gradle.testing.jacoco.tasks.JacocoReport +import java.io.File + +internal class ConvertCoverageReportsAction( + private val jacocoReportTask: JacocoReport +) : Action<Task> { + private fun findOutputFile(jacocoFile: File): File? { + val actualFile = jacocoFile.absoluteFile + if (actualFile.exists() && actualFile.parentFile.name == "jacoco") { + val folder = File(actualFile.parentFile.parentFile, "cobertura") + folder.mkdirs() + return File(folder, actualFile.name) + } + + 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.readAllBytes()) + return file + } + + + override fun execute(t: Task) { + val cover2coverScript = createPythonScript("cover2cover.py", t.temporaryDir) + val source2filenameScript = createPythonScript("source2filename.py", t.temporaryDir) + + fun cover2cover(reportFile: File, outputFile: File, sourceDirectories: Iterable<File>) { + t.project.exec { + commandLine("python3") + args(cover2coverScript.absolutePath) + args(reportFile.absolutePath) + args(sourceDirectories.map(File::getAbsolutePath)) + standardOutput = outputFile.outputStream() + } + } + + fun source2filename(reportFile: File) { + t.project.exec { + commandLine("python3") + args(source2filenameScript.absolutePath) + args(reportFile.absolutePath) + } + } + + 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) + } + } + } + } +} diff --git a/buildSrc/src/main/kotlin/de/kuschku/coverageconverter/CoverageConverterPlugin.kt b/buildSrc/src/main/kotlin/de/kuschku/coverageconverter/CoverageConverterPlugin.kt new file mode 100644 index 0000000000000000000000000000000000000000..7c16efefe95dbc32de6c1879c4d34f49cb2f71b6 --- /dev/null +++ b/buildSrc/src/main/kotlin/de/kuschku/coverageconverter/CoverageConverterPlugin.kt @@ -0,0 +1,30 @@ +package de.kuschku.coverageconverter + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.tasks.testing.Test +import org.gradle.kotlin.dsl.getByName +import org.gradle.kotlin.dsl.register +import org.gradle.testing.jacoco.tasks.JacocoReport + +class CoverageConverterPlugin : Plugin<Project> { + override fun apply(project: Project) { + project.afterEvaluate { + val testTask = tasks.getByName<Test>("test") + + val jacocoReportTask = tasks.getByName<JacocoReport>("jacocoTestReport"){ + dependsOn(testTask) + } + + tasks.register("coberturaTestReport") { + dependsOn(jacocoReportTask) + mustRunAfter(jacocoReportTask) + group = "verification" + + doLast(ConvertCoverageReportsAction(jacocoReportTask)) + } + } + } +} + diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/de.kuschku.coverageconverter.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/de.kuschku.coverageconverter.properties new file mode 100644 index 0000000000000000000000000000000000000000..170902fa7f3a294e7c595310b5df0120901520d2 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/de.kuschku.coverageconverter.properties @@ -0,0 +1 @@ +implementation-class=de.kuschku.coverageconverter.CoverageConverterPlugin diff --git a/buildSrc/src/main/resources/coverageconverter/cover2cover.py b/buildSrc/src/main/resources/coverageconverter/cover2cover.py new file mode 100644 index 0000000000000000000000000000000000000000..7561ad560695061ff0b2cdb106cc03abea6a2a21 --- /dev/null +++ b/buildSrc/src/main/resources/coverageconverter/cover2cover.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python +import sys +import xml.etree.ElementTree as ET +import re +import os.path +import time + +# 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 new file mode 100644 index 0000000000000000000000000000000000000000..2b60a9b37850051020111393d923d189e8d0c6b7 --- /dev/null +++ b/buildSrc/src/main/resources/coverageconverter/source2filename.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +import sys +import xml.etree.ElementTree as ET +import lxml.etree +import re +import os.path + + + + +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) diff --git a/protocol/build.gradle.kts b/protocol/build.gradle.kts index 27124f8ee225aafee1a81bc432af32d0b5e0d42d..a01ee699332fa9804ffe4ab91732aa69b5b25f8c 100644 --- a/protocol/build.gradle.kts +++ b/protocol/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("jvm") jacoco + id("de.kuschku.coverageconverter") } tasks.withType<Test> {