Skip to content

Commit 636ddbd

Browse files
jchybtgodzik
authored andcommitted
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 674e3e1 commit 636ddbd

23 files changed

+402
-124
lines changed

compiler/src/dotty/tools/dotc/core/Contexts.scala

+6
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import util.Store
4040
import plugins.*
4141
import java.util.concurrent.atomic.AtomicInteger
4242
import java.nio.file.InvalidPathException
43+
import dotty.tools.dotc.coverage.Coverage
4344

4445
object Contexts {
4546

@@ -958,6 +959,11 @@ object Contexts {
958959
val sources: util.HashMap[AbstractFile, SourceFile] = util.HashMap[AbstractFile, SourceFile]()
959960
val files: util.HashMap[TermName, AbstractFile] = util.HashMap()
960961

962+
/** If coverage option is used, it stores all instrumented statements (for InstrumentCoverage).
963+
* We need this information to be persisted across different runs, so it's stored here.
964+
*/
965+
private[dotc] var coverage: Coverage | Null = null
966+
961967
// Types state
962968
/** A table for hash consing unique types */
963969
private[core] val uniques: Uniques = Uniques()

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.

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
/**

compiler/test/dotty/tools/dotc/coverage/CoverageTests.scala

+28-13
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,27 @@ class CoverageTests:
4545
lines
4646
end fixWindowsPaths
4747

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

52-
Files.walk(dir).filter(runOnFile).forEach(path => {
53-
val fileName = path.getFileName.toString.stripSuffix(".scala")
54-
val targetDir = computeCoverageInTmp(path, dir, run)
5567
val targetFile = targetDir.resolve(s"scoverage.coverage")
56-
val expectFile = path.resolveSibling(s"$fileName.scoverage.check")
68+
5769
if updateCheckFiles then
5870
Files.copy(targetFile, expectFile, StandardCopyOption.REPLACE_EXISTING)
5971
else
@@ -63,9 +75,6 @@ class CoverageTests:
6375
val instructions = FileDiff.diffMessage(expectFile.toString, targetFile.toString)
6476
fail(s"Coverage report differs from expected data.\n$instructions")
6577

66-
// measurement files only exist in the "run" category
67-
// as these are generated at runtime by the scala.runtime.coverage.Invoker
68-
val expectMeasurementFile = path.resolveSibling(s"$fileName.measurement.check")
6978
if run && Files.exists(expectMeasurementFile) then
7079

7180
// Note that this assumes that the test invoked was single threaded,
@@ -86,14 +95,20 @@ class CoverageTests:
8695
})
8796

8897
/** Generates the coverage report for the given input file, in a temporary directory. */
89-
def computeCoverageInTmp(inputFile: Path, sourceRoot: Path, run: Boolean)(using TestGroup): Path =
98+
def computeCoverageInTmp(inputFile: Path, isDirectory: Boolean, sourceRoot: Path, run: Boolean)(using TestGroup): Path =
9099
val target = Files.createTempDirectory("coverage")
91100
val options = defaultOptions.and("-Ycheck:instrumentCoverage", "-coverage-out", target.toString, "-sourceroot", sourceRoot.toString)
92101
if run then
93-
val test = compileDir(inputFile.getParent.toString, options)
102+
val path = if isDirectory then inputFile.toString else inputFile.getParent.toString
103+
val test = compileDir(path, options)
104+
test.checkFilePaths.foreach { checkFilePath =>
105+
assert(checkFilePath.exists, s"Expected checkfile for $path $checkFilePath does not exist.")
106+
}
94107
test.checkRuns()
95108
else
96-
val test = compileFile(inputFile.toString, options)
109+
val test =
110+
if isDirectory then compileDir(inputFile.toString, options)
111+
else compileFile(inputFile.toString, options)
97112
test.checkCompile()
98113
target
99114

compiler/test/dotty/tools/vulpix/ParallelTesting.scala

+15-9
Original file line numberDiff line numberDiff line change
@@ -250,15 +250,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
250250
* For a given test source, returns a check file against which the result of the test run
251251
* should be compared. Is used by implementations of this trait.
252252
*/
253-
final def checkFile(testSource: TestSource): Option[JFile] = (testSource match {
254-
case ts: JointCompilationSource =>
255-
ts.files.collectFirst {
256-
case f if !f.isDirectory =>
257-
new JFile(f.getPath.replaceFirst("\\.(scala|java)$", ".check"))
258-
}
259-
case ts: SeparateCompilationSource =>
260-
Option(new JFile(ts.dir.getPath + ".check"))
261-
}).filter(_.exists)
253+
final def checkFile(testSource: TestSource): Option[JFile] = (CompilationLogic.checkFilePath(testSource)).filter(_.exists)
262254

263255
/**
264256
* Checks if the given actual lines are the same as the ones in the check file.
@@ -335,6 +327,18 @@ trait ParallelTesting extends RunnerOrchestration { self =>
335327
}
336328
}
337329

330+
object CompilationLogic {
331+
private[ParallelTesting] def checkFilePath(testSource: TestSource) = testSource match {
332+
case ts: JointCompilationSource =>
333+
ts.files.collectFirst {
334+
case f if !f.isDirectory =>
335+
new JFile(f.getPath.replaceFirst("\\.(scala|java)$", ".check"))
336+
}
337+
case ts: SeparateCompilationSource =>
338+
Option(new JFile(ts.dir.getPath + ".check"))
339+
}
340+
}
341+
338342
/** Each `Test` takes the `testSources` and performs the compilation and assertions
339343
* according to the implementing class "neg", "run" or "pos".
340344
*/
@@ -1087,6 +1091,8 @@ trait ParallelTesting extends RunnerOrchestration { self =>
10871091
def this(targets: List[TestSource]) =
10881092
this(targets, 1, true, None, false, false)
10891093

1094+
def checkFilePaths: List[JFile] = targets.map(CompilationLogic.checkFilePath).flatten
1095+
10901096
def copy(targets: List[TestSource],
10911097
times: Int = times,
10921098
shouldDelete: Boolean = shouldDelete,
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package example
2+
3+
val _ = Test.Foo
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+
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2

0 commit comments

Comments
 (0)