Skip to content

Commit cb66c7a

Browse files
TheElectronWillsmarter
authored andcommitted
Cut coverage tests in small parts and avoid lifting literals
1 parent d37d13b commit cb66c7a

36 files changed

+2019
-3394
lines changed

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ trait CommonScalaSettings:
117117
val unchecked: Setting[Boolean] = BooleanSetting("-unchecked", "Enable additional warnings where generated code depends on assumptions.", initialValue = true, aliases = List("--unchecked"))
118118
val language: Setting[List[String]] = MultiStringSetting("-language", "feature", "Enable one or more language features.", aliases = List("--language"))
119119
/* Coverage settings */
120-
val coverageOutputDir = PathSetting("-coverage", "Destination for coverage classfiles and instrumentation data.", "")
120+
val coverageOutputDir = PathSetting("-coverage-out", "Destination for coverage classfiles and instrumentation data.", "")
121121
val coverageSourceroot = PathSetting("-coverage-sourceroot", "An alternative root dir of your sources used to relativize.", ".")
122122

123123
/* Other settings */

compiler/src/dotty/tools/dotc/coverage/Coverage.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package coverage
44
import scala.collection.mutable
55

66
class Coverage:
7-
private val statementsById = mutable.Map[Int, Statement]()
7+
private val statementsById = new mutable.LongMap[Statement](256)
88

99
def statements: Iterable[Statement] = statementsById.values
1010

compiler/src/dotty/tools/dotc/coverage/Serializer.scala

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package dotty.tools.dotc
22
package coverage
33

4-
import java.io._
5-
6-
import scala.io.Source
4+
import java.nio.file.{Path, Paths, Files}
5+
import java.io.Writer
6+
import scala.language.unsafeNulls
77

88
/**
99
* Serializes scoverage data.
@@ -16,19 +16,22 @@ object Serializer:
1616

1717
/** Write out coverage data to the given data directory, using the default coverage filename */
1818
def serialize(coverage: Coverage, dataDir: String, sourceRoot: String): Unit =
19-
serialize(coverage, coverageFile(dataDir), new File(sourceRoot))
19+
serialize(coverage, Paths.get(dataDir, CoverageFileName).toAbsolutePath, Paths.get(sourceRoot).toAbsolutePath)
2020

21-
/** Write out coverage data to given file. */
22-
def serialize(coverage: Coverage, file: File, sourceRoot: File): Unit =
23-
val writer = BufferedWriter(FileWriter(file))
24-
serialize(coverage, writer, sourceRoot)
25-
writer.close()
21+
/** Write out coverage data to a file. */
22+
def serialize(coverage: Coverage, file: Path, sourceRoot: Path): Unit =
23+
val writer = Files.newBufferedWriter(file)
24+
try
25+
serialize(coverage, writer, sourceRoot)
26+
finally
27+
writer.close()
2628

27-
def serialize(coverage: Coverage, writer: Writer, sourceRoot: File): Unit =
29+
/** Write out coverage data (info about each statement that can be covered) to a writer.
30+
*/
31+
def serialize(coverage: Coverage, writer: Writer, sourceRoot: Path): Unit =
2832

2933
def getRelativePath(filePath: String): String =
30-
val base = sourceRoot.getCanonicalFile().toPath()
31-
val relPath = base.relativize(File(filePath).getCanonicalFile().toPath())
34+
val relPath = sourceRoot.relativize(Paths.get(filePath).toAbsolutePath)
3235
relPath.toString
3336

3437
def writeHeader(writer: Writer): Unit =
@@ -78,6 +81,3 @@ object Serializer:
7881
coverage.statements.toSeq
7982
.sortBy(_.id)
8083
.foreach(stmt => writeStatement(stmt, writer))
81-
82-
def coverageFile(dataDir: File): File = coverageFile(dataDir.getAbsolutePath)
83-
def coverageFile(dataDir: String): File = File(dataDir, CoverageFileName)

compiler/src/dotty/tools/dotc/transform/InstrumentCoverage.scala

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@ import dotty.tools.dotc.coverage.Location
1515
import dotty.tools.dotc.core.Symbols.defn
1616
import dotty.tools.dotc.core.Symbols.Symbol
1717
import dotty.tools.dotc.core.Decorators.toTermName
18-
import dotty.tools.dotc.util.SourcePosition
18+
import dotty.tools.dotc.util.{SourcePosition, Property}
1919
import dotty.tools.dotc.core.Constants.Constant
2020
import dotty.tools.dotc.typer.LiftCoverage
2121

22-
import scala.quoted
23-
2422
/** Implements code coverage by inserting calls to scala.runtime.Invoker
2523
* ("instruments" the source code).
2624
* The result can then be consumed by the Scoverage tool.
@@ -51,29 +49,40 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
5149
val dataDir = new File(outputPath)
5250
val newlyCreated = dataDir.mkdirs()
5351

54-
if (!newlyCreated) {
52+
if !newlyCreated then
5553
// If the directory existed before, let's clean it up.
56-
dataDir.listFiles
57-
.filter(_.getName.startsWith("scoverage"))
58-
.foreach(_.delete)
59-
}
60-
54+
dataDir.listFiles.nn
55+
.filter(_.nn.getName.nn.startsWith("scoverage"))
56+
.foreach(_.nn.delete())
57+
end if
6158
super.run
6259

6360
Serializer.serialize(coverage, outputPath, ctx.settings.coverageSourceroot.value)
6461

6562
override protected def newTransformer(using Context) = CoverageTransormer()
6663

64+
/** Transforms trees to insert calls to Invoker.invoked to compute the coverage when the code is called */
6765
private class CoverageTransormer extends Transformer:
66+
private val IgnoreLiterals = new Property.Key[Boolean]
67+
68+
private def ignoreLiteralsContext(using ctx: Context): Context =
69+
ctx.fresh.setProperty(IgnoreLiterals, true)
6870

69-
override def transform(tree: Tree)(using Context): Tree =
70-
println(tree.show + tree.toString)
71+
override def transform(tree: Tree)(using ctx: Context): Tree =
7172
tree match
7273
// simple cases
73-
case tree: (Literal | Import | Export) => tree
74-
case tree: (New | This | Super) => instrument(tree)
74+
case tree: (Import | Export | This | Super | New) => tree
7575
case tree if (tree.isEmpty || tree.isType) => tree // empty Thicket, Ident, TypTree, ...
7676

77+
// Literals must be instrumented (at least) when returned by a def,
78+
// otherwise `def d = "literal"` is not covered when called from a test.
79+
// They can be left untouched when passed in a parameter of an Apply.
80+
case tree: Literal =>
81+
if ctx.property(IgnoreLiterals).contains(true) then
82+
tree
83+
else
84+
instrument(tree)
85+
7786
// branches
7887
case tree: If =>
7988
cpy.If(tree)(
@@ -88,28 +97,42 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
8897
finalizer = instrument(transform(tree.finalizer), true)
8998
)
9099

100+
// a.f(args)
101+
case tree @ Apply(fun: Select, args) =>
102+
// don't transform the first Select, but do transform `a.b` in `a.b.f(args)`
103+
val transformedFun = cpy.Select(fun)(transform(fun.qualifier), fun.name)
104+
if needsLift(tree) then
105+
val transformed = cpy.Apply(tree)(transformedFun, args) // args will be transformed in instrumentLifted
106+
instrumentLifted(transformed)(using ignoreLiteralsContext)
107+
else
108+
val transformed = cpy.Apply(tree)(transformedFun, transform(args))
109+
instrument(transformed)(using ignoreLiteralsContext)
110+
91111
// f(args)
92112
case tree: Apply =>
93113
if needsLift(tree) then
94-
liftApply(tree)
114+
instrumentLifted(tree)(using ignoreLiteralsContext) // see comment about Literals
95115
else
96-
super.transform(tree)
116+
instrument(super.transform(tree)(using ignoreLiteralsContext))
97117

98118
// (f(x))[args]
99-
case tree @ TypeApply(fun: Apply, args) =>
119+
case TypeApply(fun: Apply, args) =>
100120
cpy.TypeApply(tree)(transform(fun), args)
101121

102122
// a.b
103-
case Select(qual, _) if (qual.symbol.exists && qual.symbol.is(JavaDefined)) =>
104-
//Java class can't be used as a value, we can't instrument the
105-
//qualifier ({<Probe>;System}.xyz() is not possible !) instrument it
106-
//as it is
107-
instrument(tree)
108-
case tree: Select =>
109-
if tree.qualifier.isInstanceOf[New] then
123+
case Select(qual, name) =>
124+
if qual.symbol.exists && qual.symbol.is(JavaDefined) then
125+
//Java class can't be used as a value, we can't instrument the
126+
//qualifier ({<Probe>;System}.xyz() is not possible !) instrument it
127+
//as it is
110128
instrument(tree)
111129
else
112-
cpy.Select(tree)(transform(tree.qualifier), tree.name)
130+
val transformed = cpy.Select(tree)(transform(qual), name)
131+
if transformed.qualifier.isDef then
132+
// instrument calls to methods without parameter list
133+
instrument(transformed)
134+
else
135+
transformed
113136

114137
case tree: CaseDef => instrumentCaseDef(tree)
115138
case tree: ValDef =>
@@ -120,6 +143,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
120143
// only transform the statements of the package
121144
cpy.PackageDef(tree)(tree.pid, transform(tree.stats))
122145
case tree: Assign =>
146+
// only transform the rhs
123147
cpy.Assign(tree)(tree.lhs, transform(tree.rhs))
124148
case tree: Template =>
125149
// Don't instrument the parents (extends) of a template since it
@@ -131,13 +155,9 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
131155

132156
// For everything else just recurse and transform
133157
case _ =>
134-
report.warning(
135-
"Unmatched: " + tree.getClass + " " + tree.symbol,
136-
tree.sourcePos
137-
)
138158
super.transform(tree)
139159

140-
def liftApply(tree: Apply)(using Context) =
160+
def instrumentLifted(tree: Apply)(using Context) =
141161
val buffer = mutable.ListBuffer[Tree]()
142162
// NOTE: that if only one arg needs to be lifted, we just lift everything
143163
val lifted = LiftCoverage.liftForCoverage(buffer, tree)
@@ -170,10 +190,10 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
170190
id = id,
171191
start = pos.start,
172192
end = pos.end,
173-
line = ctx.source.offsetToLine(pos.point),
193+
line = pos.line,
174194
desc = tree.source.content.slice(pos.start, pos.end).mkString,
175195
symbolName = tree.symbol.name.toSimpleName.toString(),
176-
treeName = tree.getClass.getSimpleName,
196+
treeName = tree.getClass.getSimpleName.nn,
177197
branch
178198
)
179199
coverage.addStatement(statement)
@@ -209,8 +229,7 @@ class InstrumentCoverage extends MacroTransform with IdentityDenotTransformer:
209229
val fun = tree.fun
210230

211231
fun.isInstanceOf[Apply] || // nested apply
212-
!isBooleanOperator(fun) ||
213-
!tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift)
232+
!isBooleanOperator(fun) && !tree.args.isEmpty && !tree.args.forall(LiftCoverage.noLift)
214233

215234
object InstrumentCoverage:
216235
val name: String = "instrumentCoverage"

compiler/src/dotty/tools/dotc/typer/EtaExpansion.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ object LiftCoverage extends LiftComplex {
161161

162162
private val LiftEverything = new Property.Key[Boolean]
163163

164-
private def liftEverything(using Context): Boolean =
164+
private inline def liftEverything(using Context): Boolean =
165165
ctx.property(LiftEverything).contains(true)
166166

167167
private def liftEverythingContext(using Context): Context =
Lines changed: 41 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,63 @@
11
package dotty.tools.dotc.coverage
22

3-
import java.util.stream.Collectors
4-
import org.junit.Assert._
5-
import dotty.BootstrappedOnlyTests
6-
import org.junit.experimental.categories.Category
73
import org.junit.Test
8-
import java.nio.file.Files
4+
import org.junit.Assert.*
5+
import org.junit.experimental.categories.Category
6+
7+
import dotty.BootstrappedOnlyTests
98
import dotty.tools.dotc.Main
10-
import scala.jdk.CollectionConverters._
119

12-
import java.io.File
10+
import java.nio.file.Files
1311
import java.nio.file.Path
1412
import java.nio.file.FileSystems
1513
import java.nio.file.Paths
16-
import java.nio.charset.StandardCharsets
17-
import scala.io.Source
18-
import java.io.BufferedOutputStream
19-
import java.io.FileOutputStream
14+
import java.nio.charset.StandardCharsets.UTF_8
15+
import java.nio.file.StandardCopyOption
2016

2117
@main def updateExpect =
22-
CoverageTests().runExpectTest(updateExpectFiles = true)
18+
CoverageTests().runExpectTest(updateCheckfiles = true)
2319

2420
@Category(Array(classOf[BootstrappedOnlyTests]))
25-
class CoverageTests {
21+
class CoverageTests:
2622

27-
val scalaFile = FileSystems.getDefault.getPathMatcher("glob:**.scala")
28-
val rootSrc = Paths.get(System.getProperty("dotty.tools.dotc.coverage.test"))
29-
val expectSrc = rootSrc.resolve("expect")
23+
private val scalaFile = FileSystems.getDefault.nn.getPathMatcher("glob:**.scala").nn
24+
private val rootSrc = Paths.get(System.getProperty("dotty.tools.dotc.coverage.test")).nn
25+
private val expectDir = rootSrc.resolve("expect").nn
3026

3127
@Category(Array(classOf[dotty.SlowTests]))
3228
@Test def expectTests: Unit =
33-
if (!scala.util.Properties.isWin) runExpectTest(updateExpectFiles = false)
34-
35-
def runExpectTest(updateExpectFiles: Boolean): Unit = {
36-
val target = generateCoverage(updateExpectFiles)
37-
val input = Source.fromFile(new File(target.toString, "scoverage.coverage"))
38-
val expectFile = new File(expectSrc.resolveSibling("scoverage.coverage.expect").toUri)
39-
40-
if (updateExpectFiles) {
41-
val outputStream = new BufferedOutputStream(new FileOutputStream(expectFile))
42-
try {
43-
input.foreach(outputStream.write(_))
44-
} finally outputStream.close
45-
} else {
46-
val expected = new String(Files.readAllBytes(expectFile.toPath), StandardCharsets.UTF_8)
47-
val obtained = input.mkString
48-
49-
assertEquals(expected, obtained)
50-
}
51-
}
52-
53-
def inputFiles(): List[Path] = {
54-
val ls = Files.walk(expectSrc)
55-
val files =
56-
try ls.filter(p => scalaFile.matches(p)).collect(Collectors.toList).asScala
57-
finally ls.close()
58-
59-
files.toList
60-
}
61-
62-
def generateCoverage(updateExpectFiles: Boolean): Path = {
29+
runExpectTest(dotty.Properties.testsUpdateCheckfile)
30+
31+
/** Runs the tests */
32+
def runExpectTest(updateCheckfiles: Boolean): Unit =
33+
val sourceRoot = if updateCheckfiles then "../" else "."
34+
35+
Files.walk(expectDir).nn.filter(scalaFile.matches).nn.forEach(p => {
36+
val path = p.nn
37+
val fileName = path.getFileName.nn.toString.nn.stripSuffix(".scala")
38+
val targetDir = computeCoverageInTmp(Seq(path), sourceRoot).nn
39+
val targetFile = targetDir.resolve(s"scoverage.coverage").nn
40+
val expectFile = expectDir.resolve(s"$fileName.scoverage.check").nn
41+
42+
if updateCheckfiles then
43+
Files.copy(targetFile, expectFile, StandardCopyOption.REPLACE_EXISTING)
44+
else
45+
val expected = new String(Files.readAllBytes(expectFile), UTF_8)
46+
val obtained = new String(Files.readAllBytes(targetFile), UTF_8)
47+
assertEquals(expected, obtained)
48+
49+
})
50+
51+
/** Generates the coverage report for the given input file, in a temporary directory. */
52+
def computeCoverageInTmp(inputFiles: Seq[Path], sourceRoot: String): Path =
6353
val target = Files.createTempDirectory("coverage")
6454
val args = Array(
65-
"-coverage",
55+
"-coverage-out",
6656
target.toString,
6757
"-coverage-sourceroot",
68-
if (updateExpectFiles) "../" else ".",
58+
sourceRoot,
6959
"-usejavacp"
70-
) ++ inputFiles().map(_.toString)
60+
) ++ inputFiles.map(_.toString)
7161
val exit = Main.process(args)
72-
assertFalse(s"dotc errors: ${exit.errorCount}", exit.hasErrors)
73-
target
74-
}
75-
}
62+
assertFalse(s"Compilation failed, ${exit.errorCount} errors", exit.hasErrors)
63+
target.nn

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -666,7 +666,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
666666
if (didFail) {
667667
reportFailed()
668668
failedTestSources.toSet.foreach(addFailedTest)
669-
reproduceInstructions.iterator.foreach(addReproduceInstruction)
669+
reproduceInstructions.foreach(addReproduceInstruction)
670670
}
671671
else reportPassed()
672672
}
@@ -980,7 +980,7 @@ trait ParallelTesting extends RunnerOrchestration { self =>
980980
cleanup()
981981

982982
if (!shouldFail && test.didFail) {
983-
fail(s"Expected no errors when compiling, failed for the following reason(s):\n${ reasonsForFailure(test) }")
983+
fail(s"Expected no errors when compiling, failed for the following reason(s):\n${reasonsForFailure(test)}\n")
984984
}
985985
else if (shouldFail && !test.didFail) {
986986
fail("Pos test should have failed, but didn't")
@@ -1077,14 +1077,16 @@ trait ParallelTesting extends RunnerOrchestration { self =>
10771077
/** Extract `Failure` set and render from `Test` */
10781078
private def reasonsForFailure(test: Test): String = {
10791079
val failureReport =
1080-
if (test.failureCount == 0) ""
1081-
else s"\n - encountered ${test.failureCount} test failures(s)"
1080+
if test.failureCount == 0 then ""
1081+
else s"encountered ${test.failureCount} test failure(s):\n"
10821082

10831083
failureReport + test.failureReasons.collect {
10841084
case test.TimeoutFailure(title) =>
10851085
s" - test '$title' timed out"
10861086
case test.JavaCompilationFailure(msg) =>
10871087
s" - java compilation failed with:\n${ msg.linesIterator.map(" " + _).mkString("\n") }"
1088+
case test.Generic =>
1089+
" - generic failure (see test output)"
10881090
}.mkString("\n")
10891091
}
10901092

0 commit comments

Comments
 (0)