Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

support testing bytecode similarity in ByteCodeTest

one similarity measure comes free of charge: it ignores which variable
is stored/loaded, everything else must be identical

like this: `similarBytecode(methNodeA, methNodeB, equalsModuloVar)`

also implemented prettier diffing
  • Loading branch information...
commit 415becdab8aae63470deac5a6e122fa7a697cbe0 1 parent a07555f
Adriaan Moors adriaanm authored
71 src/partest/scala/tools/partest/ASMConverters.scala
View
@@ -0,0 +1,71 @@
+package scala.tools.partest
+
+import scala.collection.JavaConverters._
+import scala.tools.asm
+import asm.tree.{ClassNode, MethodNode, InsnList}
+
+/** Makes using ASM from ByteCodeTests more convenient.
+ *
+ * Wraps ASM instructions in case classes so that equals and toString work
+ * for the purpose of bytecode diffing and pretty printing.
+ */
+trait ASMConverters {
+ // wrap ASM's instructions so we get case class-style `equals` and `toString`
+ object instructions {
+ def fromMethod(meth: MethodNode): List[Instruction] = {
+ val insns = meth.instructions
+ val asmToScala = new AsmToScala{ def labelIndex(l: asm.tree.AbstractInsnNode) = insns.indexOf(l) }
+
+ asmToScala.mapOver(insns.iterator.asScala.toList).asInstanceOf[List[Instruction]]
+ }
+
+ sealed abstract class Instruction { def opcode: String }
+ case class Field (opcode: String, desc: String, name: String, owner: String) extends Instruction
+ case class Incr (opcode: String, incr: Int, `var`: Int) extends Instruction
+ case class Op (opcode: String) extends Instruction
+ case class IntOp (opcode: String, operand: Int) extends Instruction
+ case class Jump (opcode: String, label: Label) extends Instruction
+ case class Ldc (opcode: String, cst: Any) extends Instruction
+ case class LookupSwitch (opcode: String, dflt: Label, keys: List[Integer], labels: List[Label]) extends Instruction
+ case class TableSwitch (opcode: String, dflt: Label, max: Int, min: Int, labels: List[Label]) extends Instruction
+ case class Method (opcode: String, desc: String, name: String, owner: String) extends Instruction
+ case class NewArray (opcode: String, desc: String, dims: Int) extends Instruction
+ case class TypeOp (opcode: String, desc: String) extends Instruction
+ case class VarOp (opcode: String, `var`: Int) extends Instruction
+ case class Label (offset: Int) extends Instruction { def opcode: String = "" }
+ case class FrameEntry (local: List[Any], stack: List[Any]) extends Instruction { def opcode: String = "" }
+ case class LineNumber (line: Int, start: Label) extends Instruction { def opcode: String = "" }
+ }
+
+ abstract class AsmToScala {
+ import instructions._
+
+ def labelIndex(l: asm.tree.AbstractInsnNode): Int
+
+ def mapOver(is: List[Any]): List[Any] = is map {
+ case i: asm.tree.AbstractInsnNode => apply(i)
+ case x => x
+ }
+
+ def op(i: asm.tree.AbstractInsnNode) = if (asm.util.Printer.OPCODES.isDefinedAt(i.getOpcode)) asm.util.Printer.OPCODES(i.getOpcode) else "?"
+ def lst[T](xs: java.util.List[T]): List[T] = if (xs == null) Nil else xs.asScala.toList
+ def apply(l: asm.tree.LabelNode): Label = this(l: asm.tree.AbstractInsnNode).asInstanceOf[Label]
+ def apply(x: asm.tree.AbstractInsnNode): Instruction = x match {
+ case i: asm.tree.FieldInsnNode => Field (op(i), i.desc: String, i.name: String, i.owner: String)
+ case i: asm.tree.IincInsnNode => Incr (op(i), i.incr: Int, i.`var`: Int)
+ case i: asm.tree.InsnNode => Op (op(i))
+ case i: asm.tree.IntInsnNode => IntOp (op(i), i.operand: Int)
+ case i: asm.tree.JumpInsnNode => Jump (op(i), this(i.label))
+ case i: asm.tree.LdcInsnNode => Ldc (op(i), i.cst: Any)
+ case i: asm.tree.LookupSwitchInsnNode => LookupSwitch (op(i), this(i.dflt), lst(i.keys), mapOver(lst(i.labels)).asInstanceOf[List[Label]])
+ case i: asm.tree.TableSwitchInsnNode => TableSwitch (op(i), this(i.dflt), i.max: Int, i.min: Int, mapOver(lst(i.labels)).asInstanceOf[List[Label]])
+ case i: asm.tree.MethodInsnNode => Method (op(i), i.desc: String, i.name: String, i.owner: String)
+ case i: asm.tree.MultiANewArrayInsnNode => NewArray (op(i), i.desc: String, i.dims: Int)
+ case i: asm.tree.TypeInsnNode => TypeOp (op(i), i.desc: String)
+ case i: asm.tree.VarInsnNode => VarOp (op(i), i.`var`: Int)
+ case i: asm.tree.LabelNode => Label (labelIndex(x))
+ case i: asm.tree.FrameNode => FrameEntry (mapOver(lst(i.local)), mapOver(lst(i.stack)))
+ case i: asm.tree.LineNumberNode => LineNumber (i.line: Int, this(i.start): Label)
+ }
+ }
+}
94 src/partest/scala/tools/partest/BytecodeTest.scala
View
@@ -28,7 +28,7 @@ import java.io.InputStream
* See test/files/jvm/bytecode-test-example for an example of bytecode test.
*
*/
-abstract class BytecodeTest {
+abstract class BytecodeTest extends ASMConverters {
/** produce the output to be compared against a checkfile */
protected def show(): Unit
@@ -40,9 +40,38 @@ abstract class BytecodeTest {
val isa = instructions.fromMethod(methA)
val isb = instructions.fromMethod(methB)
if (isa == isb) println("bytecode identical")
- else (isa, isb).zipped.foreach { case (a, b) =>
- if (a == b) println("OK : "+ a)
- else println("DIFF: "+ a +" <=> "+ b)
+ else diffInstructions(isa, isb)
+ }
+
+ import instructions._
+ // bytecode is equal modulo local variable numbering
+ def equalsModuloVar(a: Instruction, b: Instruction) = (a, b) match {
+ case _ if a == b => true
+ case (VarOp(op1, _), VarOp(op2, _)) if op1 == op2 => true
+ case _ => false
+ }
+
+ def similarBytecode(methA: MethodNode, methB: MethodNode, similar: (Instruction, Instruction) => Boolean) = {
+ val isa = fromMethod(methA)
+ val isb = fromMethod(methB)
+ if (isa == isb) println("bytecode identical")
+ else if ((isa, isb).zipped.forall { case (a, b) => similar(a, b) }) 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 Stream.continually("")
+ val isbPadded = isb.map(_.toString) orElse Stream.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""")
+ }
}
}
@@ -70,61 +99,4 @@ abstract class BytecodeTest {
val containers = DefaultJavaContext.classesInExpandedPath(Defaults.javaUserClassPath)
new JavaClassPath(containers, DefaultJavaContext)
}
-
- // wrap ASM's instructions so we get case class-style `equals` and `toString`
- object instructions {
- def fromMethod(meth: MethodNode): List[instructions.Instruction] = {
- val insns = meth.instructions
- val asmToScala = new AsmToScala{ def labelIndex(l: asm.tree.AbstractInsnNode) = insns.indexOf(l) }
-
- asmToScala.mapOver(insns.iterator.asScala.toList).asInstanceOf[List[instructions.Instruction]]
- }
-
- sealed abstract class Instruction { def opcode: Int }
- case class Field (opcode: Int, desc: String, name: String, owner: String) extends Instruction
- case class Incr (opcode: Int, incr: Int, `var`: Int) extends Instruction
- case class Op (opcode: Int) extends Instruction
- case class IntOp (opcode: Int, operand: Int) extends Instruction
- case class Jump (opcode: Int, label: Label) extends Instruction
- case class Ldc (opcode: Int, cst: Any) extends Instruction
- case class LookupSwitch (opcode: Int, dflt: Label, keys: List[Integer], labels: List[Label]) extends Instruction
- case class TableSwitch (opcode: Int, dflt: Label, max: Int, min: Int, labels: List[Label]) extends Instruction
- case class Method (opcode: Int, desc: String, name: String, owner: String) extends Instruction
- case class NewArray (opcode: Int, desc: String, dims: Int) extends Instruction
- case class TypeOp (opcode: Int, desc: String) extends Instruction
- case class VarOp (opcode: Int, `var`: Int) extends Instruction
- case class Label (opcode: Int, offset: Int) extends Instruction
- case class FrameEntry (opcode: Int, local: List[Any], stack: List[Any]) extends Instruction
- case class LineNumber (opcode: Int, line: Int, start: Label) extends Instruction
- }
-
- abstract class AsmToScala {
- import instructions._
- def labelIndex(l: asm.tree.AbstractInsnNode): Int
-
- def mapOver(is: List[Any]): List[Any] = is map {
- case i: asm.tree.AbstractInsnNode => apply(i)
- case x => x
- }
-
- def lst[T](xs: java.util.List[T]): List[T] = if (xs == null) Nil else xs.asScala.toList
- def apply(l: asm.tree.LabelNode): Label = this(l: asm.tree.AbstractInsnNode).asInstanceOf[Label]
- def apply(x: asm.tree.AbstractInsnNode): Instruction = x match {
- case i: asm.tree.FieldInsnNode => Field (i.getOpcode: Int, i.desc: String, i.name: String, i.owner: String)
- case i: asm.tree.IincInsnNode => Incr (i.getOpcode: Int, i.incr: Int, i.`var`: Int)
- case i: asm.tree.InsnNode => Op (i.getOpcode: Int)
- case i: asm.tree.IntInsnNode => IntOp (i.getOpcode: Int, i.operand: Int)
- case i: asm.tree.JumpInsnNode => Jump (i.getOpcode: Int, this(i.label))
- case i: asm.tree.LdcInsnNode => Ldc (i.getOpcode: Int, i.cst: Any)
- case i: asm.tree.LookupSwitchInsnNode => LookupSwitch (i.getOpcode: Int, this(i.dflt), lst(i.keys), mapOver(lst(i.labels)).asInstanceOf[List[Label]])
- case i: asm.tree.TableSwitchInsnNode => TableSwitch (i.getOpcode: Int, this(i.dflt), i.max: Int, i.min: Int, mapOver(lst(i.labels)).asInstanceOf[List[Label]])
- case i: asm.tree.MethodInsnNode => Method (i.getOpcode: Int, i.desc: String, i.name: String, i.owner: String)
- case i: asm.tree.MultiANewArrayInsnNode => NewArray (i.getOpcode: Int, i.desc: String, i.dims: Int)
- case i: asm.tree.TypeInsnNode => TypeOp (i.getOpcode: Int, i.desc: String)
- case i: asm.tree.VarInsnNode => VarOp (i.getOpcode: Int, i.`var`: Int)
- case i: asm.tree.LabelNode => Label (i.getOpcode: Int, labelIndex(x))
- case i: asm.tree.FrameNode => FrameEntry (i.getOpcode: Int, mapOver(lst(i.local)), mapOver(lst(i.stack)))
- case i: asm.tree.LineNumberNode => LineNumber (i.getOpcode: Int, i.line: Int, this(i.start): Label)
- }
- }
}
Please sign in to comment.
Something went wrong with that request. Please try again.