blob: 7a1522e646c3f0f9d6c84e6d0126072af6acbd75 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.codehaus.groovy.classgen.asm
import groovy.test.GroovyTestCase
import org.apache.groovy.io.StringBuilderWriter
import org.codehaus.groovy.control.CompilationUnit
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.Phases
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.util.TraceClassVisitor
import java.security.CodeSource
/**
* Abstract test case to extend to check the instructions we generate in the bytecode of groovy programs.
*/
abstract class AbstractBytecodeTestCase extends GroovyTestCase {
Class clazz
Map extractionOptions
InstructionSequence sequence
@Override
protected void setUp() {
super.setUp()
extractionOptions = [method: 'run']
}
@Override
protected void assertScript(final String script) throws Exception {
CompilationUnit unit = null
GroovyShell shell = new GroovyShell(new GroovyClassLoader() {
@Override
protected CompilationUnit createCompilationUnit(final CompilerConfiguration config, final CodeSource source) {
unit = super.createCompilationUnit(config, source)
}
})
try {
shell.evaluate(script, testClassName)
} finally {
if (unit != null) {
try {
sequence = extractSequence(unit.classes[0].bytes, extractionOptions)
if (extractionOptions.print) println(sequence)
} catch (e) {
// probably an error in the script
}
}
}
}
/**
* Compiles a script into bytecode and returns the instructions for a class.
*
* @param scriptText the script to compile
* @return the decompiled <code>InstructionSequence</code>
*/
InstructionSequence compile(Map options = [:], final String scriptText) {
options = [method: 'run', classNamePattern: '.*script', *: options]
sequence = null
clazz = null
def cu = new CompilationUnit()
def su = cu.addSource('script', scriptText)
cu.compile(Phases.CONVERSION)
if (options.conversionAction != null) {
options.conversionAction(su)
}
cu.compile(Phases.CLASS_GENERATION)
for (gc in cu.classes) {
if (gc.name ==~ options.classNamePattern) {
sequence = extractSequence(gc.bytes, options)
}
}
if (sequence == null && cu.classes) {
sequence = extractSequence(cu.classes[0].bytes, options)
}
for (gc in cu.classes) {
try {
Class c = cu.classLoader.defineClass(gc.name, gc.bytes)
if (Script.isAssignableFrom(c)) { clazz = c }
c.isInterface() // trigger verification
} catch (VerifyError e) {
throw e
} catch (Throwable t) {
t.printStackTrace()
System.err.println(sequence)
}
}
sequence
}
InstructionSequence extractSequence(final byte[] bytes, final Map options = [method: 'run']) {
def out = new StringBuilderWriter()
def tcv
tcv = new TraceClassVisitor(new ClassVisitor(CompilerConfiguration.ASM_API_VERSION) {
@Override
MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String... exceptions) {
if (options.method == name) {
// last in "tcv.p.text" is a list that will be filled by "super.visit"
tcv.p.text.add(tcv.p.text.size() - 2, '--BEGIN--\n')
try {
super.visitMethod(access, name, desc, signature, exceptions)
} finally {
tcv.p.text.add('--END--\n')
}
}
}
@Override
FieldVisitor visitField(final int access, final String name, final String desc, final String signature, final Object value) {
if (options.field == name) {
// last in "tcv.p.text" is a list that will be filled by "super.visit"
tcv.p.text.add(tcv.p.text.size() - 2, '--BEGIN--\n')
try {
super.visitField(access, name, desc, signature, value)
} finally {
tcv.p.text.add('--END--\n')
}
}
}
}, new PrintWriter(out))
new ClassReader(bytes).accept(tcv, 0)
new InstructionSequence(instructions: out.toString().split('\n')*.trim())
}
}
/**
* A sequence of instruction with matching and strict matching capabilities
* to find subsequences of bytecode instructions.
*/
class InstructionSequence {
List<String> instructions
/**
* Find a sub-sequence of instructions of the list of instructions.
*
* @param pattern the list of instructions to find in the bytecode
* @param offset at which to find the sub-sequence or remaining sub-sequence (start at offset 0)
* @param strict whether the search should be strict with contiguous instructions (false by default)
* @return true if a match is found
*/
boolean hasSequence(final List<String> pattern, final int offset = 0, final boolean strict = false) {
if (pattern.isEmpty()) return true
def idx = offset
while (true) {
idx = indexOf(pattern[0], idx)
if (idx == -1) break
// not the first call with offset 0 and check that the next instruction match
// is the exact following instruction in the pattern and in the bytecode instructions
if (strict && offset > 0 && idx != offset) return false
if (hasSequence(pattern.tail(), idx + 1, strict)) return true
idx += 1
}
return false
}
/**
* Find a strict sub-sequence of instructions of the list of instructions.
*
* @param pattern the list of instructions to find in the bytecode
* @param offset at which to find the sub-sequence or remaining sub-sequence (start at offset 0)
* @param strict whether the search should be strict with contiguous instructions (true by default)
* @return true if a match is found
*/
boolean hasStrictSequence(final List<String> pattern, final int offset = 0) {
hasSequence(pattern, offset, true)
}
/**
* Finds the index of a single instruction in a list of instructions.
*
* @param singleInst single instruction to find
* @param offset the offset from which to start the search
* @return the index of that single instruction if found, -1 otherwise
*/
private int indexOf(final String singleInst, final int offset = 0) {
for (i in offset..<instructions.size()) {
if (instructions[i].startsWith(singleInst)) {
return i
}
}
return -1
}
String toSequence() {
def sb = new StringBuilder()
for (insn in instructions) {
sb << "'$insn'," << '\n'
}
sb.toString()
}
String toString() {
instructions.join('\n')
}
}