diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala index 15775515..b292f694 100644 --- a/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala @@ -11,51 +11,74 @@ import scala.io.Source object Serializer { + val coverageDataFormatVersion = "3.0" + // 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)) + def serialize(coverage: Coverage, dataDir: String, sourceRoot: String): Unit = + serialize(coverage, coverageFile(dataDir), new File(sourceRoot)) // Write out coverage data to given file. - def serialize(coverage: Coverage, file: File): Unit = { + def serialize(coverage: Coverage, file: File, sourceRoot: File): Unit = { val writer: Writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(file), Codec.UTF8.name) ) try { - serialize(coverage, writer) + serialize(coverage, writer, sourceRoot) } finally { writer.flush() writer.close() } } - def serialize(coverage: Coverage, writer: Writer): Unit = { + def serialize( + coverage: Coverage, + writer: Writer, + sourceRoot: File + ): Unit = { + def getRelativePath(filePath: String): String = { + val base = sourceRoot.getCanonicalFile().toPath() + // NOTE: In the real world I have no idea if it's likely that you'll end + // up with weird issues on windows where the roots don't match, something + // like your root being D:/ and your file being C:/. If so this blows up. + // This happened on windows CI for me, since I was using a temp dir, and + // then trying to reletavize it off the cwd, which were in different + // drives. For now, we'll let this as is, but if 'other' has different + // root ever shows its, we'll shut that down real quick here... just not + // sure what to do in that situation yet. + val relPath = + base.relativize(new File(filePath).getCanonicalFile().toPath()) + relPath.toString + } + def writeHeader(writer: Writer): Unit = { - writer.write(s"""# Coverage data, format version: 2.0 - |# Statement data: - |# - id - |# - source path - |# - package name - |# - class name - |# - class type (Class, Object or Trait) - |# - full class name - |# - method name - |# - start offset - |# - end offset - |# - line number - |# - symbol name - |# - tree name - |# - is branch - |# - invocations count - |# - is ignored - |# - description (can be multi-line) - |# '\f' sign - |# ------------------------------------------ - |""".stripMargin.replaceAll("(\r\n)|\n|\r", "\n")) + writer.write( + s"""# Coverage data, format version: $coverageDataFormatVersion + |# Statement data: + |# - id + |# - source path + |# - package name + |# - class name + |# - class type (Class, Object or Trait) + |# - full class name + |# - method name + |# - start offset + |# - end offset + |# - line number + |# - symbol name + |# - tree name + |# - is branch + |# - invocations count + |# - is ignored + |# - description (can be multi-line) + |# '\f' sign + |# ------------------------------------------ + |""".stripMargin + ) } def writeStatement(stmt: Statement, writer: Writer): Unit = { writer.write(s"""${stmt.id} - |${stmt.location.sourcePath} + |${getRelativePath(stmt.location.sourcePath)} |${stmt.location.packageName} |${stmt.location.className} |${stmt.location.classType} @@ -71,7 +94,7 @@ object Serializer { |${stmt.ignored} |${stmt.desc} |\f - |""".stripMargin.replaceAll("(\r\n)|\n|\r", "\n")) + |""".stripMargin) } writeHeader(writer) @@ -84,13 +107,20 @@ object Serializer { def coverageFile(dataDir: String): File = new File(dataDir, Constants.CoverageFileName) - def deserialize(file: File): Coverage = { + def deserialize(file: File, sourceRoot: File): Coverage = { val source = Source.fromFile(file)(Codec.UTF8) - try deserialize(source.getLines()) + try deserialize(source.getLines(), sourceRoot) finally source.close() } - def deserialize(lines: Iterator[String]): Coverage = { + def deserialize(lines: Iterator[String], sourceRoot: File): Coverage = { + // To integrate it smoothly with rest of the report writers, + // it is necessary to again convert [sourcePath] into a + // canonical one. + def getAbsolutePath(filePath: String): String = { + new File(sourceRoot, filePath).getCanonicalPath() + } + def toStatement(lines: Iterator[String]): Statement = { val id: Int = lines.next().toInt val sourcePath = lines.next() @@ -105,7 +135,7 @@ object Serializer { fullClassName, ClassType.fromString(classType), method, - sourcePath + getAbsolutePath(sourcePath) ) val start: Int = lines.next().toInt val end: Int = lines.next().toInt @@ -133,7 +163,7 @@ object Serializer { val headerFirstLine = lines.next() require( - headerFirstLine == "# Coverage data, format version: 2.0", + headerFirstLine == s"# Coverage data, format version: $coverageDataFormatVersion", "Wrong file format" ) diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala index 02b6c557..08beb312 100644 --- a/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala @@ -51,6 +51,8 @@ class ScoveragePlugin(val global: Global) extends Plugin { options.excludedSymbols = parseExclusionEntry("excludedSymbols:", opt) } else if (opt.startsWith("dataDir:")) { options.dataDir = opt.substring("dataDir:".length) + } else if (opt.startsWith("sourceRoot:")) { + options.sourceRoot = opt.substring("sourceRoot:".length()) } else if ( opt .startsWith("extraAfterPhase:") || opt.startsWith("extraBeforePhase:") @@ -66,6 +68,10 @@ class ScoveragePlugin(val global: Global) extends Plugin { throw new RuntimeException( "Cannot invoke plugin without specifying " ) + if (!opts.exists(_.startsWith("sourceRoot:"))) + throw new RuntimeException( + "Cannot invoke plugin without specifying " + ) instrumentationComponent.setOptions(options) true } @@ -73,6 +79,7 @@ class ScoveragePlugin(val global: Global) extends Plugin { override val optionsHelp: Option[String] = Some( Seq( "-P:scoverage:dataDir: where the coverage files should be written\n", + "-P:scoverage:sourceRoot: the root dir of your sources, used for path relativization\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", @@ -107,6 +114,8 @@ class ScoveragePlugin(val global: Global) extends Plugin { } } +// TODO refactor this into a case class. We'll also refactor how we parse the +// options to get rid of all these vars class ScoverageOptions { var excludedPackages: Seq[String] = Nil var excludedFiles: Seq[String] = Nil @@ -117,6 +126,12 @@ class ScoverageOptions { ) var dataDir: String = IOUtils.getTempPath var reportTestName: Boolean = false + // TODO again, we'll refactor this later so this won't have a default here. + // However for tests we'll have to create this. However, make sure you create + // either both in temp or neither in temp, since on windows your temp dir + // will be in another drive, so the relativize functinality won't work if + // correctly. + var sourceRoot: String = IOUtils.getTempPath } class ScoverageInstrumentationComponent( @@ -179,7 +194,12 @@ class ScoverageInstrumentationComponent( s"Instrumentation completed [${coverage.statements.size} statements]" ) - Serializer.serialize(coverage, Serializer.coverageFile(options.dataDir)) + // TODO do we need to verify this sourceRoot exists? How does semanticdb do this? + Serializer.serialize( + coverage, + Serializer.coverageFile(options.dataDir), + new File(options.sourceRoot) + ) reporter.echo( s"Wrote instrumentation file [${Serializer.coverageFile(options.dataDir)}]" ) diff --git a/scalac-scoverage-plugin/src/main/scala/scoverage/report/CoverageAggregator.scala b/scalac-scoverage-plugin/src/main/scala/scoverage/report/CoverageAggregator.scala index 3e23a990..27f9c564 100644 --- a/scalac-scoverage-plugin/src/main/scala/scoverage/report/CoverageAggregator.scala +++ b/scalac-scoverage-plugin/src/main/scala/scoverage/report/CoverageAggregator.scala @@ -8,34 +8,32 @@ import scoverage.Serializer object CoverageAggregator { - @deprecated("1.4.0", "Used only by gradle-scoverage plugin") - def aggregate(baseDir: File, clean: Boolean): Option[Coverage] = { - aggregate(IOUtils.scoverageDataDirsSearch(baseDir)) - } - // to be used by gradle-scoverage plugin - def aggregate(dataDirs: Array[File]): Option[Coverage] = aggregate( - dataDirs.toSeq - ) + def aggregate(dataDirs: Array[File], sourceRoot: File): Option[Coverage] = + aggregate( + dataDirs.toSeq, + sourceRoot + ) - def aggregate(dataDirs: Seq[File]): Option[Coverage] = { + def aggregate(dataDirs: Seq[File], sourceRoot: File): Option[Coverage] = { println( s"[info] Found ${dataDirs.size} subproject scoverage data directories [${dataDirs.mkString(",")}]" ) if (dataDirs.size > 0) { - Some(aggregatedCoverage(dataDirs)) + Some(aggregatedCoverage(dataDirs, sourceRoot)) } else { None } } - def aggregatedCoverage(dataDirs: Seq[File]): Coverage = { + def aggregatedCoverage(dataDirs: Seq[File], sourceRoot: File): Coverage = { var id = 0 val coverage = Coverage() dataDirs foreach { dataDir => val coverageFile: File = Serializer.coverageFile(dataDir) if (coverageFile.exists) { - val subcoverage: Coverage = Serializer.deserialize(coverageFile) + val subcoverage: Coverage = + Serializer.deserialize(coverageFile, sourceRoot) val measurementFiles: Array[File] = IOUtils.findMeasurementFiles(dataDir) val measurements = IOUtils.invoked(measurementFiles.toIndexedSeq) diff --git a/scalac-scoverage-plugin/src/test/scala/scoverage/CoverageAggregatorTest.scala b/scalac-scoverage-plugin/src/test/scala/scoverage/CoverageAggregatorTest.scala index 3f9dc1f9..9bd2eb27 100644 --- a/scalac-scoverage-plugin/src/test/scala/scoverage/CoverageAggregatorTest.scala +++ b/scalac-scoverage-plugin/src/test/scala/scoverage/CoverageAggregatorTest.scala @@ -11,9 +11,9 @@ import scoverage.report.CoverageAggregator class CoverageAggregatorTest extends AnyFreeSpec with Matchers { // Let current directory be our source root - private val sourceRoot = new File(".") + private val sourceRoot = new File(".").getCanonicalPath() private def canonicalPath(fileName: String) = - new File(sourceRoot, fileName).getCanonicalPath + new File(sourceRoot, fileName).getCanonicalPath() "coverage aggregator" - { "should merge coverage objects with same id" in { @@ -35,7 +35,11 @@ class CoverageAggregatorTest extends AnyFreeSpec with Matchers { coverage1.add(cov1Stmt2.copy(count = 0)) val dir1 = new File(IOUtils.getTempPath, UUID.randomUUID.toString) dir1.mkdir() - Serializer.serialize(coverage1, Serializer.coverageFile(dir1)) + Serializer.serialize( + coverage1, + Serializer.coverageFile(dir1), + new File(sourceRoot) + ) val measurementsFile1 = new File(dir1, s"${Constants.MeasurementsPrefix}1") val measurementsFile1Writer = new FileWriter(measurementsFile1) @@ -47,7 +51,11 @@ class CoverageAggregatorTest extends AnyFreeSpec with Matchers { coverage2.add(cov2Stmt1) val dir2 = new File(IOUtils.getTempPath, UUID.randomUUID.toString) dir2.mkdir() - Serializer.serialize(coverage2, Serializer.coverageFile(dir2)) + Serializer.serialize( + coverage2, + Serializer.coverageFile(dir2), + new File(sourceRoot) + ) val cov3Stmt1 = Statement(location, 2, 14, 1515, 544, "", "", "", false, 1) @@ -55,7 +63,11 @@ class CoverageAggregatorTest extends AnyFreeSpec with Matchers { coverage3.add(cov3Stmt1.copy(count = 0)) val dir3 = new File(IOUtils.getTempPath, UUID.randomUUID.toString) dir3.mkdir() - Serializer.serialize(coverage3, Serializer.coverageFile(dir3)) + Serializer.serialize( + coverage3, + Serializer.coverageFile(dir3), + new File(sourceRoot) + ) val measurementsFile3 = new File(dir3, s"${Constants.MeasurementsPrefix}1") val measurementsFile3Writer = new FileWriter(measurementsFile3) @@ -63,7 +75,10 @@ class CoverageAggregatorTest extends AnyFreeSpec with Matchers { measurementsFile3Writer.close() val aggregated = - CoverageAggregator.aggregatedCoverage(Seq(dir1, dir2, dir3)) + CoverageAggregator.aggregatedCoverage( + Seq(dir1, dir2, dir3), + new File(sourceRoot) + ) aggregated.statements.toSet.size shouldBe 4 aggregated.statements.map(_.copy(id = 0)).toSet shouldBe Set(cov1Stmt1, cov1Stmt2, cov2Stmt1, cov3Stmt1).map(_.copy(id = 0)) diff --git a/scalac-scoverage-plugin/src/test/scala/scoverage/ScoverageCompiler.scala b/scalac-scoverage-plugin/src/test/scala/scoverage/ScoverageCompiler.scala index 01afc847..a6d808fd 100644 --- a/scalac-scoverage-plugin/src/test/scala/scoverage/ScoverageCompiler.scala +++ b/scalac-scoverage-plugin/src/test/scala/scoverage/ScoverageCompiler.scala @@ -119,6 +119,7 @@ class ScoverageCompiler( val instrumentationComponent = new ScoverageInstrumentationComponent(this, None, None) + instrumentationComponent.setOptions(new ScoverageOptions()) val testStore = new ScoverageTestStoreComponent(this) val validator = new PositionValidator(this) diff --git a/scalac-scoverage-plugin/src/test/scala/scoverage/SerializerTest.scala b/scalac-scoverage-plugin/src/test/scala/scoverage/SerializerTest.scala index b8108c65..636222e0 100644 --- a/scalac-scoverage-plugin/src/test/scala/scoverage/SerializerTest.scala +++ b/scalac-scoverage-plugin/src/test/scala/scoverage/SerializerTest.scala @@ -1,11 +1,13 @@ package scoverage +import java.io.File import java.io.StringWriter import org.scalatest.OneInstancePerTest import org.scalatest.funsuite.AnyFunSuite class SerializerTest extends AnyFunSuite with OneInstancePerTest { + private val sourceRoot = new File(".").getCanonicalFile() test("coverage should be serializable into plain text") { val coverage = Coverage() @@ -17,7 +19,7 @@ class SerializerTest extends AnyFunSuite with OneInstancePerTest { "org.scoverage.test", ClassType.Trait, "mymethod", - "mypath" + new File(sourceRoot, "mypath").getAbsolutePath() ), 14, 100, @@ -30,88 +32,92 @@ class SerializerTest extends AnyFunSuite with OneInstancePerTest { 1 ) ) - val expected = s"""# Coverage data, format version: 2.0 - |# Statement data: - |# - id - |# - source path - |# - package name - |# - class name - |# - class type (Class, Object or Trait) - |# - full class name - |# - method name - |# - start offset - |# - end offset - |# - line number - |# - symbol name - |# - tree name - |# - is branch - |# - invocations count - |# - is ignored - |# - description (can be multi-line) - |# '\f' sign - |# ------------------------------------------ - |14 - |mypath - |org.scoverage - |test - |Trait - |org.scoverage.test - |mymethod - |100 - |200 - |4 - |test - |DefDef - |true - |1 - |false - |def test : String - |\f - |""".stripMargin.replaceAll("(\r\n)|\n|\r", "\n") + val expected = + s"""# Coverage data, format version: ${Serializer.coverageDataFormatVersion} + |# Statement data: + |# - id + |# - source path + |# - package name + |# - class name + |# - class type (Class, Object or Trait) + |# - full class name + |# - method name + |# - start offset + |# - end offset + |# - line number + |# - symbol name + |# - tree name + |# - is branch + |# - invocations count + |# - is ignored + |# - description (can be multi-line) + |# '\f' sign + |# ------------------------------------------ + |14 + |mypath + |org.scoverage + |test + |Trait + |org.scoverage.test + |mymethod + |100 + |200 + |4 + |test + |DefDef + |true + |1 + |false + |def test : String + |\f + |""".stripMargin val writer = new StringWriter() //TODO-use UTF-8 - val actual = Serializer.serialize(coverage, writer) + val actual = Serializer.serialize(coverage, writer, sourceRoot) assert(expected === writer.toString) } test("coverage should be deserializable from plain text") { - val input = s"""# Coverage data, format version: 2.0 - |# Statement data: - |# - id - |# - source path - |# - package name - |# - class name - |# - class type (Class, Object or Trait) - |# - full class name - |# - method name - |# - start offset - |# - end offset - |# - line number - |# - symbol name - |# - tree name - |# - is branch - |# - invocations count - |# - is ignored - |# - description (can be multi-line) - |# '\f' sign - |# ------------------------------------------ - |14 - |mypath - |org.scoverage - |test - |Trait - |org.scoverage.test - |mymethod - |100 - |200 - |4 - |test - |DefDef - |true - |1 - |false - |def test : String - |\f - |""".stripMargin.split("(\r\n)|\n|\r").iterator + val input = + s"""# Coverage data, format version: ${Serializer.coverageDataFormatVersion} + |# Statement data: + |# - id + |# - source path + |# - package name + |# - class name + |# - class type (Class, Object or Trait) + |# - full class name + |# - method name + |# - start offset + |# - end offset + |# - line number + |# - symbol name + |# - tree name + |# - is branch + |# - invocations count + |# - is ignored + |# - description (can be multi-line) + |# '\f' sign + |# ------------------------------------------ + |14 + |mypath + |org.scoverage + |test + |Trait + |org.scoverage.test + |mymethod + |100 + |200 + |4 + |test + |DefDef + |true + |1 + |false + |def test : String + |\f + |""".stripMargin + .split(System.lineSeparator()) + .iterator val statements = List( Statement( Location( @@ -120,7 +126,7 @@ class SerializerTest extends AnyFunSuite with OneInstancePerTest { "org.scoverage.test", ClassType.Trait, "mymethod", - "mypath" + new File(sourceRoot, "mypath").getAbsolutePath() ), 14, 100, @@ -133,7 +139,138 @@ class SerializerTest extends AnyFunSuite with OneInstancePerTest { 1 ) ) - val coverage = Serializer.deserialize(input) + val coverage = Serializer.deserialize(input, sourceRoot) + assert(statements === coverage.statements.toList) + } + test("coverage should serialize sourcePath relatively") { + val coverage = Coverage() + coverage.add( + Statement( + Location( + "org.scoverage", + "test", + "org.scoverage.test", + ClassType.Trait, + "mymethod", + new File(sourceRoot, "mypath").getAbsolutePath() + ), + 14, + 100, + 200, + 4, + "def test : String", + "test", + "DefDef", + true, + 1 + ) + ) + val expected = + s"""# Coverage data, format version: ${Serializer.coverageDataFormatVersion} + |# Statement data: + |# - id + |# - source path + |# - package name + |# - class name + |# - class type (Class, Object or Trait) + |# - full class name + |# - method name + |# - start offset + |# - end offset + |# - line number + |# - symbol name + |# - tree name + |# - is branch + |# - invocations count + |# - is ignored + |# - description (can be multi-line) + |# '\f' sign + |# ------------------------------------------ + |14 + |mypath + |org.scoverage + |test + |Trait + |org.scoverage.test + |mymethod + |100 + |200 + |4 + |test + |DefDef + |true + |1 + |false + |def test : String + |\f + |""".stripMargin + val writer = new StringWriter() //TODO-use UTF-8 + val actual = Serializer.serialize(coverage, writer, sourceRoot) + assert(expected === writer.toString) + } + + test("coverage should deserialize sourcePath by prefixing cwd") { + val input = + s"""# Coverage data, format version: ${Serializer.coverageDataFormatVersion} + |# Statement data: + |# - id + |# - source path + |# - package name + |# - class name + |# - class type (Class, Object or Trait) + |# - full class name + |# - method name + |# - start offset + |# - end offset + |# - line number + |# - symbol name + |# - tree name + |# - is branch + |# - invocations count + |# - is ignored + |# - description (can be multi-line) + |# '\f' sign + |# ------------------------------------------ + |14 + |mypath + |org.scoverage + |test + |Trait + |org.scoverage.test + |mymethod + |100 + |200 + |4 + |test + |DefDef + |true + |1 + |false + |def test : String + |\f + |""".stripMargin.split(System.lineSeparator()).iterator + val statements = List( + Statement( + Location( + "org.scoverage", + "test", + "org.scoverage.test", + ClassType.Trait, + "mymethod", + new File(sourceRoot, "mypath").getCanonicalPath().toString() + ), + 14, + 100, + 200, + 4, + "def test : String", + "test", + "DefDef", + true, + 1 + ) + ) + val coverage = Serializer.deserialize(input, sourceRoot) assert(statements === coverage.statements.toList) } }