Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions domain/src/main/scala/scoverage/domain/coverage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,23 @@ case class Coverage()
with PackageBuilders
with FileBuilders {

private var _maxId: Int = 0
private val statementsById = mutable.Map[Int, Statement]()
override def statements = statementsById.values
def add(stmt: Statement): Unit = statementsById.put(stmt.id, stmt)

def add(stmt: Statement): Unit = {
if (stmt.id > _maxId) {
_maxId = stmt.id
}
statementsById.put(stmt.id, stmt)
}
private val ignoredStatementsById = mutable.Map[Int, Statement]()
override def ignoredStatements = ignoredStatementsById.values
def addIgnoredStatement(stmt: Statement): Unit =
def addIgnoredStatement(stmt: Statement): Unit = {
if (stmt.id > _maxId) {
_maxId = stmt.id
}
ignoredStatementsById.put(stmt.id, stmt)
}

def avgClassesPerPackage = classCount / packageCount.toDouble
def avgClassesPerPackageFormatted: String = DoubleFormat.twoFractionDigits(
Expand All @@ -46,6 +55,8 @@ case class Coverage()
def apply(ids: Iterable[(Int, String)]): Unit = ids foreach invoked
def invoked(id: (Int, String)): Unit =
statementsById.get(id._1).foreach(_.invoked(id._2))

def maxId: Int = _maxId
}

trait ClassBuilders {
Expand Down
27 changes: 27 additions & 0 deletions plugin/src/main/scala/scoverage/CoverageMerge.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package scoverage

import java.io.File

import scoverage.domain.Coverage

object CoverageMerge {
def mergePreviousAndCurrentCoverage(
lastCompiledFiles: Set[String],
previousCoverage: Coverage,
currentCoverage: Coverage
): Coverage = {
val mergedCoverage = Coverage()

previousCoverage.statements
.filterNot(stmt =>
lastCompiledFiles.contains(stmt.source) ||
!new File(stmt.source).exists()
)
.foreach { stmt =>
mergedCoverage.add(stmt)
}
currentCoverage.statements.foreach(stmt => mergedCoverage.add(stmt))

mergedCoverage
}
}
34 changes: 27 additions & 7 deletions plugin/src/main/scala/scoverage/ScoveragePlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class ScoverageInstrumentationComponent(

val statementIds = new AtomicInteger(0)
val coverage = new Coverage
val compiledFiles = Set.newBuilder[String]

override val phaseName: String = ScoveragePlugin.phaseName
override val runsAfter: List[String] =
Expand Down Expand Up @@ -121,22 +122,39 @@ class ScoverageInstrumentationComponent(
override def newPhase(prev: scala.tools.nsc.Phase): Phase = new Phase(prev) {

override def run(): Unit = {
reporter.echo(s"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.
reporter.echo(
s"Cleaning measurements files in datadir [${options.dataDir}]"
)
Serializer.clean(options.dataDir)

val coverageFile = Serializer.coverageFile(options.dataDir)
val sourceRootFile = new File(options.sourceRoot)
val previousCoverage =
if (coverageFile.exists())
Serializer.deserialize(
coverageFile,
sourceRootFile
)
else Coverage()

statementIds.set(previousCoverage.maxId)

reporter.echo("Beginning coverage instrumentation")
super.run()
reporter.echo(
s"Instrumentation completed [${coverage.statements.size} statements]"
)

val mergedCoverage = CoverageMerge.mergePreviousAndCurrentCoverage(
lastCompiledFiles = compiledFiles.result(),
previousCoverage = previousCoverage,
currentCoverage = coverage
)

Serializer.serialize(
coverage,
Serializer.coverageFile(options.dataDir),
new File(options.sourceRoot)
mergedCoverage,
coverageFile,
sourceRootFile
)
reporter.echo(
s"Wrote instrumentation file [${Serializer.coverageFile(options.dataDir)}]"
Expand All @@ -153,6 +171,8 @@ class ScoverageInstrumentationComponent(

import global._

compiledFiles += unit.source.file.absolute.canonicalPath

// contains the location of the last node
var location: domain.Location = _

Expand Down
101 changes: 101 additions & 0 deletions plugin/src/test/scala/scoverage/IncrementalCoverageTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package scoverage

import munit.FunSuite

class IncrementalCoverageTest extends FunSuite {

case class Compilation(basePath: java.io.File, code: String) {
val coverageFile = serialize.Serializer.coverageFile(basePath)
val compiler = ScoverageCompiler(basePath = basePath)

val file = compiler.writeCodeSnippetToTempFile(code)
compiler.compileSourceFiles(file)
compiler.assertNoErrors()

val coverage = serialize.Serializer.deserialize(coverageFile, basePath)
}

test(
"should keep coverage from previous compilation when compiling incrementally"
) {
val basePath = ScoverageCompiler.tempBasePath()

val compilation1 =
Compilation(basePath, """object First { def test(): Int = 42 }""")

locally {
val sourceFiles = compilation1.coverage.files.map(_.source).toSet
assertEquals(sourceFiles, Set(compilation1.file.getCanonicalPath))
}

val compilation2 =
Compilation(
basePath,
"""object Second { def test(): String = "hello" }"""
)

locally {
val sourceFiles = compilation2.coverage.files.map(_.source).toSet
assertEquals(
sourceFiles,
Set(compilation1.file, compilation2.file).map(_.getCanonicalPath)
)
}
}

test(
"should not keep coverage from previous compilation if the source file was deleted"
) {
val basePath = ScoverageCompiler.tempBasePath()

val compilation1 =
Compilation(basePath, """object First { def test(): Int = 42 }""")

locally {
val sourceFiles = compilation1.coverage.files.map(_.source).toSet

assertEquals(sourceFiles, Set(compilation1.file.getCanonicalPath))
}

compilation1.file.delete()

val compilation2 = Compilation(basePath, "")

locally {
val sourceFiles = compilation2.coverage.files.map(_.source).toSet
assertEquals(sourceFiles, Set.empty[String])
}
}

test(
"should not keep coverage from previous compilation if the source file was compiled again"
) {
val basePath = ScoverageCompiler.tempBasePath()

val compilation1 =
Compilation(basePath, """object First { def test(): Int = 42 }""")

reporter.IOUtils.writeToFile(
compilation1.file,
"""object Second { def test(): String = "hello" }""",
None
)

val coverageFile = serialize.Serializer.coverageFile(basePath)
val compiler = ScoverageCompiler(basePath = basePath)

compiler.compileSourceFiles(compilation1.file)
compiler.assertNoErrors()

val coverage = serialize.Serializer.deserialize(coverageFile, basePath)

locally {
val classNames = coverage.statements.map(_.location.className).toSet
assertEquals(
classNames,
Set("Second"),
s"First class should not be in coverage, but found: ${classNames.mkString(", ")}"
)
}
}
}
42 changes: 27 additions & 15 deletions plugin/src/test/scala/scoverage/ScoverageCompiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package scoverage
import java.io.File
import java.io.FileNotFoundException
import java.net.URL
import java.util.UUID

import scala.collection.mutable.ListBuffer
import scala.tools.nsc.Global
Expand Down Expand Up @@ -55,20 +56,30 @@ private[scoverage] object ScoverageCompiler {
s
}

def default: ScoverageCompiler = {
val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings)
new ScoverageCompiler(settings, reporter, validatePositions = true)
def tempBasePath(): File =
new File(IOUtils.getTempPath, UUID.randomUUID.toString)

def apply(
settings: scala.tools.nsc.Settings = settings,
reporter: scala.tools.nsc.reporters.Reporter =
new scala.tools.nsc.reporters.ConsoleReporter(settings),
validatePositions: Boolean = true,
basePath: File = tempBasePath()
) = {
new ScoverageCompiler(
settings,
reporter,
validatePositions,
basePath
)
}

def noPositionValidation: ScoverageCompiler = {
val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings)
new ScoverageCompiler(settings, reporter, validatePositions = false)
}
def default = ScoverageCompiler()

def defaultJS: ScoverageCompiler = {
val reporter = new scala.tools.nsc.reporters.ConsoleReporter(jsSettings)
new ScoverageCompiler(jsSettings, reporter, validatePositions = true)
}
def noPositionValidation: ScoverageCompiler =
ScoverageCompiler(validatePositions = false)

def defaultJS: ScoverageCompiler = ScoverageCompiler(settings = jsSettings)

def locationCompiler: LocationCompiler = {
val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings)
Expand Down Expand Up @@ -158,7 +169,8 @@ private[scoverage] object ScoverageCompiler {
class ScoverageCompiler(
settings: scala.tools.nsc.Settings,
rep: scala.tools.nsc.reporters.Reporter,
validatePositions: Boolean
validatePositions: Boolean,
basePath: File
) extends scala.tools.nsc.Global(settings, rep) {

def addToClassPath(file: File): Unit = {
Expand All @@ -171,8 +183,8 @@ class ScoverageCompiler(

val coverageOptions = ScoverageOptions
.default()
.copy(dataDir = IOUtils.getTempPath)
.copy(sourceRoot = IOUtils.getTempPath)
.copy(dataDir = basePath.getAbsolutePath)
.copy(sourceRoot = basePath.getAbsolutePath)

instrumentationComponent.setOptions(coverageOptions)
val testStore = new ScoverageTestStoreComponent(this)
Expand All @@ -188,7 +200,7 @@ class ScoverageCompiler(
}

def writeCodeSnippetToTempFile(code: String): File = {
val file = File.createTempFile("scoverage_snippet", ".scala")
val file = File.createTempFile("scoverage_snippet", ".scala", basePath)
IOUtils.writeToFile(file, code, None)
file.deleteOnExit()
file
Expand Down