Skip to content

Commit bbad660

Browse files
authored
Merge pull request #175 from scala/backport-lts-3.3-22303
Backport "Fix coverage serialization when encountering macro suspension " to 3.3 LTS
2 parents 674e3e1 + 83ec898 commit bbad660

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)