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