scala.tools.partest.BytecodeTest.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of scala-partest Show documentation
Show all versions of scala-partest Show documentation
Scala Compiler Testing Tool
The newest version!
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package scala.tools.partest
import scala.jdk.CollectionConverters._
import scala.tools.asm.{ClassReader, ClassWriter}
import scala.tools.asm.tree._
import java.io.{InputStream, File => JFile}
import scala.tools.testkit.ASMConverters
import AsmNode._
import scala.tools.nsc.CloseableRegistry
/**
* Provides utilities for inspecting bytecode using ASM library.
*
* HOW TO USE
* 1. Create subdirectory in test/files/jvm for your test. Let's name it \$TESTDIR.
* 2. Create \$TESTDIR/BytecodeSrc_1.scala that contains Scala source file that you
* want to inspect the bytecode for. The '_1' suffix signals to partest that it
* should compile this file first.
* 3. Create \$TESTDIR/Test.scala:
* import scala.tools.partest.BytecodeTest
* object Test extends BytecodeTest {
* def show {
* // your code that inspect ASM trees and prints values
* }
* }
* 4. Create corresponding check file.
*
* EXAMPLE
* See test/files/jvm/bytecode-test-example for an example of bytecode test.
*
*/
abstract class BytecodeTest {
import ASMConverters._
/** produce the output to be compared against a checkfile */
protected def show(): Unit
def main(args: Array[String]): Unit = show()
// asserts
def sameBytecode(methA: MethodNode, methB: MethodNode) = {
val isa = instructionsFromMethod(methA)
val isb = instructionsFromMethod(methB)
if (isa == isb) println("bytecode identical")
else diffInstructions(isa, isb)
}
// Do these classes have all the same methods, with the same names, access,
// descriptors and generic signatures? Method bodies are not considered, and
// the names of the classes containing the methods are substituted so they do
// not appear as differences.
def sameMethodAndFieldSignatures(clazzA: ClassNode, clazzB: ClassNode) =
sameCharacteristics(clazzA, clazzB)(_.characteristics)
// Same as sameMethodAndFieldSignatures, but ignoring generic signatures.
// This allows for methods which receive the same descriptor but differing
// generic signatures. In particular, this happens with value classes,
// which get a generic signature where a method written in terms of the
// underlying values does not.
def sameMethodAndFieldDescriptors(clazzA: ClassNode, clazzB: ClassNode) =
sameCharacteristics(clazzA, clazzB)(_.erasedCharacteristics)
private def sameCharacteristics(clazzA: ClassNode, clazzB: ClassNode)(f: AsmNode[_] => String): Boolean = {
val ms1 = clazzA.fieldsAndMethods.toIndexedSeq
val ms2 = clazzB.fieldsAndMethods.toIndexedSeq
val name1 = clazzA.name
val name2 = clazzB.name
if (ms1.length != ms2.length) {
println(s"Different member counts in $name1 and $name2")
false
}
else ms1.lazyZip(ms2).forall { (m1, m2) =>
val c1 = f(m1)
val c2 = f(m2).replace(name2, name1)
if (c1 == c2)
println(s"[ok] $m1")
else
println(s"[fail]\n in $name1: $c1\n in $name2: $c2")
c1 == c2
}
}
/**
* Compare the bytecodes of two methods.
*
* For the `similar` function, you probably want to pass [[scala.tools.testkit.ASMConverters.equivalentBytecode]].
*/
def similarBytecode(methA: MethodNode, methB: MethodNode, similar: (List[Instruction], List[Instruction]) => Boolean) = {
val isa = instructionsFromMethod(methA)
val isb = instructionsFromMethod(methB)
if (isa == isb) println("bytecode identical")
else if (similar(isa, isb)) println("bytecode similar")
else diffInstructions(isa, isb)
}
def diffInstructions(isa: List[Instruction], isb: List[Instruction]) = {
val len = Math.max(isa.length, isb.length)
if (len > 0 ) {
val width = isa.map(_.toString.length).max
val lineWidth = len.toString.length
(1 to len) foreach { line =>
val isaPadded = isa.map(_.toString) orElse LazyList.continually("")
val isbPadded = isb.map(_.toString) orElse LazyList.continually("")
val a = isaPadded(line-1)
val b = isbPadded(line-1)
println(s"""$line${" " * (lineWidth-line.toString.length)} ${if (a==b) "==" else "<>"} $a${" " * (width-a.length)} | $b""")
}
}
}
// loading
protected def getField(classNode: ClassNode, name: String): FieldNode =
classNode.fields.asScala.find(_.name == name) getOrElse
sys.error(s"Didn't find field '$name' in class '${classNode.name}'")
protected def getMethod(classNode: ClassNode, name: String): MethodNode =
classNode.methods.asScala.find(_.name == name) getOrElse
sys.error(s"Didn't find method '$name' in class '${classNode.name}'")
protected def loadClassNode(name: String, skipDebugInfo: Boolean = true): ClassNode = {
val classBytes: InputStream = classpath.findClassFile(name).map(_.input)
.getOrElse(sys.error(s"failed to load class '$name'; classpath = $classpath"))
val cr = new ClassReader(classBytes)
val cn = new ClassNode()
cr.accept(cn, if (skipDebugInfo) ClassReader.SKIP_DEBUG else 0)
cn
}
protected lazy val classpath: scala.tools.nsc.util.ClassPath = {
import scala.tools.nsc.classpath.AggregateClassPath
import scala.tools.nsc.classpath.ClassPathFactory
import scala.tools.util.PathResolver.Defaults
import scala.tools.nsc.Settings
// logic inspired by scala.tools.util.PathResolver implementation
// `Settings` is used to check YdisableFlatCpCaching in ZipArchiveFlatClassPath
val factory = new ClassPathFactory(new Settings(), new CloseableRegistry)
val containers = factory.classesInExpandedPath(sys.props("partest.output") + java.io.File.pathSeparator + Defaults.javaUserClassPath)
new AggregateClassPath(containers)
}
}
object BytecodeTest {
/** Parse `file` as a class file, transforms the ASM representation with `f`,
* and overwrites the original file.
*/
def modifyClassFile(file: JFile)(f: ClassNode => ClassNode): Unit = {
val rfile = new reflect.io.File(file)
def readClass: ClassNode = {
val cr = new ClassReader(rfile.toByteArray())
val cn = new ClassNode()
cr.accept(cn, 0)
cn
}
def writeClass(cn: ClassNode): Unit = {
val writer = new ClassWriter(0)
cn.accept(writer)
val os = rfile.bufferedOutput()
try {
os.write(writer.toByteArray)
} finally {
os.close()
}
}
writeClass(f(readClass))
}
}