diff --git a/.gitignore b/.gitignore
index c58d83b..f5d8cca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,17 +1,20 @@
-*.class
*.log
-# sbt specific
-.cache
-.history
-.lib/
-dist/*
+# SBT specific
target/
-lib_managed/
-src_managed/
project/boot/
project/plugins/project/
+# Eclipse specific
+.classpath
+.project
+.settings/
+
# Scala-IDE specific
-.scala_dependencies
-.worksheet
+.cache-main
+.cache-tests
+
+# IntelliJ IDEA specific
+.idea
+.idea_modules
+*.iml
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..bbef7ec
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,22 @@
+language: scala
+
+script:
+ - sbt ++$TRAVIS_SCALA_VERSION test
+
+matrix:
+ include:
+ - jdk: oraclejdk7
+ scala: 2.10.6
+ - jdk: oraclejdk7
+ scala: 2.11.8
+ - jdk: oraclejdk8
+ scala: 2.12.0-RC1
+
+before_cache:
+ - find "$HOME/.sbt/" -name '*.lock' -print0 | xargs -0 rm
+ - find "$HOME/.ivy2/" -name 'ivydata-*.properties' -print0 | xargs -0 rm
+
+cache:
+ directories:
+ - $HOME/.ivy2/cache
+ - $HOME/.sbt
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000..d043b44
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,137 @@
+import sbt._
+import sbt.Keys._
+import sbtrelease.ReleasePlugin.autoImport._
+import com.typesafe.sbt.pgp.PgpKeys
+
+
+val Org = "org.scoverage"
+val MockitoVersion = "1.10.19"
+val JUnitInterfaceVersion = "0.9"
+val JUnitVersion = "4.11"
+
+lazy val fullCrossSettings = Seq(
+ crossVersion := CrossVersion.full // because compiler api is not binary compatible
+) ++ allCrossSettings
+
+lazy val binaryCrossSettings = Seq(
+ crossScalaVersions := Seq("2.10.6", "2.11.8", "2.12.0-RC2")
+)
+
+lazy val allCrossSettings = Seq(
+ crossScalaVersions := Seq(
+ "2.10.6",
+ "2.11.8",
+ "2.12.0-M3",
+ "2.12.0-M4",
+ "2.12.0-M5",
+ "2.12.0-RC1-ceaf419",
+ "2.12.0-RC1",
+ "2.12.0-RC1-1e81a09",
+ "2.12.0-RC2")
+)
+
+val appSettings = Seq(
+ organization := Org,
+ scalaVersion := "2.11.8",
+ fork in Test := false,
+ publishMavenStyle := true,
+ publishArtifact in Test := false,
+ parallelExecution in Test := false,
+ scalacOptions := Seq("-unchecked", "-deprecation", "-feature", "-encoding", "utf8"),
+ javacOptions := {
+ CrossVersion.partialVersion(scalaVersion.value) match {
+ case Some((2, scalaMajor)) if scalaMajor < 12 => Seq("-source", "1.7", "-target", "1.7")
+ case _ => Seq()
+ }
+ },
+ libraryDependencies +=
+ "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided",
+
+ concurrentRestrictions in Global += Tags.limit(Tags.Test, 1),
+ publishTo := {
+ val nexus = "https://oss.sonatype.org/"
+
+ if (isSnapshot.value)
+ Some("snapshots" at nexus + "content/repositories/snapshots")
+ else
+ Some("releases" at nexus + "service/local/staging/deploy/maven2")
+ },
+ pomExtra := {
+ https://github.com/scoverage/scalac-scoverage-plugin-core
+
+
+ Apache 2
+ http://www.apache.org/licenses/LICENSE-2.0
+ repo
+
+
+
+ git@github.com:scoverage/scalac-scoverage-plugin-core.git
+ scm:git@github.com:scoverage/scalac-scoverage-plugin-core.git
+
+
+
+ sksamuel
+ Stephen Samuel
+ http://github.com/sksamuel
+
+
+ },
+ pomIncludeRepository := {
+ _ => false
+ }
+) ++ Seq(
+ releaseCrossBuild := true,
+ releasePublishArtifactsAction := PgpKeys.publishSigned.value
+)
+
+lazy val noPublishSettings = Seq(
+ publishArtifact := false,
+ // The above is enough for Maven repos but it doesn't prevent publishing of ivy.xml files
+ publish := {},
+ publishLocal := {}
+)
+
+lazy val junitSettings = Seq(
+ testOptions += Tests.Argument(TestFrameworks.JUnit, "-a", "-v"),
+ libraryDependencies ++= Seq(
+ "com.novocode" % "junit-interface" % JUnitInterfaceVersion % "test",
+ "junit" % "junit" % JUnitVersion % "test"
+ )
+)
+
+lazy val root = Project("scalac-scoverage", file("."))
+ .settings(name := "scalac-scoverage")
+ .settings(appSettings: _*)
+ .settings(allCrossSettings)
+ .settings(noPublishSettings)
+ .aggregate(plugin, runtimeJava, runtimeScala, pluginTests)
+
+lazy val runtimeJava = Project("scalac-scoverage-runtime-java", file("scalac-scoverage-runtime-java"))
+ .settings(name := "scalac-scoverage-runtime-java")
+ .settings(appSettings: _*)
+ .settings(binaryCrossSettings)
+ .settings(junitSettings)
+ .dependsOn(pluginTests % "test->compile")
+
+lazy val runtimeScala = Project("scalac-scoverage-runtime-scala", file("scalac-scoverage-runtime-scala"))
+ .settings(name := "scalac-scoverage-runtime-scala")
+ .settings(appSettings: _*)
+ .settings(binaryCrossSettings)
+ .settings(junitSettings)
+ .dependsOn(pluginTests % "test->compile")
+
+lazy val plugin = Project("scalac-scoverage-plugin", file("scalac-scoverage-plugin"))
+ .settings(name := "scalac-scoverage-plugin")
+ .settings(appSettings: _*)
+ .settings(fullCrossSettings)
+
+lazy val pluginTests = Project("scalac-scoverage-plugin-tests", file("scalac-scoverage-plugin-tests"))
+ .dependsOn(plugin)
+ .settings(name := "scalac-scoverage-plugin-tests")
+ .settings(appSettings: _*)
+ .settings(binaryCrossSettings)
+ .settings(libraryDependencies ++= Seq(
+ "org.mockito" % "mockito-all" % MockitoVersion,
+ "com.novocode" % "junit-interface" % JUnitInterfaceVersion
+ ))
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 0000000..35c88ba
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1 @@
+sbt.version=0.13.12
diff --git a/project/plugins.sbt b/project/plugins.sbt
new file mode 100644
index 0000000..d9d8a31
--- /dev/null
+++ b/project/plugins.sbt
@@ -0,0 +1,5 @@
+addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.8.0")
+
+addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.1")
+
+addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.3")
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/AssertUtil.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/AssertUtil.scala
new file mode 100644
index 0000000..0071f12
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/AssertUtil.scala
@@ -0,0 +1,14 @@
+package scoverage
+
+import scala.xml.Node
+
+object AssertUtil {
+
+ implicit class TypedOps[A](v1: A) {
+ def ===(v2: A): Boolean = v1 == v2
+ }
+
+ implicit class NodeOps(n1: Node) {
+ def ===(n2: Node): Boolean = n1.strict_==(n2)
+ }
+}
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/CoverageTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/CoverageTest.scala
new file mode 100644
index 0000000..38b558c
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/CoverageTest.scala
@@ -0,0 +1,35 @@
+package scoverage
+
+import AssertUtil._
+import org.junit.Assert._
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/** @author Stephen Samuel */
+@RunWith(classOf[JUnit4])
+class CoverageTest {
+
+ @Test
+ def coverageForNoStatementsIs1() = {
+ val coverage = Coverage()
+ assertTrue(1.0 === coverage.statementCoverage)
+ }
+
+ @Test
+ def coverageForNoInvokedStatementsIs0() = {
+ val coverage = Coverage()
+ coverage.add(Statement("", Location("", "", "", ClassType.Object, "", ""), 1, 2, 3, 4, "", "", "", false, 0))
+ assertTrue(0.0 === coverage.statementCoverage)
+ }
+
+ @Test
+ def coverageForInvokedStatements() = {
+ val coverage = Coverage()
+ coverage.add(Statement("", Location("", "", "", ClassType.Object, "", ""), 1, 2, 3, 4, "", "", "", false, 3))
+ coverage.add(Statement("", Location("", "", "", ClassType.Object, "", ""), 2, 2, 3, 4, "", "", "", false, 0))
+ coverage.add(Statement("", Location("", "", "", ClassType.Object, "", ""), 3, 2, 3, 4, "", "", "", false, 0))
+ coverage.add(Statement("", Location("", "", "", ClassType.Object, "", ""), 4, 2, 3, 4, "", "", "", false, 0))
+ assertTrue(0.25 === coverage.statementCoverage)
+ }
+}
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/IOUtilsTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/IOUtilsTest.scala
new file mode 100644
index 0000000..77c8bab
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/IOUtilsTest.scala
@@ -0,0 +1,83 @@
+package scoverage
+
+import java.io.{File, FileWriter}
+import java.util.UUID
+
+import AssertUtil._
+import org.junit.Assert._
+import org.junit.{After, Before, Test}
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/** @author Stephen Samuel */
+@RunWith(classOf[JUnit4])
+class IOUtilsTest {
+
+ val base = new File(IOUtils.getTempDirectory, UUID.randomUUID.toString)
+
+ @Before def setup(): Unit = {
+ base.mkdir()
+ }
+
+ @Test
+ def shouldParseMeasurementFiles() = {
+ val file = newTempFile("scoveragemeasurementtest.txt")
+ val writer = new FileWriter(file)
+ writer.write("1\n5\n9\n\n10\n")
+ writer.close()
+ val invokedSet = IOUtils.invoked(Seq(file)).toSet
+
+ assertTrue(invokedSet === Set(1, 5, 9, 10))
+ }
+
+ @Test
+ def shouldParseMultipleMeasurementFiles() = {
+ val file1 = newTempFile("scoverage.measurements.11.txt")
+ val writer1 = new FileWriter(file1)
+ writer1.write("1\n5\n9\n\n10\n")
+ writer1.close()
+ val file2 = newTempFile("scoverage.measurements.22.txt")
+ val writer2 = new FileWriter(file2)
+ writer2.write("1\n7\n14\n\n2\n")
+ writer2.close()
+
+ val files = IOUtils.findMeasurementFiles(file1.getParent)
+ val invokedSet = IOUtils.invoked(files).toSet
+
+ assertTrue(invokedSet === Set(1, 2, 5, 7, 9, 10, 14))
+ }
+
+ @Test
+ def shouldDeepSearchForReportFiles() = {
+
+ val file1 = newTempFile(Constants.XMLReportFilename)
+ val writer1 = new FileWriter(file1)
+ writer1.write("1\n3\n5\n\n\n7\n")
+ writer1.close()
+
+ val file2 = newTempFile(UUID.randomUUID + "/" + Constants.XMLReportFilename)
+ file2.getParentFile.mkdir()
+ val writer2 = new FileWriter(file2)
+ writer2.write("2\n4\n6\n\n8\n")
+ writer2.close()
+
+ val file3 = new File(file2.getParent + "/" + UUID.randomUUID + "/" + Constants.XMLReportFilename)
+ file3.getParentFile.mkdir()
+ val writer3 = new FileWriter(file3)
+ writer3.write("11\n20\n30\n\n44\n")
+ writer3.close()
+
+ val files = IOUtils.reportFileSearch(base, IOUtils.isReportFile)
+ val invokedSet = IOUtils.invoked(files).toSet
+
+ assertTrue(invokedSet === Set(1, 2, 3, 4, 5, 6, 7, 8, 11, 20, 30, 44))
+ }
+
+ @After def cleanup(): Unit = {
+ base.delete()
+ }
+
+ private def newTempFile(file: String): File = {
+ new File(s"$base/$file")
+ }
+}
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerConcurrencyTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerConcurrencyTest.scala
new file mode 100644
index 0000000..f9a74e1
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerConcurrencyTest.scala
@@ -0,0 +1,63 @@
+package scoverage
+
+import java.io.File
+import java.util.UUID
+import java.util.concurrent.Executors
+
+import AssertUtil._
+import org.junit.Assert._
+import org.junit.{After, Before, Test}
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+import scala.collection.breakOut
+import scala.concurrent._
+import scala.concurrent.duration._
+
+/**
+ * Verify that the runtime is thread-safe
+ */
+@RunWith(classOf[JUnit4])
+class InvokerConcurrencyTest {
+
+ implicit val executor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(8))
+
+ val measurementDir = new File(IOUtils.getTempDirectory, UUID.randomUUID.toString) //"target/invoker-test.measurement.concurrent")
+
+ @Before def setup(): Unit = {
+ deleteMeasurementFiles()
+ measurementDir.mkdirs()
+ }
+
+ @Test
+ def callingInvokerInvokedOnMultipleThreadsDoesNotCorruptTheMeasurementFile() = {
+
+ val testIds: Set[Int] = (1 to 1000).toSet
+
+ // Create 1k "invoked" calls on the common thread pool, to stress test
+ // the method
+ val futures: List[Future[Unit]] = testIds.map { i: Int =>
+ Future {
+ Invoker.invoked(i, measurementDir.toString)
+ }
+ }(breakOut)
+
+ futures.foreach(Await.result(_, 1.second))
+
+ // Now verify that the measurement file is not corrupted by loading it
+ val measurementFiles = IOUtils.findMeasurementFiles(measurementDir)
+ val idsFromFile = IOUtils.invoked(measurementFiles).toSet
+
+ assertTrue(idsFromFile === testIds)
+ }
+
+ @After def cleanup(): Unit = {
+ deleteMeasurementFiles()
+ measurementDir.delete()
+ }
+
+ private def deleteMeasurementFiles(): Unit = {
+ if (measurementDir.isDirectory)
+ measurementDir.listFiles().foreach(_.delete())
+ }
+}
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerMultiModuleTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerMultiModuleTest.scala
new file mode 100644
index 0000000..b530f12
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerMultiModuleTest.scala
@@ -0,0 +1,57 @@
+package scoverage
+
+import java.io.File
+import java.util.UUID
+
+import AssertUtil._
+import org.junit.Assert._
+import org.junit.{After, Before, Test}
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Verify that the runtime can handle a multi-module project
+ */
+@RunWith(classOf[JUnit4])
+class InvokerMultiModuleTest {
+
+ val measurementDir = Array(
+ new File(IOUtils.getTempDirectory, UUID.randomUUID.toString),
+ new File(IOUtils.getTempDirectory, UUID.randomUUID.toString))
+
+ @Before def setup(): Unit = {
+ deleteMeasurementFiles()
+ measurementDir.foreach(_.mkdirs())
+ }
+
+ @Test
+ def callingInvokerInvokedOnWithDifferentDirectoriesPutsMeasurementsInDifferentDirectories() = {
+
+ val testIds: Set[Int] = (1 to 10).toSet
+
+ testIds.map { i: Int => Invoker.invoked(i, measurementDir(i % 2).toString) }
+
+ // Verify measurements went to correct directory
+ val measurementFiles0 = IOUtils.findMeasurementFiles(measurementDir(0))
+ val idsFromFile0 = IOUtils.invoked(measurementFiles0).toSet
+
+ assertTrue(idsFromFile0 === testIds.filter { i: Int => i % 2 == 0 })
+
+ val measurementFiles1 = IOUtils.findMeasurementFiles(measurementDir(1))
+ val idsFromFile1 = IOUtils.invoked(measurementFiles1).toSet
+
+ assertTrue(idsFromFile1 === testIds.filter { i: Int => i % 2 == 1 })
+ }
+
+ @After def cleanup(): Unit = {
+ deleteMeasurementFiles()
+ measurementDir.foreach(_.delete)
+ }
+
+ private def deleteMeasurementFiles(): Unit = {
+ measurementDir.foreach((md) => {
+ if (md.isDirectory)
+ md.listFiles().foreach(_.delete())
+ })
+ }
+}
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerStub.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerStub.scala
new file mode 100644
index 0000000..5010e9c
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/InvokerStub.scala
@@ -0,0 +1,10 @@
+package scoverage
+
+object RuntimeInfo {
+ def runtimePath: String = ???
+ def name: String = ???
+}
+
+object Invoker {
+ def invoked(id: Int, dataDir: String): Unit = ???
+}
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationCompiler.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationCompiler.scala
new file mode 100644
index 0000000..b0cda99
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationCompiler.scala
@@ -0,0 +1,52 @@
+package scoverage
+
+import java.io.File
+
+import scala.tools.nsc.Global
+import scala.tools.nsc.plugins.PluginComponent
+import scala.tools.nsc.transform.{Transform, TypingTransformers}
+
+class LocationCompiler(settings: scala.tools.nsc.Settings, reporter: scala.tools.nsc.reporters.Reporter)
+ extends scala.tools.nsc.Global(settings, reporter) {
+
+ val locations = List.newBuilder[(String, Location)]
+ private val locationSetter = new LocationSetter(this)
+
+ def compile(code: String): Unit = {
+ val files = writeCodeSnippetToTempFile(code)
+ val command = new scala.tools.nsc.CompilerCommand(List(files.getAbsolutePath), settings)
+ new Run().compile(command.files)
+ }
+
+ def writeCodeSnippetToTempFile(code: String): File = {
+ val file = File.createTempFile("code_snippet", ".scala")
+ IOUtils.writeToFile(file, code)
+ file.deleteOnExit()
+ file
+ }
+
+ class LocationSetter(val global: Global) extends PluginComponent with TypingTransformers with Transform {
+
+ override val phaseName = "location-setter"
+ override val runsAfter = List("typer")
+ override val runsBefore = List("patmat")
+
+ override protected def newTransformer(unit: global.CompilationUnit): global.Transformer = new Transformer(unit)
+
+ class Transformer(unit: global.CompilationUnit) extends TypingTransformer(unit) {
+
+ override def transform(tree: global.Tree) = {
+ for (location <- Location(global)(tree)) {
+ locations += (tree.getClass.getSimpleName -> location)
+ }
+ super.transform(tree)
+ }
+ }
+
+ }
+
+ override def computeInternalPhases() {
+ super.computeInternalPhases()
+ addToPhasesSet(locationSetter, "sets locations")
+ }
+}
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationTest.scala
new file mode 100644
index 0000000..7563aaa
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/LocationTest.scala
@@ -0,0 +1,258 @@
+package scoverage
+
+import org.junit.Assert._
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(classOf[JUnit4])
+class LocationTest {
+
+ @Test
+ def processTopLevelTypesForClasses() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.test\nclass Sammy")
+ val loc = compiler.locations.result().find(_._1 == "Template").get._2
+ assertEquals(loc.packageName, "com.test")
+ assertEquals(loc.className, "Sammy")
+ assertEquals(loc.fullClassName, "com.test.Sammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Class)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def processTopLevelTypesForObjects() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.test\nobject Bammy { def foo = 'boo } ")
+ val loc = compiler.locations.result().find(_._1 == "Template").get._2
+ assertEquals(loc.packageName, "com.test")
+ assertEquals(loc.className, "Bammy")
+ assertEquals(loc.fullClassName, "com.test.Bammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Object)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def processTopLevelTypesForTraits() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.test\ntrait Gammy { def goo = 'hoo } ")
+ val loc = compiler.locations.result().find(_._1 == "Template").get._2
+ assertEquals(loc.packageName, "com.test")
+ assertEquals(loc.className, "Gammy")
+ assertEquals(loc.fullClassName, "com.test.Gammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Trait)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def shouldCorrectlyProcessMethods() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.methodtest \n class Hammy { def foo = 'boo } ")
+ val loc = compiler.locations.result().find(_._2.method == "foo").get._2
+ assertEquals(loc.packageName, "com.methodtest")
+ assertEquals(loc.className, "Hammy")
+ assertEquals(loc.fullClassName, "com.methodtest.Hammy")
+ assertEquals(loc.classType, ClassType.Class)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def shouldCorrectlyProcessNestedMethods() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.methodtest \n class Hammy { def foo = { def goo = { getClass; 3 }; goo } } ")
+ val loc = compiler.locations.result().find(_._2.method == "goo").get._2
+ assertEquals(loc.packageName, "com.methodtest")
+ assertEquals(loc.className, "Hammy")
+ assertEquals(loc.fullClassName, "com.methodtest.Hammy")
+ assertEquals(loc.classType, ClassType.Class)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def shouldProcessAnonFunctionsAsInsideTheEnclosingMethod() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.methodtest \n class Jammy { def moo = { Option(\"bat\").map(_.length) } } ")
+ val loc = compiler.locations.result().find(_._1 == "Function").get._2
+ assertEquals(loc.packageName, "com.methodtest")
+ assertEquals(loc.className, "Jammy")
+ assertEquals(loc.fullClassName, "com.methodtest.Jammy")
+ assertEquals(loc.method, "moo")
+ assertEquals(loc.classType, ClassType.Class)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def shouldUseOuterPackageForNestedClasses() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.methodtest \n class Jammy { class Pammy } ")
+ val loc = compiler.locations.result().find(_._2.className == "Pammy").get._2
+ assertEquals(loc.packageName, "com.methodtest")
+ assertEquals(loc.className, "Pammy")
+ assertEquals(loc.fullClassName, "com.methodtest.Jammy.Pammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Class)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+
+ }
+
+ @Test
+ def shouldUseOuterPackageForNestedObjects() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.methodtest \n class Jammy { object Zammy } ")
+ val loc = compiler.locations.result().find(_._2.className == "Zammy").get._2
+ assertEquals(loc.packageName, "com.methodtest")
+ assertEquals(loc.className, "Zammy")
+ assertEquals(loc.fullClassName, "com.methodtest.Jammy.Zammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Object)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def shouldUseOuterPackageForNestedTraits() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.methodtest \n class Jammy { trait Mammy } ")
+ val loc = compiler.locations.result().find(_._2.className == "Mammy").get._2
+ assertEquals(loc.packageName, "com.methodtest")
+ assertEquals(loc.className, "Mammy")
+ assertEquals(loc.fullClassName, "com.methodtest.Jammy.Mammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Trait)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+
+ @Test
+ def shouldSupportNestedPackagesForClasses() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.a \n " +
+ "package b \n" +
+ "class Kammy ")
+ val loc = compiler.locations.result().find(_._1 == "Template").get._2
+ assertEquals(loc.packageName, "com.a.b")
+ assertEquals(loc.className, "Kammy")
+ assertEquals(loc.fullClassName, "com.a.b.Kammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Class)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def shouldSupportNestedPackagesForObjects() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.a \n " +
+ "package b \n" +
+ "object Kammy ")
+ val loc = compiler.locations.result().find(_._1 == "Template").get._2
+ assertEquals(loc.packageName, "com.a.b")
+ assertEquals(loc.className, "Kammy")
+ assertEquals(loc.fullClassName, "com.a.b.Kammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Object)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def shouldSupportNestedPackagesForTraits() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.a \n " +
+ "package b \n" +
+ "trait Kammy ")
+ val loc = compiler.locations.result().find(_._1 == "Template").get._2
+ assertEquals(loc.packageName, "com.a.b")
+ assertEquals(loc.className, "Kammy")
+ assertEquals(loc.fullClassName, "com.a.b.Kammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Trait)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+
+ @Test
+ def shouldUseNoneMethodNameForClassConstructorBody() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.b \n class Tammy { val name = 'sam } ")
+ val loc = compiler.locations.result().find(_._1 == "ValDef").get._2
+ assertEquals(loc.packageName, "com.b")
+ assertEquals(loc.className, "Tammy")
+ assertEquals(loc.fullClassName, "com.b.Tammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Class)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def shouldUseNoneMethodNameForObjectConstructorBody() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.b \n object Yammy { val name = 'sam } ")
+ val loc = compiler.locations.result().find(_._1 == "ValDef").get._2
+ assertEquals(loc.packageName, "com.b")
+ assertEquals(loc.className, "Yammy")
+ assertEquals(loc.fullClassName, "com.b.Yammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Object)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def shouldUseNoneMethodNameForTraitConstructorBody() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.b \n trait Wammy { val name = 'sam } ")
+ val loc = compiler.locations.result().find(_._1 == "ValDef").get._2
+ assertEquals(loc.packageName, "com.b")
+ assertEquals(loc.className, "Wammy")
+ assertEquals(loc.fullClassName, "com.b.Wammy")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Trait)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def anonClassShouldReportEnclosingClass() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler
+ .compile(
+ "package com.a; object A { def foo(b : B) : Unit = b.invoke }; trait B { def invoke : Unit }; class C { A.foo(new B { def invoke = () }) }")
+ println()
+ println(compiler.locations.result().mkString("\n"))
+ val loc = compiler.locations.result().filter(_._1 == "Template").last._2
+ assertEquals(loc.packageName, "com.a")
+ assertEquals(loc.className, "C")
+ assertEquals(loc.fullClassName, "com.a.C")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Class)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def anonClassImplementedMethodShouldReportEnclosingMethod() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile(
+ "package com.a; object A { def foo(b : B) : Unit = b.invoke }; trait B { def invoke : Unit }; class C { A.foo(new B { def invoke = () }) }")
+ val loc = compiler.locations.result().filter(_._1 == "DefDef").last._2
+ assertEquals(loc.packageName, "com.a")
+ assertEquals(loc.className, "C")
+ assertEquals(loc.fullClassName, "com.a.C")
+ assertEquals(loc.method, "invoke")
+ assertEquals(loc.classType, ClassType.Class)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+
+ @Test
+ def doublyNestedClassesShouldReportCorrectFullClassName() = {
+ val compiler = ScoverageCompiler.locationCompiler
+ compiler.compile("package com.a \n object Foo { object Boo { object Moo { val name = 'sam } } }")
+ val loc = compiler.locations.result().find(_._1 == "ValDef").get._2
+ assertEquals(loc.packageName, "com.a")
+ assertEquals(loc.className, "Moo")
+ assertEquals(loc.fullClassName, "com.a.Foo.Boo.Moo")
+ assertEquals(loc.method, "")
+ assertEquals(loc.classType, ClassType.Object)
+ assertTrue(loc.sourcePath.endsWith(".scala"))
+ }
+}
+
+
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginASTSupportTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginASTSupportTest.scala
new file mode 100644
index 0000000..d5d521e
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginASTSupportTest.scala
@@ -0,0 +1,105 @@
+package scoverage
+
+import org.junit.Assert._
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/** @author Stephen Samuel */
+@RunWith(classOf[JUnit4])
+class PluginASTSupportTest {
+
+ @Test
+ def scoverageComponentShouldIgnoreBasicMacros() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet( """
+ | object MyMacro {
+ | import scala.language.experimental.macros
+ | import scala.reflect.macros.Context
+ | def test = macro testImpl
+ | def testImpl(c: Context): c.Expr[Unit] = {
+ | import c.universe._
+ | reify {
+ | println("macro test")
+ | }
+ | }
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ }
+
+ @Test
+ def scoverageComponentShouldIgnoreComplexMacros11() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet( """ object ComplexMacro {
+ |
+ | import scala.language.experimental.macros
+ | import scala.reflect.macros.Context
+ |
+ | def debug(params: Any*) = macro debugImpl
+ |
+ | def debugImpl(c: Context)(params: c.Expr[Any]*) = {
+ | import c.universe._
+ |
+ | val trees = params map {param => (param.tree match {
+ | case Literal(Constant(_)) => reify { print(param.splice) }
+ | case _ => reify {
+ | val variable = c.Expr[String](Literal(Constant(show(param.tree)))).splice
+ | print(s"$variable = ${param.splice}")
+ | }
+ | }).tree
+ | }
+ |
+ | val separators = (1 until trees.size).map(_ => (reify { print(", ") }).tree) :+ (reify { println() }).tree
+ | val treesWithSeparators = trees zip separators flatMap {p => List(p._1, p._2)}
+ |
+ | c.Expr[Unit](Block(treesWithSeparators.toList, Literal(Constant(()))))
+ | }
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ }
+
+ // https://github.com/scoverage/scalac-scoverage-plugin/issues/32
+ @Test
+ def exhaustiveWarningsShouldNotBeGeneratedForUnchecked() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet( """object PartialMatchObject {
+ | def partialMatchExample(s: Option[String]): Unit = {
+ | (s: @unchecked) match {
+ | case Some(str) => println(str)
+ | }
+ | }
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ }
+
+
+
+ // https://github.com/scoverage/scalac-scoverage-plugin/issues/45
+ @Test
+ def compileFinalValsInAnnotations() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet( """object Foo {
+ | final val foo = 1L
+ |}
+ |@SerialVersionUID(Foo.foo)
+ |class Bar
+ |""".stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ }
+
+ @Test
+ def typeParamWithDefaultArgSupported() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet( """class TypeTreeObjects {
+ | class Container {
+ | def typeParamAndDefaultArg[C](name: String = "sammy"): String = name
+ | }
+ | new Container().typeParamAndDefaultArg[Any]()
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ }
+}
+
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginCoverageTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginCoverageTest.scala
new file mode 100644
index 0000000..20b776e
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/PluginCoverageTest.scala
@@ -0,0 +1,359 @@
+package scoverage
+
+import org.junit.Assert._
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/** @author Stephen Samuel */
+@RunWith(classOf[JUnit4])
+class PluginCoverageTest {
+
+ @Test
+ def scoverageShouldInstrumentDefaultArgumentsWithMethods() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """ object DefaultArgumentsObject {
+ | val defaultName = "world"
+ | def makeGreeting(name: String = defaultName): String = {
+ | s"Hello, $name"
+ | }
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ // we expect:
+ // instrumenting the default-param which becomes a method call invocation
+ // the method makeGreeting is entered.
+ compiler.assertNMeasuredStatements(2)
+ }
+
+ @Test
+ def scoverageShouldSkipMacros() = {
+ val compiler = ScoverageCompiler.default
+ val code = if (ScoverageCompiler.ShortScalaVersion == "2.10")
+ """
+ import scala.language.experimental.macros
+ import scala.reflect.macros.Context
+ object Impl {
+ def poly[T: c.WeakTypeTag](c: Context) = c.literal(c.weakTypeOf[T].toString)
+ }
+
+ object Macros {
+ def poly[T] = macro Impl.poly[T]
+ }"""
+ else
+ """
+ import scala.language.experimental.macros
+ import scala.reflect.macros.Context
+ class Impl(val c: Context) {
+ import c.universe._
+ def poly[T: c.WeakTypeTag] = c.literal(c.weakTypeOf[T].toString)
+ }
+ object Macros {
+ def poly[T] = macro Impl.poly[T]
+ }"""
+ compiler.compileCodeSnippet(code)
+ assertTrue(!compiler.reporter.hasErrors)
+ compiler.assertNMeasuredStatements(0)
+ }
+
+ @Test
+ def scoverageShouldInstrumentFinalVals() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """ object FinalVals {
+ | final val name = {
+ | val name = "sammy"
+ | if (System.currentTimeMillis() > 0) {
+ | println(name)
+ | }
+ | }
+ | println(name)
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ // we should have 3 statements - initialising the val, executing println, and executing the parameter
+ compiler.assertNMeasuredStatements(8)
+ }
+
+ @Test
+ def scoverageShouldNotInstrumentTheMatchAsAStatement() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """ object A {
+ | System.currentTimeMillis() match {
+ | case x => println(x)
+ | }
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+
+ /** should have the following statements instrumented:
+ * the selector, clause 1
+ */
+ compiler.assertNMeasuredStatements(2)
+ }
+
+ @Test
+ def scoverageShouldInstrumentMatchGuards() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """ object A {
+ | System.currentTimeMillis() match {
+ | case l if l < 1000 => println("a")
+ | case l if l > 1000 => println("b")
+ | case _ => println("c")
+ | }
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+
+ /** should have the following statements instrumented:
+ * the selector, guard 1, clause 1, guard 2, clause 2, clause 3
+ */
+ compiler.assertNMeasuredStatements(6)
+ }
+
+ @Test
+ def scoverageShouldInstrumentNonBasicSelector() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """ trait A {
+ | def someValue = "sammy"
+ | def foo(a:String) = someValue match {
+ | case any => "yes"
+ | }
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ // should instrument:
+ // the someValue method entry
+ // the selector call
+ // case block "yes" literal
+ compiler.assertNMeasuredStatements(3)
+ }
+
+ @Test
+ def scoverageShouldInstrumentConditionalSelectorsInAMatch() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """ trait A {
+ | def foo(a:String) = (if (a == "hello") 1 else 2) match {
+ | case any => "yes"
+ | }
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ // should instrument:
+ // the if clause,
+ // thenp block,
+ // thenp literal "1",
+ // elsep block,
+ // elsep literal "2",
+ // case block "yes" literal
+ compiler.assertNMeasuredStatements(6)
+ }
+
+ // https://github.com/scoverage/sbt-scoverage/issues/16
+ @Test
+ def scoverageShouldInstrumentForLoopsButNotTheGeneratedScaffolding() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """ trait A {
+ | def print1(list: List[String]) = for (string: String <- list) println(string)
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ // should instrument:
+ // the def method entry
+ // foreach body
+ // specifically we want to avoid the withFilter partial function added by the compiler
+ compiler.assertNMeasuredStatements(2)
+ }
+
+ @Test
+ def scoverageShouldInstrumentForLoopGuards() = {
+ val compiler = ScoverageCompiler.default
+
+ compiler.compileCodeSnippet(
+ """object A {
+ | def foo(list: List[String]) = for (string: String <- list if string.length > 5)
+ | println(string)
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ // should instrument:
+ // foreach body
+ // the guard
+ // but we want to avoid the withFilter partial function added by the compiler
+ compiler.assertNMeasuredStatements(3)
+ }
+
+ @Test
+ def scoverageShouldCorrectlyHandleNewWithArgsApplyWithListOfArgs() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """ object A {
+ | new String(new String(new String))
+ | } """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ // should have 3 statements, one for each of the nested strings
+ compiler.assertNMeasuredStatements(3)
+ }
+
+ @Test
+ def scoverageShouldCorrectlyHandleNoArgsNewApplyEmptyListOfArgs() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """ object A {
+ | new String
+ | } """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ // should have 1. the apply that wraps the select.
+ compiler.assertNMeasuredStatements(1)
+ }
+
+ @Test
+ def scoverageShouldCorrectlyHandleNewThatInvokesNestedStatements() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """
+ | object A {
+ | val value = new java.util.concurrent.CountDownLatch(if (System.currentTimeMillis > 1) 5 else 10)
+ | } """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ // should have 6 statements - the apply/new statement, two literals, the if cond, if elsep, if thenp
+ compiler.assertNMeasuredStatements(6)
+ }
+
+ @Test
+ def scoverageShouldInstrumentValRHS() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """object A {
+ | val name = BigDecimal(50.0)
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ compiler.assertNMeasuredStatements(1)
+ }
+
+ @Test
+ def scoverageShouldNotInstrumentFunctionTupleWrapping() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """
+ | sealed trait Foo
+ | case class Bar(s: String) extends Foo
+ | case object Baz extends Foo
+ |
+ | object Foo {
+ | implicit val fooOrdering: Ordering[Foo] = Ordering.fromLessThan {
+ | case (Bar(_), Baz) => true
+ | case (Bar(a), Bar(b)) => a < b
+ | case (_, _) => false
+ | }
+ | }
+ """.stripMargin)
+
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ // should have 4 profiled statements: the outer apply, the true, the a < b, the false
+ // we are testing that we don't instrument the tuple2 call used here
+ compiler.assertNMeasuredStatements(4)
+ }
+
+ @Test
+ def scoverageShouldInstrumentAllCaseStatementsInAnExplicitMatch() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """ trait A {
+ | def foo(name: Any) = name match {
+ | case i : Int => 1
+ | case b : Boolean => println("boo")
+ | case _ => 3
+ | }
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ assertTrue(!compiler.reporter.hasWarnings)
+ // should have one statement for each case body
+ // selector is a constant so would be ignored.
+ compiler.assertNMeasuredStatements(3)
+ }
+
+ @Test
+ def pluginShouldSupportYields() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """
+ | object Yielder {
+ | val holidays = for ( name <- Seq("sammy", "clint", "lee");
+ | place <- Seq("london", "philly", "iowa") ) yield {
+ | name + " has been to " + place
+ | }
+ | }""".stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ // 2 statements for the two applies in Seq, one for each literal which is 6, one for the flat map,
+ // one for the map, one for the yield op.
+ compiler.assertNMeasuredStatements(11)
+ }
+
+ @Test
+ def pluginShouldNotInstrumentLocalMacroImplementation() = {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """
+ | object MyMacro {
+ | import scala.language.experimental.macros
+ | import scala.reflect.macros.Context
+ | def test = macro testImpl
+ | def testImpl(c: Context): c.Expr[Unit] = {
+ | import c.universe._
+ | reify {
+ | println("macro test")
+ | }
+ | }
+ |} """.stripMargin)
+ assertTrue(!compiler.reporter.hasErrors)
+ compiler.assertNoCoverage()
+ }
+
+ /* Make sure this is covered in another repo, then delete
+
+ test("plugin should not instrument expanded macro code github.com/skinny-framework/skinny-framework/issues/97") {
+ val compiler = ScoverageCompiler.default
+ scalaLoggingDeps.foreach(compiler.addToClassPath(_))
+ compiler.compileCodeSnippet( s"""import ${scalaLoggingPackageName}.StrictLogging
+ |class MacroTest extends StrictLogging {
+ | logger.info("will break")
+ |} """.stripMargin)
+ assert(!compiler.reporter.hasErrors)
+ assert(!compiler.reporter.hasWarnings)
+ compiler.assertNoCoverage()
+ }
+
+ ignore("plugin should handle return inside catch github.com/scoverage/scalac-scoverage-plugin/issues/93") {
+ val compiler = ScoverageCompiler.default
+ compiler.compileCodeSnippet(
+ """
+ | object bob {
+ | def fail(): Boolean = {
+ | try {
+ | true
+ | } catch {
+ | case _: Throwable =>
+ | Option(true) match {
+ | case Some(bool) => return recover(bool) // comment this return and instrumentation succeeds
+ | case _ =>
+ | }
+ | false
+ | }
+ | }
+ | def recover(it: Boolean): Boolean = it
+ | }
+ """.stripMargin)
+ assert(!compiler.reporter.hasErrors)
+ assert(!compiler.reporter.hasWarnings)
+ compiler.assertNMeasuredStatements(11)
+ }
+ */
+}
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/RegexCoverageFilterTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/RegexCoverageFilterTest.scala
new file mode 100644
index 0000000..69a5e95
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/RegexCoverageFilterTest.scala
@@ -0,0 +1,226 @@
+package scoverage
+
+import org.mockito.Mockito
+import org.mockito.Mockito._
+
+import AssertUtil._
+import org.junit.Assert._
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+import scala.reflect.internal.util.{BatchSourceFile, SourceFile, NoFile}
+import scala.reflect.io.AbstractFile
+
+@RunWith(classOf[JUnit4])
+class RegexCoverageFilterTest {
+
+ @Test
+ def isClassIncludedShouldReturnTrueForEmptyExcludes() = {
+ assertTrue(new RegexCoverageFilter(Nil, Nil, Nil).isClassIncluded("x"))
+ }
+
+ @Test
+ def isClassIncludedShouldNotCrashForEmptyInput() = {
+ assertTrue(new RegexCoverageFilter(Nil, Nil, Nil).isClassIncluded(""))
+ }
+
+ @Test
+ def isClassIncludedShouldExcludeScoverageArrowScoverage() = {
+ assertTrue(!new RegexCoverageFilter(Seq("scoverage"), Nil, Nil).isClassIncluded("scoverage"))
+ }
+
+ @Test
+ def isClassIncludedShouldIncludeScoverageArrowScoverageeee() = {
+ assertTrue(new RegexCoverageFilter(Seq("scoverage"), Nil, Nil).isClassIncluded("scoverageeee"))
+ }
+
+ @Test
+ def isClassIncludedShouldExcludeScoverageStarArrowScoverageeee() = {
+ assertTrue(!new RegexCoverageFilter(Seq("scoverage*"), Nil, Nil).isClassIncluded("scoverageeee"))
+ }
+
+ @Test
+ def isClassIncludedShouldIncludeEeeArrowScoverageeee() = {
+ assertTrue(new RegexCoverageFilter(Seq("eee"), Nil, Nil).isClassIncluded("scoverageeee"))
+ }
+
+ @Test
+ def isClassIncludedShouldExcludeDotStarEeeArrowScoverageeee() = {
+ assertTrue(!new RegexCoverageFilter(Seq(".*eee"), Nil, Nil).isClassIncluded("scoverageeee"))
+ }
+
+ val abstractFile = mock(classOf[AbstractFile])
+ Mockito.when(abstractFile.path).thenReturn("sammy.scala")
+
+ @Test
+ def isFileIncludedShouldReturnTrueForEmptyExcludes() = {
+ val file = new BatchSourceFile(abstractFile, Array.emptyCharArray)
+ assertTrue(new RegexCoverageFilter(Nil, Nil, Nil).isFileIncluded(file))
+ }
+
+ @Test
+ def isFileIncludedShouldExcludeByFilename() = {
+ val file = new BatchSourceFile(abstractFile, Array.emptyCharArray)
+ assertFalse(new RegexCoverageFilter(Nil, Seq("sammy"), Nil).isFileIncluded(file))
+ }
+
+ @Test
+ def isFileIncludedShouldExcludeByRegexWildcard() = {
+ val file = new BatchSourceFile(abstractFile, Array.emptyCharArray)
+ assertFalse(new RegexCoverageFilter(Nil, Seq("sam.*"), Nil).isFileIncluded(file))
+ }
+
+ @Test
+ def isFileIncludedShouldNotExcludeNonMatchingRegex() = {
+ val file = new BatchSourceFile(abstractFile, Array.emptyCharArray)
+ assertTrue(new RegexCoverageFilter(Nil, Seq("qweqeqwe"), Nil).isFileIncluded(file))
+ }
+
+ val options = new ScoverageOptions()
+
+ @Test
+ def isSymbolIncludedShouldReturnTrueForEmptyExcludes() = {
+ assertTrue(new RegexCoverageFilter(Nil, Nil, Nil).isSymbolIncluded("x"))
+ }
+
+ @Test
+ def isSymbolIncludedShouldNotCrashForEmptyInput() = {
+ assertTrue(new RegexCoverageFilter(Nil, Nil, Nil).isSymbolIncluded(""))
+ }
+
+ @Test
+ def isSymbolIncludedShouldExcludeScoverageArrowScoverage() = {
+ assertTrue(!new RegexCoverageFilter(Nil, Nil, Seq("scoverage")).isSymbolIncluded("scoverage"))
+ }
+
+ @Test
+ def isSymbolIncludedShouldIncludeScoverageArrowScoverageeee() = {
+ assertTrue(new RegexCoverageFilter(Nil, Nil, Seq("scoverage")).isSymbolIncluded("scoverageeee"))
+ }
+
+ @Test
+ def isSymbolIncludedShouldExcludeScoverageStarArrowScoverageeee() = {
+ assertTrue(!new RegexCoverageFilter(Nil, Nil, Seq("scoverage*")).isSymbolIncluded("scoverageeee"))
+ }
+
+ @Test
+ def isSymbolIncludedShouldIncludeEeeArrowScoverageeee() = {
+ assertTrue(new RegexCoverageFilter(Nil, Nil, Seq("eee")).isSymbolIncluded("scoverageeee"))
+ }
+
+ @Test
+ def isSymbolIncludedShouldExcludeDotStarEeeArrowScoverageeee() = {
+ assertTrue(!new RegexCoverageFilter(Nil, Nil, Seq(".*eee")).isSymbolIncluded("scoverageeee"))
+ }
+
+ @Test
+ def isSymbolIncludedShouldExcludeScalaReflectApiExprsExpr() = {
+ assertTrue(!new RegexCoverageFilter(Nil, Nil, options.excludedSymbols).isSymbolIncluded("scala.reflect.api.Exprs.Expr"))
+ }
+
+ @Test
+ def isSymbolIncludedShouldExcludeScalaReflectMacrosUniverseTree() = {
+ assertTrue(!new RegexCoverageFilter(Nil, Nil, options.excludedSymbols).isSymbolIncluded("scala.reflect.macros.Universe.Tree"))
+ }
+
+ @Test
+ def isSymbolIncludedShouldExcludeScalaReflectApiTreesTree() = {
+ assertTrue(!new RegexCoverageFilter(Nil, Nil, options.excludedSymbols).isSymbolIncluded("scala.reflect.api.Trees.Tree"))
+ }
+
+ @Test
+ def getExcludedLineNumbersShouldExcludeNoLinesIfNoMagicCommentsAreFound() = {
+ val file =
+ """1
+ |2
+ |3
+ |4
+ |5
+ |6
+ |7
+ |8
+ """.stripMargin
+
+ val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file))
+ assertTrue(numbers === List.empty)
+ }
+
+ @Test
+ def getExcludedLineNumbersShouldExcludeLinesBetweenMagicComments() = {
+ val file =
+ """1
+ |2
+ |3
+ | // $COVERAGE-OFF$
+ |5
+ |6
+ |7
+ |8
+ | // $COVERAGE-ON$
+ |10
+ |11
+ | // $COVERAGE-OFF$
+ |13
+ | // $COVERAGE-ON$
+ |15
+ |16
+ """.stripMargin
+
+ val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file))
+ assertTrue(numbers === List(Range(4, 9), Range(12, 14)))
+ }
+
+ @Test
+ def getExcludedLineNumbersShouldExcludeAllLinesAfterAnUnpairedMagicComment() = {
+ val file =
+ """1
+ |2
+ |3
+ | // $COVERAGE-OFF$
+ |5
+ |6
+ |7
+ |8
+ | // $COVERAGE-ON$
+ |10
+ |11
+ | // $COVERAGE-OFF$
+ |13
+ |14
+ |15
+ """.stripMargin
+
+ val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file))
+ assertTrue(numbers === List(Range(4, 9), Range(12, 16)))
+ }
+
+ @Test
+ def getExcludedLineNumbersShouldAllowTextCommentsOnTheSameLineAsTheMarkers() = {
+ val file =
+ """1
+ |2
+ |3
+ | // $COVERAGE-OFF$ because the next lines are boring
+ |5
+ |6
+ |7
+ |8
+ | // $COVERAGE-ON$ resume coverage here
+ |10
+ |11
+ | // $COVERAGE-OFF$ but ignore this bit
+ |13
+ |14
+ |15
+ """.stripMargin
+
+ val numbers = new RegexCoverageFilter(Nil, Nil, Nil).getExcludedLineNumbers(mockSourceFile(file))
+ assertTrue(numbers === List(Range(4, 9), Range(12, 16)))
+ }
+
+ private def mockSourceFile(contents: String): SourceFile = {
+ new BatchSourceFile(NoFile, contents.toCharArray)
+ }
+}
+
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/ScoverageCompiler.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/ScoverageCompiler.scala
new file mode 100644
index 0000000..f33d7d5
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/ScoverageCompiler.scala
@@ -0,0 +1,161 @@
+package scoverage
+
+import java.io.{File, FileNotFoundException}
+import java.net.URL
+
+import scala.collection.mutable.ListBuffer
+import scala.tools.nsc.{Settings, Global}
+import scala.tools.nsc.plugins.PluginComponent
+import scala.tools.nsc.transform.{Transform, TypingTransformers}
+
+/** @author Stephen Samuel */
+object ScoverageCompiler {
+
+ val ScalaVersion = scala.util.Properties.versionNumberString
+
+ val ShortScalaVersion = (ScalaVersion split "[.]").toList match {
+ case init :+ last if last forall (_.isDigit) => init mkString "."
+ case _ => ScalaVersion
+ }
+
+
+ def classPath = getScalaJars.map(_.getAbsolutePath) :+ sbtCompileDir.getAbsolutePath :+ runtimeClasses.getAbsolutePath
+
+ def settings: Settings = {
+ val s = new scala.tools.nsc.Settings
+ s.Xprint.value = List("all")
+ s.Yrangepos.value = true
+ s.Yposdebug.value = true
+ s.classpath.value = classPath.mkString(File.pathSeparator)
+
+ val path = s"./scalac-scoverage-plugin/target/scala-$ShortScalaVersion/test-generated-classes"
+ new File(path).mkdirs()
+ s.d.value = path
+ s
+ }
+
+ def default: ScoverageCompiler = {
+ val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings)
+ new ScoverageCompiler(settings, reporter)
+ }
+
+ def locationCompiler: LocationCompiler = {
+ val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings)
+ new LocationCompiler(settings, reporter)
+ }
+
+ private def getScalaJars: List[File] = {
+ val scalaJars = List("scala-compiler", "scala-library", "scala-reflect")
+ scalaJars.map(findScalaJar)
+ }
+
+ private def sbtCompileDir: File = {
+ val dir = new File(s"./scalac-scoverage-plugin/target/scala-$ShortScalaVersion/classes")
+ if (!dir.exists)
+ throw new FileNotFoundException(s"Could not locate SBT compile directory for plugin files [$dir]")
+ dir
+ }
+
+ private def runtimeClasses: File = new File(s"${RuntimeInfo.runtimePath}/target/scala-$ShortScalaVersion/classes")
+
+ private def findScalaJar(artifactId: String): File = findIvyJar("org.scala-lang", artifactId, ScalaVersion)
+
+ private def findIvyJar(groupId: String, artifactId: String, version: String, packaging: String = "jar"): File = {
+ val userHome = System.getProperty("user.home")
+ val jarPath = s"$userHome/.ivy2/cache/$groupId/$artifactId/${packaging}s/${artifactId}-${version}.jar"
+ val file = new File(jarPath)
+ if (!file.exists)
+ throw new FileNotFoundException(s"Could not locate [$jarPath].")
+ file
+ }
+}
+
+class ScoverageCompiler(settings: scala.tools.nsc.Settings, reporter: scala.tools.nsc.reporters.Reporter)
+ extends scala.tools.nsc.Global(settings, reporter) {
+
+ def addToClassPath(file: File): Unit = {
+ settings.classpath.value = settings.classpath.value + File.pathSeparator + file.getAbsolutePath
+ }
+
+ val instrumentationComponent = new ScoverageInstrumentationComponent(this, None, None)
+ instrumentationComponent.setOptions(new ScoverageOptions())
+ val testStore = new ScoverageTestStoreComponent(this)
+ val validator = new PositionValidator(this)
+
+ def compileSourceFiles(files: File*): ScoverageCompiler = {
+ val command = new scala.tools.nsc.CompilerCommand(files.map(_.getAbsolutePath).toList, settings)
+ new Run().compile(command.files)
+ this
+ }
+
+ def writeCodeSnippetToTempFile(code: String): File = {
+ val file = File.createTempFile("scoverage_snippet", ".scala")
+ IOUtils.writeToFile(file, code)
+ file.deleteOnExit()
+ file
+ }
+
+ def compileCodeSnippet(code: String): ScoverageCompiler = compileSourceFiles(writeCodeSnippetToTempFile(code))
+
+ def compileSourceResources(urls: URL*): ScoverageCompiler = {
+ compileSourceFiles(urls.map(_.getFile).map(new File(_)): _*)
+ }
+
+ def assertNoCoverage() = assert(!testStore.sources.mkString(" ").contains(s"scoverage.Invoker.invoked"))
+
+ def assertNMeasuredStatements(n: Int): Unit = {
+ for (k <- 1 to n) {
+ assert(testStore.sources.mkString(" ").contains(s"scoverage.Invoker.invoked($k,"),
+ s"Should be $n invoked statements but missing #$k")
+ }
+ assert(!testStore.sources.mkString(" ").contains(s"scoverage.Invoker.invoked(${n + 1},"),
+ s"Found statement ${n + 1} but only expected $n")
+ }
+
+ class PositionValidator(val global: Global) extends PluginComponent with TypingTransformers with Transform {
+
+ override val phaseName = "scoverage-validator"
+ override val runsAfter = List("typer")
+ override val runsBefore = List("scoverage-instrumentation")
+
+ override protected def newTransformer(unit: global.CompilationUnit): global.Transformer = new Transformer(unit)
+
+ class Transformer(unit: global.CompilationUnit) extends TypingTransformer(unit) {
+
+ override def transform(tree: global.Tree) = {
+ global.validatePositions(tree)
+ tree
+ }
+ }
+
+ }
+
+ class ScoverageTestStoreComponent(val global: Global) extends PluginComponent with TypingTransformers with Transform {
+
+ val sources = new ListBuffer[String]
+
+ override val phaseName = "scoverage-teststore"
+ override val runsAfter = List("jvm")
+ override val runsBefore = List("terminal")
+
+ override protected def newTransformer(unit: global.CompilationUnit): global.Transformer = new Transformer(unit)
+
+ class Transformer(unit: global.CompilationUnit) extends TypingTransformer(unit) {
+
+ override def transform(tree: global.Tree) = {
+ sources append tree.toString
+ tree
+ }
+ }
+
+ }
+
+ override def computeInternalPhases() {
+ super.computeInternalPhases()
+ addToPhasesSet(validator, "scoverage validator")
+ addToPhasesSet(instrumentationComponent, "scoverage instrumentationComponent")
+ addToPhasesSet(testStore, "scoverage teststore")
+ }
+}
+
+
diff --git a/scalac-scoverage-plugin-tests/src/main/scala/scoverage/SerializerTest.scala b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/SerializerTest.scala
new file mode 100644
index 0000000..7995fd7
--- /dev/null
+++ b/scalac-scoverage-plugin-tests/src/main/scala/scoverage/SerializerTest.scala
@@ -0,0 +1,35 @@
+package scoverage
+
+import java.io.StringWriter
+
+import AssertUtil._
+import org.junit.Assert._
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+import scala.xml.Utility
+
+@RunWith(classOf[JUnit4])
+class SerializerTest {
+
+ @Test
+ def coverageShouldBeSerializableIntoXml() = {
+ val coverage = Coverage()
+ coverage.add(
+ Statement(
+ "mysource",
+ Location("org.scoverage", "test", "org.scoverage.test", ClassType.Trait, "mymethod", "mypath"),
+ 14, 100, 200, 4, "def test : String", "test", "DefDef", true, 32
+ )
+ )
+ val expected =
+
+ mysource org.scoverage test Trait org.scoverage.test mymethod mypath 14 100 200 4 def test : String test DefDef true 32 false
+
+
+ val writer = new StringWriter()
+ val actual = Serializer.serialize(coverage, writer)
+ assertTrue(Utility.trim(expected) === Utility.trim(xml.XML.loadString(writer.toString)))
+ }
+}
diff --git a/scalac-scoverage-plugin/src/main/resources/scalac-plugin.xml b/scalac-scoverage-plugin/src/main/resources/scalac-plugin.xml
new file mode 100644
index 0000000..76fd451
--- /dev/null
+++ b/scalac-scoverage-plugin/src/main/resources/scalac-plugin.xml
@@ -0,0 +1,4 @@
+
+ scoverage
+ scoverage.ScoveragePlugin
+
\ No newline at end of file
diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/Constants.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/Constants.scala
new file mode 100644
index 0000000..3259e3b
--- /dev/null
+++ b/scalac-scoverage-plugin/src/main/scala/scoverage/Constants.scala
@@ -0,0 +1,13 @@
+package scoverage
+
+object Constants {
+ // the file that contains the statement mappings
+ val CoverageFileName = "scoverage.coverage.xml"
+ // the final scoverage report
+ val XMLReportFilename = "scoverage.xml"
+ val XMLReportFilenameWithDebug = "scoverage-debug.xml"
+ // directory that contains all the measurement data but not reports
+ val DataDir = "scoverage-data"
+ // the prefix the measurement files have
+ val MeasurementsPrefix = "scoverage.measurements."
+}
diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala
new file mode 100644
index 0000000..1636d08
--- /dev/null
+++ b/scalac-scoverage-plugin/src/main/scala/scoverage/CoverageFilter.scala
@@ -0,0 +1,102 @@
+package scoverage
+
+import scala.collection.mutable
+import scala.reflect.internal.util.{Position, SourceFile}
+
+/**
+ * Methods related to filtering the instrumentation and coverage.
+ *
+ * @author Stephen Samuel
+ */
+trait CoverageFilter {
+ def isClassIncluded(className: String): Boolean
+ def isFileIncluded(file: SourceFile): Boolean
+ def isLineIncluded(position: Position): Boolean
+ def isSymbolIncluded(symbolName: String): Boolean
+ def getExcludedLineNumbers(sourceFile: SourceFile): List[Range]
+}
+
+object AllCoverageFilter extends CoverageFilter {
+ override def getExcludedLineNumbers(sourceFile: SourceFile): List[Range] = Nil
+ override def isLineIncluded(position: Position): Boolean = true
+ override def isClassIncluded(className: String): Boolean = true
+ override def isFileIncluded(file: SourceFile): Boolean = true
+ override def isSymbolIncluded(symbolName: String): Boolean = true
+}
+
+class RegexCoverageFilter(excludedPackages: Seq[String],
+ excludedFiles: Seq[String],
+ excludedSymbols: Seq[String]) extends CoverageFilter {
+
+ val excludedClassNamePatterns = excludedPackages.map(_.r.pattern)
+ val excludedFilePatterns = excludedFiles.map(_.r.pattern)
+ val excludedSymbolPatterns = excludedSymbols.map(_.r.pattern)
+
+ /**
+ * We cache the excluded ranges to avoid scanning the source code files
+ * repeatedly. For a large project there might be a lot of source code
+ * data, so we only hold a weak reference.
+ */
+ val linesExcludedByScoverageCommentsCache: mutable.Map[SourceFile, List[Range]] = mutable.WeakHashMap.empty
+
+ final val scoverageExclusionCommentsRegex =
+ """(?ms)^\s*//\s*(\$COVERAGE-OFF\$).*?(^\s*//\s*\$COVERAGE-ON\$|\Z)""".r
+
+ /**
+ * True if the given className has not been excluded by the
+ * `excludedPackages` option.
+ */
+ override def isClassIncluded(className: String): Boolean = {
+ excludedClassNamePatterns.isEmpty || !excludedClassNamePatterns.exists(_.matcher(className).matches)
+ }
+
+ override def isFileIncluded(file: SourceFile): Boolean = {
+ def isFileMatch(file: SourceFile) = excludedFilePatterns.exists(_.matcher(file.path.replace(".scala", "")).matches)
+ excludedFilePatterns.isEmpty || !isFileMatch(file)
+ }
+
+ /**
+ * True if the line containing `position` has not been excluded by a magic comment.
+ */
+ def isLineIncluded(position: Position): Boolean = {
+ if (position.isDefined) {
+ val excludedLineNumbers = getExcludedLineNumbers(position.source)
+ val lineNumber = position.line
+ !excludedLineNumbers.exists(_.contains(lineNumber))
+ } else {
+ true
+ }
+ }
+
+ override def isSymbolIncluded(symbolName: String): Boolean = {
+ excludedSymbolPatterns.isEmpty || !excludedSymbolPatterns.exists(_.matcher(symbolName).matches)
+ }
+
+ /**
+ * Checks the given sourceFile for any magic comments which exclude lines
+ * from coverage. Returns a list of Ranges of lines that should be excluded.
+ *
+ * The line numbers returned are conventional 1-based line numbers (i.e. the
+ * first line is line number 1)
+ */
+ def getExcludedLineNumbers(sourceFile: SourceFile): List[Range] = {
+ linesExcludedByScoverageCommentsCache.get(sourceFile) match {
+ case Some(lineNumbers) => lineNumbers
+ case None =>
+ val lineNumbers = scoverageExclusionCommentsRegex.findAllIn(sourceFile.content).matchData.map { m =>
+ // Asking a SourceFile for the line number of the char after
+ // the end of the file gives an exception
+ val endChar = math.min(m.end(2), sourceFile.content.length - 1)
+ // Most of the compiler API appears to use conventional
+ // 1-based line numbers (e.g. "Position.line"), but it appears
+ // that the "offsetToLine" method in SourceFile uses 0-based
+ // line numbers
+ Range(
+ 1 + sourceFile.offsetToLine(m.start(1)),
+ 1 + sourceFile.offsetToLine(endChar))
+ }.toList
+ linesExcludedByScoverageCommentsCache.put(sourceFile, lineNumbers)
+ lineNumbers
+ }
+ }
+}
diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/DoubleFormat.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/DoubleFormat.scala
new file mode 100644
index 0000000..0b689eb
--- /dev/null
+++ b/scalac-scoverage-plugin/src/main/scala/scoverage/DoubleFormat.scala
@@ -0,0 +1,19 @@
+package scoverage
+
+import java.text.{DecimalFormat, DecimalFormatSymbols}
+import java.util.Locale
+
+object DoubleFormat {
+ private[this] val twoFractionDigitsFormat: DecimalFormat = {
+ val fmt = new DecimalFormat()
+ fmt.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US))
+ fmt.setMinimumIntegerDigits(1)
+ fmt.setMinimumFractionDigits(2)
+ fmt.setMaximumFractionDigits(2)
+ fmt.setGroupingUsed(false)
+ fmt
+ }
+
+ def twoFractionDigits(d: Double) = twoFractionDigitsFormat.format(d)
+
+}
diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/IOUtils.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/IOUtils.scala
new file mode 100644
index 0000000..409eb81
--- /dev/null
+++ b/scalac-scoverage-plugin/src/main/scala/scoverage/IOUtils.scala
@@ -0,0 +1,85 @@
+package scoverage
+
+import java.io._
+
+import scala.collection.{Set, mutable}
+import scala.io.Source
+
+/** @author Stephen Samuel */
+object IOUtils {
+
+ def getTempDirectory: File = new File(getTempPath)
+ def getTempPath: String = System.getProperty("java.io.tmpdir")
+
+ def readStreamAsString(in: InputStream): String = Source.fromInputStream(in).mkString
+
+ private val UnixSeperator: Char = '/'
+ private val WindowsSeperator: Char = '\\'
+ private val UTF8Encoding: String = "UTF-8"
+
+ def getName(path: String): Any = {
+ val index = {
+ val lastUnixPos = path.lastIndexOf(UnixSeperator)
+ val lastWindowsPos = path.lastIndexOf(WindowsSeperator)
+ Math.max(lastUnixPos, lastWindowsPos)
+ }
+ path.drop(index + 1)
+ }
+
+ def reportFile(outputDir: File, debug: Boolean = false): File = debug match {
+ case true => new File(outputDir, Constants.XMLReportFilenameWithDebug)
+ case false => new File(outputDir, Constants.XMLReportFilename)
+ }
+
+ def clean(dataDir: File): Unit = findMeasurementFiles(dataDir).foreach(_.delete)
+ def clean(dataDir: String): Unit = clean(new File(dataDir))
+
+ def writeToFile(file: File, str: String) = {
+ val writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), UTF8Encoding))
+ try {
+ writer.write(str)
+ } finally {
+ writer.close()
+ }
+ }
+
+ /**
+ * Returns the measurement file for the current thread.
+ */
+ def measurementFile(dataDir: File): File = measurementFile(dataDir.getAbsolutePath)
+ def measurementFile(dataDir: String): File = new File(dataDir, Constants.MeasurementsPrefix + Thread.currentThread.getId)
+
+ def findMeasurementFiles(dataDir: String): Array[File] = findMeasurementFiles(new File(dataDir))
+ def findMeasurementFiles(dataDir: File): Array[File] = dataDir.listFiles(new FileFilter {
+ override def accept(pathname: File): Boolean = pathname.getName.startsWith(Constants.MeasurementsPrefix)
+ })
+
+ def reportFileSearch(baseDir: File, condition: File => Boolean): Seq[File] = {
+ def search(file: File): Seq[File] = file match {
+ case dir if dir.isDirectory => dir.listFiles().toSeq.map(search).flatten
+ case f if isReportFile(f) => Seq(f)
+ case _ => Nil
+ }
+ search(baseDir)
+ }
+
+ val isMeasurementFile = (file: File) => file.getName.startsWith(Constants.MeasurementsPrefix)
+ val isReportFile = (file: File) => file.getName == Constants.XMLReportFilename
+ val isDebugReportFile = (file: File) => file.getName == Constants.XMLReportFilenameWithDebug
+
+ // loads all the invoked statement ids from the given files
+ def invoked(files: Seq[File]): Set[Int] = {
+ val acc = mutable.Set[Int]()
+ files.foreach { file =>
+ val reader = Source.fromFile(file)
+ for ( line <- reader.getLines() ) {
+ if (!line.isEmpty) {
+ acc += line.toInt
+ }
+ }
+ reader.close()
+ }
+ acc
+ }
+
+}
diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/Location.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/Location.scala
new file mode 100644
index 0000000..7845811
--- /dev/null
+++ b/scalac-scoverage-plugin/src/main/scala/scoverage/Location.scala
@@ -0,0 +1,65 @@
+package scoverage
+
+import scala.tools.nsc.Global
+
+/**
+ * @param packageName the name of the enclosing package
+ * @param className the name of the closest enclosing class
+ * @param fullClassName the fully qualified name of the closest enclosing class
+ */
+case class Location(packageName: String,
+ className: String,
+ fullClassName: String,
+ classType: ClassType,
+ method: String,
+ sourcePath: String) extends java.io.Serializable
+
+object Location {
+
+ def apply(global: Global): global.Tree => Option[Location] = { tree =>
+
+ def packageName(s: global.Symbol): String = {
+ s.enclosingPackage.fullName
+ }
+
+ def className(s: global.Symbol): String = {
+ // anon functions are enclosed in proper classes.
+ if (s.enclClass.isAnonymousFunction || s.enclClass.isAnonymousClass) className(s.owner)
+ else s.enclClass.nameString
+ }
+
+ def classType(s: global.Symbol): ClassType = {
+ if (s.enclClass.isTrait) ClassType.Trait
+ else if (s.enclClass.isModuleOrModuleClass) ClassType.Object
+ else ClassType.Class
+ }
+
+ def fullClassName(s: global.Symbol): String = {
+ // anon functions are enclosed in proper classes.
+ if (s.enclClass.isAnonymousFunction || s.enclClass.isAnonymousClass) fullClassName(s.owner)
+ else s.enclClass.fullNameString
+ }
+
+ def enclosingMethod(s: global.Symbol): String = {
+ // check if we are in a proper method and return that, otherwise traverse up
+ if (s.enclClass.isAnonymousFunction ) enclosingMethod(s.owner)
+ else if (s.enclMethod.isPrimaryConstructor) ""
+ else Option(s.enclMethod.nameString).getOrElse("")
+ }
+
+ def sourcePath(symbol: global.Symbol): String = {
+ Option(symbol.sourceFile).map(_.canonicalPath).getOrElse("")
+ }
+
+ Option(tree.symbol) map {
+ symbol =>
+ Location(
+ packageName(symbol),
+ className(symbol),
+ fullClassName(symbol),
+ classType(symbol),
+ enclosingMethod(symbol),
+ sourcePath(symbol))
+ }
+ }
+}
\ No newline at end of file
diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala
new file mode 100644
index 0000000..a3bec6b
--- /dev/null
+++ b/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala
@@ -0,0 +1,110 @@
+package scoverage
+
+import java.io._
+
+import scala.xml.Utility
+
+object Serializer {
+
+ // Write out coverage data to the given data directory, using the default coverage filename
+ def serialize(coverage: Coverage, dataDir: String): Unit = serialize(coverage, coverageFile(dataDir))
+
+ // Write out coverage data to given file.
+ def serialize(coverage: Coverage, file: File): Unit = {
+ val writer = new BufferedWriter(new FileWriter(file))
+ serialize(coverage, writer)
+ writer.close()
+ }
+
+ def serialize(coverage: Coverage, writer: Writer): Unit = {
+ def writeStatement(stmt: Statement, writer: Writer): Unit = {
+ writer.write {
+ val xml =
+
+ {stmt.source}
+
+
+ {stmt.location.packageName}
+
+
+ {stmt.location.className}
+
+
+ {stmt.location.classType.toString}
+
+
+ {stmt.location.fullClassName}
+
+
+ {stmt.location.method}
+
+
+ {stmt.location.sourcePath}
+
+
+ {stmt.id.toString}
+
+
+ {stmt.start.toString}
+
+
+ {stmt.end.toString}
+
+
+ {stmt.line.toString}
+
+
+ {escape(stmt.desc)}
+
+
+ {escape(stmt.symbolName)}
+
+
+ {escape(stmt.treeName)}
+
+
+ {stmt.branch.toString}
+
+
+ {stmt.count.toString}
+
+
+ {stmt.ignored.toString}
+
+
+ Utility.trim(xml) + "\n"
+ }
+ }
+ writer.write("\n")
+ coverage.statements.foreach(stmt => writeStatement(stmt, writer))
+ writer.write("")
+ }
+
+ def coverageFile(dataDir: File): File = coverageFile(dataDir.getAbsolutePath)
+ def coverageFile(dataDir: String): File = new File(dataDir, Constants.CoverageFileName)
+
+ /**
+ * This method ensures that the output String has only
+ * valid XML unicode characters as specified by the
+ * XML 1.0 standard. For reference, please see
+ * the
+ * standard. This method will return an empty
+ * String if the input is null or empty.
+ *
+ * @param in The String whose non-valid characters we want to remove.
+ * @return The in String, stripped of non-valid characters.
+ * @see http://blog.mark-mclaren.info/2007/02/invalid-xml-characters-when-valid-utf8_5873.html
+ *
+ */
+ def escape(in: String): String = {
+ val out = new StringBuilder()
+ for ( current <- Option(in).getOrElse("").toCharArray ) {
+ if ((current == 0x9) || (current == 0xA) || (current == 0xD) ||
+ ((current >= 0x20) && (current <= 0xD7FF)) ||
+ ((current >= 0xE000) && (current <= 0xFFFD)) ||
+ ((current >= 0x10000) && (current <= 0x10FFFF)))
+ out.append(current)
+ }
+ out.mkString
+ }
+}
diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/coverage.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/coverage.scala
new file mode 100644
index 0000000..6591c99
--- /dev/null
+++ b/scalac-scoverage-plugin/src/main/scala/scoverage/coverage.scala
@@ -0,0 +1,192 @@
+package scoverage
+
+import java.io.File
+
+import scoverage.DoubleFormat.twoFractionDigits
+
+import scala.collection.mutable
+
+/**
+ * @author Stephen Samuel */
+case class Coverage()
+ extends CoverageMetrics
+ with MethodBuilders
+ with java.io.Serializable
+ with ClassBuilders
+ with PackageBuilders
+ with FileBuilders {
+
+ private val statementsById = mutable.Map[Int, Statement]()
+ override def statements = statementsById.values
+ def add(stmt: Statement): Unit = statementsById.put(stmt.id, stmt)
+
+ private val ignoredStatementsById = mutable.Map[Int, Statement]()
+ override def ignoredStatements = ignoredStatementsById.values
+ def addIgnoredStatement(stmt: Statement): Unit = ignoredStatementsById.put(stmt.id, stmt)
+
+
+ def avgClassesPerPackage = classCount / packageCount.toDouble
+ def avgClassesPerPackageFormatted: String = twoFractionDigits(avgClassesPerPackage)
+
+ def avgMethodsPerClass = methodCount / classCount.toDouble
+ def avgMethodsPerClassFormatted: String = twoFractionDigits(avgMethodsPerClass)
+
+ def loc = files.map(_.loc).sum
+ def linesPerFile = loc / fileCount.toDouble
+ def linesPerFileFormatted: String = twoFractionDigits(linesPerFile)
+
+ // returns the classes by least coverage
+ def risks(limit: Int) = classes.toSeq.sortBy(_.statementCount).reverse.sortBy(_.statementCoverage).take(limit)
+
+ def apply(ids: Iterable[Int]): Unit = ids foreach invoked
+ def invoked(id: Int): Unit = statementsById.get(id).foreach(_.invoked())
+}
+
+trait MethodBuilders {
+ def statements: Iterable[Statement]
+ def methods: Seq[MeasuredMethod] = {
+ statements.groupBy(stmt => stmt.location.packageName + "/" + stmt.location.className + "/" + stmt.location.method)
+ .map(arg => MeasuredMethod(arg._1, arg._2))
+ .toSeq
+ }
+ def methodCount = methods.size
+}
+
+trait PackageBuilders {
+ def statements: Iterable[Statement]
+ def packageCount = packages.size
+ def packages: Seq[MeasuredPackage] = {
+ statements.groupBy(_.location.packageName).map(arg => MeasuredPackage(arg._1, arg._2)).toSeq.sortBy(_.name)
+ }
+}
+
+trait ClassBuilders {
+ def statements: Iterable[Statement]
+ def classes = statements.groupBy(_.location.fullClassName).map(arg => MeasuredClass(arg._1, arg._2))
+ def classCount: Int = classes.size
+}
+
+trait FileBuilders {
+ def statements: Iterable[Statement]
+ def files: Iterable[MeasuredFile] = statements.groupBy(_.source).map(arg => MeasuredFile(arg._1, arg._2))
+ def fileCount: Int = files.size
+}
+
+case class MeasuredMethod(name: String, statements: Iterable[Statement]) extends CoverageMetrics {
+ override def ignoredStatements: Iterable[Statement] = Seq()
+}
+
+case class MeasuredClass(fullClassName: String, statements: Iterable[Statement])
+ extends CoverageMetrics with MethodBuilders {
+
+ def source: String = statements.head.source
+ def loc = statements.map(_.line).max
+
+ /**
+ * The class name for display is the FQN minus the package,
+ * for example "com.a.Foo.Bar.Baz" should display as "Foo.Bar.Baz"
+ * and "com.a.Foo" should display as "Foo".
+ *
+ * This is used in the class lists in the package and overview pages.
+ */
+ def displayClassName = statements.headOption.map(_.location).map { location =>
+ location.fullClassName.stripPrefix(location.packageName + ".")
+ }.getOrElse(fullClassName)
+
+ override def ignoredStatements: Iterable[Statement] = Seq()
+}
+
+case class MeasuredPackage(name: String, statements: Iterable[Statement])
+ extends CoverageMetrics with ClassCoverage with ClassBuilders with FileBuilders {
+ override def ignoredStatements: Iterable[Statement] = Seq()
+}
+
+case class MeasuredFile(source: String, statements: Iterable[Statement])
+ extends CoverageMetrics with ClassCoverage with ClassBuilders {
+ def filename = new File(source).getName
+ def loc = statements.map(_.line).max
+
+ override def ignoredStatements: Iterable[Statement] = Seq()
+}
+
+case class Statement(source: String,
+ location: Location,
+ id: Int,
+ start: Int,
+ end: Int,
+ line: Int,
+ desc: String,
+ symbolName: String,
+ treeName: String,
+ branch: Boolean,
+ var count: Int = 0,
+ ignored: Boolean = false) extends java.io.Serializable {
+ def invoked(): Unit = count = count + 1
+ def isInvoked = count > 0
+}
+
+sealed trait ClassType
+object ClassType {
+ case object Object extends ClassType
+ case object Class extends ClassType
+ case object Trait extends ClassType
+ def fromString(str: String): ClassType = {
+ str.toLowerCase match {
+ case "object" => Object
+ case "trait" => Trait
+ case _ => Class
+ }
+ }
+}
+
+case class ClassRef(name: String) {
+ lazy val simpleName = name.split(".").last
+ lazy val getPackage = name.split(".").dropRight(1).mkString(".")
+}
+
+object ClassRef {
+ def fromFilepath(path: String) = ClassRef(path.replace('/', '.'))
+ def apply(_package: String, className: String): ClassRef = ClassRef(_package.replace('/', '.') + "." + className)
+}
+
+trait CoverageMetrics {
+ def statements: Iterable[Statement]
+ def statementCount: Int = statements.size
+
+ def ignoredStatements: Iterable[Statement]
+ def ignoredStatementCount: Int = ignoredStatements.size
+
+ def invokedStatements: Iterable[Statement] = statements.filter(_.count > 0)
+ def invokedStatementCount = invokedStatements.size
+ def statementCoverage: Double = if (statementCount == 0) 1 else invokedStatementCount / statementCount.toDouble
+ def statementCoveragePercent = statementCoverage * 100
+ def statementCoverageFormatted: String = twoFractionDigits(statementCoveragePercent)
+ def branches: Iterable[Statement] = statements.filter(_.branch)
+ def branchCount: Int = branches.size
+ def branchCoveragePercent = branchCoverage * 100
+ def invokedBranches: Iterable[Statement] = branches.filter(_.count > 0)
+ def invokedBranchesCount = invokedBranches.size
+
+ /**
+ * @see http://stackoverflow.com/questions/25184716/scoverage-ambiguous-measurement-from-branch-coverage
+ */
+ def branchCoverage: Double = {
+ // if there are zero branches, then we have a single line of execution.
+ // in that case, if there is at least some coverage, we have covered the branch.
+ // if there is no coverage then we have not covered the branch
+ if (branchCount == 0) {
+ if (statementCoverage > 0) 1
+ else 0
+ } else {
+ invokedBranchesCount / branchCount.toDouble
+ }
+ }
+ def branchCoverageFormatted: String = twoFractionDigits(branchCoveragePercent)
+}
+
+trait ClassCoverage {
+ this: ClassBuilders =>
+ val statements: Iterable[Statement]
+ def invokedClasses: Int = classes.count(_.statements.count(_.count > 0) > 0)
+ def classCoverage: Double = invokedClasses / classes.size.toDouble
+}
diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala
new file mode 100644
index 0000000..d0bec2f
--- /dev/null
+++ b/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala
@@ -0,0 +1,634 @@
+package scoverage
+
+import java.io.File
+import java.util.concurrent.atomic.AtomicInteger
+
+import scala.reflect.internal.ModifierFlags
+import scala.reflect.internal.util.SourceFile
+import scala.tools.nsc.Global
+import scala.tools.nsc.plugins.{PluginComponent, Plugin}
+import scala.tools.nsc.transform.{Transform, TypingTransformers}
+
+/** @author Stephen Samuel */
+class ScoveragePlugin(val global: Global) extends Plugin {
+
+ override val name: String = "scoverage"
+ override val description: String = "scoverage code coverage compiler plugin"
+ private val (extraAfterPhase, extraBeforePhase) = processPhaseOptions(pluginOptions)
+ val instrumentationComponent = new ScoverageInstrumentationComponent(global, extraAfterPhase, extraBeforePhase)
+ override val components: List[PluginComponent] = List(instrumentationComponent)
+
+ override def processOptions(opts: List[String], error: String => Unit) {
+ val options = new ScoverageOptions
+ for (opt <- opts) {
+ if (opt.startsWith("excludedPackages:")) {
+ options.excludedPackages = opt.substring("excludedPackages:".length).split(";").map(_.trim).filterNot(_.isEmpty)
+ } else if (opt.startsWith("excludedFiles:")) {
+ options.excludedFiles = opt.substring("excludedFiles:".length).split(";").map(_.trim).filterNot(_.isEmpty)
+ } else if (opt.startsWith("excludedSymbols:")) {
+ options.excludedSymbols = opt.substring("excludedSymbols:".length).split(";").map(_.trim).filterNot(_.isEmpty)
+ } else if (opt.startsWith("dataDir:")) {
+ options.dataDir = opt.substring("dataDir:".length)
+ } else if (opt.startsWith("extraAfterPhase:") || opt.startsWith("extraBeforePhase:")){
+ // skip here, these flags are processed elsewhere
+ } else {
+ error("Unknown option: " + opt)
+ }
+ }
+ if (!opts.exists(_.startsWith("dataDir:")))
+ throw new RuntimeException("Cannot invoke plugin without specifying ")
+ instrumentationComponent.setOptions(options)
+ }
+
+ override val optionsHelp: Option[String] = Some(Seq(
+ "-P:scoverage:dataDir: where the coverage files should be written\n",
+ "-P:scoverage:excludedPackages:; semicolon separated list of regexs for packages to exclude",
+ "-P:scoverage:excludedFiles:; semicolon separated list of regexs for paths to exclude",
+ "-P:scoverage:excludedSymbols:; semicolon separated list of regexs for symbols to exclude",
+ "-P:scoverage:extraAfterPhase: phase after which scoverage phase runs (must be after typer phase)",
+ "-P:scoverage:extraBeforePhase: phase before which scoverage phase runs (must be before patmat phase)",
+ " Any classes whose fully qualified name matches the regex will",
+ " be excluded from coverage."
+ ).mkString("\n"))
+
+ // copied from scala 2.11
+ private def pluginOptions: List[String] = {
+ // Process plugin options of form plugin:option
+ def namec = name + ":"
+ global.settings.pluginOptions.value filter (_ startsWith namec) map (_ stripPrefix namec)
+ }
+
+ private def processPhaseOptions(opts: List[String]): (Option[String], Option[String]) = {
+ var afterPhase: Option[String] = None
+ var beforePhase: Option[String] = None
+ for (opt <- opts) {
+ if (opt.startsWith("extraAfterPhase:")) {
+ afterPhase = Some(opt.substring("extraAfterPhase:".length))
+ }
+ if (opt.startsWith("extraBeforePhase:")) {
+ beforePhase = Some(opt.substring("extraBeforePhase:".length))
+ }
+ }
+ (afterPhase, beforePhase)
+ }
+}
+
+class ScoverageOptions {
+ var excludedPackages: Seq[String] = Nil
+ var excludedFiles: Seq[String] = Nil
+ var excludedSymbols: Seq[String] = Seq("scala.reflect.api.Exprs.Expr", "scala.reflect.api.Trees.Tree", "scala.reflect.macros.Universe.Tree")
+ var dataDir: String = IOUtils.getTempPath
+}
+
+class ScoverageInstrumentationComponent(val global: Global, extraAfterPhase: Option[String], extraBeforePhase: Option[String])
+ extends PluginComponent
+ with TypingTransformers
+ with Transform {
+
+ import global._
+
+ val statementIds = new AtomicInteger(0)
+ val coverage = new Coverage
+
+ override val phaseName: String = "scoverage-instrumentation"
+ override val runsAfter: List[String] = List("typer") ::: extraAfterPhase.toList
+ override val runsBefore: List[String] = List("patmat") ::: extraBeforePhase.toList
+
+ /**
+ * Our options are not provided at construction time, but shortly after,
+ * so they start as None.
+ * You must call "setOptions" before running any commands that rely on
+ * the options.
+ */
+ private var options: ScoverageOptions = new ScoverageOptions()
+ private var coverageFilter: CoverageFilter = AllCoverageFilter
+
+ def setOptions(options: ScoverageOptions): Unit = {
+ this.options = options
+ coverageFilter = new RegexCoverageFilter(options.excludedPackages, options.excludedFiles, options.excludedSymbols)
+ new File(options.dataDir).mkdirs() // ensure data directory is created
+ }
+
+ override def newPhase(prev: scala.tools.nsc.Phase): Phase = new Phase(prev) {
+
+ override def run(): Unit = {
+ reporter.echo(s"[info] Cleaning datadir [${options.dataDir}]")
+ // we clean the data directory, because if the code has changed, then the number / order of
+ // statements has changed by definition. So the old data would reference statements incorrectly
+ // and thus skew the results.
+ IOUtils.clean(options.dataDir)
+
+ reporter.echo("[info] Beginning coverage instrumentation")
+ super.run()
+ reporter.echo(s"[info] Instrumentation completed [${coverage.statements.size} statements]")
+
+ Serializer.serialize(coverage, Serializer.coverageFile(options.dataDir))
+ reporter.echo(s"[info] Wrote instrumentation file [${Serializer.coverageFile(options.dataDir)}]")
+ reporter.echo(s"[info] Will write measurement data to [${options.dataDir}]")
+ }
+ }
+
+ protected def newTransformer(unit: CompilationUnit): Transformer = new Transformer(unit)
+
+ class Transformer(unit: global.CompilationUnit) extends TypingTransformer(unit) {
+
+ import global._
+
+ // contains the location of the last node
+ var location: Location = null
+
+ /**
+ * The 'start' of the position, if it is available, else -1
+ * We cannot use 'isDefined' to test whether pos.start will work, as some
+ * classes (e.g. scala.reflect.internal.util.OffsetPosition have
+ * isDefined true, but throw on `start`
+ */
+ def safeStart(tree: Tree): Int = scala.util.Try(tree.pos.start).getOrElse(-1)
+ def safeEnd(tree: Tree): Int = scala.util.Try(tree.pos.end).getOrElse(-1)
+ def safeLine(tree: Tree): Int = if (tree.pos.isDefined) tree.pos.line else -1
+ def safeSource(tree: Tree): Option[SourceFile] = if (tree.pos.isDefined) Some(tree.pos.source) else None
+
+ def invokeCall(id: Int): Tree = {
+ Apply(
+ Select(
+ Select(
+ Ident("scoverage"),
+ newTermName("Invoker")
+ ),
+ newTermName("invoked")
+ ),
+ List(
+ Literal(
+ Constant(id)
+ ),
+ Literal(
+ Constant(options.dataDir)
+ )
+ )
+ )
+ }
+
+ override def transform(tree: Tree) = process(tree)
+
+ def transformStatements(trees: List[Tree]): List[Tree] = trees.map(process)
+
+ def transformForCases(cases: List[CaseDef]): List[CaseDef] = {
+ // we don't instrument the synthetic case _ => false clause
+ cases.dropRight(1).map(c => {
+ treeCopy.CaseDef(
+ // in a for-loop we don't care about instrumenting the guards, as they are synthetically generated
+ c, c.pat, process(c.guard), process(c.body)
+ )
+ }) ++ cases.takeRight(1)
+ }
+
+ def transformCases(cases: List[CaseDef]): List[CaseDef] = {
+ cases.map(c => {
+ treeCopy.CaseDef(
+ c, c.pat, process(c.guard), process(c.body)
+ )
+ })
+ }
+
+ def instrument(tree: Tree, original: Tree, branch: Boolean = false): Tree = {
+ safeSource(tree) match {
+ case None =>
+ reporter.echo(s"[warn] Could not instrument [${tree.getClass.getSimpleName}/${tree.symbol}]. No pos.")
+ tree
+ case Some(source) =>
+ val id = statementIds.incrementAndGet
+ val statement = Statement(
+ source.path,
+ location,
+ id,
+ safeStart(tree),
+ safeEnd(tree),
+ safeLine(tree),
+ original.toString,
+ Option(original.symbol).fold("")(_.fullNameString),
+ tree.getClass.getSimpleName,
+ branch
+ )
+ if (tree.pos.isDefined && !isStatementIncluded(tree.pos)) {
+ coverage.add(statement.copy(ignored = true))
+ tree
+ } else {
+ coverage.add(statement)
+
+ val apply = invokeCall(id)
+ val block = Block(List(apply), tree)
+ localTyper.typed(atPos(tree.pos)(block))
+ }
+ }
+ }
+
+ def isClassIncluded(symbol: Symbol): Boolean = coverageFilter.isClassIncluded(symbol.fullNameString)
+ def isFileIncluded(source: SourceFile): Boolean = coverageFilter.isFileIncluded(source)
+ def isStatementIncluded(pos: Position): Boolean = coverageFilter.isLineIncluded(pos)
+ def isSymbolIncluded(symbol: Symbol): Boolean = coverageFilter.isSymbolIncluded(symbol.fullNameString)
+
+ def updateLocation(t: Tree) {
+ Location(global)(t) match {
+ case Some(loc) => this.location = loc
+ case _ => reporter.warning(t.pos, s"[warn] Cannot update location for $t")
+ }
+ }
+
+ def transformPartial(c: ClassDef): ClassDef = {
+ treeCopy.ClassDef(
+ c, c.mods, c.name, c.tparams,
+ treeCopy.Template(
+ c.impl, c.impl.parents, c.impl.self, c.impl.body.map {
+ case d: DefDef if d.name.toString == "applyOrElse" =>
+ d.rhs match {
+ case Match(selector, cases) =>
+ treeCopy.DefDef(
+ d, d.mods, d.name, d.tparams, d.vparamss, d.tpt,
+ treeCopy.Match(
+ // note: do not transform last case as that is the default handling
+ d.rhs, selector, transformCases(cases.init) :+ cases.last
+ )
+ )
+ case _ =>
+ reporter.error(c.pos ,"Cannot instrument partial function apply. Please file bug report")
+ d
+ }
+ case other => other
+ }
+ )
+ )
+ }
+
+ def debug(t: Tree) {
+ import scala.reflect.runtime.{universe => u}
+ reporter.echo(t.getClass.getSimpleName + ": LINE " + safeLine(t) + ": " + u.showRaw(t))
+ }
+
+ def traverseApplication(t: Tree): Tree = {
+ t match {
+ case a: ApplyToImplicitArgs => treeCopy.Apply(a, traverseApplication(a.fun), transformStatements(a.args))
+ case Apply(Select(_, name), List(fun@Function(params, body)))
+ if name.toString == "withFilter" && fun.symbol.isSynthetic && fun.toString.contains("check$ifrefutable$1") => t
+ case a: Apply => treeCopy.Apply(a, traverseApplication(a.fun), transformStatements(a.args))
+ case a: TypeApply => treeCopy.TypeApply(a, traverseApplication(a.fun), transformStatements(a.args))
+ case s: Select => treeCopy.Select(s, traverseApplication(s.qualifier), s.name)
+ case i: Ident => i
+ case t: This => t
+ case other => process(other)
+ }
+ }
+
+ private def isSynthetic(t: Tree): Boolean = Option(t.symbol).fold(false)(_.isSynthetic)
+ private def isNonSynthetic(t: Tree): Boolean = !isSynthetic(t)
+ private def containsNonSynthetic(t: Tree): Boolean = isNonSynthetic(t) || t.children.exists(containsNonSynthetic)
+
+ def allConstArgs(args: List[Tree]) = args.forall(arg => arg.isInstanceOf[Literal] || arg.isInstanceOf[Ident])
+
+ def process(tree: Tree): Tree = {
+ tree match {
+
+ // // non ranged inside ranged will break validation after typer, which only kicks in for yrangepos.
+ // case t if !t.pos.isRange => super.transform(t)
+
+ // ignore macro expanded code, do not send to super as we don't want any children to be instrumented
+ case t if t.attachments.all.toString().contains("MacroExpansionAttachment") => t
+
+ // /**
+ // * Object creation from new.
+ // * Ignoring creation calls to anon functions
+ // */
+ // case a: GenericApply if a.symbol.isConstructor && a.symbol.enclClass.isAnonymousFunction => tree
+ // case a: GenericApply if a.symbol.isConstructor => instrument(a)
+
+ /**
+ * When an apply has no parameters, or is an application of purely literals or idents
+ * then we can simply instrument the outer call. Ie, we can treat it all as one single statement
+ * for the purposes of code coverage.
+ * This will include calls to case apply.
+ */
+ case a: GenericApply if allConstArgs(a.args) => instrument(a, a)
+
+ /**
+ * Applications of methods with non trivial args means the args themselves
+ * must also be instrumented
+ */
+ //todo remove once scala merges into Apply proper
+ case a: ApplyToImplicitArgs =>
+ instrument(treeCopy.Apply(a, traverseApplication(a.fun), transformStatements(a.args)), a)
+
+ // handle 'new' keywords, instrumenting parameter lists
+ case a@Apply(s@Select(New(tpt), name), args) =>
+ instrument(treeCopy.Apply(a, s, transformStatements(args)), a)
+ case a: Apply =>
+ instrument(treeCopy.Apply(a, traverseApplication(a.fun), transformStatements(a.args)), a)
+ case a: TypeApply =>
+ instrument(treeCopy.TypeApply(a, traverseApplication(a.fun), transformStatements(a.args)), a)
+
+ /** pattern match with syntax `Assign(lhs, rhs)`.
+ * This AST node corresponds to the following Scala code:
+ * lhs = rhs
+ */
+ case assign: Assign => treeCopy.Assign(assign, assign.lhs, process(assign.rhs))
+
+ /** pattern match with syntax `Block(stats, expr)`.
+ * This AST node corresponds to the following Scala code:
+ * { stats; expr }
+ * If the block is empty, the `expr` is set to `Literal(Constant(()))`.
+ */
+ case b: Block =>
+ treeCopy.Block(b, transformStatements(b.stats), transform(b.expr))
+
+ // special support to handle partial functions
+ case c: ClassDef if c.symbol.isAnonymousFunction &&
+ c.symbol.enclClass.superClass.nameString.contains("AbstractPartialFunction") =>
+ if (isClassIncluded(c.symbol)) {
+ transformPartial(c)
+ } else {
+ c
+ }
+
+ // scalac generated classes, we just instrument the enclosed methods/statements
+ // the location would stay as the source class
+ case c: ClassDef if c.symbol.isAnonymousClass || c.symbol.isAnonymousFunction =>
+ if (isFileIncluded(c.pos.source) && isClassIncluded(c.symbol))
+ super.transform(tree)
+ else {
+ c
+ }
+
+ case c: ClassDef =>
+ if (isFileIncluded(c.pos.source) && isClassIncluded(c.symbol)) {
+ updateLocation(c)
+ super.transform(tree)
+ } else {
+ c
+ }
+
+ // ignore macro definitions in 2.11
+ case DefDef(mods, _, _, _, _, _) if mods.isMacro => tree
+
+ // this will catch methods defined as macros, eg def test = macro testImpl
+ // it will not catch macro implementations
+ case d: DefDef if d.symbol != null
+ && d.symbol.annotations.size > 0
+ && d.symbol.annotations.toString() == "macroImpl" =>
+ tree
+
+ // will catch macro implementations, as they must end with Expr, however will catch
+ // any method that ends in Expr. // todo add way of allowing methods that return Expr
+ case d: DefDef if d.symbol != null && !isSymbolIncluded(d.tpt.symbol) =>
+ tree
+
+ // we can ignore primary constructors because they are just empty at this stage, the body is added later.
+ case d: DefDef if d.symbol.isPrimaryConstructor => tree
+
+ /**
+ * Case class accessors for vals
+ * EG for case class CreditReject(req: MarketOrderRequest, client: ActorRef)
+ * def req: com.sksamuel.scoverage.samples.MarketOrderRequest
+ * def client: akka.actor.ActorRef
+ */
+ case d: DefDef if d.symbol.isCaseAccessor => tree
+
+ // Compiler generated case apply and unapply. Ignore these
+ case d: DefDef if d.symbol.isCaseApplyOrUnapply => tree
+
+ /**
+ * Lazy stable DefDefs are generated as the impl for lazy vals.
+ */
+ case d: DefDef if d.symbol.isStable && d.symbol.isGetter && d.symbol.isLazy =>
+ updateLocation(d)
+ treeCopy.DefDef(d, d.mods, d.name, d.tparams, d.vparamss, d.tpt, process(d.rhs))
+
+ /**
+ * Stable getters are methods generated for access to a top level val.
+ * Should be ignored as this is compiler generated code.
+ *
+ * Eg
+ * def MaxCredit: scala.math.BigDecimal = CreditEngine.this.MaxCredit
+ * def alwaysTrue: String = InstrumentLoader.this.alwaysTrue
+ */
+ case d: DefDef if d.symbol.isStable && d.symbol.isGetter => tree
+
+ /** Accessors are auto generated setters and getters.
+ * Eg
+ * private def _clientName: String =
+ * def cancellable: akka.actor.Cancellable = PriceEngine.this.cancellable
+ * def cancellable_=(x$1: akka.actor.Cancellable): Unit = PriceEngine.this.cancellable = x$1
+ */
+ case d: DefDef if d.symbol.isAccessor => tree
+
+ // was `abstract' for members | trait is virtual
+ case d: DefDef if tree.symbol.isDeferred => tree
+
+ /** eg
+ * override def hashCode(): Int
+ * def copy$default$1: com.sksamuel.scoverage.samples.MarketOrderRequest
+ * def $default$3: Option[org.joda.time.LocalDate] @scala.annotation.unchecked.uncheckedVariance = scala.None
+ */
+ case d: DefDef if d.symbol.isSynthetic => tree
+
+ /** Match all remaining def definitions
+ *
+ * If the return type is not specified explicitly (i.e. is meant to be inferred),
+ * this is expressed by having `tpt` set to `TypeTree()` (but not to an `EmptyTree`!).
+ */
+ case d: DefDef =>
+ updateLocation(d)
+ treeCopy.DefDef(d, d.mods, d.name, d.tparams, d.vparamss, d.tpt, process(d.rhs))
+
+ case EmptyTree => tree
+
+ // handle function bodies. This AST node corresponds to the following Scala code: vparams => body
+ case f: Function =>
+ treeCopy.Function(tree, f.vparams, process(f.body))
+
+ case _: Ident => tree
+
+ // the If statement itself doesn't need to be instrumented, because instrumenting the condition is
+ // enough to determine if the If statement was executed.
+ // The two procedures (then and else) are instrumented separately to determine if we entered
+ // both branches.
+ case i: If =>
+ treeCopy.If(i,
+ process(i.cond),
+ instrument(process(i.thenp), i.thenp, branch = true),
+ instrument(process(i.elsep), i.elsep, branch = true))
+
+ case _: Import => tree
+
+ // labeldefs are never written natively in scala
+ case l: LabelDef =>
+ treeCopy.LabelDef(tree, l.name, l.params, transform(l.rhs))
+
+ // profile access to a literal for function args todo do we need to do this?
+ case l: Literal => instrument(l, l)
+
+ // pattern match clauses will be instrumented per case
+ case m@Match(selector: Tree, cases: List[CaseDef]) =>
+ // we can be fairly sure this was generated as part of a for loop
+ if (selector.toString.contains("check$")
+ && selector.tpe.annotations.mkString == "unchecked"
+ && m.cases.last.toString == "case _ => false") {
+ treeCopy.Match(tree, process(selector), transformForCases(cases))
+ } else {
+ // if the selector was added by compiler, we don't want to instrument it....
+ // that usually means some construct is being transformed into a match
+ if (Option(selector.symbol).exists(_.isSynthetic))
+ treeCopy.Match(tree, selector, transformCases(cases))
+ else
+ // .. but we will if it was a user match
+ treeCopy.Match(tree, process(selector), transformCases(cases))
+ }
+
+ // a synthetic object is a generated object, such as case class companion
+ case m: ModuleDef if m.symbol.isSynthetic =>
+ updateLocation(m)
+ super.transform(tree)
+
+ // user defined objects
+ case m: ModuleDef =>
+ if (isFileIncluded(m.pos.source) && isClassIncluded(m.symbol)) {
+ updateLocation(m)
+ super.transform(tree)
+ } else {
+ m
+ }
+
+ /**
+ * match with syntax `New(tpt)`.
+ * This AST node corresponds to the following Scala code:
+ *
+ * `new` T
+ *
+ * This node always occurs in the following context:
+ *
+ * (`new` tpt).[targs](args)
+ *
+ * For example, an AST representation of:
+ *
+ * new Example[Int](2)(3)
+ *
+ * is the following code:
+ *
+ * Apply(
+ * Apply(
+ * TypeApply(
+ * Select(New(TypeTree(typeOf[Example])), nme.CONSTRUCTOR)
+ * TypeTree(typeOf[Int])),
+ * List(Literal(Constant(2)))),
+ * List(Literal(Constant(3))))
+ *
+ */
+ case n: New => n
+
+ case s@Select(n@New(tpt), name) =>
+ instrument(treeCopy.Select(s, n, name), s)
+
+ case p: PackageDef =>
+ if (isClassIncluded(p.symbol)) treeCopy.PackageDef(p, p.pid, transformStatements(p.stats))
+ else p
+
+ // This AST node corresponds to the following Scala code: `return` expr
+ case r: Return =>
+ treeCopy.Return(r, transform(r.expr))
+
+ /** pattern match with syntax `Select(qual, name)`.
+ * This AST node corresponds to the following Scala code:
+ *
+ * qualifier.selector
+ *
+ * Should only be used with `qualifier` nodes which are terms, i.e. which have `isTerm` returning `true`.
+ * Otherwise `SelectFromTypeTree` should be used instead.
+ *
+ * foo.Bar // represented as Select(Ident(), )
+ * Foo#Bar // represented as SelectFromTypeTree(Ident(), )
+ */
+ case s: Select if location == null => tree
+
+ /**
+ * I think lazy selects are the LHS of a lazy assign.
+ * todo confirm we can ignore
+ */
+ case s: Select if s.symbol.isLazy => tree
+
+ case s: Select => instrument(treeCopy.Select(s, traverseApplication(s.qualifier), s.name), s)
+
+ case s: Super => tree
+
+ // This AST node corresponds to the following Scala code: qual.this
+ case t: This => super.transform(tree)
+
+ // This AST node corresponds to the following Scala code: `throw` expr
+ case t: Throw => instrument(tree, tree)
+
+ // This AST node corresponds to the following Scala code: expr: tpt
+ case t: Typed => super.transform(tree)
+
+ // instrument trys, catches and finally as separate blocks
+ case Try(t: Tree, cases: List[CaseDef], f: Tree) =>
+ treeCopy.Try(tree,
+ instrument(process(t), t, branch = true),
+ transformCases(cases),
+ instrument(process(f), f, branch = true))
+
+ // type aliases, type parameters, abstract types
+ case t: TypeDef => super.transform(tree)
+
+ case t: Template =>
+ updateLocation(t)
+ treeCopy.Template(tree, t.parents, t.self, transformStatements(t.body))
+
+ case _: TypeTree => super.transform(tree)
+
+ /**
+ * We can ignore lazy val defs as they are implemented by a generated defdef
+ */
+ case v: ValDef if v.symbol.isLazy =>
+ val w = v
+ tree
+
+ /**
+ * val default: A1 => B1 =
+ * val x1: Any = _
+ */
+ case v: ValDef if v.symbol.isSynthetic =>
+ val w = v
+ tree
+
+ /**
+ * Vals declared in case constructors
+ */
+ case v: ValDef if v.symbol.isParamAccessor && v.symbol.isCaseAccessor =>
+ val w = v
+ tree
+
+ // we need to remove the final mod so that we keep the code in order to check its invoked
+ case v: ValDef if v.mods.isFinal =>
+ updateLocation(v)
+ treeCopy.ValDef(v, v.mods.&~(ModifierFlags.FINAL), v.name, v.tpt, process(v.rhs))
+
+ /**
+ * This AST node corresponds to any of the following Scala code:
+ *
+ * mods `val` name: tpt = rhs
+ * mods `var` name: tpt = rhs
+ * mods name: tpt = rhs // in signatures of function and method definitions
+ * self: Bar => // self-types
+ *
+ * For user defined value statements, we will instrument the RHS.
+ *
+ * This includes top level non-lazy vals. Lazy vals are generated as stable defs.
+ */
+ case v: ValDef =>
+ updateLocation(v)
+ treeCopy.ValDef(tree, v.mods, v.name, v.tpt, process(v.rhs))
+
+ case _ =>
+ reporter.warning(tree.pos, "BUG: Unexpected construct: " + tree.getClass + " " + tree.symbol)
+ super.transform(tree)
+ }
+ }
+ }
+}
+
diff --git a/scalac-scoverage-runtime-java/src/main/scala/scoverage/Invoker.scala b/scalac-scoverage-runtime-java/src/main/scala/scoverage/Invoker.scala
new file mode 100644
index 0000000..c4005df
--- /dev/null
+++ b/scalac-scoverage-runtime-java/src/main/scala/scoverage/Invoker.scala
@@ -0,0 +1,7 @@
+package scoverage
+
+object Invoker {
+
+ // We explicitly call the java conversion else predef.scala will be used, and that may itself be instrumented.
+ def invoked(id: Int, dataDir: String) = InvokerJ.invokedJ(java.lang.Integer.valueOf(id), dataDir)
+}
diff --git a/scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java b/scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java
new file mode 100644
index 0000000..d08ce4e
--- /dev/null
+++ b/scalac-scoverage-runtime-java/src/main/scala/scoverage/InvokerJ.java
@@ -0,0 +1,63 @@
+package scoverage;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class InvokerJ {
+
+ private static String MeasurementsPrefix = "scoverage.measurements.";
+
+ private static final ThreadLocal> threadFiles =
+ new ThreadLocal>() {
+ @Override protected HashMap initialValue() {
+ return new HashMap();
+ }
+ };
+
+ private static ConcurrentHashMap ids = new ConcurrentHashMap();
+
+ /**
+ * We record that the given id has been invoked by appending its id to the coverage
+ * data file.
+ *
+ * This will happen concurrently on as many threads as the application is using,
+ * so we use one file per thread, named for the thread id.
+ *
+ * This method is not thread-safe if the threads are in different JVMs, because
+ * the thread IDs may collide.
+ * You may not use `scoverage` on multiple processes in parallel without risking
+ * corruption of the measurement file.
+ *
+ * @param id the id of the statement that was invoked
+ * @param dataDir the directory where the measurement data is held
+ */
+ public static void invokedJ(final Integer id, final String dataDir)throws IOException {
+ String idStr = Integer.toString(id);
+ String key = new String(dataDir + idStr);
+
+ if (!ids.containsKey(key)) {
+ // Each thread writes to a separate measurement file, to reduce contention
+ // and because file appends via FileWriter are not atomic on Windows.
+ HashMap files = threadFiles.get();
+ if(!files.containsKey(dataDir))
+ files.put(dataDir, new FileWriter(measurementFile(dataDir), true));
+ FileWriter writer = files.get(dataDir);
+ writer.append(idStr + '\n').flush();
+
+ ids.put(key, Boolean.TRUE);
+ }
+ }
+
+ private static File measurementFile(File dataDir){
+ return measurementFile(dataDir.getAbsolutePath());
+ }
+
+ private static File measurementFile(String dataDir) {
+ StringBuilder sb = new StringBuilder(MeasurementsPrefix);
+ String threadId = Long.toString(Thread.currentThread().getId());
+ return new File(dataDir, sb.append(threadId).toString());
+ }
+}
diff --git a/scalac-scoverage-runtime-java/src/test/scala/scoverage/AllTests.scala b/scalac-scoverage-runtime-java/src/test/scala/scoverage/AllTests.scala
new file mode 100644
index 0000000..eefc213
--- /dev/null
+++ b/scalac-scoverage-runtime-java/src/test/scala/scoverage/AllTests.scala
@@ -0,0 +1,16 @@
+package scoverage
+
+object RuntimeInfo {
+ def runtimePath: String = "./scalac-scoverage-runtime-java"
+ def name: String = "java"
+}
+
+class ThisCoverageTest extends CoverageTest
+class ThisInvokerConcurrencyTest extends InvokerConcurrencyTest
+class ThisInvokerMultiModuleTest extends InvokerMultiModuleTest
+class ThisIOUtilsTest extends IOUtilsTest
+class ThisLocationTest extends LocationTest
+class ThisPluginASTSupportTest extends PluginASTSupportTest
+class ThisPluginCoverageTest extends PluginCoverageTest
+class ThisRegexCoverageFilterTest extends RegexCoverageFilterTest
+class ThisSerializerTest extends SerializerTest
diff --git a/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala b/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala
new file mode 100644
index 0000000..ac43c51
--- /dev/null
+++ b/scalac-scoverage-runtime-scala/src/main/scala/scoverage/Invoker.scala
@@ -0,0 +1,51 @@
+package scoverage
+
+import java.io.{File, FileWriter}
+import scala.collection.concurrent.TrieMap
+
+/** @author Stephen Samuel */
+object Invoker {
+
+ private val MeasurementsPrefix = "scoverage.measurements."
+ private val threadFiles = new ThreadLocal[TrieMap[String, FileWriter]]
+ private val ids = TrieMap.empty[(String, Int), Any]
+
+ /**
+ * We record that the given id has been invoked by appending its id to the coverage
+ * data file.
+ *
+ * This will happen concurrently on as many threads as the application is using,
+ * so we use one file per thread, named for the thread id.
+ *
+ * This method is not thread-safe if the threads are in different JVMs, because
+ * the thread IDs may collide.
+ * You may not use `scoverage` on multiple processes in parallel without risking
+ * corruption of the measurement file.
+ *
+ * @param id the id of the statement that was invoked
+ * @param dataDir the directory where the measurement data is held
+ */
+ def invoked(id: Int, dataDir: String): Unit = {
+ // [sam] we can do this simple check to save writing out to a file.
+ // This won't work across JVMs but since there's no harm in writing out the same id multiple
+ // times since for coverage we only care about 1 or more, (it just slows things down to
+ // do it more than once), anything we can do to help is good. This helps especially with code
+ // that is executed many times quickly, eg tight loops.
+ if (!ids.contains(dataDir, id)) {
+ // Each thread writes to a separate measurement file, to reduce contention
+ // and because file appends via FileWriter are not atomic on Windows.
+ var files = threadFiles.get()
+ if (files == null) {
+ files = TrieMap.empty[String, FileWriter]
+ threadFiles.set(files)
+ }
+ val writer = files.getOrElseUpdate(dataDir, new FileWriter(measurementFile(dataDir), true))
+ writer.append(id.toString + '\n').flush()
+
+ ids.put((dataDir, id), ())
+ }
+ }
+
+ def measurementFile(dataDir: File): File = measurementFile(dataDir.getAbsolutePath)
+ def measurementFile(dataDir: String): File = new File(dataDir, MeasurementsPrefix + Thread.currentThread.getId)
+}
diff --git a/scalac-scoverage-runtime-scala/src/test/scala/scoverage/AllTests.scala b/scalac-scoverage-runtime-scala/src/test/scala/scoverage/AllTests.scala
new file mode 100644
index 0000000..8f5064a
--- /dev/null
+++ b/scalac-scoverage-runtime-scala/src/test/scala/scoverage/AllTests.scala
@@ -0,0 +1,16 @@
+package scoverage
+
+object RuntimeInfo {
+ def runtimePath: String = "./scalac-scoverage-runtime-scala"
+ def name: String = "scala"
+}
+
+class ThisCoverageTest extends CoverageTest
+class ThisInvokerConcurrencyTest extends InvokerConcurrencyTest
+class ThisInvokerMultiModuleTest extends InvokerMultiModuleTest
+class ThisIOUtilsTest extends IOUtilsTest
+class ThisLocationTest extends LocationTest
+class ThisPluginASTSupportTest extends PluginASTSupportTest
+class ThisPluginCoverageTest extends PluginCoverageTest
+class ThisRegexCoverageFilterTest extends RegexCoverageFilterTest
+class ThisSerializerTest extends SerializerTest
diff --git a/version.sbt b/version.sbt
new file mode 100644
index 0000000..c5438ff
--- /dev/null
+++ b/version.sbt
@@ -0,0 +1 @@
+version in ThisBuild := "2.0.0-M0"