diff --git a/README.md b/README.md
index adff017..fb0d602 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,11 @@ Restart sonarqube either using the update center or manually.
The rules in scalastyle are almost all deactivated. They must be activated and either make scala rules inherit scalastyle rules or change the project's rules.
+For more information about either scalastyle rules or scoverage results please consult their upstream documentation first:
+
+* [NCR-CoDE/sonar-scalastyle](https://github.com/NCR-CoDE/sonar-scalastyle)
+* [RadoBuransky/sonar-scoverage-plugin](https://github.com/RadoBuransky/sonar-scoverage-plugin)
+
# Build from source
```mvn package```
@@ -19,7 +24,7 @@ sonar-runner -D sonar.projectKey=Sagacify:sonar-scala
```
# Contributing
-Any contribution in the form of a pull request or a signed patch will be accepted.
+Contributions wre accepted in the form of a pull request or a signed patch.
Please follow the semantic changelog to format your commits [cfr]((https://github.com/Sagacify/komitet-gita-bezopasnosti).
All changes are submitted to automated tests that must pass for the pull-request to be merged.
@@ -47,5 +52,7 @@ Here is a list of the main ones.
[NCR-CoDE/sonar-scalastyle](https://github.com/NCR-CoDE/sonar-scalastyle)
+[RadoBuransky/sonar-scoverage-plugin](https://github.com/RadoBuransky/sonar-scoverage-plugin)
+
# Integration
-Sonar-scala integrates the latest code from the [Sonar Scalastyle Plugin](https://github.com/NCR-CoDE/sonar-scalastyle) directly. All thise files must keep their original license. Also their history was pulled along with them. Any further change upstream should be incorporated using cherry-picks or merges.
+For ease of use, Sonar Scala directly integrates the latest code from the [Sonar Scalastyle Plugin](https://github.com/NCR-CoDE/sonar-scalastyle) and [Sonar Scoverage Plugin](https://github.com/RadoBuransky/sonar-scoverage-plugin). This is possible as all three projects are released under the LGPL3 license. Nevertheless, all merged files are to keep their original copyright, classpath, and commit history. Any further change upstream should be incorporated using cherry-picks or merges.
diff --git a/plugin/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb b/plugin/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb
new file mode 100644
index 0000000..065d5c8
--- /dev/null
+++ b/plugin/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb
@@ -0,0 +1,10 @@
+<% measure=measure('scoverage')
+ if measure
+%>
+
+
+ Statement coverage : <%= format_measure(measure, :suffix => ' %') %>
+ <%= dashboard_configuration.selected_period? ? format_variation(measure) : trend_icon(measure) -%>
+
+
+<% end %>
\ No newline at end of file
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala
new file mode 100644
index 0000000..be34dd4
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala
@@ -0,0 +1,22 @@
+package com.buransky.plugins.scoverage
+
+import com.buransky.plugins.scoverage.language.Scala
+import org.sonar.api.resources.Languages
+import org.sonar.api.{Extension, ExtensionProvider, ServerExtension}
+
+import scala.collection.JavaConversions._
+import scala.collection.mutable.ListBuffer
+
+class ScoverageExtensionProvider(languages: Languages) extends ExtensionProvider with ServerExtension {
+ override def provide(): java.util.List[Class[_ <: Extension]] = {
+ val result = ListBuffer[Class[_ <: Extension]]()
+
+ if (languages.get(Scala.key) == null) {
+ // Fix issue with multiple Scala plugins:
+ // https://github.com/RadoBuransky/sonar-scoverage-plugin/issues/31
+ result += classOf[Scala]
+ }
+
+ result
+ }
+}
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala
new file mode 100644
index 0000000..8fa95a9
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala
@@ -0,0 +1,45 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage
+
+import com.buransky.plugins.scoverage.measure.ScalaMetrics
+import com.buransky.plugins.scoverage.sensor.ScoverageSensor
+import com.buransky.plugins.scoverage.widget.ScoverageWidget
+import org.sonar.api.{Extension, SonarPlugin}
+
+import scala.collection.JavaConversions._
+import scala.collection.mutable.ListBuffer
+
+/**
+ * Plugin entry point.
+ *
+ * @author Rado Buransky
+ */
+class ScoveragePlugin extends SonarPlugin {
+ override def getExtensions: java.util.List[Class[_ <: Extension]] =
+ ListBuffer(
+ classOf[ScoverageExtensionProvider],
+ classOf[ScalaMetrics],
+ classOf[ScoverageSensor],
+ classOf[ScoverageWidget]
+ )
+
+ override val toString = getClass.getSimpleName
+}
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala
new file mode 100644
index 0000000..72d40d0
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala
@@ -0,0 +1,39 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage
+
+import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer
+
+/**
+ * Interface for Scoverage report parser.
+ *
+ * @author Rado Buransky
+ */
+trait ScoverageReportParser {
+ def parse(reportFilePath: String, pathSanitizer: PathSanitizer): ProjectStatementCoverage
+}
+
+/**
+ * Common Scoverage exception.
+ *
+ * @author Rado Buransky
+ */
+case class ScoverageException(message: String, source: Throwable = null)
+ extends Exception(message, source)
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala
new file mode 100644
index 0000000..49ec40c
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala
@@ -0,0 +1,145 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage
+
+/**
+ * Statement coverage represents rate at which are statements of a certain source code unit
+ * being covered by tests.
+ *
+ * @author Rado Buransky
+ */
+sealed trait StatementCoverage {
+ /**
+ * Percentage rate ranging from 0 up to 100%.
+ */
+ lazy val rate: Double =
+ if (statementCount == 0)
+ 0.0
+ else
+ (coveredStatementsCount.toDouble / statementCount.toDouble) * 100.0
+
+ /**
+ * Total number of all statements within the source code unit,
+ */
+ def statementCount: Int
+
+ /**
+ * Number of statements covered by unit tests.
+ */
+ def coveredStatementsCount: Int
+
+ require(statementCount >= 0, "Statements count cannot be negative! [" + statementCount + "]")
+ require(coveredStatementsCount >= 0, "Statements count cannot be negative! [" +
+ coveredStatementsCount + "]")
+ require(coveredStatementsCount <= statementCount,
+ "Number of covered statements cannot be more than total number of statements! [" +
+ statementCount + ", " + coveredStatementsCount + "]")
+}
+
+/**
+ * Allows to build tree structure from state coverage values.
+ */
+trait NodeStatementCoverage extends StatementCoverage {
+ def name: String
+ def children: Iterable[NodeStatementCoverage]
+ def statementSum: Int = children.map(_.statementSum).sum
+ def coveredStatementsSum: Int = children.map(_.coveredStatementsSum).sum
+}
+
+/**
+ * Root node. In multi-module projects it can contain other ProjectStatementCoverage
+ * elements as children.
+ */
+case class ProjectStatementCoverage(name: String, children: Iterable[NodeStatementCoverage])
+ extends NodeStatementCoverage {
+ // projects' coverage values are defined as sums of their child values
+ val statementCount = statementSum
+ val coveredStatementsCount = coveredStatementsSum
+}
+
+/**
+ * Physical directory in file system.
+ */
+case class DirectoryStatementCoverage(name: String, children: Iterable[NodeStatementCoverage])
+ extends NodeStatementCoverage {
+ // directories' coverage values are defined as sums of their DIRECT child values
+ val statementCount = children.filter(_.isInstanceOf[FileStatementCoverage]).map(_.statementCount).sum
+ val coveredStatementsCount = children.filter(_.isInstanceOf[FileStatementCoverage]).map(_.coveredStatementsCount).sum
+}
+
+/**
+ * Scala source code file.
+ */
+case class FileStatementCoverage(name: String, statementCount: Int, coveredStatementsCount: Int,
+ statements: Iterable[CoveredStatement]) extends NodeStatementCoverage {
+ // leaf implementation sums==values
+ val children = List.empty[NodeStatementCoverage]
+ override val statementSum = statementCount
+ override val coveredStatementsSum = coveredStatementsCount
+}
+
+/**
+ * Position a Scala source code file.
+ */
+case class StatementPosition(line: Int, pos: Int)
+
+/**
+ * Coverage information about the Scala statement.
+ *
+ * @param start Starting position of the statement.
+ * @param end Ending position of the statement.
+ * @param hitCount How many times has the statement been hit by unit tests. Zero means
+ * that the statement is not covered.
+ */
+case class CoveredStatement(start: StatementPosition, end: StatementPosition, hitCount: Int)
+
+/**
+ * Aggregated statement coverage for a given source code line.
+ */
+case class CoveredLine(line: Int, hitCount: Int)
+
+object StatementCoverage {
+ /**
+ * Utility method to transform statement coverage to line coverage. Pessimistic logic is used
+ * meaning that line hit count is minimum of hit counts of all statements on the given line.
+ *
+ * Example: If a line contains two statements, one is covered by 3 hits, the other one is
+ * without any hits, then the whole line is treated as uncovered.
+ *
+ * @param statements Statement coverage.
+ * @return Line coverage.
+ */
+ def statementCoverageToLineCoverage(statements: Iterable[CoveredStatement]): Iterable[CoveredLine] = {
+ // Handle statements that end on a different line than start
+ val multilineStatements = statements.filter { s => s.start.line != s.end.line }
+ val extraStatements = multilineStatements.flatMap { s =>
+ for (i <- (s.start.line + 1) to s.end.line)
+ yield CoveredStatement(StatementPosition(i, 0), StatementPosition(i, 0), s.hitCount)
+ }
+
+ // Group statements by starting line
+ val lineStatements = (statements ++ extraStatements).groupBy(_.start.line)
+
+ // Pessimistic approach: line hit count is a minimum of hit counts of all statements on the line
+ lineStatements.map { lineStatement =>
+ CoveredLine(lineStatement._1, lineStatement._2.map(_.hitCount).min)
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala
new file mode 100644
index 0000000..43d881c
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala
@@ -0,0 +1,37 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.language
+
+import org.sonar.api.resources.AbstractLanguage
+
+/**
+ * Scala language.
+ *
+ * @author Rado Buransky
+ */
+class Scala extends AbstractLanguage(Scala.key, Scala.name) {
+ val getFileSuffixes = Array(Scala.fileExtension)
+}
+
+object Scala {
+ val key = "scala"
+ val name = "Scala"
+ val fileExtension = "scala"
+}
\ No newline at end of file
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala
new file mode 100644
index 0000000..abce710
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala
@@ -0,0 +1,66 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.measure
+
+import org.sonar.api.measures.{CoreMetrics, Metric, Metrics}
+import org.sonar.api.measures.Metric.ValueType
+import scala.collection.JavaConversions._
+import scala.collection.mutable.ListBuffer
+
+/**
+ * Statement coverage metric definition.
+ *
+ * @author Rado Buransky
+ */
+class ScalaMetrics extends Metrics {
+ override def getMetrics = ListBuffer(ScalaMetrics.statementCoverage, ScalaMetrics.coveredStatements, ScalaMetrics.totalStatements).toList
+}
+
+object ScalaMetrics {
+ private val STATEMENT_COVERAGE_KEY = "scoverage"
+ private val COVERED_STATEMENTS_KEY = "covered_statements"
+ private val TOTAL_STATEMENTS_KEY = "total_statements"
+
+ lazy val statementCoverage = new Metric.Builder(STATEMENT_COVERAGE_KEY,
+ "Statement coverage", ValueType.PERCENT)
+ .setDescription("Statement coverage by tests")
+ .setDirection(Metric.DIRECTION_BETTER)
+ .setQualitative(true)
+ .setDomain(CoreMetrics.DOMAIN_TESTS)
+ .setWorstValue(0.0)
+ .setBestValue(100.0)
+ .create[java.lang.Double]()
+
+ lazy val coveredStatements = new Metric.Builder(COVERED_STATEMENTS_KEY,
+ "Covered statements", Metric.ValueType.INT)
+ .setDescription("Number of statements covered by tests")
+ .setDirection(Metric.DIRECTION_BETTER)
+ .setQualitative(false)
+ .setDomain(CoreMetrics.DOMAIN_SIZE)
+ .create[java.lang.Integer]()
+
+ lazy val totalStatements = new Metric.Builder(TOTAL_STATEMENTS_KEY,
+ "Total statements", Metric.ValueType.INT)
+ .setDescription("Number of all statements covered by tests and uncovered")
+ .setDirection(Metric.DIRECTION_BETTER)
+ .setQualitative(false)
+ .setDomain(CoreMetrics.DOMAIN_SIZE)
+ .create[java.lang.Integer]()
+}
\ No newline at end of file
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala
new file mode 100644
index 0000000..5acd4f7
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala
@@ -0,0 +1,86 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.pathcleaner
+
+import java.io.File
+import org.apache.commons.io.FileUtils
+import org.apache.commons.io.FileUtils
+import BruteForceSequenceMatcher._
+import com.buransky.plugins.scoverage.util.PathUtil
+import scala.collection.JavaConversions._
+import org.sonar.api.utils.log.Loggers
+
+object BruteForceSequenceMatcher {
+
+ val extensions = Array[String]("java", "scala")
+
+ type PathSeq = Seq[String]
+}
+
+/**
+ * Helper that allows to convert a report path into a source folder relative path by testing it against
+ * the tree of source files.
+ *
+ * Assumes that all report paths of a given report have a common root. Dependent of the scoverage
+ * report this root is either something outside the actual project (absolute path), the base dir of the project
+ * (report path relative to base dir) or some sub folder of the project.
+ *
+ * By reverse mapping a report path against the tree of all file children of the source folder the correct filesystem file
+ * can be found and the report path can be converted into a source dir relative path. *
+ *
+ * @author Michael Zinsmaier
+ */
+class BruteForceSequenceMatcher(baseDir: File, sourcePath: String) extends PathSanitizer {
+
+ private val sourceDir = initSourceDir()
+ require(sourceDir.isAbsolute)
+ require(sourceDir.isDirectory)
+
+ private val log = Loggers.get(classOf[BruteForceSequenceMatcher])
+ private val sourcePathLength = PathUtil.splitPath(sourceDir.getAbsolutePath).size
+ private val filesMap = initFilesMap()
+
+
+ def getSourceRelativePath(reportPath: PathSeq): Option[PathSeq] = {
+ // match with file system map of files
+ val relPathOption = for {
+ absPathCandidates <- filesMap.get(reportPath.last)
+ path <- absPathCandidates.find(absPath => absPath.endsWith(reportPath))
+ } yield path.drop(sourcePathLength)
+
+ relPathOption
+ }
+
+ // mock able helpers that allow us to remove the dependency to the real file system during tests
+
+ private[pathcleaner] def initSourceDir(): File = {
+ val sourceDir = new File(baseDir, sourcePath)
+ sourceDir
+ }
+
+ private[pathcleaner] def initFilesMap(): Map[String, Seq[PathSeq]] = {
+ val srcFiles = FileUtils.iterateFiles(sourceDir, extensions, true)
+ val paths = srcFiles.map(file => PathUtil.splitPath(file.getAbsolutePath)).toSeq
+
+ // group them by filename, in case multiple files have the same name
+ paths.groupBy(path => path.last)
+ }
+
+}
\ No newline at end of file
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala
new file mode 100644
index 0000000..d4a1b79
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala
@@ -0,0 +1,34 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.pathcleaner
+
+/**
+ * @author Michael Zinsmaier
+ */
+trait PathSanitizer {
+
+ /** tries to convert the given path such that it is relative to the
+ * projects/modules source directory.
+ *
+ * @return Some(source folder relative path) or None if the path cannot be converted
+ */
+ def getSourceRelativePath(path: Seq[String]): Option[Seq[String]]
+
+}
\ No newline at end of file
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala
new file mode 100644
index 0000000..78091d7
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala
@@ -0,0 +1,259 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.sensor
+
+import com.buransky.plugins.scoverage.language.Scala
+import com.buransky.plugins.scoverage.measure.ScalaMetrics
+import com.buransky.plugins.scoverage.pathcleaner.{BruteForceSequenceMatcher, PathSanitizer}
+import com.buransky.plugins.scoverage.util.LogUtil
+import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser
+import com.buransky.plugins.scoverage.{CoveredStatement, DirectoryStatementCoverage, FileStatementCoverage, _}
+import org.sonar.api.batch.fs.{FileSystem, InputFile, InputPath}
+import org.sonar.api.batch.{CoverageExtension, Sensor, SensorContext}
+import org.sonar.api.config.Settings
+import org.sonar.api.measures.{CoverageMeasuresBuilder, Measure}
+import org.sonar.api.resources.{Project, Resource}
+import org.sonar.api.scan.filesystem.PathResolver
+import org.sonar.api.utils.log.Loggers
+
+import scala.collection.JavaConversions._
+
+/**
+ * Main sensor for importing Scoverage report to Sonar.
+ *
+ * @author Rado Buransky
+ */
+class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem: FileSystem)
+ extends Sensor with CoverageExtension {
+ private val log = Loggers.get(classOf[ScoverageSensor])
+ protected val SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath"
+ protected lazy val scoverageReportParser: ScoverageReportParser = XmlScoverageReportParser()
+
+ override def shouldExecuteOnProject(project: Project): Boolean = fileSystem.languages().contains(Scala.key)
+
+ override def analyse(project: Project, context: SensorContext) {
+ scoverageReportPath match {
+ case Some(reportPath) =>
+ // Single-module project
+ val srcOption = Option(settings.getString(project.getName() + ".sonar.sources"))
+ val sonarSources = srcOption match {
+ case Some(src) => src
+ case None => {
+ log.warn(s"could not find settings key ${project.getName()}.sonar.sources assuming src/main/scala.")
+ "src/main/scala"
+ }
+ }
+ val pathSanitizer = createPathSanitizer(sonarSources)
+ processProject(scoverageReportParser.parse(reportPath, pathSanitizer), project, context, sonarSources)
+
+ case None =>
+ // Multi-module project has report path set for each module individually
+ analyseMultiModuleProject(project, context)
+ }
+ }
+
+ override val toString = getClass.getSimpleName
+
+ protected def createPathSanitizer(sonarSources: String): PathSanitizer
+ = new BruteForceSequenceMatcher(fileSystem.baseDir(), sonarSources)
+
+ private lazy val scoverageReportPath: Option[String] = {
+ settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY) match {
+ case null => None
+ case path: String =>
+ pathResolver.relativeFile(fileSystem.baseDir, path) match {
+ case report: java.io.File if !report.exists || !report.isFile =>
+ log.error(LogUtil.f("Report not found at {}"), report)
+ None
+
+ case report: java.io.File => Some(report.getAbsolutePath)
+ }
+ }
+ }
+
+ private def analyseMultiModuleProject(project: Project, context: SensorContext) {
+ project.isModule match {
+ case true => log.warn(LogUtil.f("Report path not set for " + project.name + " module! [" +
+ project.name + "." + SCOVERAGE_REPORT_PATH_PROPERTY + "]"))
+ case _ =>
+ // Compute overall statement coverage from submodules
+ val totalStatementCount = project.getModules.map(analyseStatementCountForModule(_, context)).sum
+ val coveredStatementCount = project.getModules.map(analyseCoveredStatementCountForModule(_, context)).sum
+
+ if (totalStatementCount > 0) {
+ // Convert to percentage
+ val overall = (coveredStatementCount.toDouble / totalStatementCount.toDouble) * 100.0
+
+ // Set overall statement coverage
+ context.saveMeasure(project, createStatementCoverage(overall))
+
+ log.info(LogUtil.f("Overall statement coverage is " + ("%1.2f" format overall)))
+ }
+ }
+ }
+
+ private def analyseCoveredStatementCountForModule(module: Project, context: SensorContext): Long = {
+ // Aggregate modules
+ context.getMeasure(module, ScalaMetrics.coveredStatements) match {
+ case null =>
+ log.debug(LogUtil.f("Module has no statement coverage. [" + module.name + "]"))
+ 0
+ case moduleCoveredStatementCount: Measure[_] =>
+ log.debug(LogUtil.f("Covered statement count for " + module.name + " module. [" +
+ moduleCoveredStatementCount.getValue + "]"))
+
+ moduleCoveredStatementCount.getValue.toLong
+ }
+ }
+
+ private def analyseStatementCountForModule(module: Project, context: SensorContext): Long = {
+ // Aggregate modules
+ context.getMeasure(module, ScalaMetrics.totalStatements) match {
+ case null =>
+ log.debug(LogUtil.f("Module has no number of statements. [" + module.name + "]"))
+ 0
+
+ case moduleStatementCount: Measure[_] =>
+ log.debug(LogUtil.f("Statement count for " + module.name + " module. [" +
+ moduleStatementCount.getValue + "]"))
+
+ moduleStatementCount.getValue.toLong
+ }
+ }
+
+ private def processProject(projectCoverage: ProjectStatementCoverage, project: Project, context: SensorContext, sonarSources: String) {
+ // Save measures
+ saveMeasures(context, project, projectCoverage)
+
+ log.info(LogUtil.f("Statement coverage for " + project.getKey + " is " + ("%1.2f" format projectCoverage.rate)))
+
+ // Process children
+ processChildren(projectCoverage.children, context, sonarSources)
+ }
+
+ private def processDirectory(directoryCoverage: DirectoryStatementCoverage, context: SensorContext, parentDirectory: String) {
+ // save measures if any
+ if (directoryCoverage.statementCount > 0) {
+ val path = appendFilePath(parentDirectory, directoryCoverage.name)
+
+ getResource(path, context, false) match {
+ case Some(srcDir) => {
+ // Save directory measures
+ saveMeasures(context, srcDir, directoryCoverage)
+ }
+ case None =>
+ }
+ }
+ // Process children
+ processChildren(directoryCoverage.children, context, appendFilePath(parentDirectory, directoryCoverage.name))
+ }
+
+ private def processFile(fileCoverage: FileStatementCoverage, context: SensorContext, directory: String) {
+ val path = appendFilePath(directory, fileCoverage.name)
+
+ getResource(path, context, true) match {
+ case Some(scalaSourceFile) => {
+ // Save measures
+ saveMeasures(context, scalaSourceFile, fileCoverage)
+ // Save line coverage. This is needed just for source code highlighting.
+ saveLineCoverage(fileCoverage.statements, scalaSourceFile, context)
+ }
+ case None =>
+ }
+ }
+
+ private def getResource(path: String, context: SensorContext, isFile: Boolean): Option[Resource] = {
+
+ val inputOption: Option[InputPath] = if (isFile) {
+ val p = fileSystem.predicates()
+ Option(fileSystem.inputFile(p.and(
+ p.hasRelativePath(path),
+ p.hasLanguage(Scala.key),
+ p.hasType(InputFile.Type.MAIN))))
+ } else {
+ Option(fileSystem.inputDir(pathResolver.relativeFile(fileSystem.baseDir(), path)))
+ }
+
+ inputOption match {
+ case Some(path: InputPath) =>
+ Some(context.getResource(path))
+ case None => {
+ log.warn(s"File or directory not found in file system! ${path}")
+ None
+ }
+ }
+ }
+
+ private def saveMeasures(context: SensorContext, resource: Resource, statementCoverage: StatementCoverage) {
+ context.saveMeasure(resource, createStatementCoverage(statementCoverage.rate))
+ context.saveMeasure(resource, createStatementCount(statementCoverage.statementCount))
+ context.saveMeasure(resource, createCoveredStatementCount(statementCoverage.coveredStatementsCount))
+
+ log.debug(LogUtil.f("Save measures [" + statementCoverage.rate + ", " + statementCoverage.statementCount +
+ ", " + statementCoverage.coveredStatementsCount + ", " + resource.getKey + "]"))
+ }
+
+ private def saveLineCoverage(coveredStatements: Iterable[CoveredStatement], resource: Resource,
+ context: SensorContext) {
+ // Convert statements to lines
+ val coveredLines = StatementCoverage.statementCoverageToLineCoverage(coveredStatements)
+
+ // Set line hits
+ val coverage = CoverageMeasuresBuilder.create()
+ coveredLines.foreach { coveredLine =>
+ coverage.setHits(coveredLine.line, coveredLine.hitCount)
+ }
+
+ // Save measures
+ coverage.createMeasures().toList.foreach(context.saveMeasure(resource, _))
+ }
+
+ private def processChildren(children: Iterable[StatementCoverage], context: SensorContext, directory: String) {
+ children.foreach(processChild(_, context, directory))
+ }
+
+ private def processChild(dirOrFile: StatementCoverage, context: SensorContext, directory: String) {
+ dirOrFile match {
+ case dir: DirectoryStatementCoverage => processDirectory(dir, context, directory)
+ case file: FileStatementCoverage => processFile(file, context, directory)
+ case _ => throw new IllegalStateException("Not a file or directory coverage! [" +
+ dirOrFile.getClass.getName + "]")
+ }
+ }
+
+ private def createStatementCoverage[T <: Serializable](rate: Double): Measure[T] =
+ new Measure[T](ScalaMetrics.statementCoverage, rate)
+
+ private def createStatementCount[T <: Serializable](statements: Int): Measure[T] =
+ new Measure(ScalaMetrics.totalStatements, statements.toDouble, 0)
+
+ private def createCoveredStatementCount[T <: Serializable](coveredStatements: Int): Measure[T] =
+ new Measure(ScalaMetrics.coveredStatements, coveredStatements.toDouble, 0)
+
+ private def appendFilePath(src: String, name: String) = {
+ val result = src match {
+ case java.io.File.separator => java.io.File.separator
+ case empty if empty.isEmpty => ""
+ case other => other + java.io.File.separator
+ }
+
+ result + name
+ }
+}
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala
new file mode 100644
index 0000000..bc28dd1
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala
@@ -0,0 +1,29 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.util
+
+/**
+ * Logging helper.
+ *
+ * @author Rado Buransky
+ */
+object LogUtil {
+ def f(msg: String) = "[scoverage] " + msg
+}
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala
new file mode 100644
index 0000000..1981fcf
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala
@@ -0,0 +1,43 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.util
+
+import java.io.File
+import scala.Iterator
+/**
+ * File path helper.
+ *
+ * @author Rado Buransky
+ */
+object PathUtil {
+
+ def splitPath(filePath: String): List[String] = {
+ new FileParentIterator(new File(filePath)).toList.reverse
+ }
+
+ class FileParentIterator(private var f: File) extends Iterator[String] {
+ def hasNext: Boolean = f != null && !f.getName().isEmpty()
+ def next(): String = {
+ val name = f.getName()
+ f = f.getParentFile
+ name
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala
new file mode 100644
index 0000000..ba0a06e
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala
@@ -0,0 +1,33 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.widget
+
+import org.sonar.api.web.{RubyRailsWidget, AbstractRubyTemplate}
+
+/**
+ * UI widget that can be added to the main dashboard to display overall statement coverage for the project.
+ *
+ * @author Rado Buransky
+ */
+class ScoverageWidget extends AbstractRubyTemplate with RubyRailsWidget {
+ val getId = "scoverage"
+ val getTitle = "Statement coverage"
+ override val getTemplatePath = "/com/buransky/plugins/scoverage/widget.html.erb"
+}
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala
new file mode 100644
index 0000000..d77c5e4
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala
@@ -0,0 +1,227 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.xml
+
+import java.io.File
+
+import com.buransky.plugins.scoverage._
+import com.buransky.plugins.scoverage.util.PathUtil
+import org.sonar.api.utils.log.Loggers
+
+import scala.annotation.tailrec
+import scala.collection.mutable
+import scala.io.Source
+import scala.xml.parsing.ConstructingParser
+import scala.xml.{MetaData, NamespaceBinding, Text}
+import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer
+
+/**
+ * Scoverage XML parser based on ConstructingParser provided by standard Scala library.
+ *
+ * @author Rado Buransky
+ */
+class XmlScoverageReportConstructingParser(source: Source, pathSanitizer: PathSanitizer) extends ConstructingParser(source, false) {
+ private val log = Loggers.get(classOf[XmlScoverageReportConstructingParser])
+
+ private val CLASS_ELEMENT = "class"
+ private val FILENAME_ATTRIBUTE = "filename"
+ private val STATEMENT_ELEMENT = "statement"
+ private val START_ATTRIBUTE = "start"
+ private val LINE_ATTRIBUTE = "line"
+ private val INVOCATION_COUNT_ATTRIBUTE = "invocation-count"
+
+ val statementsInFile: mutable.HashMap[String, List[CoveredStatement]] = mutable.HashMap.empty
+ var currentFilePath: Option[String] = None
+
+ def parse(): ProjectStatementCoverage = {
+ // Initialize
+ nextch()
+
+ // Parse
+ document()
+
+ // Transform map to project
+ projectFromMap(statementsInFile.toMap)
+ }
+
+ override def elemStart(pos: Int, pre: String, label: String, attrs: MetaData, scope: NamespaceBinding) {
+ label match {
+ case CLASS_ELEMENT =>
+ currentFilePath = Some(fixLeadingSlash(getText(attrs, FILENAME_ATTRIBUTE)))
+ log.debug("Current file path: " + currentFilePath.get)
+
+ case STATEMENT_ELEMENT =>
+ currentFilePath match {
+ case Some(cfp) =>
+ val start = getInt(attrs, START_ATTRIBUTE)
+ val line = getInt(attrs, LINE_ATTRIBUTE)
+ val hits = getInt(attrs, INVOCATION_COUNT_ATTRIBUTE)
+
+ // Add covered statement to the mutable map
+ val pos = StatementPosition(line, start)
+ addCoveredStatement(cfp, CoveredStatement(pos, pos, hits))
+
+ log.debug("Statement added: " + line + ", " + hits + ", " + start)
+
+ case None => throw new ScoverageException("Current file path not set!")
+ }
+ case _ => // Nothing to do
+ }
+
+ super.elemStart(pos, pre, label, attrs, scope)
+ }
+
+ private def addCoveredStatement(filePath: String, coveredStatement: CoveredStatement) {
+ statementsInFile.get(filePath) match {
+ case None => statementsInFile.put(filePath, List(coveredStatement))
+ case Some(s) => statementsInFile.update(filePath, coveredStatement :: s)
+ }
+ }
+
+ /**
+ * Remove this when scoverage is fixed! It's just a hack.
+ * Old Scoverage has incorrectly added leading '/' to relative file paths.
+ */
+ private def fixLeadingSlash(filePath: String) = {
+ if (filePath.startsWith(File.separator) && !new File(filePath).exists()) {
+ filePath.drop(File.separator.length)
+ }
+ else
+ filePath
+ }
+
+ private def getInt(attrs: MetaData, name: String) = getText(attrs, name).toInt
+
+ private def getText(attrs: MetaData, name: String): String = {
+ attrs.get(name) match {
+ case Some(attr) =>
+ attr match {
+ case text: Text => text.toString()
+ case _ => throw new ScoverageException("Not a text attribute!")
+ }
+ case None => throw new ScoverageException("Attribute doesn't exit! [" + name + "]")
+ }
+ }
+
+ private case class DirOrFile(name: String, var children: List[DirOrFile],
+ coverage: Option[FileStatementCoverage]) {
+ def get(name: String) = children.find(_.name == name)
+
+ @tailrec
+ final def add(chain: DirOrFile) {
+ get(chain.name) match {
+ case None => children = chain :: children
+ case Some(child) =>
+ chain.children match {
+ case h :: t =>
+ if (t != Nil)
+ throw new IllegalStateException("This is not a linear chain!")
+ child.add(h)
+ case _ => // Duplicate file? Should not happen.
+ }
+ }
+ }
+
+ def toStatementCoverage: NodeStatementCoverage = {
+ val childNodes = children.map(_.toStatementCoverage)
+
+ childNodes match {
+ case Nil => coverage.get
+ case _ => DirectoryStatementCoverage(name, childNodes)
+ }
+ }
+
+ def toProjectStatementCoverage: ProjectStatementCoverage = {
+ toStatementCoverage match {
+ case node: NodeStatementCoverage => ProjectStatementCoverage("", node.children)
+ case _ => throw new ScoverageException("Illegal statement coverage!")
+ }
+ }
+ }
+
+ private def projectFromMap(statementsInFile: Map[String, List[CoveredStatement]]):
+ ProjectStatementCoverage = {
+
+ // Transform to file statement coverage
+ val files = fileStatementCoverage(statementsInFile)
+
+ // Transform file paths to chain of case classes
+ val chained = files.map(fsc => pathToChain(fsc._1, fsc._2)).flatten
+
+ // Merge chains into one tree
+ val root = DirOrFile("", Nil, None)
+ chained.foreach(root.add)
+
+ // Transform file system tree into coverage structure tree
+ root.toProjectStatementCoverage
+ }
+
+ private def pathToChain(filePath: String, coverage: FileStatementCoverage): Option[DirOrFile] = {
+ // helper
+ def convertToDirOrFile(relPath: Seq[String]) = {
+ // Get directories
+ val dirs = for (i <- 0 to relPath.length - 2)
+ yield DirOrFile(relPath(i), Nil, None)
+
+ // Chain directories
+ for (i <- 0 to dirs.length - 2)
+ dirs(i).children = List(dirs(i + 1))
+
+ // Get file
+ val file = DirOrFile(relPath(relPath.length - 1).toString, Nil, Some(coverage))
+
+ if (dirs.isEmpty) {
+ // File in root dir
+ file
+ } else {
+ // Append file
+ dirs.last.children = List(file)
+ dirs.head
+ }
+ }
+
+ // processing
+ val path = PathUtil.splitPath(filePath)
+
+ if (path.length < 1)
+ throw new ScoverageException("Path cannot be empty!")
+
+ pathSanitizer.getSourceRelativePath(path) match {
+ case Some(relPath) => Some(convertToDirOrFile(relPath))
+ case None => {
+ log.warn(s"skipping file coverage results for $path, was not able to retrieve the file in the configured source dir")
+ None
+ }
+ }
+ }
+
+ private def fileStatementCoverage(statementsInFile: Map[String, List[CoveredStatement]]):
+ Map[String, FileStatementCoverage] = {
+ statementsInFile.map { sif =>
+ val fileStatementCoverage = FileStatementCoverage(PathUtil.splitPath(sif._1).last,
+ sif._2.length, coveredStatements(sif._2), sif._2)
+
+ (sif._1, fileStatementCoverage)
+ }
+ }
+
+ private def coveredStatements(statements: Iterable[CoveredStatement]) =
+ statements.count(_.hitCount > 0)
+}
\ No newline at end of file
diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala
new file mode 100644
index 0000000..fe2036a
--- /dev/null
+++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala
@@ -0,0 +1,59 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.xml
+
+import com.buransky.plugins.scoverage.util.LogUtil
+import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageException, ScoverageReportParser}
+import org.sonar.api.utils.log.Loggers
+
+import scala.io.Source
+import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer
+
+/**
+ * Bridge between parser implementation and coverage provider.
+ *
+ * @author Rado Buransky
+ */
+class XmlScoverageReportParser extends ScoverageReportParser {
+ private val log = Loggers.get(classOf[XmlScoverageReportParser])
+
+ def parse(reportFilePath: String, pathSanitizer: PathSanitizer): ProjectStatementCoverage = {
+ require(reportFilePath != null)
+ require(!reportFilePath.trim.isEmpty)
+
+ log.debug(LogUtil.f("Reading report. [" + reportFilePath + "]"))
+
+ val parser = new XmlScoverageReportConstructingParser(sourceFromFile(reportFilePath), pathSanitizer)
+ parser.parse()
+ }
+
+ private def sourceFromFile(scoverageReportPath: String) = {
+ try {
+ Source.fromFile(scoverageReportPath)
+ }
+ catch {
+ case ex: Exception => throw ScoverageException("Cannot parse file! [" + scoverageReportPath + "]", ex)
+ }
+ }
+}
+
+object XmlScoverageReportParser {
+ def apply() = new XmlScoverageReportParser
+}
\ No newline at end of file
diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala
new file mode 100644
index 0000000..5fec3f6
--- /dev/null
+++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala
@@ -0,0 +1,117 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.pathcleaner
+
+import org.junit.runner.RunWith
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.FlatSpec
+import com.buransky.plugins.scoverage.pathcleaner.BruteForceSequenceMatcher.PathSeq
+import org.scalatest.Matchers
+import java.io.File
+import org.mockito.Mockito._
+
+@RunWith(classOf[JUnitRunner])
+class BruteForceSequenceMatcherSpec extends FlatSpec with Matchers with MockitoSugar {
+
+ // file-map of all files under baseDir/sonar.sources organized by their filename
+ val filesMap: Map[String, Seq[PathSeq]] = Map (
+ "rootTestFile.scala" -> List(List("testProject", "main", "rootTestFile.scala")),
+ "nestedTestFile.scala" -> List(List("testProject", "main", "some", "folders", "nestedTestFile.scala")),
+ "multiFile.scala" -> List(
+ List("testProject", "main", "some", "multiFile.scala"),
+ List("testProject", "main", "some", "folder", "multiFile.scala")
+ )
+ )
+
+ // baseDir = testProject sonar.sources = main
+ val testee = new BruteForceSequenceMatcherTestee("/testProject/main", filesMap)
+
+
+
+ behavior of "BruteForceSequenceMatcher with absolute report filenames"
+
+ it should "provide just the filename for top level files" in {
+ testee.getSourceRelativePath(List("testProject", "main", "rootTestFile.scala")).get shouldEqual List("rootTestFile.scala")
+ }
+
+ it should "provide the filename and the folders for nested files" in {
+ testee.getSourceRelativePath(List("testProject", "main", "some", "folders", "nestedTestFile.scala")).get shouldEqual List("some", "folders", "nestedTestFile.scala")
+ }
+
+ it should "find the correct file if multiple files with same name exist" in {
+ testee.getSourceRelativePath(List("testProject", "main", "some", "multiFile.scala")).get shouldEqual List("some", "multiFile.scala")
+ testee.getSourceRelativePath(List("testProject", "main", "some", "folder", "multiFile.scala")).get shouldEqual List("some", "folder", "multiFile.scala")
+ }
+
+
+
+
+ behavior of "BruteForceSequenceMatcher with filenames relative to the base dir"
+
+ it should "provide just the filename for top level files" in {
+ testee.getSourceRelativePath(List("main", "rootTestFile.scala")).get shouldEqual List("rootTestFile.scala")
+ }
+
+ it should "provide the filename and the folders for nested files" in {
+ testee.getSourceRelativePath(List("main", "some", "folders", "nestedTestFile.scala")).get shouldEqual List("some", "folders", "nestedTestFile.scala")
+ }
+
+ it should "find the correct file if multiple files with same name exist" in {
+ testee.getSourceRelativePath(List("main", "some", "multiFile.scala")).get shouldEqual List("some", "multiFile.scala")
+ testee.getSourceRelativePath(List("main", "some", "folder", "multiFile.scala")).get shouldEqual List("some", "folder", "multiFile.scala")
+ }
+
+
+
+
+ behavior of "BruteForceSequenceMatcher with filenames relative to the src dir"
+
+ it should "provide just the filename for top level files" in {
+ testee.getSourceRelativePath(List("rootTestFile.scala")).get shouldEqual List("rootTestFile.scala")
+ }
+
+ it should "provide the filename and the folders for nested files" in {
+ testee.getSourceRelativePath(List("some", "folders", "nestedTestFile.scala")).get shouldEqual List("some", "folders", "nestedTestFile.scala")
+ }
+
+ it should "find the correct file if multiple files with same name exist" in {
+ testee.getSourceRelativePath(List("some", "multiFile.scala")).get shouldEqual List("some", "multiFile.scala")
+ testee.getSourceRelativePath(List("some", "folder", "multiFile.scala")).get shouldEqual List("some", "folder", "multiFile.scala")
+ }
+
+
+
+
+ class BruteForceSequenceMatcherTestee(absoluteSrcPath: String, filesMap: Map[String, Seq[PathSeq]])
+ extends BruteForceSequenceMatcher(mock[File], "") {
+
+ def srcDir = {
+ val dir = mock[File]
+ when(dir.isAbsolute).thenReturn(true)
+ when(dir.isDirectory).thenReturn(true)
+ when(dir.getAbsolutePath).thenReturn(absoluteSrcPath)
+ dir
+ }
+
+ override private[pathcleaner] def initSourceDir(): File = srcDir
+ override private[pathcleaner] def initFilesMap(): Map[String, Seq[PathSeq]] = filesMap
+ }
+}
diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala
new file mode 100644
index 0000000..1dbca60
--- /dev/null
+++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala
@@ -0,0 +1,122 @@
+/*
+* Sonar Scoverage Plugin
+* Copyright (C) 2013 Rado Buransky
+* dev@sonar.codehaus.org
+*
+* This program is free software; you can redistribute it and/or
+* modify it under the terms of the GNU Lesser General Public
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or (at your option) any later version.
+*
+* 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
+* Lesser General Public License for more details.
+*
+* You should have received a copy of the GNU Lesser General Public
+* License along with this program; if not, write to the Free Software
+* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+*/
+package com.buransky.plugins.scoverage.sensor
+
+import java.io.File
+import java.util
+
+import com.buransky.plugins.scoverage.language.Scala
+import com.buransky.plugins.scoverage.{FileStatementCoverage, DirectoryStatementCoverage, ProjectStatementCoverage, ScoverageReportParser}
+import org.junit.runner.RunWith
+import org.mockito.Mockito._
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{FlatSpec, Matchers}
+import org.sonar.api.batch.fs.{FilePredicate, FilePredicates, FileSystem}
+import org.sonar.api.config.Settings
+import org.sonar.api.resources.Project
+import org.sonar.api.resources.Project.AnalysisType
+import org.sonar.api.scan.filesystem.PathResolver
+
+import scala.collection.JavaConversions._
+import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer
+import org.mockito.Matchers.any
+
+
+@RunWith(classOf[JUnitRunner])
+class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar {
+ behavior of "shouldExecuteOnProject"
+
+ it should "succeed for Scala project" in new ShouldExecuteOnProject {
+ checkShouldExecuteOnProject(List("scala"), true)
+ }
+
+ it should "succeed for mixed projects" in new ShouldExecuteOnProject {
+ checkShouldExecuteOnProject(List("scala", "java"), true)
+ }
+
+ it should "fail for Java project" in new ShouldExecuteOnProject {
+ checkShouldExecuteOnProject(List("java"), false)
+ }
+
+ class ShouldExecuteOnProject extends ScoverageSensorScope {
+ protected def checkShouldExecuteOnProject(languages: Iterable[String], expectedResult: Boolean) {
+ // Setup
+ val project = mock[Project]
+ when(fileSystem.languages()).thenReturn(new util.TreeSet(languages))
+
+ // Execute & asser
+ shouldExecuteOnProject(project) should equal(expectedResult)
+
+ verify(fileSystem, times(1)).languages
+
+ }
+ }
+
+ behavior of "analyse for single project"
+
+ it should "set 0% coverage for a project without children" in new AnalyseScoverageSensorScope {
+ // Setup
+ val pathToScoverageReport = "#path-to-scoverage-report#"
+ val reportAbsolutePath = "#report-absolute-path#"
+ val projectStatementCoverage =
+ ProjectStatementCoverage("project-name", List(
+ DirectoryStatementCoverage(File.separator, List(
+ DirectoryStatementCoverage("home", List(
+ FileStatementCoverage("a.scala", 3, 2, Nil)
+ ))
+ )),
+ DirectoryStatementCoverage("x", List(
+ FileStatementCoverage("b.scala", 1, 0, Nil)
+ ))
+ ))
+ val reportFile = mock[java.io.File]
+ val moduleBaseDir = mock[java.io.File]
+ val filePredicates = mock[FilePredicates]
+ when(reportFile.exists).thenReturn(true)
+ when(reportFile.isFile).thenReturn(true)
+ when(reportFile.getAbsolutePath).thenReturn(reportAbsolutePath)
+ when(settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY)).thenReturn(pathToScoverageReport)
+ when(fileSystem.baseDir).thenReturn(moduleBaseDir)
+ when(fileSystem.predicates).thenReturn(filePredicates)
+ when(fileSystem.inputFiles(any[FilePredicate]())).thenReturn(Nil)
+ when(pathResolver.relativeFile(moduleBaseDir, pathToScoverageReport)).thenReturn(reportFile)
+ when(scoverageReportParser.parse(any[String](), any[PathSanitizer]())).thenReturn(projectStatementCoverage)
+
+ // Execute
+ analyse(project, context)
+ }
+
+ class AnalyseScoverageSensorScope extends ScoverageSensorScope {
+ val project = mock[Project]
+ val context = new TestSensorContext
+
+ override protected lazy val scoverageReportParser = mock[ScoverageReportParser]
+ override protected def createPathSanitizer(sonarSources: String) = mock[PathSanitizer]
+ }
+
+ class ScoverageSensorScope extends {
+ val scala = new Scala
+ val settings = mock[Settings]
+ val pathResolver = mock[PathResolver]
+ val fileSystem = mock[FileSystem]
+ } with ScoverageSensor(settings, pathResolver, fileSystem)
+
+}
diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala
new file mode 100644
index 0000000..17f7f47
--- /dev/null
+++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala
@@ -0,0 +1,130 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.sensor
+
+import java.lang.Double
+import java.util.Date
+import java.{io, util}
+
+import org.sonar.api.batch.fs.{FileSystem, InputFile, InputPath}
+import org.sonar.api.batch.rule.ActiveRules
+import org.sonar.api.batch.sensor.dependency.NewDependency
+import org.sonar.api.batch.sensor.duplication.NewDuplication
+import org.sonar.api.batch.sensor.highlighting.NewHighlighting
+import org.sonar.api.batch.sensor.issue.NewIssue
+import org.sonar.api.batch.sensor.measure.NewMeasure
+import org.sonar.api.batch.{AnalysisMode, Event, SensorContext}
+import org.sonar.api.config.Settings
+import org.sonar.api.design.Dependency
+import org.sonar.api.measures.{Measure, MeasuresFilter, Metric}
+import org.sonar.api.resources.{ProjectLink, Resource}
+import org.sonar.api.rules.Violation
+
+import scala.collection.mutable
+
+class TestSensorContext extends SensorContext {
+
+ private val measures = mutable.Map[String, Measure[_ <: io.Serializable]]()
+
+ override def saveDependency(dependency: Dependency): Dependency = ???
+
+ override def isExcluded(reference: Resource): Boolean = ???
+
+ override def deleteLink(key: String): Unit = ???
+
+ override def isIndexed(reference: Resource, acceptExcluded: Boolean): Boolean = ???
+
+ override def saveViolations(violations: util.Collection[Violation]): Unit = ???
+
+ override def getParent(reference: Resource): Resource = ???
+
+ override def getOutgoingDependencies(from: Resource): util.Collection[Dependency] = ???
+
+ override def saveSource(reference: Resource, source: String): Unit = ???
+
+ override def getMeasures[M](filter: MeasuresFilter[M]): M = ???
+
+ override def getMeasures[M](resource: Resource, filter: MeasuresFilter[M]): M = ???
+
+ override def deleteEvent(event: Event): Unit = ???
+
+ override def saveViolation(violation: Violation, force: Boolean): Unit = ???
+
+ override def saveViolation(violation: Violation): Unit = ???
+
+ override def saveResource(resource: Resource): String = ???
+
+ override def getEvents(resource: Resource): util.List[Event] = ???
+
+ override def getDependencies: util.Set[Dependency] = ???
+
+ override def getIncomingDependencies(to: Resource): util.Collection[Dependency] = ???
+
+ override def index(resource: Resource): Boolean = ???
+
+ override def index(resource: Resource, parentReference: Resource): Boolean = ???
+
+ override def saveLink(link: ProjectLink): Unit = ???
+
+ override def getMeasure[G <: io.Serializable](metric: Metric[G]): Measure[G] = measures.get(metric.getKey).orNull.asInstanceOf[Measure[G]]
+
+ override def getMeasure[G <: io.Serializable](resource: Resource, metric: Metric[G]): Measure[G] = ???
+
+ override def getChildren(reference: Resource): util.Collection[Resource] = ???
+
+ override def createEvent(resource: Resource, name: String, description: String, category: String, date: Date): Event = ???
+
+ override def getResource[R <: Resource](reference: R): R = ???
+
+ override def getResource(inputPath: InputPath): Resource = ???
+
+ override def saveMeasure(measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ???
+
+ override def saveMeasure(metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ???
+
+ override def saveMeasure(resource: Resource, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ???
+
+ override def saveMeasure(resource: Resource, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = {
+ measures.put(resource.getKey, measure)
+ measure
+ }
+
+ override def saveMeasure(inputFile: InputFile, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ???
+
+ override def saveMeasure(inputFile: InputFile, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ???
+
+ override def newDuplication(): NewDuplication = ???
+
+ override def activeRules(): ActiveRules = ???
+
+ override def newHighlighting(): NewHighlighting = ???
+
+ override def analysisMode(): AnalysisMode = ???
+
+ override def fileSystem(): FileSystem = ???
+
+ override def newDependency(): NewDependency = ???
+
+ override def settings(): Settings = ???
+
+ override def newMeasure[G <: io.Serializable](): NewMeasure[G] = ???
+
+ override def newIssue(): NewIssue = ???
+}
\ No newline at end of file
diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala
new file mode 100644
index 0000000..4f095ef
--- /dev/null
+++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala
@@ -0,0 +1,53 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.util
+
+import org.scalatest.{FlatSpec, Matchers}
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+@RunWith(classOf[JUnitRunner])
+class PathUtilSpec extends FlatSpec with Matchers {
+
+ val osName = System.getProperty("os.name")
+ val separator = System.getProperty("file.separator")
+
+ behavior of s"splitPath for $osName"
+
+ it should "ignore the empty path" in {
+ PathUtil.splitPath("") should equal(List.empty[String])
+ }
+
+ it should "ignore a separator at the beginning" in {
+ PathUtil.splitPath(s"${separator}a") should equal(List("a"))
+ }
+
+ it should "work with separator in the middle" in {
+ PathUtil.splitPath(s"a${separator}b") should equal(List("a", "b"))
+ }
+
+ it should "work with an OS dependent absolute path" in {
+ if (osName.startsWith("Windows")) {
+ PathUtil.splitPath("C:\\test\\2") should equal(List("test", "2"))
+ } else {
+ PathUtil.splitPath("/test/2") should equal(List("test", "2"))
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala
new file mode 100644
index 0000000..d4ab1e1
--- /dev/null
+++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala
@@ -0,0 +1,133 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.xml
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.{Matchers, FlatSpec}
+import scala.io.Source
+import com.buransky.plugins.scoverage.xml.data.XmlReportFile1
+import scala._
+import com.buransky.plugins.scoverage.{ProjectStatementCoverage, FileStatementCoverage, DirectoryStatementCoverage}
+import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer
+import com.buransky.plugins.scoverage.StatementCoverage
+import com.buransky.plugins.scoverage.NodeStatementCoverage
+
+@RunWith(classOf[JUnitRunner])
+class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers {
+ behavior of "parse source"
+
+ it must "parse old broken Scoverage 0.95 file correctly" in {
+ val sanitizer = new PathSanitizer() {
+ def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] = {
+ // do nothing
+ Some(path)
+ }
+ }
+ assertReportFile(XmlReportFile1.scoverage095Data, 24.53, sanitizer)(assertScoverage095Data)
+ }
+
+ it must "parse new fixed Scoverage 1.0.4 file correctly" in {
+ val sanitizer = new PathSanitizer() {
+ def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] = {
+ // drop first 6 = /a1b2c3/workspace/sonar-test/src/main/scala
+ Some(path.drop(6))
+ }
+ }
+ assertReportFile(XmlReportFile1.scoverage104Data, 50.0, sanitizer) { projectCoverage =>
+ assert(projectCoverage.name === "")
+ assert(projectCoverage.children.size.toInt === 1)
+
+ projectCoverage.children.head match {
+ case rootDir: DirectoryStatementCoverage => {
+ val rr = checkNode(rootDir, "com", 0, 0, 0.0).head
+ val test = checkNode(rr, "rr", 0, 0, 0.0).head
+ val sonar = checkNode(test, "test", 0, 0, 0.0).head
+ val mainClass = checkNode(sonar, "sonar", 2, 1, 50.0).head
+
+ checkNode(mainClass, "MainClass.scala", 2, 1, 50.0)
+ }
+ case other => fail(s"This is not a directory statement coverage! [$other]")
+ }
+ }
+ }
+
+ it must "parse file1 correctly even without XML declaration" in {
+ val sanitizer = new PathSanitizer() {
+ def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] = {
+ // do nothing
+ Some(path)
+ }
+ }
+ assertReportFile(XmlReportFile1.dataWithoutDeclaration, 24.53, sanitizer)(assertScoverage095Data)
+ }
+
+ private def assertReportFile(data: String, expectedCoverage: Double, pathSanitizer: PathSanitizer)(f: (ProjectStatementCoverage) => Unit) {
+ val parser = new XmlScoverageReportConstructingParser(Source.fromString(data), pathSanitizer)
+ val projectCoverage = parser.parse()
+
+ // Assert coverage
+ checkRate(expectedCoverage, projectCoverage.rate)
+
+ f(projectCoverage)
+ }
+
+ private def assertScoverage095Data(projectCoverage: ProjectStatementCoverage): Unit = {
+ // Assert structure
+ projectCoverage.name should equal("")
+
+ val projectChildren = projectCoverage.children.toList
+ projectChildren.length should equal(1)
+ projectChildren.head shouldBe a [DirectoryStatementCoverage]
+
+ val aaa = projectChildren.head.asInstanceOf[DirectoryStatementCoverage]
+ aaa.name should equal("aaa")
+ checkRate(24.53, aaa.rate)
+
+ val aaaChildren = aaa.children.toList.sortBy(_.statementCount)
+ aaaChildren.length should equal(2)
+
+ aaaChildren(1) shouldBe a [FileStatementCoverage]
+ val errorCode = aaaChildren(1).asInstanceOf[FileStatementCoverage]
+ errorCode.name should equal("ErrorCode.scala")
+ errorCode.statementCount should equal (46)
+ errorCode.coveredStatementsCount should equal (13)
+
+ aaaChildren.head shouldBe a [FileStatementCoverage]
+ val graph = aaaChildren.head.asInstanceOf[FileStatementCoverage]
+ graph.name should equal("Graph.scala")
+ graph.statementCount should equal (7)
+ graph.coveredStatementsCount should equal (0)
+ }
+
+ private def checkRate(expected: Double, real: Double) {
+ BigDecimal(real).setScale(2, BigDecimal.RoundingMode.HALF_UP).should(equal(BigDecimal(expected)))
+ }
+
+ private def checkNode(node: NodeStatementCoverage, name: String, count: Int, covered: Int, rate: Double): Iterable[NodeStatementCoverage] = {
+ node.name shouldEqual name
+ node.statementCount shouldEqual count
+ node.coveredStatementsCount shouldEqual covered
+
+ checkRate(rate, node.rate)
+
+ node.children
+ }
+}
diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala
new file mode 100644
index 0000000..5d457c7
--- /dev/null
+++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala
@@ -0,0 +1,42 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.xml
+
+import org.scalatest.{FlatSpec, Matchers}
+import org.scalatest.junit.JUnitRunner
+import org.junit.runner.RunWith
+import com.buransky.plugins.scoverage.ScoverageException
+
+@RunWith(classOf[JUnitRunner])
+class XmlScoverageReportParserSpec extends FlatSpec with Matchers {
+ behavior of "parse file path"
+
+ it must "fail for null path" in {
+ the[IllegalArgumentException] thrownBy XmlScoverageReportParser().parse(null.asInstanceOf[String], null)
+ }
+
+ it must "fail for empty path" in {
+ the[IllegalArgumentException] thrownBy XmlScoverageReportParser().parse("", null)
+ }
+
+ it must "fail for not existing path" in {
+ the[ScoverageException] thrownBy XmlScoverageReportParser().parse("/x/a/b/c/1/2/3/4.xml", null)
+ }
+}
diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala
new file mode 100644
index 0000000..82ec85c
--- /dev/null
+++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala
@@ -0,0 +1,837 @@
+/*
+ * Sonar Scoverage Plugin
+ * Copyright (C) 2013 Rado Buransky
+ * dev@sonar.codehaus.org
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
+ */
+package com.buransky.plugins.scoverage.xml.data
+
+object XmlReportFile1 {
+ val scoverage104Data =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |""".stripMargin
+
+ val scoverage095Data =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | MyServiceClientError.this.error("zipcodeinvalid")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | 2
+ |
+ |
+ | 3
+ |
+ |
+ | scala.Some.apply[String]("One")
+ |
+ |
+ | new $anon()
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | MyServiceLogicError.this.error("logicfailed")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | StructuredErrorCode.this.parent.toString()
+ |
+ |
+ | p.==("")
+ |
+ |
+ | ""
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(8, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ""
+ |}
+ |
+ |
+ | p.+("-")
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(10, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | p.+("-")
+ |}
+ |
+ |
+ | StructuredErrorCode.this.name
+ |
+ |
+ | if ({
+ | scoverage.Invoker.invoked(7, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | p.==("")
+ |})
+ | {
+ | scoverage.Invoker.invoked(9, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | {
+ | scoverage.Invoker.invoked(8, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ""
+ | }
+ | }
+ |else
+ | {
+ | scoverage.Invoker.invoked(11, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | {
+ | scoverage.Invoker.invoked(10, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | p.+("-")
+ | }
+ | }.+({
+ | scoverage.Invoker.invoked(12, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | StructuredErrorCode.this.name
+ |})
+ |
+ |
+ |
+ |
+ |
+ |
+ | errorCode.==(this)
+ |
+ |
+ | true
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(2, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | true
+ |}
+ |
+ |
+ | StructuredErrorCode.this.parent.is(errorCode)
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(4, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | StructuredErrorCode.this.parent.is(errorCode)
+ |}
+ |
+ |
+ |
+ |
+ |
+ |
+ | StructuredErrorCode.apply(name, this)
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | ClientError.required
+ |
+ |
+ | scala.this.Predef.println({
+ | scoverage.Invoker.invoked(25, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ClientError.required
+ |})
+ |
+ |
+ | ClientError.invalid
+ |
+ |
+ | scala.this.Predef.println({
+ | scoverage.Invoker.invoked(27, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ClientError.invalid
+ |})
+ |
+ |
+ | scala.this.Predef.println(MySqlError)
+ |
+ |
+ | MySqlError.syntax
+ |
+ |
+ | scala.this.Predef.println({
+ | scoverage.Invoker.invoked(30, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | MySqlError.syntax
+ |})
+ |
+ |
+ | MyServiceLogicError.logicFailed
+ |
+ |
+ | scala.this.Predef.println({
+ | scoverage.Invoker.invoked(32, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | MyServiceLogicError.logicFailed
+ |})
+ |
+ |
+ | ClientError.required
+ |
+ |
+ | e
+ |
+ |
+ | scala.this.Predef.println("required")
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(36, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | scala.this.Predef.println("required")
+ |}
+ |
+ |
+ | scala.this.Predef.println("invalid")
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(38, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | scala.this.Predef.println("invalid")
+ |}
+ |
+ |
+ | ()
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(40, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ()
+ |}
+ |
+ |
+ | MyServiceServerError.mongoDbError.is(ServerError)
+ |
+ |
+ | scala.this.Predef.println("This is a server error")
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(43, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | scala.this.Predef.println("This is a server error")
+ |}
+ |
+ |
+ | ()
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(45, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ()
+ |}
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | MySqlError.this.error("syntax")
+ |
+ |
+ | MySqlError.this.error("connection")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | MyServiceServerError.this.error("mongodberror")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | ""
+ |
+ |
+ |
+ |
+ |
+ |
+ | false
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | ServerError.this.error("solar")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | ClientError.this.error("required")
+ |
+ |
+ | ClientError.this.error("invalid")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | aaa.MakeRectangleModelFromFile.apply(null)
+ |
+ |
+ | x.isInstanceOf[Serializable]
+ |
+ |
+ | scala.this.Predef.println({
+ | scoverage.Invoker.invoked(52, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | x.isInstanceOf[Serializable]
+ |})
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+
+ val dataWithoutDeclaration =
+ """
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | MyServiceClientError.this.error("zipcodeinvalid")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | 2
+ |
+ |
+ | 3
+ |
+ |
+ | scala.Some.apply[String]("One")
+ |
+ |
+ | new $anon()
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | MyServiceLogicError.this.error("logicfailed")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | StructuredErrorCode.this.parent.toString()
+ |
+ |
+ | p.==("")
+ |
+ |
+ | ""
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(8, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ""
+ |}
+ |
+ |
+ | p.+("-")
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(10, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | p.+("-")
+ |}
+ |
+ |
+ | StructuredErrorCode.this.name
+ |
+ |
+ | if ({
+ | scoverage.Invoker.invoked(7, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | p.==("")
+ |})
+ | {
+ | scoverage.Invoker.invoked(9, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | {
+ | scoverage.Invoker.invoked(8, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ""
+ | }
+ | }
+ |else
+ | {
+ | scoverage.Invoker.invoked(11, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | {
+ | scoverage.Invoker.invoked(10, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | p.+("-")
+ | }
+ | }.+({
+ | scoverage.Invoker.invoked(12, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | StructuredErrorCode.this.name
+ |})
+ |
+ |
+ |
+ |
+ |
+ |
+ | errorCode.==(this)
+ |
+ |
+ | true
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(2, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | true
+ |}
+ |
+ |
+ | StructuredErrorCode.this.parent.is(errorCode)
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(4, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | StructuredErrorCode.this.parent.is(errorCode)
+ |}
+ |
+ |
+ |
+ |
+ |
+ |
+ | StructuredErrorCode.apply(name, this)
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | ClientError.required
+ |
+ |
+ | scala.this.Predef.println({
+ | scoverage.Invoker.invoked(25, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ClientError.required
+ |})
+ |
+ |
+ | ClientError.invalid
+ |
+ |
+ | scala.this.Predef.println({
+ | scoverage.Invoker.invoked(27, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ClientError.invalid
+ |})
+ |
+ |
+ | scala.this.Predef.println(MySqlError)
+ |
+ |
+ | MySqlError.syntax
+ |
+ |
+ | scala.this.Predef.println({
+ | scoverage.Invoker.invoked(30, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | MySqlError.syntax
+ |})
+ |
+ |
+ | MyServiceLogicError.logicFailed
+ |
+ |
+ | scala.this.Predef.println({
+ | scoverage.Invoker.invoked(32, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | MyServiceLogicError.logicFailed
+ |})
+ |
+ |
+ | ClientError.required
+ |
+ |
+ | e
+ |
+ |
+ | scala.this.Predef.println("required")
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(36, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | scala.this.Predef.println("required")
+ |}
+ |
+ |
+ | scala.this.Predef.println("invalid")
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(38, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | scala.this.Predef.println("invalid")
+ |}
+ |
+ |
+ | ()
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(40, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ()
+ |}
+ |
+ |
+ | MyServiceServerError.mongoDbError.is(ServerError)
+ |
+ |
+ | scala.this.Predef.println("This is a server error")
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(43, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | scala.this.Predef.println("This is a server error")
+ |}
+ |
+ |
+ | ()
+ |
+ |
+ | {
+ | scoverage.Invoker.invoked(45, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | ()
+ |}
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | MySqlError.this.error("syntax")
+ |
+ |
+ | MySqlError.this.error("connection")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | MyServiceServerError.this.error("mongodberror")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | ""
+ |
+ |
+ |
+ |
+ |
+ |
+ | false
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | ServerError.this.error("solar")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | ClientError.this.error("required")
+ |
+ |
+ | ClientError.this.error("invalid")
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ | aaa.MakeRectangleModelFromFile.apply(null)
+ |
+ |
+ | x.isInstanceOf[Serializable]
+ |
+ |
+ | scala.this.Predef.println({
+ | scoverage.Invoker.invoked(52, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement");
+ | x.isInstanceOf[Serializable]
+ |})
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+ """.stripMargin
+}
diff --git a/samples/maven/.gitignore b/samples/maven/.gitignore
new file mode 100644
index 0000000..ab7ac4c
--- /dev/null
+++ b/samples/maven/.gitignore
@@ -0,0 +1,32 @@
+*.class
+*.log
+
+# Package files
+*.war
+*.jar
+*.ear
+
+# Maven
+out/
+target/
+
+# Eclipse
+.project
+.classpath
+.settings/
+
+# IDEA
+*.iml
+.idea
+
+# OSX
+.DS_STORE
+.Trashes
+
+# Windows
+Desktop.ini
+Thumbs.db
+
+# Python
+*.pyc
+
diff --git a/samples/maven/README b/samples/maven/README
new file mode 100644
index 0000000..73907c2
--- /dev/null
+++ b/samples/maven/README
@@ -0,0 +1,3 @@
+Run with:
+
+`mvn clean scoverage:report sonar:sonar`
\ No newline at end of file
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module1/pom.xml b/samples/maven/combined-scala-java-multi-module-sonar/module1/pom.xml
new file mode 100644
index 0000000..3d2929f
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/module1/pom.xml
@@ -0,0 +1,12 @@
+
+ 4.0.0
+
+ combined-scala-java-multi-module-sonar
+ test
+ 1.0.0
+
+ module1
+ jar
+ 1.0.0
+
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/java/module1/HelloWorld.java b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/java/module1/HelloWorld.java
new file mode 100644
index 0000000..056006e
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/java/module1/HelloWorld.java
@@ -0,0 +1,16 @@
+package module1;
+
+/**
+ * Created by tim on 01/05/15.
+ */
+public class HelloWorld {
+
+ public String hello() {
+ return "Hello";
+ }
+
+ public void notCovered() {
+ System.out.println("YOLO");
+ }
+
+}
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/scala/module1/HelloScala.scala b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/scala/module1/HelloScala.scala
new file mode 100644
index 0000000..c7057ca
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/scala/module1/HelloScala.scala
@@ -0,0 +1,10 @@
+package module1
+
+class HelloScala {
+
+ case class TryOut(some: String, fields: List[String])
+
+ def test = "Hello"
+
+ def someOther = 42
+}
\ No newline at end of file
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/java/module1/HelloWorldTest.java b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/java/module1/HelloWorldTest.java
new file mode 100644
index 0000000..ebd0817
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/java/module1/HelloWorldTest.java
@@ -0,0 +1,11 @@
+package module1;
+
+import static org.junit.Assert.assertEquals;
+
+public class HelloWorldTest {
+
+ @org.junit.Test
+ public void testHello() throws Exception {
+ assertEquals("Hello", new HelloWorld().hello());
+ }
+}
\ No newline at end of file
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/scala/HelloScalaTest.scala b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/scala/HelloScalaTest.scala
new file mode 100644
index 0000000..30dc4da
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/scala/HelloScalaTest.scala
@@ -0,0 +1,16 @@
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.{FlatSpec, ShouldMatchers}
+import module1.HelloScala
+
+@RunWith(classOf[JUnitRunner])
+class HelloScalaTest extends FlatSpec with ShouldMatchers {
+
+ "it" should "work" in {
+ val scala: HelloScala = new HelloScala()
+ scala.test should equal("Hello")
+
+ scala.TryOut("String", List()) should not equal(true)
+ }
+
+}
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module2/pom.xml b/samples/maven/combined-scala-java-multi-module-sonar/module2/pom.xml
new file mode 100644
index 0000000..511f9a4
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/module2/pom.xml
@@ -0,0 +1,12 @@
+
+ 4.0.0
+
+ combined-scala-java-multi-module-sonar
+ test
+ 1.0.0
+
+ module2
+ jar
+ 1.0.0
+
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/java/module2/HelloWorld2.java b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/java/module2/HelloWorld2.java
new file mode 100644
index 0000000..c4632f8
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/java/module2/HelloWorld2.java
@@ -0,0 +1,16 @@
+package module2;
+
+/**
+ * Created by tim on 01/05/15.
+ */
+public class HelloWorld2 {
+
+ public String hello() {
+ return "Hello";
+ }
+
+ public void notCovered() {
+ System.out.println("YOLO");
+ }
+
+}
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/scala/module2/HelloScala2.scala b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/scala/module2/HelloScala2.scala
new file mode 100644
index 0000000..7047194
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/scala/module2/HelloScala2.scala
@@ -0,0 +1,10 @@
+package module2
+
+class HelloScala2 {
+
+ case class TryOut(some: String, fields: List[String])
+
+ def test = "Hello"
+
+ def someOther = 42
+}
\ No newline at end of file
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/java/HelloWorld2Test.java b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/java/HelloWorld2Test.java
new file mode 100644
index 0000000..329eb10
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/java/HelloWorld2Test.java
@@ -0,0 +1,11 @@
+import module2.HelloWorld2;
+
+import static org.junit.Assert.assertEquals;
+
+public class HelloWorld2Test {
+
+ @org.junit.Test
+ public void testHello() throws Exception {
+ assertEquals("Hello", new HelloWorld2().hello());
+ }
+}
\ No newline at end of file
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/scala/HelloScala2Test.scala b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/scala/HelloScala2Test.scala
new file mode 100644
index 0000000..9364c6e
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/scala/HelloScala2Test.scala
@@ -0,0 +1,16 @@
+import module2.HelloScala2
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.{FlatSpec, ShouldMatchers}
+
+@RunWith(classOf[JUnitRunner])
+class HelloScala2Test extends FlatSpec with ShouldMatchers {
+
+ "it" should "work" in {
+ val scala: HelloScala2 = new HelloScala2()
+ scala.test should equal("Hello")
+
+ scala.TryOut("String", List()) should not equal(true)
+ }
+
+}
diff --git a/samples/maven/combined-scala-java-multi-module-sonar/pom.xml b/samples/maven/combined-scala-java-multi-module-sonar/pom.xml
new file mode 100644
index 0000000..000db69
--- /dev/null
+++ b/samples/maven/combined-scala-java-multi-module-sonar/pom.xml
@@ -0,0 +1,127 @@
+
+ 4.0.0
+
+ test
+ combined-scala-java-multi-module-sonar
+ pom
+ 1.0.0
+
+
+ module1
+ module2
+
+
+
+ 2.11.6
+ 0.13.8
+
+ scoverage
+ jacoco
+ target/surefire-reports
+ target/scoverage.xml
+ src
+ target/jacoco.exec
+ src/test/**
+ UTF-8
+
+
+
+
+
+
+ com.google.code.sbt-compiler-maven-plugin
+ sbt-compiler-maven-plugin
+ 1.0.0-beta5
+
+
+
+ compile
+ testCompile
+ addScalaSources
+
+ default-sbt-compile
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.2
+
+ true
+ true
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.7.4.201502262128
+
+
+ pre-test
+
+ prepare-agent
+
+
+
+
+
+
+ org.scoverage
+ scoverage-maven-plugin
+ 1.0.4
+
+ true
+
+
+
+ instrument
+
+
+ pre-compile
+
+ post-compile
+
+
+
+ scoverage-report
+
+
+ report-only
+
+ prepare-package
+
+
+
+
+
+
+
+
+ org.scalatest
+ scalatest_2.11
+ 2.2.1
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.9.5
+ test
+
+
+ junit
+ junit
+ 4.11
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.0.13
+
+
+
+
diff --git a/samples/maven/combined-scala-java-sonar/pom.xml b/samples/maven/combined-scala-java-sonar/pom.xml
new file mode 100644
index 0000000..1a0a890
--- /dev/null
+++ b/samples/maven/combined-scala-java-sonar/pom.xml
@@ -0,0 +1,103 @@
+
+ 4.0.0
+
+ test
+ combined-scala-java-sonar-single-module
+ jar
+ 1.0.0
+
+
+ 2.11.6
+
+ scoverage
+ jacoco
+ target/surefire-reports
+ target/scoverage.xml
+ target/notthere.xml
+ src
+ target/jacoco.exec
+ src/test/java/**,src/test/scala/**
+ UTF-8
+
+
+
+
+
+
+ com.google.code.sbt-compiler-maven-plugin
+ sbt-compiler-maven-plugin
+ 1.0.0-beta5
+
+
+
+ compile
+ testCompile
+ addScalaSources
+
+ default-sbt-compile
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.2
+
+ true
+ true
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.7.4.201502262128
+
+
+ pre-test
+
+ prepare-agent
+
+
+
+
+
+
+ org.scoverage
+ scoverage-maven-plugin
+ 1.0.4
+
+ true
+
+
+
+
+
+
+
+ org.scalatest
+ scalatest_2.11
+ 2.2.1
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.9.5
+ test
+
+
+ junit
+ junit
+ 4.11
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.0.13
+
+
+
+
diff --git a/samples/maven/combined-scala-java-sonar/src/main/java/module1/HelloWorld.java b/samples/maven/combined-scala-java-sonar/src/main/java/module1/HelloWorld.java
new file mode 100644
index 0000000..056006e
--- /dev/null
+++ b/samples/maven/combined-scala-java-sonar/src/main/java/module1/HelloWorld.java
@@ -0,0 +1,16 @@
+package module1;
+
+/**
+ * Created by tim on 01/05/15.
+ */
+public class HelloWorld {
+
+ public String hello() {
+ return "Hello";
+ }
+
+ public void notCovered() {
+ System.out.println("YOLO");
+ }
+
+}
diff --git a/samples/maven/combined-scala-java-sonar/src/main/scala/module1/HelloScala.scala b/samples/maven/combined-scala-java-sonar/src/main/scala/module1/HelloScala.scala
new file mode 100644
index 0000000..c7057ca
--- /dev/null
+++ b/samples/maven/combined-scala-java-sonar/src/main/scala/module1/HelloScala.scala
@@ -0,0 +1,10 @@
+package module1
+
+class HelloScala {
+
+ case class TryOut(some: String, fields: List[String])
+
+ def test = "Hello"
+
+ def someOther = 42
+}
\ No newline at end of file
diff --git a/samples/maven/combined-scala-java-sonar/src/test/java/module1/HelloWorldTest.java b/samples/maven/combined-scala-java-sonar/src/test/java/module1/HelloWorldTest.java
new file mode 100644
index 0000000..ebd0817
--- /dev/null
+++ b/samples/maven/combined-scala-java-sonar/src/test/java/module1/HelloWorldTest.java
@@ -0,0 +1,11 @@
+package module1;
+
+import static org.junit.Assert.assertEquals;
+
+public class HelloWorldTest {
+
+ @org.junit.Test
+ public void testHello() throws Exception {
+ assertEquals("Hello", new HelloWorld().hello());
+ }
+}
\ No newline at end of file
diff --git a/samples/maven/combined-scala-java-sonar/src/test/scala/HelloScalaTest.scala b/samples/maven/combined-scala-java-sonar/src/test/scala/HelloScalaTest.scala
new file mode 100644
index 0000000..30dc4da
--- /dev/null
+++ b/samples/maven/combined-scala-java-sonar/src/test/scala/HelloScalaTest.scala
@@ -0,0 +1,16 @@
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.{FlatSpec, ShouldMatchers}
+import module1.HelloScala
+
+@RunWith(classOf[JUnitRunner])
+class HelloScalaTest extends FlatSpec with ShouldMatchers {
+
+ "it" should "work" in {
+ val scala: HelloScala = new HelloScala()
+ scala.test should equal("Hello")
+
+ scala.TryOut("String", List()) should not equal(true)
+ }
+
+}
diff --git a/samples/sbt/multi-module/.gitignore b/samples/sbt/multi-module/.gitignore
new file mode 100644
index 0000000..0c08aab
--- /dev/null
+++ b/samples/sbt/multi-module/.gitignore
@@ -0,0 +1,3 @@
+.idea
+.idea_modules
+target
\ No newline at end of file
diff --git a/samples/sbt/multi-module/README.md b/samples/sbt/multi-module/README.md
new file mode 100644
index 0000000..c03f628
--- /dev/null
+++ b/samples/sbt/multi-module/README.md
@@ -0,0 +1,17 @@
+# Multi-module SBT sample project for Sonar Scoverage plugin #
+
+ 1. Create quality profile for Scala language and set it to be used by default.
+
+ 2. Run scoverage to generate coverage reports:
+
+ $ sbt clean coverage test
+
+ 3. And then run Sonar runner to upload data from reports to the Sonar server:
+
+ $ sonar-runner
+
+## Requirements ##
+
+- Installed Sonar Scoverage plugin
+- Installed SBT
+- Installed Sonar runner
\ No newline at end of file
diff --git a/samples/sbt/multi-module/build.sbt b/samples/sbt/multi-module/build.sbt
new file mode 100644
index 0000000..95a652a
--- /dev/null
+++ b/samples/sbt/multi-module/build.sbt
@@ -0,0 +1,9 @@
+organization in ThisBuild := "com.buransky"
+
+scalaVersion in ThisBuild := "2.11.6"
+
+version in ThisBuild := "5.1.0"
+
+lazy val module1 = project
+
+lazy val module2 = project
diff --git a/samples/sbt/multi-module/module1/build.sbt b/samples/sbt/multi-module/module1/build.sbt
new file mode 100644
index 0000000..c40c574
--- /dev/null
+++ b/samples/sbt/multi-module/module1/build.sbt
@@ -0,0 +1,3 @@
+name := Common.baseName + "-module1"
+
+libraryDependencies += Common.scalatest
\ No newline at end of file
diff --git a/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala
new file mode 100644
index 0000000..d3f76a2
--- /dev/null
+++ b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala
@@ -0,0 +1,29 @@
+package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1
+
+import scala.util.Random
+
+trait Beer {
+ val volume: Double
+ def isGood: Boolean = (volume > 0.0)
+}
+
+case object EmptyBeer extends {
+ val volume = 0.0
+} with Beer
+
+trait SlovakBeer extends Beer {
+ override def isGood = Random.nextBoolean
+}
+
+trait BelgianBeer extends Beer {
+ if (volume > 0.25)
+ throw new IllegalArgumentException("Too big beer for belgian beer!")
+
+ override def isGood = true
+}
+
+case class HordonBeer(volume: Double) extends SlovakBeer {
+ override def isGood = false
+}
+
+case class ChimayBeer(volume: Double) extends BelgianBeer
diff --git a/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Pub.scala b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Pub.scala
new file mode 100644
index 0000000..e8a21a1
--- /dev/null
+++ b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Pub.scala
@@ -0,0 +1,11 @@
+package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1
+
+trait Pub {
+ def offer: Iterable[_ <: Beer]
+ def giveMeGreat: Beer
+}
+
+object Delirium extends Pub {
+ def offer = List(HordonBeer(0.5), ChimayBeer(0.2))
+ def giveMeGreat = offer.filter(_.isGood).filter(_.volume > 0.3).head
+}
\ No newline at end of file
diff --git a/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/BeerSpec.scala b/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/BeerSpec.scala
new file mode 100644
index 0000000..8502ce1
--- /dev/null
+++ b/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/BeerSpec.scala
@@ -0,0 +1,18 @@
+package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1
+
+import org.scalatest.{Matchers, FlatSpec}
+
+class BeerSpec extends FlatSpec with Matchers {
+ behavior of "Beer"
+
+ "isGood" must "be true if not empty" in {
+ val beer = new Beer { val volume = 0.1 }
+ beer.isGood should equal(true)
+ }
+
+ behavior of "EmptyBeer"
+
+ it must "be empty" in {
+ EmptyBeer.volume should equal(0.0)
+ }
+}
diff --git a/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/PubSpec.scala b/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/PubSpec.scala
new file mode 100644
index 0000000..71c40a4
--- /dev/null
+++ b/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/PubSpec.scala
@@ -0,0 +1,11 @@
+package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1
+
+import org.scalatest.{FlatSpec, Matchers}
+
+class PubSpec extends FlatSpec with Matchers {
+ behavior of "Delirium"
+
+ it must "give me what I want" in {
+ the[NoSuchElementException] thrownBy Delirium.giveMeGreat
+ }
+}
\ No newline at end of file
diff --git a/samples/sbt/multi-module/module2/build.sbt b/samples/sbt/multi-module/module2/build.sbt
new file mode 100644
index 0000000..983896c
--- /dev/null
+++ b/samples/sbt/multi-module/module2/build.sbt
@@ -0,0 +1,3 @@
+name := Common.baseName + "-module2"
+
+libraryDependencies += Common.scalatest
\ No newline at end of file
diff --git a/samples/sbt/multi-module/module2/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/Animal.scala b/samples/sbt/multi-module/module2/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/Animal.scala
new file mode 100644
index 0000000..6272933
--- /dev/null
+++ b/samples/sbt/multi-module/module2/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/Animal.scala
@@ -0,0 +1,13 @@
+package com.buransky.plugins.scoverage.samples.sbt.multiModule.module2
+
+trait Animal {
+ val legs: Int
+ val eyes: Int
+ val canFly: Boolean
+ val canSwim: Boolean
+}
+
+object Animal {
+ def fancy(farm: Iterable[Animal]): Iterable[Animal] =
+ farm.filter(_.legs > 10).filter(_.canFly).filter(_.canSwim)
+}
\ No newline at end of file
diff --git a/samples/sbt/multi-module/module2/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/AnimalSpec.scala b/samples/sbt/multi-module/module2/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/AnimalSpec.scala
new file mode 100644
index 0000000..2088f82
--- /dev/null
+++ b/samples/sbt/multi-module/module2/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/AnimalSpec.scala
@@ -0,0 +1,11 @@
+package com.buransky.plugins.scoverage.samples.sbt.multiModule.module2
+
+import org.scalatest.{Matchers, FlatSpec}
+
+class AnimalSpec extends FlatSpec with Matchers {
+ behavior of "fancy"
+
+ it should "do nothing" in {
+ Animal.fancy(Nil) should equal(Nil)
+ }
+}
diff --git a/samples/sbt/multi-module/project/Common.scala b/samples/sbt/multi-module/project/Common.scala
new file mode 100644
index 0000000..9f60f92
--- /dev/null
+++ b/samples/sbt/multi-module/project/Common.scala
@@ -0,0 +1,6 @@
+import sbt._
+
+object Common {
+ val baseName = "multi-module"
+ val scalatest = "org.scalatest" % "scalatest_2.11" % "2.2.4"
+}
\ No newline at end of file
diff --git a/samples/sbt/multi-module/project/build.properties b/samples/sbt/multi-module/project/build.properties
new file mode 100644
index 0000000..df58110
--- /dev/null
+++ b/samples/sbt/multi-module/project/build.properties
@@ -0,0 +1 @@
+sbt.version=0.13.6
\ No newline at end of file
diff --git a/samples/sbt/multi-module/project/plugins.sbt b/samples/sbt/multi-module/project/plugins.sbt
new file mode 100644
index 0000000..5918bda
--- /dev/null
+++ b/samples/sbt/multi-module/project/plugins.sbt
@@ -0,0 +1,3 @@
+resolvers += Classpaths.sbtPluginReleases
+
+addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.0.4")
\ No newline at end of file
diff --git a/samples/sbt/multi-module/sonar-project.properties b/samples/sbt/multi-module/sonar-project.properties
new file mode 100644
index 0000000..c783f1b
--- /dev/null
+++ b/samples/sbt/multi-module/sonar-project.properties
@@ -0,0 +1,15 @@
+sonar.projectKey=com.buransky:multi-module
+sonar.projectName=Sonar Scoverage plugin multi-module sample project
+sonar.projectVersion=5.1.0
+
+sonar.language=scala
+
+sonar.modules=module1,module2
+
+module1.sonar.sources=src/main/scala
+module1.sonar.tests=src/test/scala
+module1.sonar.scoverage.reportPath=target/scala-2.11/scoverage-report/scoverage.xml
+
+module2.sonar.sources=src/main/scala
+module2.sonar.tests=src/test/scala
+module2.sonar.scoverage.reportPath=target/scala-2.11/scoverage-report/scoverage.xml