Skip to content

Commit 46f1f53

Browse files
authored
Convert sourcepath to relative. (#385)
This also introduces a new config setting, `sourceRoot` that will need to be set by the plugins using this to ensure that no matter where this is executed from that the relativization process will work as expected.
1 parent b019d87 commit 46f1f53

File tree

6 files changed

+333
-132
lines changed

6 files changed

+333
-132
lines changed

Diff for: scalac-scoverage-plugin/src/main/scala/scoverage/Serializer.scala

+63-33
Original file line numberDiff line numberDiff line change
@@ -11,51 +11,74 @@ import scala.io.Source
1111

1212
object Serializer {
1313

14+
val coverageDataFormatVersion = "3.0"
15+
1416
// Write out coverage data to the given data directory, using the default coverage filename
15-
def serialize(coverage: Coverage, dataDir: String): Unit =
16-
serialize(coverage, coverageFile(dataDir))
17+
def serialize(coverage: Coverage, dataDir: String, sourceRoot: String): Unit =
18+
serialize(coverage, coverageFile(dataDir), new File(sourceRoot))
1719

1820
// Write out coverage data to given file.
19-
def serialize(coverage: Coverage, file: File): Unit = {
21+
def serialize(coverage: Coverage, file: File, sourceRoot: File): Unit = {
2022
val writer: Writer = new BufferedWriter(
2123
new OutputStreamWriter(new FileOutputStream(file), Codec.UTF8.name)
2224
)
2325
try {
24-
serialize(coverage, writer)
26+
serialize(coverage, writer, sourceRoot)
2527
} finally {
2628
writer.flush()
2729
writer.close()
2830
}
2931
}
3032

31-
def serialize(coverage: Coverage, writer: Writer): Unit = {
33+
def serialize(
34+
coverage: Coverage,
35+
writer: Writer,
36+
sourceRoot: File
37+
): Unit = {
38+
def getRelativePath(filePath: String): String = {
39+
val base = sourceRoot.getCanonicalFile().toPath()
40+
// NOTE: In the real world I have no idea if it's likely that you'll end
41+
// up with weird issues on windows where the roots don't match, something
42+
// like your root being D:/ and your file being C:/. If so this blows up.
43+
// This happened on windows CI for me, since I was using a temp dir, and
44+
// then trying to reletavize it off the cwd, which were in different
45+
// drives. For now, we'll let this as is, but if 'other' has different
46+
// root ever shows its, we'll shut that down real quick here... just not
47+
// sure what to do in that situation yet.
48+
val relPath =
49+
base.relativize(new File(filePath).getCanonicalFile().toPath())
50+
relPath.toString
51+
}
52+
3253
def writeHeader(writer: Writer): Unit = {
33-
writer.write(s"""# Coverage data, format version: 2.0
34-
|# Statement data:
35-
|# - id
36-
|# - source path
37-
|# - package name
38-
|# - class name
39-
|# - class type (Class, Object or Trait)
40-
|# - full class name
41-
|# - method name
42-
|# - start offset
43-
|# - end offset
44-
|# - line number
45-
|# - symbol name
46-
|# - tree name
47-
|# - is branch
48-
|# - invocations count
49-
|# - is ignored
50-
|# - description (can be multi-line)
51-
|# '\f' sign
52-
|# ------------------------------------------
53-
|""".stripMargin.replaceAll("(\r\n)|\n|\r", "\n"))
54+
writer.write(
55+
s"""# Coverage data, format version: $coverageDataFormatVersion
56+
|# Statement data:
57+
|# - id
58+
|# - source path
59+
|# - package name
60+
|# - class name
61+
|# - class type (Class, Object or Trait)
62+
|# - full class name
63+
|# - method name
64+
|# - start offset
65+
|# - end offset
66+
|# - line number
67+
|# - symbol name
68+
|# - tree name
69+
|# - is branch
70+
|# - invocations count
71+
|# - is ignored
72+
|# - description (can be multi-line)
73+
|# '\f' sign
74+
|# ------------------------------------------
75+
|""".stripMargin
76+
)
5477
}
5578

5679
def writeStatement(stmt: Statement, writer: Writer): Unit = {
5780
writer.write(s"""${stmt.id}
58-
|${stmt.location.sourcePath}
81+
|${getRelativePath(stmt.location.sourcePath)}
5982
|${stmt.location.packageName}
6083
|${stmt.location.className}
6184
|${stmt.location.classType}
@@ -71,7 +94,7 @@ object Serializer {
7194
|${stmt.ignored}
7295
|${stmt.desc}
7396
|\f
74-
|""".stripMargin.replaceAll("(\r\n)|\n|\r", "\n"))
97+
|""".stripMargin)
7598
}
7699

77100
writeHeader(writer)
@@ -84,13 +107,20 @@ object Serializer {
84107
def coverageFile(dataDir: String): File =
85108
new File(dataDir, Constants.CoverageFileName)
86109

87-
def deserialize(file: File): Coverage = {
110+
def deserialize(file: File, sourceRoot: File): Coverage = {
88111
val source = Source.fromFile(file)(Codec.UTF8)
89-
try deserialize(source.getLines())
112+
try deserialize(source.getLines(), sourceRoot)
90113
finally source.close()
91114
}
92115

93-
def deserialize(lines: Iterator[String]): Coverage = {
116+
def deserialize(lines: Iterator[String], sourceRoot: File): Coverage = {
117+
// To integrate it smoothly with rest of the report writers,
118+
// it is necessary to again convert [sourcePath] into a
119+
// canonical one.
120+
def getAbsolutePath(filePath: String): String = {
121+
new File(sourceRoot, filePath).getCanonicalPath()
122+
}
123+
94124
def toStatement(lines: Iterator[String]): Statement = {
95125
val id: Int = lines.next().toInt
96126
val sourcePath = lines.next()
@@ -105,7 +135,7 @@ object Serializer {
105135
fullClassName,
106136
ClassType.fromString(classType),
107137
method,
108-
sourcePath
138+
getAbsolutePath(sourcePath)
109139
)
110140
val start: Int = lines.next().toInt
111141
val end: Int = lines.next().toInt
@@ -133,7 +163,7 @@ object Serializer {
133163

134164
val headerFirstLine = lines.next()
135165
require(
136-
headerFirstLine == "# Coverage data, format version: 2.0",
166+
headerFirstLine == s"# Coverage data, format version: $coverageDataFormatVersion",
137167
"Wrong file format"
138168
)
139169

Diff for: scalac-scoverage-plugin/src/main/scala/scoverage/plugin.scala

+21-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class ScoveragePlugin(val global: Global) extends Plugin {
5151
options.excludedSymbols = parseExclusionEntry("excludedSymbols:", opt)
5252
} else if (opt.startsWith("dataDir:")) {
5353
options.dataDir = opt.substring("dataDir:".length)
54+
} else if (opt.startsWith("sourceRoot:")) {
55+
options.sourceRoot = opt.substring("sourceRoot:".length())
5456
} else if (
5557
opt
5658
.startsWith("extraAfterPhase:") || opt.startsWith("extraBeforePhase:")
@@ -66,13 +68,18 @@ class ScoveragePlugin(val global: Global) extends Plugin {
6668
throw new RuntimeException(
6769
"Cannot invoke plugin without specifying <dataDir>"
6870
)
71+
if (!opts.exists(_.startsWith("sourceRoot:")))
72+
throw new RuntimeException(
73+
"Cannot invoke plugin without specifying <sourceRoot>"
74+
)
6975
instrumentationComponent.setOptions(options)
7076
true
7177
}
7278

7379
override val optionsHelp: Option[String] = Some(
7480
Seq(
7581
"-P:scoverage:dataDir:<pathtodatadir> where the coverage files should be written\n",
82+
"-P:scoverage:sourceRoot:<pathtosourceRoot> the root dir of your sources, used for path relativization\n",
7683
"-P:scoverage:excludedPackages:<regex>;<regex> semicolon separated list of regexs for packages to exclude",
7784
"-P:scoverage:excludedFiles:<regex>;<regex> semicolon separated list of regexs for paths to exclude",
7885
"-P:scoverage:excludedSymbols:<regex>;<regex> semicolon separated list of regexs for symbols to exclude",
@@ -107,6 +114,8 @@ class ScoveragePlugin(val global: Global) extends Plugin {
107114
}
108115
}
109116

117+
// TODO refactor this into a case class. We'll also refactor how we parse the
118+
// options to get rid of all these vars
110119
class ScoverageOptions {
111120
var excludedPackages: Seq[String] = Nil
112121
var excludedFiles: Seq[String] = Nil
@@ -117,6 +126,12 @@ class ScoverageOptions {
117126
)
118127
var dataDir: String = IOUtils.getTempPath
119128
var reportTestName: Boolean = false
129+
// TODO again, we'll refactor this later so this won't have a default here.
130+
// However for tests we'll have to create this. However, make sure you create
131+
// either both in temp or neither in temp, since on windows your temp dir
132+
// will be in another drive, so the relativize functinality won't work if
133+
// correctly.
134+
var sourceRoot: String = IOUtils.getTempPath
120135
}
121136

122137
class ScoverageInstrumentationComponent(
@@ -179,7 +194,12 @@ class ScoverageInstrumentationComponent(
179194
s"Instrumentation completed [${coverage.statements.size} statements]"
180195
)
181196

182-
Serializer.serialize(coverage, Serializer.coverageFile(options.dataDir))
197+
// TODO do we need to verify this sourceRoot exists? How does semanticdb do this?
198+
Serializer.serialize(
199+
coverage,
200+
Serializer.coverageFile(options.dataDir),
201+
new File(options.sourceRoot)
202+
)
183203
reporter.echo(
184204
s"Wrote instrumentation file [${Serializer.coverageFile(options.dataDir)}]"
185205
)

Diff for: scalac-scoverage-plugin/src/main/scala/scoverage/report/CoverageAggregator.scala

+10-12
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,32 @@ import scoverage.Serializer
88

99
object CoverageAggregator {
1010

11-
@deprecated("1.4.0", "Used only by gradle-scoverage plugin")
12-
def aggregate(baseDir: File, clean: Boolean): Option[Coverage] = {
13-
aggregate(IOUtils.scoverageDataDirsSearch(baseDir))
14-
}
15-
1611
// to be used by gradle-scoverage plugin
17-
def aggregate(dataDirs: Array[File]): Option[Coverage] = aggregate(
18-
dataDirs.toSeq
19-
)
12+
def aggregate(dataDirs: Array[File], sourceRoot: File): Option[Coverage] =
13+
aggregate(
14+
dataDirs.toSeq,
15+
sourceRoot
16+
)
2017

21-
def aggregate(dataDirs: Seq[File]): Option[Coverage] = {
18+
def aggregate(dataDirs: Seq[File], sourceRoot: File): Option[Coverage] = {
2219
println(
2320
s"[info] Found ${dataDirs.size} subproject scoverage data directories [${dataDirs.mkString(",")}]"
2421
)
2522
if (dataDirs.size > 0) {
26-
Some(aggregatedCoverage(dataDirs))
23+
Some(aggregatedCoverage(dataDirs, sourceRoot))
2724
} else {
2825
None
2926
}
3027
}
3128

32-
def aggregatedCoverage(dataDirs: Seq[File]): Coverage = {
29+
def aggregatedCoverage(dataDirs: Seq[File], sourceRoot: File): Coverage = {
3330
var id = 0
3431
val coverage = Coverage()
3532
dataDirs foreach { dataDir =>
3633
val coverageFile: File = Serializer.coverageFile(dataDir)
3734
if (coverageFile.exists) {
38-
val subcoverage: Coverage = Serializer.deserialize(coverageFile)
35+
val subcoverage: Coverage =
36+
Serializer.deserialize(coverageFile, sourceRoot)
3937
val measurementFiles: Array[File] =
4038
IOUtils.findMeasurementFiles(dataDir)
4139
val measurements = IOUtils.invoked(measurementFiles.toIndexedSeq)

Diff for: scalac-scoverage-plugin/src/test/scala/scoverage/CoverageAggregatorTest.scala

+21-6
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import scoverage.report.CoverageAggregator
1111
class CoverageAggregatorTest extends AnyFreeSpec with Matchers {
1212

1313
// Let current directory be our source root
14-
private val sourceRoot = new File(".")
14+
private val sourceRoot = new File(".").getCanonicalPath()
1515
private def canonicalPath(fileName: String) =
16-
new File(sourceRoot, fileName).getCanonicalPath
16+
new File(sourceRoot, fileName).getCanonicalPath()
1717

1818
"coverage aggregator" - {
1919
"should merge coverage objects with same id" in {
@@ -35,7 +35,11 @@ class CoverageAggregatorTest extends AnyFreeSpec with Matchers {
3535
coverage1.add(cov1Stmt2.copy(count = 0))
3636
val dir1 = new File(IOUtils.getTempPath, UUID.randomUUID.toString)
3737
dir1.mkdir()
38-
Serializer.serialize(coverage1, Serializer.coverageFile(dir1))
38+
Serializer.serialize(
39+
coverage1,
40+
Serializer.coverageFile(dir1),
41+
new File(sourceRoot)
42+
)
3943
val measurementsFile1 =
4044
new File(dir1, s"${Constants.MeasurementsPrefix}1")
4145
val measurementsFile1Writer = new FileWriter(measurementsFile1)
@@ -47,23 +51,34 @@ class CoverageAggregatorTest extends AnyFreeSpec with Matchers {
4751
coverage2.add(cov2Stmt1)
4852
val dir2 = new File(IOUtils.getTempPath, UUID.randomUUID.toString)
4953
dir2.mkdir()
50-
Serializer.serialize(coverage2, Serializer.coverageFile(dir2))
54+
Serializer.serialize(
55+
coverage2,
56+
Serializer.coverageFile(dir2),
57+
new File(sourceRoot)
58+
)
5159

5260
val cov3Stmt1 =
5361
Statement(location, 2, 14, 1515, 544, "", "", "", false, 1)
5462
val coverage3 = Coverage()
5563
coverage3.add(cov3Stmt1.copy(count = 0))
5664
val dir3 = new File(IOUtils.getTempPath, UUID.randomUUID.toString)
5765
dir3.mkdir()
58-
Serializer.serialize(coverage3, Serializer.coverageFile(dir3))
66+
Serializer.serialize(
67+
coverage3,
68+
Serializer.coverageFile(dir3),
69+
new File(sourceRoot)
70+
)
5971
val measurementsFile3 =
6072
new File(dir3, s"${Constants.MeasurementsPrefix}1")
6173
val measurementsFile3Writer = new FileWriter(measurementsFile3)
6274
measurementsFile3Writer.write("2\n")
6375
measurementsFile3Writer.close()
6476

6577
val aggregated =
66-
CoverageAggregator.aggregatedCoverage(Seq(dir1, dir2, dir3))
78+
CoverageAggregator.aggregatedCoverage(
79+
Seq(dir1, dir2, dir3),
80+
new File(sourceRoot)
81+
)
6782
aggregated.statements.toSet.size shouldBe 4
6883
aggregated.statements.map(_.copy(id = 0)).toSet shouldBe
6984
Set(cov1Stmt1, cov1Stmt2, cov2Stmt1, cov3Stmt1).map(_.copy(id = 0))

Diff for: scalac-scoverage-plugin/src/test/scala/scoverage/ScoverageCompiler.scala

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ class ScoverageCompiler(
119119

120120
val instrumentationComponent =
121121
new ScoverageInstrumentationComponent(this, None, None)
122+
122123
instrumentationComponent.setOptions(new ScoverageOptions())
123124
val testStore = new ScoverageTestStoreComponent(this)
124125
val validator = new PositionValidator(this)

0 commit comments

Comments
 (0)