为什么我在 Kotlin 中的这个特定 Cucumber 步骤中收到 ArrayIndexOutOfBoundsException 运行?

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

我是 运行 Cucumber JVM 功能文件,使用 Java8 和 PicoContainer。我已经删除了这些步骤,所以它们是空的,但我仍然遇到错误。这是我的特色:

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

(是的,这是一个完整的旅程,而不是 BDD 场景。)

无论出于何种原因,运行 Kotlin 中的这一步导致错误:

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 
        })
    }
}

虽然这个 Java class 运行得很好:

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
        });
    }
}

我非常困惑,尤其是因为 Kotlin 步骤 class 中的 "Then" 是 运行 完美的,而这另一个步骤运行没有错误:

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
        })
    }
}

奔跑者,完成:

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 {
}

堆栈跟踪是:

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.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.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access[=16=]0(ParentRunner.java:58)
at org.junit.runners.ParentRunner.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.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access[=16=]0(ParentRunner.java:58)
at org.junit.runners.ParentRunner.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

怎么回事?

当前使用 Kotlin 步骤签入的所有源代码都已注释掉,here。 (请原谅我的混乱,因为我对我正在使用的很多东西都是新手;从最初的峰值开始重构正在进行中。)

这似乎是 Kotlin 编译匿名代码块的优化、Cucumber 关于 JVM 如何存储对 lambda 的引用的假设以及 Cucumber 使用一些不应接近的 JVM 内部构件之间的不幸交互!

您的 other Kotlin steps 由于各种(不同)原因未触发错误。

简而言之,如果 Kotlin 可以将块或 lambda 实现为静态单例,那么它可以实现,大概是出于性能原因。这会干扰 Cucumber 执行的一些非常规反射魔法(详情如下)。

修复 将在 Cucumber 代码中添加额外的检查,尽管可以说更好的修复是重写 Cucumber 代码以使用 generics reflection properly

解决方法 是确保 Kotlin 不会通过包含对包含实例的引用来优化 lambda。甚至像对 this:

的引用一样简单
When("^I import it into Montecarluni$") {
    this
    // your code
}

足以说服 Kotlin 不执行优化。

详情

当 Cucumber 添加带有 lambda 的步骤定义时,例如cucumber.api.java8.En 它内省 lambda 以获取有关泛型的信息。

这样做的方法是使用访问 hack 来访问 lambda 的 class 定义中的 sun.reflect.ConstantPool 字段。这是一种本机类型,是 class 的实现细节,存储对 class 使用的常量的引用。然后 Cucumber 向后迭代这些寻找代表 lambda 构造函数的常量。然后它使用 另一个 内部 hack,一个在 jdk.internal.org.objectweb.asm.Type 上调用 getArgumentTypes 的静态方法来计算 lambda 的签名。

运行 javap -v 针对生成的 classes,似乎当 Kotlin 将 lambda 块变成静态单例时,它添加了一个名为 INSTANCE 的常量字段然后出现在 class 的常量池中。该字段是匿名内部 class 的实例,其名称类似于 ClipboardSteps 而不是 lambda 本身,因此其内部类型字符串破坏了 getArgumentTypes 内的迷你解析器,这是错误你看到了。

所以 Cucumber 中的 quick fix 是检查常量池成员的名称是否为 "<init>",代表 lambda 的构造函数,并忽略其他任何内容,例如我们的 INSTANCE 会员。

正确的解决方法 是重写 Cucumber 的类型自省以完全不使用常量池!