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

Lun*_*ore 4 bdd picocontainer kotlin cucumber-jvm cucumber-java

我正在运行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
Run Code Online (Sandbox Code Playgroud)

(是的,这是一个完整的旅程而不是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 
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

我完全被困惑了,尤其是因为Kotlin步骤类中的"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
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

赛跑者,完成:

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 {
}
Run Code Online (Sandbox Code Playgroud)

Stacktrace是:

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
Run Code Online (Sandbox Code Playgroud)

这是怎么回事?

目前用Kotlin步检查的所有源代码都在这里注释掉了.(请原谅这个烂摊子,因为我对我正在使用的很多东西都是新手;从最初的尖峰重构正在进行中.)

Dan*_*rth 12

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

您的其他Kotlin步骤不会因各种(不同)原因触发错误.

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

修复将是在Cucumber代码中添加一个额外的检查,虽然可以说更好的解决方法是重写Cucumber代码以正确使用泛型反射.

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

When("^I import it into Montecarluni$") {
    this
    // your code
}
Run Code Online (Sandbox Code Playgroud)

足以说服Kotlin不要进行优化.

细节

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

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

对生成的类运行javap -v,似乎当Kotlin将一个lambda块放入一个静态单例时,它会添加一个常量字段INSTANCE,然后出现在类的常量池中.这个字段是一个匿名内部类的实例,其名称与ClipboardSteps$1而不是lambda一样,因此其内部类型字符串会破坏内部的迷你解析器getArgumentTypes,这就是您所看到的错误.

因此,Cucumber中的快速修复方法是检查常量池成员的名称是否"<init>"代表lambda的构造函数,并忽略其他任何内容,例如我们的INSTANCE成员.

适当的修复将改写黄瓜的类型内省不使用常量池了!