Skip to content

Commit

Permalink
Add filter for Kotlin coroutines (#802)
Browse files Browse the repository at this point in the history
  • Loading branch information
Godin authored and marchof committed Dec 21, 2018
1 parent 9ad5e6c commit d919b8e
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 14 deletions.
7 changes: 6 additions & 1 deletion org.jacoco.core.test.validation.kotlin/pom.xml
Expand Up @@ -25,7 +25,7 @@


<properties> <properties>
<bytecode.version>6</bytecode.version> <bytecode.version>6</bytecode.version>
<kotlin.version>1.2.60</kotlin.version> <kotlin.version>1.3.0</kotlin.version>
</properties> </properties>


<dependencies> <dependencies>
Expand All @@ -39,6 +39,11 @@
<artifactId>kotlin-stdlib</artifactId> <artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version> <version>${kotlin.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>1.0.1</version>
</dependency>
</dependencies> </dependencies>


<build> <build>
Expand Down
@@ -0,0 +1,26 @@
/*******************************************************************************
* Copyright (c) 2009, 2018 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Evgeny Mandrikov - initial API and implementation
*
*******************************************************************************/
package org.jacoco.core.test.validation.kotlin;

import org.jacoco.core.test.validation.ValidationTestBase;
import org.jacoco.core.test.validation.kotlin.targets.KotlinCoroutineTarget;

/**
* Test of coroutines.
*/
public class KotlinCoroutineTest extends ValidationTestBase {

public KotlinCoroutineTest() {
super(KotlinCoroutineTarget.class);
}

}
@@ -0,0 +1,36 @@
/*******************************************************************************
* Copyright (c) 2009, 2018 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Evgeny Mandrikov - initial API and implementation
*
*******************************************************************************/
package org.jacoco.core.test.validation.kotlin.targets

import kotlinx.coroutines.runBlocking
import org.jacoco.core.test.validation.targets.Stubs.nop

/**
* Test target for coroutines.
*/
object KotlinCoroutineTarget {

suspend fun suspendingFunction() {
}

@JvmStatic
fun main(args: Array<String>) {

runBlocking { // assertFullyCovered()
nop() // assertFullyCovered()
suspendingFunction() // assertFullyCovered()
nop() // assertFullyCovered()
} // assertFullyCovered()

}

}
Expand Up @@ -223,23 +223,28 @@ public void nextIsInvokeSuper() {
} }


@Test @Test
public void nextIsNew() { public void nextIsType() {
m.visitInsn(Opcodes.NOP); m.visitInsn(Opcodes.NOP);
m.visitTypeInsn(Opcodes.NEW, "descriptor"); m.visitTypeInsn(Opcodes.NEW, "descriptor");


// should set cursor to null when opcode mismatch
matcher.cursor = m.instructions.getFirst();
matcher.nextIsType(Opcodes.CHECKCAST, "descriptor");
assertNull(matcher.cursor);

// should set cursor to null when descriptor mismatch // should set cursor to null when descriptor mismatch
matcher.cursor = m.instructions.getFirst(); matcher.cursor = m.instructions.getFirst();
matcher.nextIsNew("another_descriptor"); matcher.nextIsType(Opcodes.NEW, "another_descriptor");
assertNull(matcher.cursor); assertNull(matcher.cursor);


// should set cursor to next instruction when match // should set cursor to next instruction when match
matcher.cursor = m.instructions.getFirst(); matcher.cursor = m.instructions.getFirst();
matcher.nextIsNew("descriptor"); matcher.nextIsType(Opcodes.NEW, "descriptor");
assertSame(m.instructions.getLast(), matcher.cursor); assertSame(m.instructions.getLast(), matcher.cursor);


// should not do anything when cursor is null // should not do anything when cursor is null
matcher.cursor = null; matcher.cursor = null;
matcher.nextIsNew("descriptor"); matcher.nextIsType(Opcodes.NEW, "descriptor");
} }


@Test @Test
Expand Down
@@ -0,0 +1,135 @@
/*******************************************************************************
* Copyright (c) 2009, 2018 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Evgeny Mandrikov - initial API and implementation
*
*******************************************************************************/
package org.jacoco.core.internal.analysis.filter;

import org.jacoco.core.internal.instr.InstrSupport;
import org.junit.Test;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.MethodNode;

/**
* Unit test for {@link KotlinCoroutineFilter}.
*/
public class KotlinCoroutineFilterTest extends FilterTestBase {

private final IFilter filter = new KotlinCoroutineFilter();

private final MethodNode m = new MethodNode(InstrSupport.ASM_API_VERSION, 0,
"invokeSuspend", "(Ljava/lang/Object;)Ljava/lang/Object;", null,
null);

/**
* <pre>
* runBlocking {
* nop()
* suspendingFunction()
* nop()
* }
* </pre>
*/
@Test
public void should_filter() {
context.classAnnotations
.add(KotlinGeneratedFilter.KOTLIN_METADATA_DESC);

m.visitLabel(new Label());
m.visitMethodInsn(Opcodes.INVOKESTATIC,
"kotlin/coroutines/intrinsics/IntrinsicsKt",
"getCOROUTINE_SUSPENDED", "()Ljava/lang/Object;", false);
m.visitVarInsn(Opcodes.ASTORE, 3);

m.visitVarInsn(Opcodes.ALOAD, 0);
// line of "runBlocking"
m.visitFieldInsn(Opcodes.GETFIELD, "", "label", "I");
final Label dflt = new Label();
final Label state0 = new Label();
final Label state1 = new Label();
m.visitTableSwitchInsn(0, 1, dflt, state0, state1);
final Range range1 = new Range();
range1.fromInclusive = m.instructions.getLast();

m.visitLabel(state0);

{
m.visitVarInsn(Opcodes.ALOAD, 1);
m.visitInsn(Opcodes.DUP);
m.visitTypeInsn(Opcodes.INSTANCEOF, "kotlin/Result$Failure");
Label label = new Label();
m.visitJumpInsn(Opcodes.IFEQ, label);
m.visitTypeInsn(Opcodes.CHECKCAST, "kotlin/Result$Failure");
m.visitFieldInsn(Opcodes.GETFIELD, "kotlin/Result$Failure",
"exception", "Ljava/lang/Throwable");
m.visitInsn(Opcodes.ATHROW);
m.visitInsn(Opcodes.POP);
range1.toInclusive = m.instructions.getLast();
m.visitLabel(label);
}

// line before "suspendingFunction"
m.visitInsn(Opcodes.NOP);

// line of "suspendingFunction"
m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "", "suspendingFunction",
"(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", false);

m.visitInsn(Opcodes.DUP);
final Range range2 = new Range();
range2.fromInclusive = m.instructions.getLast();
m.visitVarInsn(Opcodes.ALOAD, 3);
final Label continuationLabelAfterLoadedResult = new Label();
m.visitJumpInsn(Opcodes.IF_ACMPNE, continuationLabelAfterLoadedResult);
// line of "runBlocking"
m.visitVarInsn(Opcodes.ALOAD, 3);
m.visitInsn(Opcodes.ARETURN);

m.visitLabel(state1);

{
m.visitVarInsn(Opcodes.ALOAD, 1);
m.visitInsn(Opcodes.DUP);
m.visitTypeInsn(Opcodes.INSTANCEOF, "kotlin/Result$Failure");
final Label label = new Label();
m.visitJumpInsn(Opcodes.IFEQ, label);
m.visitTypeInsn(Opcodes.CHECKCAST, "kotlin/Result$Failure");
m.visitFieldInsn(Opcodes.GETFIELD, "kotlin/Result$Failure",
"exception", "Ljava/lang/Throwable");
m.visitInsn(Opcodes.ATHROW);
m.visitInsn(Opcodes.POP);
m.visitLabel(label);
}
m.visitVarInsn(Opcodes.ALOAD, 1);
range2.toInclusive = m.instructions.getLast();
m.visitLabel(continuationLabelAfterLoadedResult);

// line after "suspendingFunction"
m.visitInsn(Opcodes.NOP);
m.visitInsn(Opcodes.ARETURN);

m.visitLabel(dflt);
final Range range0 = new Range();
range0.fromInclusive = m.instructions.getLast();
m.visitTypeInsn(Opcodes.NEW, "java/lang/IllegalStateException");
m.visitInsn(Opcodes.DUP);
m.visitLdcInsn("call to 'resume' before 'invoke' with coroutine");
m.visitMethodInsn(Opcodes.INVOKESPECIAL,
"java/lang/IllegalStateException", "<init>",
"(Ljava/lang/String;)V", false);
m.visitInsn(Opcodes.ATHROW);
range0.toInclusive = m.instructions.getLast();

filter.filter(m, context, output);

assertIgnored(range0, range1, range2);
}

}
Expand Up @@ -42,16 +42,15 @@ final void firstIsALoad0(final MethodNode methodNode) {
} }


/** /**
* Moves {@link #cursor} to next instruction if it is <code>NEW</code> with * Moves {@link #cursor} to next instruction if it is {@link TypeInsnNode}
* given operand, otherwise sets it to <code>null</code>. * with given opcode and operand, otherwise sets it to <code>null</code>.
*/ */
final void nextIsNew(final String desc) { final void nextIsType(final int opcode, final String desc) {
nextIs(Opcodes.NEW); nextIs(opcode);
if (cursor == null) { if (cursor == null) {
return; return;
} }
final TypeInsnNode i = (TypeInsnNode) cursor; if (((TypeInsnNode) cursor).desc.equals(desc)) {
if (desc.equals(i.desc)) {
return; return;
} }
cursor = null; cursor = null;
Expand Down
Expand Up @@ -42,7 +42,8 @@ public static IFilter all() {
new KotlinLateinitFilter(), new KotlinWhenFilter(), new KotlinLateinitFilter(), new KotlinWhenFilter(),
new KotlinWhenStringFilter(), new KotlinWhenStringFilter(),
new KotlinUnsafeCastOperatorFilter(), new KotlinUnsafeCastOperatorFilter(),
new KotlinDefaultArgumentsFilter(), new KotlinInlineFilter()); new KotlinDefaultArgumentsFilter(), new KotlinInlineFilter(),
new KotlinCoroutineFilter());
} }


private Filters(final IFilter... filters) { private Filters(final IFilter... filters) {
Expand Down

0 comments on commit d919b8e

Please sign in to comment.