Skip to content

Commit f06b95f

Browse files
authored
Fix coverage serialization when encountering macro suspension (scala#22303)
Fixes scala#22247 The fix is simple, as we mainly move the coverage object to a global ContextBase object, which persists it between runs. Initially I thought that appending the newly generated coverage indices would be enough, but if the macro suspends after the InstrumentCoverage phase runs, we end up with duplicate indices. For that reason, when generating indexes for a compilation unit, we also remove the previously generated ones for the same compilation unit. To support having multiple scala files compiled in the coverage tests I had to slightly adjust the suite. While doing that, I noticed that some check files for run tests were ignored, as they were incorrectly named. I added an assertion that throws when `.check` do not exist and renamed the files appropriately (having to add some additional ones as well).
1 parent a50a1e4 commit f06b95f

23 files changed

+378
-32
lines changed

Diff for: compiler/src/dotty/tools/dotc/core/Contexts.scala

+6
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import util.Store
4141
import plugins.*
4242
import java.util.concurrent.atomic.AtomicInteger
4343
import java.nio.file.InvalidPathException
44+
import dotty.tools.dotc.coverage.Coverage
4445

4546
object Contexts {
4647

@@ -982,6 +983,11 @@ object Contexts {
982983
/** Was best effort file used during compilation? */
983984
private[core] var usedBestEffortTasty = false
984985

986+
/** If coverage option is used, it stores all instrumented statements (for InstrumentCoverage).
987+
* We need this information to be persisted across different runs, so it's stored here.
988+
*/
989+
private[dotc] var coverage: Coverage | Null = null
990+
985991
// Types state
986992
/** A table for hash consing unique types */
987993
private[core] val uniques: Uniques = Uniques()

Diff for: compiler/src/dotty/tools/dotc/coverage/Coverage.scala

+12
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,27 @@ package dotty.tools.dotc
22
package coverage
33

44
import scala.collection.mutable
5+
import java.nio.file.Path
56

67
/** Holds a list of statements to include in the coverage reports. */
78
class Coverage:
89
private val statementsById = new mutable.LongMap[Statement](256)
910

11+
private var statementId: Int = 0
12+
13+
def nextStatementId(): Int =
14+
statementId += 1
15+
statementId - 1
16+
17+
1018
def statements: Iterable[Statement] = statementsById.values
1119

1220
def addStatement(stmt: Statement): Unit = statementsById(stmt.id) = stmt
1321

22+
def removeStatementsFromFile(sourcePath: Path) =
23+
val removedIds = statements.filter(_.location.sourcePath == sourcePath).map(_.id.toLong)
24+
removedIds.foreach(statementsById.remove)
25+
1426

1527
/**
1628
* A statement that can be invoked, and thus counted as "covered" by code coverage tools.

Diff for: compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala

+8-10
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,6 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
3838
override def isEnabled(using ctx: Context) =
3939
ctx.settings.coverageOutputDir.value.nonEmpty
4040

41-
// counter to assign a unique id to each statement
42-
private var statementId = 0
43-
44-
// stores all instrumented statements
45-
private val coverage = Coverage()
46-
4741
private var coverageExcludeClasslikePatterns: List[Pattern] = Nil
4842
private var coverageExcludeFilePatterns: List[Pattern] = Nil
4943

@@ -61,12 +55,17 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
6155
.foreach(_.nn.delete())
6256
end if
6357

58+
// Initialise a coverage object if it does not exist yet
59+
if ctx.base.coverage == null then
60+
ctx.base.coverage = Coverage()
61+
6462
coverageExcludeClasslikePatterns = ctx.settings.coverageExcludeClasslikes.value.map(_.r.pattern)
6563
coverageExcludeFilePatterns = ctx.settings.coverageExcludeFiles.value.map(_.r.pattern)
6664

65+
ctx.base.coverage.nn.removeStatementsFromFile(ctx.compilationUnit.source.file.absolute.jpath)
6766
super.run
6867

69-
Serializer.serialize(coverage, outputPath, ctx.settings.sourceroot.value)
68+
Serializer.serialize(ctx.base.coverage.nn, outputPath, ctx.settings.sourceroot.value)
7069

7170
private def isClassIncluded(sym: Symbol)(using Context): Boolean =
7271
val fqn = sym.fullName.toText(ctx.printerFn(ctx)).show
@@ -110,8 +109,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
110109
* @return the statement's id
111110
*/
112111
private def recordStatement(tree: Tree, pos: SourcePosition, branch: Boolean)(using ctx: Context): Int =
113-
val id = statementId
114-
statementId += 1
112+
val id = ctx.base.coverage.nn.nextStatementId()
115113

116114
val sourceFile = pos.source
117115
val statement = Statement(
@@ -127,7 +125,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
127125
treeName = tree.getClass.getSimpleName.nn,
128126
branch
129127
)
130-
coverage.addStatement(statement)
128+
ctx.base.coverage.nn.addStatement(statement)
131129
id
132130

133131
/**

Diff for: compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala

+28-13
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,27 @@ class CoverageTests:
5454
lines
5555
end fixWindowsPaths
5656

57-
def runOnFile(p: Path): Boolean =
58-
scalaFile.matches(p) &&
59-
(Properties.testsFilter.isEmpty || Properties.testsFilter.exists(p.toString.contains))
57+
def runOnFileOrDir(p: Path): Boolean =
58+
(scalaFile.matches(p) || Files.isDirectory(p))
59+
&& (p != dir)
60+
&& (Properties.testsFilter.isEmpty || Properties.testsFilter.exists(p.toString.contains))
61+
62+
Files.walk(dir, 1).filter(runOnFileOrDir).forEach(path => {
63+
// measurement files only exist in the "run" category
64+
// as these are generated at runtime by the scala.runtime.coverage.Invoker
65+
val (targetDir, expectFile, expectMeasurementFile) =
66+
if Files.isDirectory(path) then
67+
val dirName = path.getFileName().toString
68+
assert(!Files.walk(path).filter(scalaFile.matches(_)).toArray.isEmpty, s"No scala files found in test directory: ${path}")
69+
val targetDir = computeCoverageInTmp(path, isDirectory = true, dir, run)
70+
(targetDir, path.resolve(s"test.scoverage.check"), path.resolve(s"test.measurement.check"))
71+
else
72+
val fileName = path.getFileName.toString.stripSuffix(".scala")
73+
val targetDir = computeCoverageInTmp(path, isDirectory = false, dir, run)
74+
(targetDir, path.resolveSibling(s"${fileName}.scoverage.check"), path.resolveSibling(s"${fileName}.measurement.check"))
6075

61-
Files.walk(dir).filter(runOnFile).forEach(path => {
62-
val fileName = path.getFileName.toString.stripSuffix(".scala")
63-
val targetDir = computeCoverageInTmp(path, dir, run)
6476
val targetFile = targetDir.resolve(s"scoverage.coverage")
65-
val expectFile = path.resolveSibling(s"$fileName.scoverage.check")
77+
6678
if updateCheckFiles then
6779
Files.copy(targetFile, expectFile, StandardCopyOption.REPLACE_EXISTING)
6880
else
@@ -72,9 +84,6 @@ class CoverageTests:
7284
val instructions = FileDiff.diffMessage(expectFile.toString, targetFile.toString)
7385
fail(s"Coverage report differs from expected data.\n$instructions")
7486

75-
// measurement files only exist in the "run" category
76-
// as these are generated at runtime by the scala.runtime.coverage.Invoker
77-
val expectMeasurementFile = path.resolveSibling(s"$fileName.measurement.check")
7887
if run && Files.exists(expectMeasurementFile) then
7988

8089
// Note that this assumes that the test invoked was single threaded,
@@ -95,14 +104,20 @@ class CoverageTests:
95104
})
96105

97106
/** Generates the coverage report for the given input file, in a temporary directory. */
98-
def computeCoverageInTmp(inputFile: Path, sourceRoot: Path, run: Boolean)(using TestGroup): Path =
107+
def computeCoverageInTmp(inputFile: Path, isDirectory: Boolean, sourceRoot: Path, run: Boolean)(using TestGroup): Path =
99108
val target = Files.createTempDirectory("coverage")
100109
val options = defaultOptions.and("-Ycheck:instrumentCoverage", "-coverage-out", target.toString, "-sourceroot", sourceRoot.toString)
101110
if run then
102-
val test = compileDir(inputFile.getParent.toString, options)
111+
val path = if isDirectory then inputFile.toString else inputFile.getParent.toString
112+
val test = compileDir(path, options)
113+
test.checkFilePaths.foreach { checkFilePath =>
114+
assert(checkFilePath.exists, s"Expected checkfile for $path $checkFilePath does not exist.")
115+
}
103116
test.checkRuns()
104117
else
105-
val test = compileFile(inputFile.toString, options)
118+
val test =
119+
if isDirectory then compileDir(inputFile.toString, options)
120+
else compileFile(inputFile.toString, options)
106121
test.checkCompile()
107122
target
108123

Diff for: compiler/test/dotty/tools/vulpix/ParallelTesting.scala

+15-9
Original file line numberDiff line numberDiff line change
@@ -258,15 +258,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
258258
* For a given test source, returns a check file against which the result of the test run
259259
* should be compared. Is used by implementations of this trait.
260260
*/
261-
final def checkFile(testSource: TestSource): Option[JFile] = (testSource match {
262-
case ts: JointCompilationSource =>
263-
ts.files.collectFirst {
264-
case f if !f.isDirectory =>
265-
new JFile(f.getPath.replaceFirst("\\.(scala|java)$", ".check"))
266-
}
267-
case ts: SeparateCompilationSource =>
268-
Option(new JFile(ts.dir.getPath + ".check"))
269-
}).filter(_.exists)
261+
final def checkFile(testSource: TestSource): Option[JFile] = (CompilationLogic.checkFilePath(testSource)).filter(_.exists)
270262

271263
/**
272264
* Checks if the given actual lines are the same as the ones in the check file.
@@ -343,6 +335,18 @@ trait ParallelTesting extends RunnerOrchestration { self =>
343335
}
344336
}
345337

338+
object CompilationLogic {
339+
private[ParallelTesting] def checkFilePath(testSource: TestSource) = testSource match {
340+
case ts: JointCompilationSource =>
341+
ts.files.collectFirst {
342+
case f if !f.isDirectory =>
343+
new JFile(f.getPath.replaceFirst("\\.(scala|java)$", ".check"))
344+
}
345+
case ts: SeparateCompilationSource =>
346+
Option(new JFile(ts.dir.getPath + ".check"))
347+
}
348+
}
349+
346350
/** Each `Test` takes the `testSources` and performs the compilation and assertions
347351
* according to the implementing class "neg", "run" or "pos".
348352
*/
@@ -1157,6 +1161,8 @@ trait ParallelTesting extends RunnerOrchestration { self =>
11571161
def this(targets: List[TestSource]) =
11581162
this(targets, 1, true, None, false, false)
11591163

1164+
def checkFilePaths: List[JFile] = targets.map(CompilationLogic.checkFilePath).flatten
1165+
11601166
def copy(targets: List[TestSource],
11611167
times: Int = times,
11621168
shouldDelete: Boolean = shouldDelete,

Diff for: tests/coverage/pos/macro-late-suspend/Test.scala

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package example
2+
3+
sealed trait Test
4+
5+
object Test {
6+
case object Foo extends Test
7+
8+
val visitorType = mkVisitorType[Test]
9+
10+
trait Visitor[A] {
11+
type V[a] = visitorType.Out[a]
12+
}
13+
}

Diff for: tests/coverage/pos/macro-late-suspend/UsesTest.scala

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package example
2+
3+
val _ = Test.Foo
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package example
2+
3+
import scala.quoted.*
4+
5+
private def mkVisitorTypeImpl[T: Type](using q: Quotes): Expr[VisitorType[T]] =
6+
'{new VisitorType[T]{}}
7+
8+
transparent inline def mkVisitorType[T]: VisitorType[T] = ${ mkVisitorTypeImpl[T] }
9+
10+
trait VisitorType[T] {
11+
type Out[A]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Coverage data, format version: 3.0
2+
# Statement data:
3+
# - id
4+
# - source path
5+
# - package name
6+
# - class name
7+
# - class type (Class, Object or Trait)
8+
# - full class name
9+
# - method name
10+
# - start offset
11+
# - end offset
12+
# - line number
13+
# - symbol name
14+
# - tree name
15+
# - is branch
16+
# - invocations count
17+
# - is ignored
18+
# - description (can be multi-line)
19+
# ' ' sign
20+
# ------------------------------------------
21+
1
22+
macro-late-suspend/VisitorMacros.scala
23+
example
24+
VisitorMacros$package
25+
Object
26+
example.VisitorMacros$package
27+
mkVisitorTypeImpl
28+
124
29+
144
30+
6
31+
unpickleExprV2
32+
Apply
33+
false
34+
0
35+
false
36+
new VisitorType[T]{}
37+
38+
2
39+
macro-late-suspend/VisitorMacros.scala
40+
example
41+
VisitorMacros$package
42+
Object
43+
example.VisitorMacros$package
44+
mkVisitorTypeImpl
45+
40
46+
69
47+
5
48+
mkVisitorTypeImpl
49+
DefDef
50+
false
51+
0
52+
false
53+
private def mkVisitorTypeImpl
54+
55+
3
56+
macro-late-suspend/Test.scala
57+
example
58+
Test
59+
Object
60+
example.Test
61+
<init>
62+
102
63+
121
64+
8
65+
<init>
66+
Apply
67+
false
68+
0
69+
false
70+
mkVisitorType[Test]
71+
72+
4
73+
macro-late-suspend/UsesTest.scala
74+
example
75+
UsesTest$package
76+
Object
77+
example.UsesTest$package
78+
<init>
79+
22
80+
22
81+
3
82+
<none>
83+
Literal
84+
true
85+
0
86+
false
87+
88+

Diff for: tests/coverage/run/i16940/i16940.check

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2

Diff for: tests/coverage/run/i18233-min/i182233-min.check

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
List()
2+
List(abc, def)

Diff for: tests/coverage/run/i18233/i18233.check

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Baz

Diff for: tests/coverage/run/macro-suspend/Macro.check

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
>>> hello <<<

Diff for: tests/coverage/run/macro-suspend/Macro.scala

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import scala.quoted.{Expr, Quotes}
2+
3+
object Macro:
4+
inline def decorate(inline s: String): String = ${ decorateQuotes('s) }
5+
def decorateQuotes(s: Expr[String])(using Quotes): Expr[String] = '{ ">>> " + $s + " <<<" }
6+
7+
object Greeting:
8+
def greet() = "hello"

Diff for: tests/coverage/run/macro-suspend/Test.scala

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
object Test:
2+
def main(args: Array[String]): Unit =
3+
println(Macro.decorate(Greeting.greet()))
4+

0 commit comments

Comments
 (0)