/**
* Copyright (C) 2010-2017 Gordon Fraser, Andrea Arcuri and EvoSuite
* contributors
*
* This file is part of EvoSuite.
*
* EvoSuite is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3.0 of the License, or
* (at your option) any later version.
*
* EvoSuite is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
*/
package org.evosuite.junit;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.evosuite.testcase.ExecutionTracer;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.IincInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.objectweb.asm.util.CheckClassAdapter;
/**
* This class executes an existing test case in binary form to determine its
* runtime values. To do that, the test case (NOT the SUT) is instrumented.
*
* @author roessler
*/
public class TestRuntimeValuesDeterminer extends RunListener {
public static class CursorableTrace {
private final List<ExecutedLine> trace = new ArrayList<ExecutedLine>();
private int idx = 0;
private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(CursorableTrace.class);
public void advanceLoop() {
int lastIdx = idx;
ExecutedLine lastLine = trace.get(idx);
idx++;
ExecutedLine nextLine = trace.get(idx);
while (nextLine.getLineNumber() > lastLine.getLineNumber()) {
lastLine = nextLine;
idx++;
nextLine = trace.get(idx);
}
logger.debug("Advancing current line in trace for advanceLoop from {} to {}.", trace.get(lastIdx)
.getLineNumber(), trace.get(idx).getLineNumber());
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CursorableTrace other = (CursorableTrace) obj;
return trace.equals(other.trace);
}
public void executedLine(Integer lineNumber, Map<String, Object> variableValues) {
trace.add(new ExecutedLine(lineNumber, variableValues));
}
public Object getVariableValueAfter(Integer lineNumber, String variable) {
int lastIdx = idx;
ExecutedLine executedLine = trace.get(idx);
while (!executedLine.getLineNumber().equals(lineNumber)) {
idx++;
executedLine = trace.get(idx);
}
if (lastIdx != idx) {
logger.debug("Advancing current line in trace for getVariableValuesAfter from {} to {}.",
trace.get(lastIdx).getLineNumber(), trace.get(idx).getLineNumber());
}
logger.debug("Getting variable value for line {} and variable {}.", lineNumber, variable);
return executedLine.getVariableValues().get(variable);
}
@Override
public int hashCode() {
return trace.hashCode();
}
@Override
public String toString() {
return "CursorableTrace[" + idx + "=" + trace.get(idx) + " from " + trace.size() + "]";
}
protected void updateLastLineValues(String variable, Object value) {
ExecutedLine executedLine = trace.get(trace.size() - 1);
executedLine.getVariableValues().put(variable, value);
}
}
public static class RuntimeValue {
private final int lineNumber;
private final Object value;
public RuntimeValue(int lineNumber, Object value) {
this.lineNumber = lineNumber;
this.value = value;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
RuntimeValue other = (RuntimeValue) obj;
if (lineNumber != other.lineNumber) {
return false;
}
if (value == null) {
if (other.value != null) {
return false;
}
} else if (!value.equals(other.value)) {
return false;
}
return true;
}
public int getLineNumber() {
return lineNumber;
}
public Object getValue() {
return value;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + lineNumber;
result = prime * result + ((value == null) ? 0 : value.hashCode());
return result;
}
@Override
public String toString() {
return "RuntimeValue [lineNumber=" + lineNumber + ", value=" + value + "]";
}
}
private static class ExecutedLine {
private final Integer lineNumber;
private final Map<String, Object> variableValues;
public ExecutedLine(Integer lineNumber, Map<String, Object> variableValues) {
assert lineNumber != null;
this.lineNumber = lineNumber;
assert variableValues != null;
this.variableValues = new HashMap<String, Object>(variableValues);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ExecutedLine other = (ExecutedLine) obj;
if (!lineNumber.equals(other.lineNumber)) {
return false;
}
if (!variableValues.equals(other.variableValues)) {
return false;
}
return true;
}
public Integer getLineNumber() {
return lineNumber;
}
public Map<String, Object> getVariableValues() {
return variableValues;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + lineNumber.hashCode();
result = prime * result + variableValues.hashCode();
return result;
}
@Override
public String toString() {
return "[" + lineNumber + "=" + variableValues + "]";
}
}
private static class TestValuesDeterminerClassVisitor extends ClassVisitor {
private final String fullyQualifiedTargetClass;
public TestValuesDeterminerClassVisitor(String fullyQualifiedTargetClass, ClassWriter cw) {
super(Opcodes.ASM4, cw);
this.fullyQualifiedTargetClass = fullyQualifiedTargetClass;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodNode methodNode = new MethodNode(access, name, desc, signature, exceptions);
MethodVisitor next = super.visitMethod(access, name, desc, signature, exceptions);
return new TestValuesDeterminerMethodVisitor(fullyQualifiedTargetClass, methodNode, next);
}
}
private static class TestValuesDeterminerMethodVisitor extends MethodVisitor {
private final org.slf4j.Logger logger = org.slf4j.LoggerFactory
.getLogger(TestRuntimeValuesDeterminer.TestValuesDeterminerClassVisitor.class);
private final String fullyQualifiedTargetClass;
private int currentLine;
private MethodVisitor next;
public TestValuesDeterminerMethodVisitor(String fullyQualifiedTargetClass, MethodNode methodNode,
MethodVisitor next) {
super(Opcodes.ASM4, methodNode);
this.fullyQualifiedTargetClass = fullyQualifiedTargetClass;
this.next = next;
}
@SuppressWarnings("unchecked")
@Override
public void visitEnd() {
MethodNode methodNode = (MethodNode) mv;
Iterator<AbstractInsnNode> nodeIter = methodNode.instructions.iterator();
while (nodeIter.hasNext()) {
AbstractInsnNode insnNode = nodeIter.next();
if (insnNode.getType() == AbstractInsnNode.LINE) {
currentLine = ((LineNumberNode) insnNode).line;
methodNode.instructions.insertBefore(insnNode, getLineNumberInstrumentation());
continue;
}
if ((insnNode.getType() == AbstractInsnNode.VAR_INSN)
|| (insnNode.getType() == AbstractInsnNode.FIELD_INSN)
|| (insnNode.getType() == AbstractInsnNode.IINC_INSN)
|| (insnNode.getType() == AbstractInsnNode.INT_INSN)
|| (insnNode.getType() == AbstractInsnNode.INSN)) {
methodNode.instructions.insert(insnNode, getInstrumentation(insnNode));
}
}
methodNode.accept(next);
}
private int getInstructionIndex(AbstractInsnNode insnNode) {
try {
Field indexField = AbstractInsnNode.class.getDeclaredField("index");
indexField.setAccessible(true);
Object indexValue = indexField.get(insnNode);
return ((Integer) indexValue).intValue();
} catch (Exception exc) {
throw new RuntimeException(exc);
}
}
private InsnList getInstrumentation(AbstractInsnNode insnNode) {
switch (insnNode.getOpcode()) {
case Opcodes.ISTORE:
return localVarValue(insnNode, Opcodes.ILOAD, "I");
case Opcodes.LSTORE:
return localVarValue(insnNode, Opcodes.LLOAD, "J");
case Opcodes.FSTORE:
return localVarValue(insnNode, Opcodes.FLOAD, "F");
case Opcodes.DSTORE:
return localVarValue(insnNode, Opcodes.DLOAD, "F");
case Opcodes.ASTORE:
return localVarValue(insnNode, Opcodes.ALOAD, "Ljava/lang/Object;");
case Opcodes.IINC:
return localVarValue(insnNode, Opcodes.ILOAD, "I");
case Opcodes.IASTORE:
case Opcodes.LASTORE: // -
case Opcodes.FASTORE: // -
case Opcodes.DASTORE: // -
case Opcodes.AASTORE: // -
case Opcodes.BASTORE: // -
case Opcodes.CASTORE: // -
case Opcodes.SASTORE:
// throw new RuntimeException("Not implemented!");
logger.error("XASTORE not implemented!");
return new InsnList();
case Opcodes.PUTSTATIC:
// throw new RuntimeException("Not implemented!");
logger.error("PUTSTATIC not implemented!);");
return new InsnList();
case Opcodes.PUTFIELD: // -
if (insnNode instanceof FieldInsnNode) {
InsnList instrumentation = new InsnList();
logger.error("FieldInsnNode not implemented!");
// FieldInsnNode fieldInsnNode = (FieldInsnNode) insnNode;
// instrumentation.add(new InsnNode(Opcodes.DUP));
// instrumentation.add(new FieldInsnNode(Opcodes.GETFIELD,
// fieldInsnNode.owner, fieldInsnNode.name,
// fieldInsnNode.desc));
// instrumentation.add(new
// LdcInsnNode(fieldInsnNode.owner));
// instrumentation.add(new LdcInsnNode(fieldInsnNode.name));
// instrumentation.add(new LdcInsnNode(currentLine));
// instrumentation.add(new
// MethodInsnNode(Opcodes.INVOKESTATIC,
// "org/evosuite/junit/TestRuntimeValuesDeterminer",
// "fieldValueChanged",
// "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;I)V"));
// logger.debug("Adding fieldValueChanged for field {}#{} in line {}.",
// new Object[] {
// fieldInsnNode.owner, fieldInsnNode.name, currentLine });
return instrumentation;
}
throw new RuntimeException("Not implemented!");
}
return new InsnList();
}
private InsnList getLineNumberInstrumentation() {
InsnList instrumentation = new InsnList();
instrumentation.add(new LdcInsnNode(currentLine));
instrumentation.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
"org/evosuite/junit/TestRuntimeValuesDeterminer", "execLine", "(I)V"));
return instrumentation;
}
private LocalVariableNode getLocalVariableNode(int varIdx, AbstractInsnNode insnNode, MethodNode methodNode) {
int instrIdx = getInstructionIndex(insnNode);
List<?> localVariables = methodNode.localVariables;
for (int idx = 0; idx < localVariables.size(); idx++) {
LocalVariableNode localVariableNode = (LocalVariableNode) localVariables.get(idx);
if (localVariableNode.index == varIdx) {
int scopeEndInstrIdx = getInstructionIndex(localVariableNode.end);
if (scopeEndInstrIdx >= instrIdx) {
// still valid for current line
return localVariableNode;
}
}
}
throw new RuntimeException("Variable with index " + varIdx + " and end >= " + currentLine
+ " not found for method " + fullyQualifiedTargetClass + "#" + methodNode.name + "!");
}
private InsnList localVarValue(AbstractInsnNode insnNode, int opositeOpcode, String param) {
int varIdx = -1;
if (insnNode instanceof VarInsnNode) {
varIdx = ((VarInsnNode) insnNode).var;
} else if (insnNode instanceof IincInsnNode) {
varIdx = ((IincInsnNode) insnNode).var;
} else {
throw new RuntimeException("Not implemented for type " + insnNode.getClass());
}
InsnList instrumentation = new InsnList();
MethodNode methodNode = (MethodNode) mv;
if (methodNode.localVariables.size() <= varIdx) {
throw new RuntimeException("varInsnNode is pointing outside of local variables!");
}
LocalVariableNode localVariableNode = getLocalVariableNode(varIdx, insnNode, methodNode);
instrumentation.add(new VarInsnNode(opositeOpcode, varIdx));
instrumentation.add(new LdcInsnNode(localVariableNode.name));
instrumentation.add(new LdcInsnNode(currentLine));
instrumentation.add(new MethodInsnNode(Opcodes.INVOKESTATIC,
"org/evosuite/junit/TestRuntimeValuesDeterminer", "localVarValueChanged", "(" + param
+ "Ljava/lang/String;I)V"));
logger.debug("Adding localVarValueChanged for var {} in line {}.", localVariableNode.name, currentLine);
return instrumentation;
}
}
private static class TransformingClassLoader extends ClassLoader {
private final String testClass;
public TransformingClassLoader(String testClass) {
assert testClass != null;
this.testClass = testClass;
}
@Override
public Class<?> loadClass(String fullyQualifiedTargetClass) throws ClassNotFoundException {
if (!testClass.equals(fullyQualifiedTargetClass)) {
return super.loadClass(fullyQualifiedTargetClass);
}
String className = fullyQualifiedTargetClass.replace('.', '/');
InputStream is = java.lang.ClassLoader.getSystemResourceAsStream(className + ".class");
if (is == null) {
throw new ClassNotFoundException("Class " + fullyQualifiedTargetClass + "could not be found!");
}
ClassReader reader = null;
try {
reader = new ClassReader(is);
} catch (IOException exc) {
throw new ClassNotFoundException();
}
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new TestValuesDeterminerClassVisitor(fullyQualifiedTargetClass, writer);
CheckClassAdapter checkClassAdapter = new CheckClassAdapter(cv);
reader.accept(checkClassAdapter, ClassReader.SKIP_FRAMES);
byte[] byteBuffer = writer.toByteArray();
Class<?> result = defineClass(fullyQualifiedTargetClass, byteBuffer, 0, byteBuffer.length);
return result;
}
}
/**
* <p>
* execLine
* </p>
*
* @param lineNumber
* a int.
*/
public static void execLine(int lineNumber) {
instance.lineExecuted(lineNumber);
}
/**
* <p>
* fieldValueChanged
* </p>
*
* @param newValue
* a {@link java.lang.Object} object.
* @param owner
* a {@link java.lang.String} object.
* @param fieldName
* a {@link java.lang.String} object.
* @param lineNumber
* a int.
*/
public static void fieldValueChanged(Object newValue, String owner, String fieldName, int lineNumber) {
System.out.println("FieldValue " + owner + "#" + fieldName + " changed in line " + lineNumber + " to value: "
+ newValue);
}
/**
* <p>
* localVarValueChanged
* </p>
*
* @param newValue
* a double.
* @param localVar
* a {@link java.lang.String} object.
* @param lineNumber
* a int.
*/
public static void localVarValueChanged(double newValue, String localVar, int lineNumber) {
instance.localVarValueChanged(localVar, lineNumber, newValue);
}
/**
* <p>
* localVarValueChanged
* </p>
*
* @param newValue
* a float.
* @param localVar
* a {@link java.lang.String} object.
* @param lineNumber
* a int.
*/
public static void localVarValueChanged(float newValue, String localVar, int lineNumber) {
instance.localVarValueChanged(localVar, lineNumber, newValue);
}
/**
* <p>
* localVarValueChanged
* </p>
*
* @param newValue
* a int.
* @param localVar
* a {@link java.lang.String} object.
* @param lineNumber
* a int.
*/
public static void localVarValueChanged(int newValue, String localVar, int lineNumber) {
instance.localVarValueChanged(localVar, lineNumber, newValue);
}
/**
* <p>
* localVarValueChanged
* </p>
*
* @param newValue
* a long.
* @param localVar
* a {@link java.lang.String} object.
* @param lineNumber
* a int.
*/
public static void localVarValueChanged(long newValue, String localVar, int lineNumber) {
instance.localVarValueChanged(localVar, lineNumber, newValue);
}
/**
* <p>
* localVarValueChanged
* </p>
*
* @param newValue
* a {@link java.lang.Object} object.
* @param localVar
* a {@link java.lang.String} object.
* @param lineNumber
* a int.
*/
public static void localVarValueChanged(Object newValue, String localVar, int lineNumber) {
instance.localVarValueChanged(localVar, lineNumber, newValue);
}
private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(TestRuntimeValuesDeterminer.class);
private static TestRuntimeValuesDeterminer instance;
private static Object lock = new Object();
/**
* <p>
* Getter for the field <code>instance</code>.
* </p>
*
* @param testClass
* a {@link java.lang.String} object.
* @return a {@link org.evosuite.junit.TestRuntimeValuesDeterminer} object.
*/
public static TestRuntimeValuesDeterminer getInstance(String testClass) {
synchronized (lock) {
if ((instance == null) || !instance.testClass.equals(testClass)) {
instance = new TestRuntimeValuesDeterminer(testClass);
}
return instance;
}
}
private final Map<Integer, Integer> lineExecCnts = new HashMap<Integer, Integer>();
private final Map<String, CursorableTrace> methodTraces = new HashMap<String, CursorableTrace>();
private final Map<String, Map<String, List<RuntimeValue>>> methodVariables = new HashMap<String, Map<String, List<RuntimeValue>>>();
private final String testClass;
private String currentTest;
private CursorableTrace currentTrace;
private Map<String, Object> variableValues;
private TestRuntimeValuesDeterminer(String testClass) {
this.testClass = testClass;
}
/**
* <p>
* determineRuntimeValues
* </p>
*/
public void determineRuntimeValues() {
try {
synchronized (lock) {
Class<?> testClass = instrumentTest();
// testClass.getConstructors().length
boolean enabled = ExecutionTracer.isEnabled();
ExecutionTracer.disable();
JUnitCore jUnitCore = new JUnitCore();
jUnitCore.addListener(this);
Result result = jUnitCore.run(testClass);
currentTest = null;
logger.info("Ran {} tests to determine runtime values.", result.getRunCount());
for (Failure failure : result.getFailures()) {
if (failure.getDescription().getDisplayName().startsWith("initializationError")) {
failure.getException().printStackTrace();
throw new RuntimeException(failure.getException());
}
}
if (enabled) {
ExecutionTracer.enable();
}
}
} catch (RuntimeException exc) {
if (exc.getCause() instanceof ClassNotFoundException) {
logger.error("Unable to load class. Will continue without execution.");
}
}
}
/**
* <p>
* getExecutionCount
* </p>
*
* @param lineNumber
* a int.
* @return a int.
*/
public int getExecutionCount(int lineNumber) {
Integer result = lineExecCnts.get(lineNumber);
if (result == null) {
return 0;
}
return result;
}
/**
* <p>
* getMethodTrace
* </p>
*
* @param method
* a {@link java.lang.String} object.
* @return a
* {@link org.evosuite.junit.TestRuntimeValuesDeterminer.CursorableTrace}
* object.
*/
public CursorableTrace getMethodTrace(String method) {
return methodTraces.get(method);
}
/** {@inheritDoc} */
@Override
public void testStarted(Description description) throws Exception {
if (!description.getClassName().equals(testClass)) {
throw new RuntimeException("Wrong test executed. Should be " + testClass + " but was "
+ description.getClassName());
}
currentTest = description.getMethodName();
currentTrace = new CursorableTrace();
methodTraces.put(currentTest, currentTrace);
variableValues = new HashMap<String, Object>();
}
private Class<?> instrumentTest() {
logger.info("Instrumenting class '{}'.", testClass);
TransformingClassLoader classLoader = new TransformingClassLoader(testClass);
try {
return classLoader.loadClass(testClass);
} catch (ClassNotFoundException exc) {
throw new RuntimeException(exc);
}
}
private void lineExecuted(Integer lineNumber) {
Integer execCnt = lineExecCnts.get(lineNumber);
if (execCnt == null) {
execCnt = 0;
}
execCnt++;
lineExecCnts.put(lineNumber, execCnt);
if (currentTrace != null) {
currentTrace.executedLine(lineNumber, variableValues);
}
}
private void localVarValueChanged(String localVar, int lineNumber, Object newValue) {
Map<String, List<RuntimeValue>> variableValues = methodVariables.get(currentTest);
if (variableValues == null) {
variableValues = new HashMap<String, List<RuntimeValue>>();
methodVariables.put(currentTest, variableValues);
}
List<RuntimeValue> values = variableValues.get(localVar);
if (values == null) {
values = new ArrayList<RuntimeValue>();
variableValues.put(localVar, values);
}
values.add(new RuntimeValue(lineNumber, newValue));
this.variableValues.put(localVar, newValue);
currentTrace.updateLastLineValues(localVar, newValue);
}
}