org.apache.maven.plugins
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
diff --git a/sonar-project.properties b/sonar-project.properties
index ab8daf5..b920e61 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -1,6 +1,6 @@
# Required metadata
sonar.projectName=Sonar Scala Plugin
-sonar.projectVersion=0.0.2
+sonar.projectVersion=0.0.3
# Comma-separated paths to directories with sources (required)
sonar.sources=src
@@ -8,3 +8,6 @@ sonar.exclusions=src/test/resources/**,
# Encoding of the source files
sonar.sourceEncoding=UTF-8
+
+# Code Coverage reports
+sonar.scoverage.reportPath=target/scoverage.xml
diff --git a/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb b/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb
new file mode 100644
index 0000000..065d5c8
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala
new file mode 100644
index 0000000..72d40d0
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala b/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala
new file mode 100644
index 0000000..49ec40c
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala b/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala
new file mode 100644
index 0000000..43d881c
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala b/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala
new file mode 100644
index 0000000..abce710
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala b/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala
new file mode 100644
index 0000000..dea6f4a
--- /dev/null
+++ b/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala
@@ -0,0 +1,85 @@
+/*
+ * 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 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)
+ }
+
+}
diff --git a/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala b/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala
new file mode 100644
index 0000000..d4a1b79
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala
new file mode 100644
index 0000000..78091d7
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala b/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala
new file mode 100644
index 0000000..bc28dd1
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala b/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala
new file mode 100644
index 0000000..1981fcf
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala b/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala
new file mode 100644
index 0000000..ba0a06e
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala
new file mode 100644
index 0000000..d77c5e4
--- /dev/null
+++ b/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/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala
new file mode 100644
index 0000000..fe2036a
--- /dev/null
+++ b/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/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala b/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala
index a1c346b..770758e 100644
--- a/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala
+++ b/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala
@@ -3,15 +3,18 @@ package com.sagacify.sonar.scala
import scala.collection.JavaConversions._
import scala.collection.mutable.ListBuffer
+import com.buransky.plugins.scoverage.measure.ScalaMetrics
+import com.buransky.plugins.scoverage.sensor.ScoverageSensor
+import com.buransky.plugins.scoverage.widget.ScoverageWidget
+import com.ncredinburgh.sonar.scalastyle.ScalastyleQualityProfile
+import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository
+import com.ncredinburgh.sonar.scalastyle.ScalastyleSensor
import org.sonar.api.config.Settings
import org.sonar.api.Extension
import org.sonar.api.resources.AbstractLanguage
import org.sonar.api.SonarPlugin
import scalariform.lexer.ScalaLexer
import scalariform.lexer.Token
-import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository
-import com.ncredinburgh.sonar.scalastyle.ScalastyleQualityProfile
-import com.ncredinburgh.sonar.scalastyle.ScalastyleSensor
/**
* Defines Scala as a language for SonarQube.
@@ -40,7 +43,10 @@ class ScalaPlugin extends SonarPlugin {
classOf[ScalaSensor],
classOf[ScalastyleRepository],
classOf[ScalastyleQualityProfile],
- classOf[ScalastyleSensor]
+ classOf[ScalastyleSensor],
+ classOf[ScalaMetrics],
+ classOf[ScoverageSensor],
+ classOf[ScoverageWidget]
)
override val toString = getClass.getSimpleName
diff --git a/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala
new file mode 100644
index 0000000..5fec3f6
--- /dev/null
+++ b/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/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala
new file mode 100644
index 0000000..ead1de9
--- /dev/null
+++ b/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/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala b/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala
new file mode 100644
index 0000000..9c12ad9
--- /dev/null
+++ b/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 = ???
+// }
diff --git a/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala
new file mode 100644
index 0000000..4f095ef
--- /dev/null
+++ b/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/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala
new file mode 100644
index 0000000..d4ab1e1
--- /dev/null
+++ b/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/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala
new file mode 100644
index 0000000..5d457c7
--- /dev/null
+++ b/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/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala
new file mode 100644
index 0000000..82ec85c
--- /dev/null
+++ b/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
+}