4

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

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 类运行得很好:

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 步骤类中的“然后”运行良好,而其他步骤运行没有错误:

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$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

这是怎么回事?

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

4

1 回答 1

12

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

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

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

修复方法是在 Cucumber 代码中添加额外的检查,尽管可以说更好的修复方法是重写 Cucumber 代码以正确使用泛型反射

一种解决方法是确保 Kotlin 不会通过包含对包含实例的引用来优化 lambda。甚至像引用这样简单的东西this

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

足以说服 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,然后出现在类的常量池中。此字段是匿名内部类的实例,其名称为 likeClipboardSteps$1而不是 lambda 本身,因此其内部类型字符串会破坏内部的迷你解析器getArgumentTypes,这是您看到的错误。

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

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

于 2017-05-15T14:01:37.567 回答