package hu.advancedweb.scott.instrumentation.transformation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* Instruments test methods to call Scott Runtime to track variable states.
*
* @author David Csakvari
*/
public class StateEmitterTestMethodVisitor extends MethodVisitor {
private int lineNumber;
private int lineNumberForMethodCallTrack;
private Set<Integer> localVariables = new HashSet<>();
private List<LocalVariableScope> localVariableScopes = new ArrayList<>();
private Set<AccessedField> accessedFields;
private String methodName;
private String className;
private boolean clearTrackedDataAtStart;
public StateEmitterTestMethodVisitor(MethodVisitor mv, String className, String methodName, boolean clearTrackedDataAtStart) {
super(Opcodes.ASM5, mv);
Logger.log("Visiting: " + className + "." + methodName);
this.className = className;
this.methodName = methodName;
this.clearTrackedDataAtStart = clearTrackedDataAtStart;
}
@Override
public void visitCode() {
super.visitCode();
// clear previously tracked data
if (clearTrackedDataAtStart) {
instrumentToClearTrackedDataAndSignalStartOfRecording();
}
// track initial field states
for (AccessedField accessedField : accessedFields) {
instrumentToTrackFieldState(accessedField, lineNumber);
}
// track method arguments
for (LocalVariableScope localVariableScope : localVariableScopes) {
if (localVariableScope.start == 0) {
instrumentToTrackVariableName(localVariableScope, lineNumber);
instrumentToTrackVariableState(localVariableScope, lineNumber);
}
}
}
@Override
public void visitLineNumber(int lineNumber, Label label) {
this.lineNumberForMethodCallTrack = this.lineNumber;
this.lineNumber = lineNumber;
super.visitLineNumber(lineNumber, label);
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
localVariables.clear();
return super.visitAnnotation(desc, visible);
}
@Override
public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
/*
* Track where lambda expressions are defined.
*/
if ("java/lang/invoke/LambdaMetafactory".equals(bsm.getOwner())) {
if (bsmArgs[1] instanceof Handle) {
Handle handle = (Handle)bsmArgs[1];
String methodName = handle.getName();
if (methodName.startsWith("lambda$")) {
instrumentToTrackMethodStart(handle.getName(), lineNumber);
}
}
}
super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
if (this.lineNumberForMethodCallTrack == 0) {
this.lineNumberForMethodCallTrack = this.lineNumber;
}
if (!owner.startsWith("org/mockito")) {
// track every variable state after method calls
for (LocalVariableScope localVariableScope : localVariableScopes) {
if (!localVariables.contains(localVariableScope.var)) continue;
if (isVariableInScope(localVariableScope.var)) {
instrumentToTrackVariableState(localVariableScope, lineNumberForMethodCallTrack);
}
}
// track every field state after method calls
for (AccessedField accessedField : accessedFields) {
instrumentToTrackFieldState(accessedField, lineNumberForMethodCallTrack);
}
}
this.lineNumberForMethodCallTrack = this.lineNumber;
/*
* Visit the method instruction after placing the tracking code
* because otherwise we might confuse Mockito, see Issue #25.
* Because of this, the tracking code has to book the values to the previous line
* for the first method call for every line.
*/
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
@Override
public void visitVarInsn(int opcode, int var) {
super.visitVarInsn(opcode, var);
// Track variable state and name at variable stores. (Typical variable assignments.)
if (VariableType.isStoreOperation(opcode)) {
localVariables.add(var);
LocalVariableScope lvs = getLocalVariableScope(var);
if (lvs != null) {
/*
* This null-check is the workaround for issue #15:
* If a variable declaration is the last statement in a code block,
* then the variable name is not present in the compiled bytecode.
* With this workaround Scott can still track the assigned value to such variables.
*/
instrumentToTrackVariableName(lvs, lineNumber);
instrumentToTrackVariableState(lvs, lineNumber);
}
}
}
@Override
public void visitIincInsn(int var, int increment) {
super.visitIincInsn(var, increment);
// Track variable state at variable increases (e.g. i++).
LocalVariableScope lvs = getLocalVariableScope(var);
instrumentToTrackVariableState(lvs, lineNumber);
}
private void instrumentToClearTrackedDataAndSignalStartOfRecording() {
Logger.log(" - instrumentToClearTrackedDataAndSignalStartOfRecording");
super.visitLdcInsn(className);
super.visitLdcInsn(methodName);
super.visitMethodInsn(Opcodes.INVOKESTATIC, "hu/advancedweb/scott/runtime/track/StateRegistry", "startTracking", "(Ljava/lang/String;Ljava/lang/String;)V", false);
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
super.visitFieldInsn(opcode, owner, name, desc);
if (Opcodes.PUTFIELD == opcode|| Opcodes.PUTSTATIC == opcode) {
for (AccessedField accessedField : accessedFields) {
if (accessedField.name.equals(name)) {
instrumentToTrackFieldState(accessedField, lineNumber);
break;
}
}
}
}
private void instrumentToTrackMethodStart(String methodName, int lineNumber) {
Logger.log(" - instrumentToTrackMethodStart of " + methodName + " at " + lineNumber);
super.visitLdcInsn(lineNumber);
super.visitLdcInsn(methodName);
super.visitMethodInsn(Opcodes.INVOKESTATIC, "hu/advancedweb/scott/runtime/track/StateRegistry", "trackMethodStart", "(ILjava/lang/String;)V", false);
}
private void instrumentToTrackVariableState(LocalVariableScope localVariableScope, int lineNumber) {
Logger.log(" - instrumentToTrackVariableState of variable at " + getLineNumberBoundedByScope(lineNumber, localVariableScope) + ": " + localVariableScope);
super.visitVarInsn(localVariableScope.variableType.loadOpcode, localVariableScope.var);
super.visitLdcInsn(getLineNumberBoundedByScope(lineNumber, localVariableScope));
super.visitLdcInsn(localVariableScope.var);
super.visitLdcInsn(methodName);
super.visitMethodInsn(Opcodes.INVOKESTATIC, "hu/advancedweb/scott/runtime/track/StateRegistry", "trackLocalVariableState", "(" + localVariableScope.variableType.desc + "IILjava/lang/String;)V", false);
}
private int getLineNumberBoundedByScope(int lineNumber, LocalVariableScope localVariableScope) {
return Math.min(localVariableScope.end, Math.max(lineNumber, localVariableScope.start));
}
private void instrumentToTrackFieldState(AccessedField accessedField, int lineNumber) {
Logger.log(" - instrumentToTrackFieldState at " + lineNumber + ": " + accessedField);
final int opcode;
if (accessedField.isStatic) {
opcode = Opcodes.GETSTATIC;
} else {
opcode = Opcodes.GETFIELD;
super.visitVarInsn(Opcodes.ALOAD, 0);
}
String desc = accessedField.desc;
if (desc.startsWith("L") || desc.startsWith("[")) {
desc = VariableType.REFERENCE.desc;
}
super.visitFieldInsn(opcode, accessedField.owner, accessedField.name, accessedField.desc);
super.visitLdcInsn(accessedField.name);
super.visitLdcInsn(lineNumber);
super.visitLdcInsn(accessedField.isStatic);
super.visitLdcInsn(accessedField.owner);
super.visitMethodInsn(Opcodes.INVOKESTATIC, "hu/advancedweb/scott/runtime/track/StateRegistry", "trackFieldState", "(" + desc + "Ljava/lang/String;IZLjava/lang/String;)V", false);
}
private void instrumentToTrackVariableName(LocalVariableScope localVariableScope, int lineNumber) {
Logger.log(" - instrumentToTrackVariableName at " + getLineNumberBoundedByScope(lineNumber, localVariableScope) + ": " + localVariableScope);
super.visitLdcInsn(localVariableScope.name);
super.visitLdcInsn(getLineNumberBoundedByScope(lineNumber, localVariableScope));
super.visitLdcInsn(localVariableScope.var);
super.visitLdcInsn(methodName);
super.visitMethodInsn(Opcodes.INVOKESTATIC, "hu/advancedweb/scott/runtime/track/StateRegistry", "trackVariableName", "(Ljava/lang/String;IILjava/lang/String;)V", false);
}
private boolean isVariableInScope(int var) {
return getLocalVariableScope(var) != null;
}
private LocalVariableScope getLocalVariableScope(int var) {
// check the scopes in reverse order in case of multiple var declarations on the same line
List<LocalVariableScope> localVariableScopesReversed = new ArrayList<>(localVariableScopes);
Collections.reverse(localVariableScopesReversed);
for (LocalVariableScope localVariableScope : localVariableScopes) {
if (localVariableScope.var == var &&
localVariableScope.start <= lineNumber &&
localVariableScope.end >= lineNumber) {
return localVariableScope;
}
}
return null;
}
public void setLocalVariableScopes(List<LocalVariableScope> localVariableScopes) {
this.localVariableScopes = localVariableScopes;
}
public void setAccessedFields(Set<AccessedField> accessedFields) {
this.accessedFields = accessedFields;
}
}