kotlinbddcucumber-jvmcucumber-javapicocontainer

Why am I getting an ArrayIndexOutOfBoundsException running this particular Cucumber step in Kotlin?


I'm running with a Cucumber JVM feature file, using Java8 and PicoContainer. I've stripped these steps right down so they're empty, and I'm still getting an error. Here's my feature:

Feature: Full Journey

Scenario: Can load a typical JIRA csv and calculate the distribution from it

Given a typical JIRA export "/closed_only_JIRA.csv"
When I import it into Montecarluni
Then I should see the distribution
"""
6, 15, 3, 14, 2, 5, 6, 8, 5, 10, 15, 4, 2, 1
"""
When I copy it to the clipboard
Then I should be able to paste it somewhere else

(Yes, this is a full journey rather than a BDD scenario.)

For whatever reason, running this step in Kotlin causes an error:

import cucumber.api.java8.En

class ClipboardSteps(val world : World) : En {
    init {
        When("^I copy it to the clipboard$", {
            // Errors even without any code here 
        })
    }
}

While this Java class runs just fine:

import cucumber.api.java8.En;

public class JavaClipboardSteps implements En {

    public JavaClipboardSteps(World world) {
        When("^I copy it to the clipboard$", () -> {
            // Works just fine with code or without
        });
    }
}

I'm utterly bemused, not least because a "Then" in that Kotlin steps class is running perfectly, and this other step runs without error:

import cucumber.api.java8.En

class FileImportSteps(val world: World) : En {
    init {
        // There's a Given here

        When("^I import it into Montecarluni$", {
            // There's some code here
        })
    }
}

The Runner, for completion:

import cucumber.api.CucumberOptions
import cucumber.api.junit.Cucumber
import org.junit.runner.RunWith

@RunWith(Cucumber::class)
@CucumberOptions(
    format = arrayOf("pretty"),
    glue = arrayOf("com.lunivore.montecarluni.glue"),
    features = arrayOf("."))
class Runner {
}

Stacktrace is:

cucumber.runtime.CucumberException: java.lang.ArrayIndexOutOfBoundsException: 52

at cucumber.runtime.java.JavaBackend.addStepDefinition(JavaBackend.java:166)
at cucumber.api.java8.En.Then(En.java:280)
at com.lunivore.montecarluni.glue.DistributionSteps.<init>(DistributionSteps.kt:8)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at org.picocontainer.injectors.AbstractInjector.newInstance(AbstractInjector.java:145)
at org.picocontainer.injectors.ConstructorInjector$1.run(ConstructorInjector.java:342)
at org.picocontainer.injectors.AbstractInjector$ThreadLocalCyclicDependencyGuard.observe(AbstractInjector.java:270)
at org.picocontainer.injectors.ConstructorInjector.getComponentInstance(ConstructorInjector.java:364)
at org.picocontainer.injectors.AbstractInjectionFactory$LifecycleAdapter.getComponentInstance(AbstractInjectionFactory.java:56)
at org.picocontainer.behaviors.AbstractBehavior.getComponentInstance(AbstractBehavior.java:64)
at org.picocontainer.behaviors.Stored.getComponentInstance(Stored.java:91)
at org.picocontainer.DefaultPicoContainer.getInstance(DefaultPicoContainer.java:699)
at org.picocontainer.DefaultPicoContainer.getComponent(DefaultPicoContainer.java:647)
at org.picocontainer.DefaultPicoContainer.getComponent(DefaultPicoContainer.java:678)
at cucumber.runtime.java.picocontainer.PicoFactory.getInstance(PicoFactory.java:40)
at cucumber.runtime.java.JavaBackend.buildWorld(JavaBackend.java:131)
at cucumber.runtime.Runtime.buildBackendWorlds(Runtime.java:141)
at cucumber.runtime.model.CucumberScenario.run(CucumberScenario.java:38)
at cucumber.runtime.junit.ExecutionUnitRunner.run(ExecutionUnitRunner.java:102)
at cucumber.runtime.junit.FeatureRunner.runChild(FeatureRunner.java:63)
at cucumber.runtime.junit.FeatureRunner.runChild(FeatureRunner.java:18)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at cucumber.runtime.junit.FeatureRunner.run(FeatureRunner.java:70)
at cucumber.api.junit.Cucumber.runChild(Cucumber.java:95)
at cucumber.api.junit.Cucumber.runChild(Cucumber.java:38)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at cucumber.api.junit.Cucumber.run(Cucumber.java:100)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

Caused by: java.lang.ArrayIndexOutOfBoundsException: 52
at jdk.internal.org.objectweb.asm.Type.getArgumentTypes(Type.java:358)
at cucumber.runtime.java8.ConstantPoolTypeIntrospector.getGenericTypes(ConstantPoolTypeIntrospector.java:32)
at cucumber.runtime.java.Java8StepDefinition.getParameterInfos(Java8StepDefinition.java:54)
at cucumber.runtime.java.Java8StepDefinition.<init>(Java8StepDefinition.java:44)
at cucumber.runtime.java.JavaBackend.addStepDefinition(JavaBackend.java:162)
... 44 more

What's going on?

All source code currently checked in with Kotlin step commented out, here. (Please excuse the mess as I'm new to a lot of the stuff I'm using; refactoring from the initial spike is ongoing.)


Solution

  • This seems to be an unfortunate interaction between an optimisation Kotlin makes compiling anonymous code blocks, an assumption Cucumber makes about how the JVM stores references to lambdas, and Cucumber's use of some JVM internals that it shouldn't be going near!

    Your other Kotlin steps don't trigger the bug for various (different) reasons.

    Briefly, if Kotlin can implement a block or lambda as a static singleton then it does, presumably for performance reasons. This interferes with some unconventional reflection magic Cucumber performs (details below).

    The fix would be to add an additional check in the Cucumber code, although arguably a better fix would be to rewrite the Cucumber code to use generics reflection properly.

    A workaround is to ensure Kotlin doesn't optimise the lambda by including a reference to the containing instance. Even something as simple as a reference to this:

    When("^I import it into Montecarluni$") {
        this
        // your code
    }
    

    is enough to convince Kotlin not to perform the optimisation.

    The details

    When Cucumber adds a step definition with a lambda in e.g. cucumber.api.java8.En it introspects the lambda for information about generics.

    The way it does this is to use an access hack to reach a sun.reflect.ConstantPool field in the lambda's class definition. This is a native type and is an implementation detail of the class, storing references to constants the class uses. Cucumber then iterates backwards through these looking for a constant that represents the lambda's constructor. It then uses another internal hack, a static method called getArgumentTypes on jdk.internal.org.objectweb.asm.Type, to figure out the lambda's signature.

    Running javap -v against the generated classes, it appears that when Kotlin makes a lambda block into a static singleton it adds a constant field called INSTANCE which then appears in the class's constant pool. This field is an instance of an anonymous inner class with a name like ClipboardSteps$1 rather than a lambda as such, so its internal typestring breaks the mini-parser inside getArgumentTypes, which is the error you are seeing.

    So the quick fix in Cucumber is to check that the constant pool member's name is "<init>", which represents the lambda's constructor, and to ignore anything else, like our INSTANCE member.

    The proper fix would be to rewrite Cucumber's type introspection to not use the constant pool at all!