Skip to content

Commit 784df3e

Browse files
committed
Fix scala#2185: Add bytecode idempotency checks.
1 parent ab124cb commit 784df3e

File tree

2 files changed

+116
-28
lines changed

2 files changed

+116
-28
lines changed

compiler/src/dotty/tools/dotc/core/tasty/TastyPickler.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Decorators._
1414
class TastyPickler {
1515

1616
private val sections = new mutable.ArrayBuffer[(NameRef, TastyBuffer)]
17-
val uuid = UUID.randomUUID()
17+
private val uuid = UUID.fromString("3cee1b79-c03a-4125-b337-d067b5cb3a94") // TODO: use a hash of the tasty tree
1818

1919
private val headerBuffer = {
2020
val buf = new TastyBuffer(24)

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 115 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ package dotty
22
package tools
33
package dotc
44

5-
import org.junit.{ Test, BeforeClass, AfterClass }
5+
import java.io.File
66

7+
import org.junit.{AfterClass, Assert, BeforeClass, Test}
78
import java.nio.file._
8-
import java.util.stream.{ Stream => JStream }
9+
import java.util.stream.{Stream => JStream}
10+
911
import scala.collection.JavaConverters._
1012
import scala.util.matching.Regex
1113
import scala.concurrent.duration._
14+
import vulpix.{ParallelTesting, SummaryReport, SummaryReporting, TestConfiguration}
1215

13-
import vulpix.{ ParallelTesting, SummaryReport, SummaryReporting, TestConfiguration }
16+
import scala.io.Source
1417

1518

1619
class CompilationTests extends ParallelTesting {
@@ -231,30 +234,6 @@ class CompilationTests extends ParallelTesting {
231234
compileDir("../library/src",
232235
allowDeepSubtypes.and("-Ycheck-reentrant", "-strict", "-priorityclasspath", defaultOutputDir))
233236

234-
def sources(paths: JStream[Path], excludedFiles: List[String] = Nil): List[String] =
235-
paths.iterator().asScala
236-
.filter(path =>
237-
(path.toString.endsWith(".scala") || path.toString.endsWith(".java"))
238-
&& !excludedFiles.contains(path.getFileName.toString))
239-
.map(_.toString).toList
240-
241-
val compilerDir = Paths.get("../compiler/src")
242-
val compilerSources = sources(Files.walk(compilerDir))
243-
244-
val backendDir = Paths.get("../scala-backend/src/compiler/scala/tools/nsc/backend")
245-
val backendJvmDir = Paths.get("../scala-backend/src/compiler/scala/tools/nsc/backend/jvm")
246-
247-
// NOTE: Keep these exclusions synchronized with the ones in the sbt build (Build.scala)
248-
val backendExcluded =
249-
List("JavaPlatform.scala", "Platform.scala", "ScalaPrimitives.scala")
250-
val backendJvmExcluded =
251-
List("BCodeICodeCommon.scala", "GenASM.scala", "GenBCode.scala", "ScalacBackendInterface.scala")
252-
253-
val backendSources =
254-
sources(Files.list(backendDir), excludedFiles = backendExcluded)
255-
val backendJvmSources =
256-
sources(Files.list(backendJvmDir), excludedFiles = backendJvmExcluded)
257-
258237
def dotty1 = {
259238
compileList(
260239
"dotty1",
@@ -285,6 +264,115 @@ class CompilationTests extends ParallelTesting {
285264
} :: Nil
286265
}.map(_.checkCompile()).foreach(_.delete())
287266
}
267+
268+
@Test def bytecodeIdemporency: Unit = {
269+
var failed = 0
270+
var total = 0
271+
val blacklisted = Set(
272+
"pos/Map/scala/collection/immutable/Map",
273+
"pos/Map/scala/collection/immutable/AbstractMap",
274+
"pos/t1203a/NodeSeq"
275+
)
276+
def checkIdempotency(): Unit = {
277+
def listFilesIn(i: Int) = {
278+
def getListOfFiles(file: File): List[File] =
279+
if (file.isDirectory) file.listFiles.flatMap(getListOfFiles).toList
280+
else if (file.toString.endsWith(".class") || file.toString.endsWith(".tasty")) List(file)
281+
else Nil
282+
getListOfFiles(new File(s"../out/idempotency$i"))
283+
}
284+
285+
def groupedFiles: List[(File, File, File, File)] = {
286+
val files = listFilesIn(1) ++ listFilesIn(2)
287+
val groups = files.groupBy(f => f.toString.substring("../out/idempotencyN/".length, f.toString.length - 6))
288+
groups.filterNot(x => blacklisted(x._1)).valuesIterator.flatMap { g =>
289+
def pred(f: File, i: Int, tasty: Boolean) =
290+
f.toString.contains("idempotency" + i) && f.toString.endsWith(if (tasty) ".tasty" else ".class")
291+
val class1 = g.find(f => pred(f, 1, false))
292+
val class2 = g.find(f => pred(f, 2, false))
293+
val tasty1 = g.find(f => pred(f, 1, true))
294+
val tasty2 = g.find(f => pred(f, 2, true))
295+
assert(class1.isDefined, "Could not find class in idempotency1 for " + class2)
296+
assert(class2.isDefined, "Could not find class in idempotency2 for " + class1)
297+
if (tasty1.isEmpty || tasty2.isEmpty) Nil
298+
else List(Tuple4(class1.get, tasty1.get, class2.get, tasty2.get))
299+
}.toList
300+
}
301+
302+
for ((class1, tasty1, class2, tasty2) <- groupedFiles) {
303+
total += 1
304+
val bytes1 = Files.readAllBytes(class1.toPath)
305+
val bytes2 = Files.readAllBytes(class2.toPath)
306+
if (!java.util.Arrays.equals(bytes1, bytes2)) {
307+
failed += 1
308+
val tastyBytes1 = Files.readAllBytes(tasty1.toPath)
309+
val tastyBytes2 = Files.readAllBytes(tasty2.toPath)
310+
if (java.util.Arrays.equals(tastyBytes1, tastyBytes2))
311+
println(s"Idempotency test failed between $class1 and $class1 (same tasty)")
312+
else
313+
println(s"Idempotency test failed between $tasty1 and $tasty2")
314+
/* Dump bytes to console, could be useful if issue only appears in CI.
315+
* Create the .class locally with Files.write(path, Array[Byte](...)) with the printed array
316+
*/
317+
// println(bytes1.mkString("Array[Byte](", ",", ")"))
318+
// println(bytes2.mkString("Array[Byte](", ",", ")"))
319+
}
320+
}
321+
}
322+
323+
val opt = defaultOptions.and("-YemitTasty")
324+
325+
def idempotency1() = {
326+
compileList("dotty1", compilerSources ++ backendSources ++ backendJvmSources, opt) +
327+
compileFilesInDir("../tests/pos", opt)
328+
}
329+
def idempotency2() = {
330+
compileList("dotty1", compilerSources ++ backendSources ++ backendJvmSources, opt) +
331+
compileFilesInDir("../tests/pos", opt)
332+
}
333+
334+
val tests = (idempotency1() + idempotency2()).keepOutput.checkCompile()
335+
336+
assert(new java.io.File("../out/idempotency1/").exists)
337+
assert(new java.io.File("../out/idempotency2/").exists)
338+
339+
val t0 = System.currentTimeMillis()
340+
checkIdempotency()
341+
println(s"checked bytecode idempotency (${(System.currentTimeMillis() - t0) / 1000.0} sec)")
342+
343+
tests.delete()
344+
345+
assert(failed == 0, s"Failed $failed idempotency checks (out of $total)")
346+
}
347+
348+
349+
private val (compilerSources, backendSources, backendJvmSources) = {
350+
def sources(paths: JStream[Path], excludedFiles: List[String] = Nil): List[String] =
351+
paths.iterator().asScala
352+
.filter(path =>
353+
(path.toString.endsWith(".scala") || path.toString.endsWith(".java"))
354+
&& !excludedFiles.contains(path.getFileName.toString))
355+
.map(_.toString).toList
356+
357+
val compilerDir = Paths.get("../compiler/src")
358+
val compilerSources0 = sources(Files.walk(compilerDir))
359+
360+
val backendDir = Paths.get("../scala-backend/src/compiler/scala/tools/nsc/backend")
361+
val backendJvmDir = Paths.get("../scala-backend/src/compiler/scala/tools/nsc/backend/jvm")
362+
363+
// NOTE: Keep these exclusions synchronized with the ones in the sbt build (Build.scala)
364+
val backendExcluded =
365+
List("JavaPlatform.scala", "Platform.scala", "ScalaPrimitives.scala")
366+
val backendJvmExcluded =
367+
List("BCodeICodeCommon.scala", "GenASM.scala", "GenBCode.scala", "ScalacBackendInterface.scala")
368+
369+
val backendSources0 =
370+
sources(Files.list(backendDir), excludedFiles = backendExcluded)
371+
val backendJvmSources0 =
372+
sources(Files.list(backendJvmDir), excludedFiles = backendJvmExcluded)
373+
374+
(compilerSources0, backendSources0, backendJvmSources0)
375+
}
288376
}
289377

290378
object CompilationTests {

0 commit comments

Comments
 (0)