Skip to content
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...
1 parent a07555f commit 415becdab8aae63470deac5a6e122fa7a697cbe0 @adriaanm adriaanm committed Jan 31, 2013
View
71 src/partest/scala/tools/partest/ASMConverters.scala
@@ -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)
+ }
+ }
+}
View
94 src/partest/scala/tools/partest/BytecodeTest.scala
@@ -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)
- }
- }
}

0 comments on commit 415becd

Please sign in to comment.
Something went wrong with that request. Please try again.