diff --git a/src/main/scala/com/github/sbt/jacoco/report/DirectoriesSourceFileLocator.scala b/src/main/scala/com/github/sbt/jacoco/report/DirectoriesSourceFileLocator.scala
index 9745fa30..165a3c6d 100644
--- a/src/main/scala/com/github/sbt/jacoco/report/DirectoriesSourceFileLocator.scala
+++ b/src/main/scala/com/github/sbt/jacoco/report/DirectoriesSourceFileLocator.scala
@@ -16,7 +16,7 @@ import java.io.{File, Reader}
import org.jacoco.report.{DirectorySourceFileLocator, ISourceFileLocator}
-class DirectoriesSourceFileLocator(directories: Seq[File], sourceSettings: JacocoSourceSettings)
+class DirectoriesSourceFileLocator(private[report] val directories: Seq[File], sourceSettings: JacocoSourceSettings)
extends ISourceFileLocator {
override def getSourceFile(packageName: String, fileName: String): Reader = {
diff --git a/src/main/scala/com/github/sbt/jacoco/report/JacocoReportFormats.scala b/src/main/scala/com/github/sbt/jacoco/report/JacocoReportFormats.scala
index 4ab5d866..763496d4 100644
--- a/src/main/scala/com/github/sbt/jacoco/report/JacocoReportFormats.scala
+++ b/src/main/scala/com/github/sbt/jacoco/report/JacocoReportFormats.scala
@@ -12,7 +12,13 @@
package com.github.sbt.jacoco.report
-import com.github.sbt.jacoco.report.formats.{CSVReportFormat, HTMLReportFormat, ScalaHTMLReportFormat, XMLReportFormat}
+import com.github.sbt.jacoco.report.formats.{
+ CSVReportFormat,
+ CoberturaReportFormat,
+ HTMLReportFormat,
+ ScalaHTMLReportFormat,
+ XMLReportFormat
+}
object JacocoReportFormats {
@@ -39,4 +45,11 @@ object JacocoReportFormats {
* '''Note:''' does not support Scala language constructs.
*/
val CSV = new CSVReportFormat()
+
+ /**
+ * COBERTURA report containing metrics at instruction level and embedded source code.
+ *
+ * '''Note:''' does not support Scala language constructs.
+ */
+ val COBERTURA = new CoberturaReportFormat()
}
diff --git a/src/main/scala/com/github/sbt/jacoco/report/formats/CoberturaReportFormat.scala b/src/main/scala/com/github/sbt/jacoco/report/formats/CoberturaReportFormat.scala
new file mode 100644
index 00000000..ac6fa9ef
--- /dev/null
+++ b/src/main/scala/com/github/sbt/jacoco/report/formats/CoberturaReportFormat.scala
@@ -0,0 +1,205 @@
+/*
+ * This file is part of sbt-jacoco.
+ *
+ * Copyright (c) Joachim Hofer & contributors
+ * All rights reserved.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package com.github.sbt.jacoco.report.formats
+
+import com.github.sbt.jacoco.report.DirectoriesSourceFileLocator
+import org.jacoco.core.analysis.{IBundleCoverage, IClassCoverage, IMethodCoverage, IPackageCoverage}
+import org.jacoco.core.data.{ExecutionData, SessionInfo}
+import org.jacoco.report.{IReportGroupVisitor, IReportVisitor, ISourceFileLocator, JavaNames}
+import sbt.IO
+
+import java.io.*
+import java.util
+import scala.collection.mutable
+
+class CoberturaReportFormat extends JacocoReportFormat {
+ override def createVisitor(directory: File, encoding: String): IReportVisitor = {
+ val covDirectory = new File(directory, "coverage-report")
+ IO.createDirectory(covDirectory)
+ val formatter = new CoberturaFormatter()
+ formatter.createVisitor(new FileOutputStream(new File(covDirectory, "cobertura.xml")))
+ }
+}
+
+class CoberturaRootVisitor(writer: Writer) extends IReportVisitor {
+
+ private var indent = 0
+ private var groupVisitor: IReportGroupVisitor = _
+ private var sessionInfos: java.util.List[SessionInfo] = new java.util.LinkedList[SessionInfo]()
+ private val javaNames = new JavaNames()
+
+ writer.write("\n")
+ writer.write("\n")
+
+ override def visitInfo(
+ sessionInfoList: java.util.List[SessionInfo],
+ collection: java.util.Collection[ExecutionData]
+ ): Unit = {
+ this.sessionInfos = sessionInfoList
+ }
+
+ override def visitEnd(): Unit = writer.close()
+
+ private val noCallback: () => Unit = () => ()
+
+ /**
+ * Write tag into output
+ *
+ * @param tag
+ * tag name
+ * @param attributes
+ * tag attributes
+ * @param callback
+ * callback to process nested tags
+ */
+ private def writeTag(tag: String, attributes: Map[String, Any], callback: () => Unit = noCallback): Unit = {
+ writer.write(
+ (" " * indent) + s"<$tag" + attributes
+ .map(i => s""" ${i._1}="${i._2}"""")
+ .mkString + (if (callback == noCallback) "/" else "") + ">\n"
+ )
+ if (callback != noCallback) {
+ indent += 4
+ callback()
+ indent -= 4
+ writer.write((" " * indent) + s"$tag>\n")
+ }
+ }
+
+ private def writePackages(packages: util.Collection[IPackageCoverage]): Unit = {
+ writeTag(
+ "packages",
+ Map.empty,
+ () =>
+ packages.forEach { p =>
+ writeTag(
+ "package",
+ Map(
+ "name" -> javaNames.getPackageName(p.getName),
+ "line-rate" -> p.getLineCounter.getCoveredRatio,
+ "branch-rate" -> p.getBranchCounter.getCoveredRatio,
+ "complexity" -> p.getComplexityCounter.getCoveredRatio
+ ),
+ () => writeClasses(p.getClasses)
+ )
+ }
+ )
+ }
+
+ private def writeClasses(classes: util.Collection[IClassCoverage]): Unit = {
+ writeTag(
+ "classes",
+ Map.empty,
+ () =>
+ classes.forEach { cl =>
+ writeTag(
+ "class",
+ Map(
+ "name" -> javaNames.getClassName(cl.getName, cl.getSignature, cl.getSuperName, cl.getInterfaceNames),
+ "filename" -> s"""${cl.getPackageName}/${cl.getSourceFileName}""",
+ "line-rate" -> cl.getLineCounter.getCoveredRatio,
+ "branch-rate" -> cl.getBranchCounter.getCoveredRatio,
+ "complexity" -> cl.getComplexityCounter.getCoveredRatio
+ ),
+ () => writeMethods(cl.getName, cl.getMethods)
+ )
+ }
+ )
+ }
+
+ private def writeMethods(className: String, methods: util.Collection[IMethodCoverage]): Unit = {
+ writeTag(
+ "methods",
+ Map.empty,
+ () =>
+ methods.forEach { m =>
+ writeTag(
+ "method",
+ Map(
+ "name" -> javaNames.getMethodName(className, m.getName, m.getDesc, m.getSignature),
+ "signature" -> m.getSignature,
+ "line-rate" -> m.getLineCounter.getCoveredRatio,
+ "branch-rate" -> m.getBranchCounter.getCoveredRatio,
+ "complexity" -> m.getComplexityCounter.getCoveredRatio
+ ),
+ () => writeLines(m)
+ )
+ }
+ )
+ }
+
+ private def writeLines(m: IMethodCoverage): Unit = {
+ writeTag(
+ "lines",
+ Map.empty,
+ () =>
+ for (nr <- m.getFirstLine to m.getLastLine) {
+ val line = m.getLine(nr)
+ if (line.getStatus >= 1) {
+ writeTag(
+ "line",
+ Map(
+ "number" -> nr,
+ "hits" -> (if (line.getStatus == 1) 0 else 1),
+ "branch" -> (line.getBranchCounter.getStatus == 1)
+ )
+ )
+ }
+ }
+ )
+ }
+
+ override def visitBundle(bundle: IBundleCoverage, locator: ISourceFileLocator): Unit = {
+ writeTag(
+ "coverage",
+ Map(
+ "line-rate" -> bundle.getLineCounter.getCoveredRatio,
+ "lines-valid" -> bundle.getLineCounter.getTotalCount,
+ "lines-covered" -> bundle.getLineCounter.getCoveredCount,
+ "branches-valid" -> bundle.getBranchCounter.getTotalCount,
+ "branches-covered" -> bundle.getBranchCounter.getCoveredCount,
+ "branch-rate" -> bundle.getBranchCounter.getCoveredRatio,
+ "complexity" -> bundle.getComplexityCounter.getCoveredRatio,
+ "version" -> "1.0",
+ "timestamp" -> System.currentTimeMillis()
+ ),
+ () => {
+ writeTag(
+ "sources",
+ Map.empty,
+ () => {
+ val sourcePathsRoot: Seq[File] = locator.asInstanceOf[DirectoriesSourceFileLocator].directories
+ val hashSet = new mutable.HashSet[File]()
+ sourcePathsRoot.foreach { p =>
+ val arr: Array[File] = p.listFiles((dir, name) => name != "resources")
+ if (arr != null) arr.foreach(hashSet.add)
+ }
+ val sourcePaths = hashSet.toList
+ writer.write(" --source\n")
+ sourcePaths.foreach(source => writer.write(s" ${source.getPath}\n"))
+ }
+ )
+ writePackages(bundle.getPackages)
+ }
+ )
+ }
+
+ override def visitGroup(name: String): IReportGroupVisitor = groupVisitor
+
+}
+
+class CoberturaFormatter() {
+ def createVisitor(output: OutputStream): IReportVisitor = {
+ new CoberturaRootVisitor(new OutputStreamWriter(output, "UTF-8"))
+ }
+}