Skip to content

Commit

Permalink
Fix JaCoCo instrumented constructor skipped.
Browse files Browse the repository at this point in the history
Detect JaCoCo instrumentation from the original constructor by checking if the beginning one or two instructions are from JaCoCo. Simply by instructions' type and content.

An then strip JaCoCo constructor probes data initialization instructions from `callSuper` instructions for JaCoCo instrumentation was detected. And insert these instructions into the beginning of the original construction.

Overall, we assume that the JaCoCo instrumentation detecting and JaCoCo initialization instructions are in general cases and obey the JaCoCo implementations.

Note that there can be many instructions before the call super() or this(), so this can still break the instruction compatibility?

Tested by: JaCoCo offline integration tests.

Signed-off-by: ZSmallX <diosmallx@gmail.com>
  • Loading branch information
ZSmallX committed Mar 19, 2023
1 parent 1349544 commit 943b71e
Showing 1 changed file with 56 additions and 42 deletions.
Expand Up @@ -36,7 +36,6 @@
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.objectweb.asm.tree.VarInsnNode;
import org.robolectric.util.PerfStatsCollector;

/**
Expand Down Expand Up @@ -212,12 +211,13 @@ private void addSetToSparseArray(MutableClass mutableClass) {
}

/**
* Checks if the first or second instruction is a Jacoco load instruction. Robolectric is not
* capable at the moment of re-instrumenting Jacoco-instrumented constructors, so these are
* currently skipped.
* Checks if the first or second instruction is a Jacoco load instruction.
*
* <p>Note that in most of cases, JaCoCo load instructions should locate at the beginning of the
* constructor.
*
* @param ctor constructor method node
* @return whether or not the constructor can be instrumented
* @return whether or not the constructor was instrumented by JaCoCo
*/
private boolean isJacocoInstrumented(MethodNode ctor) {
AbstractInsnNode[] insns = ctor.instructions.toArray();
Expand All @@ -229,42 +229,13 @@ private boolean isJacocoInstrumented(MethodNode ctor) {
if ((node instanceof LdcInsnNode && ((LdcInsnNode) node).cst instanceof ConstantDynamic)) {
ConstantDynamic cst = (ConstantDynamic) ((LdcInsnNode) node).cst;
return cst.getName().equals("$jacocoData");
} else if (insns.length > 1
&& insns[0] instanceof LabelNode
&& insns[1] instanceof MethodInsnNode) {
return "$jacocoInit".equals(((MethodInsnNode) insns[1]).name);
} else if (node instanceof MethodInsnNode) {
return "$jacocoInit".equals(((MethodInsnNode) node).name);
}
}
return false;
}

/**
* Adds a call $$robo$init, which instantiates a shadow object if required. This is to support
* custom shadows for Jacoco-instrumented classes (except cnstructor shadows).
*/
protected void addCallToRoboInit(MutableClass mutableClass, MethodNode ctor) {
AbstractInsnNode returnNode =
Iterables.find(
ctor.instructions,
node -> {
if (node.getOpcode() == Opcodes.INVOKESPECIAL) {
MethodInsnNode mNode = (MethodInsnNode) node;
return (mNode.owner.equals(mutableClass.internalClassName)
|| mNode.owner.equals(mutableClass.classNode.superName));
}
return false;
},
null);
ctor.instructions.insert(
returnNode,
new MethodInsnNode(
Opcodes.INVOKEVIRTUAL,
mutableClass.classType.getInternalName(),
ROBO_INIT_METHOD_NAME,
"()V"));
ctor.instructions.insert(returnNode, new VarInsnNode(Opcodes.ALOAD, 0));
}

private void instrumentMethods(MutableClass mutableClass) {
if (mutableClass.isInterface()) {
for (MethodNode method : mutableClass.getMethods()) {
Expand All @@ -278,11 +249,7 @@ private void instrumentMethods(MutableClass mutableClass) {
method.name = ShadowConstants.STATIC_INITIALIZER_METHOD_NAME;
mutableClass.addMethod(generateStaticInitializerNotifierMethod(mutableClass));
} else if (method.name.equals("<init>")) {
if (isJacocoInstrumented(method)) {
addCallToRoboInit(mutableClass, method);
} else {
instrumentConstructor(mutableClass, method);
}
instrumentConstructor(mutableClass, method);
} else if (!isSyntheticAccessorMethod(method) && !Modifier.isAbstract(method.access)) {
instrumentNormalMethod(mutableClass, method);
}
Expand Down Expand Up @@ -366,7 +333,8 @@ private static boolean isSyntheticAccessorMethod(MethodNode method) {
*
* <ul>
* <li>The original constructor will be stripped of its instructions leading up to, and
* including, the call to super() or this(). It is also renamed to $$robo$$__constructor__
* including, possible instrumented calls (like jacoco) before the call to super() or this()
* and the call to super() or this(). It is also renamed to $$robo$$__constructor__
* <li>A method called __constructor__ is created and its job is to call
* $$robo$$__constructor__. The __constructor__ method is what gets shadowed if a Shadow
* wants to shadow a constructor.
Expand All @@ -383,7 +351,15 @@ protected void instrumentConstructor(MutableClass mutableClass, MethodNode metho
int methodAccess = method.access;
makeMethodPrivate(method);

boolean isJacocoInstrumented = isJacocoInstrumented(method);

InsnList callSuper = extractCallToSuperConstructor(mutableClass, method);

if (isJacocoInstrumented) {
InsnList initJaCoCo = extractInitJaCoCoFromConstructorCallSuper(callSuper);
method.instructions.insert(initJaCoCo);
}

method.name = directMethodName(mutableClass, ShadowConstants.CONSTRUCTOR_METHOD_NAME);
mutableClass.addMethod(
redirectorMethod(mutableClass, method, ShadowConstants.CONSTRUCTOR_METHOD_NAME));
Expand All @@ -407,6 +383,44 @@ protected void instrumentConstructor(MutableClass mutableClass, MethodNode metho
mutableClass.addMethod(initMethodNode);
}

/**
* JaCoCo instrumentation initializes its probes before constructor to collect the coverage.
*
* <p>Those JaCoCo instrumented instructions are supported to be stripped from call super
* instructions and insert to the original constructor in case of breaking the following
* instructions.
*/
private InsnList extractInitJaCoCoFromConstructorCallSuper(InsnList callSuper) {
InsnList removedInstructions = new InsnList();

AbstractInsnNode[] insns = callSuper.toArray();
for (int i = 0; i < insns.length; i++) {
AbstractInsnNode node = insns[i];

if (node.getOpcode() == Opcodes.INVOKESTATIC) {
MethodInsnNode mnode = (MethodInsnNode) node;
if ("$jacocoInit".equals(mnode.name)) {
if (i + 1 >= insns.length) {
throw new RuntimeException("huh? JaCoCo instrumented instructions completed?!");
}

if (insns[i + 1].getOpcode() != Opcodes.ASTORE) {
throw new RuntimeException("huh? JaCoCo instrumented instructions no astore?!");
}

callSuper.remove(insns[i]);
callSuper.remove(insns[i + 1]);
removedInstructions.add(insns[i]);
removedInstructions.add(insns[i + 1]);

return removedInstructions;
}
}
}

throw new RuntimeException("huh? JaCoCo instrumented?!");
}

/**
* Checks to see if there are instructions after RETURN. If there are, it will check to see if
* they belong in the call-to-super, or the shadowable part of the constructor.
Expand Down

0 comments on commit 943b71e

Please sign in to comment.