Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[java8] support lambda references to Kotlin value class #2342

Closed
hakanai opened this issue Jul 17, 2021 · 4 comments
Closed

[java8] support lambda references to Kotlin value class #2342

hakanai opened this issue Jul 17, 2021 · 4 comments
Labels
🙅 wontfix This will not be worked on ⚡ enhancement Request for new functionality

Comments

@hakanai
Copy link

hakanai commented Jul 17, 2021

Given you are implementing a feature:

Feature: HTML-safe Strings
  Scenario: A string is created
    Given an HTML-safe string html_safe("blah")
    Then it converts to HTML "blah"

And step definitions:

import assertk.assertThat
import assertk.assertions.isEqualTo
import io.cucumber.java8.En

class HtmlSafeStringStepdefs: En {
    var htmlSafeString: HtmlSafeString? = null

    init {
        ParameterType("html_safe_string", "html_safe\\({string}\\)") { string: String ->
            HtmlSafeString(string)
        }
        Given("an HTML-safe string {html_safe_string})") { string: HtmlSafeString ->
            htmlSafeString = string
        }
        Then("it converts to HTML {string}") { string: String ->
            assertThat(htmlSafeString).isEqualTo(string)
        }
    }
}

And supposing you implemented the class like this:

@JvmInline
value class HtmlSafeString(private val content: String) {
    fun toHtml() {
        content
    }
}

When you compile, everything goes fine.

Then when you run, you expect the specifications to be run.

But you actually get this:

Failed to instantiate class com.example.bug.HtmlSafeStringStepdefs
io.cucumber.core.exception.CucumberException: Failed to instantiate class com.example.bug.HtmlSafeStringStepdefs
	at io.cucumber.core.runtime.ObjectFactoryServiceLoader$DefaultJavaObjectFactory.cacheNewInstance(ObjectFactoryServiceLoader.java:135)
	at io.cucumber.core.runtime.ObjectFactoryServiceLoader$DefaultJavaObjectFactory.getInstance(ObjectFactoryServiceLoader.java:121)
	at io.cucumber.java8.Java8Backend.buildWorld(Java8Backend.java:63)
	at io.cucumber.core.runner.Runner.buildBackendWorlds(Runner.java:167)
	at io.cucumber.core.runner.Runner.runPickle(Runner.java:62)
	at io.cucumber.junit.PickleRunners$NoStepDescriptions.run(PickleRunners.java:149)
	at io.cucumber.junit.FeatureRunner.runChild(FeatureRunner.java:83)
	at io.cucumber.junit.FeatureRunner.runChild(FeatureRunner.java:24)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at io.cucumber.junit.Cucumber.runChild(Cucumber.java:185)
	at io.cucumber.junit.Cucumber.runChild(Cucumber.java:83)
	at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
	at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
	at io.cucumber.junit.Cucumber$RunCucumber.evaluate(Cucumber.java:219)
	at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.runTestClass(JUnitTestClassExecutor.java:110)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:58)
	at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecutor.execute(JUnitTestClassExecutor.java:38)
	at org.gradle.api.internal.tasks.testing.junit.AbstractJUnitTestClassProcessor.processTestClass(AbstractJUnitTestClassProcessor.java:62)
	at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
	at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
	at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
	at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:119)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
	at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
	at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
	at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
	at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
	at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
	at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
	at io.cucumber.core.runtime.ObjectFactoryServiceLoader$DefaultJavaObjectFactory.cacheNewInstance(ObjectFactoryServiceLoader.java:129)
	... 54 more
Caused by: java.lang.IllegalStateException: Expected single 'accept' method on body class, found '[]'
	at io.cucumber.java8.AbstractGlueDefinition.getAcceptMethod(AbstractGlueDefinition.java:41)
	at io.cucumber.java8.AbstractGlueDefinition.<init>(AbstractGlueDefinition.java:21)
	at io.cucumber.java8.Java8ParameterTypeDefinition.<init>(Java8ParameterTypeDefinition.java:21)
	at io.cucumber.java8.LambdaGlue.ParameterType(LambdaGlue.java:464)
	at com.example.bug.HtmlSafeStringStepdefs.<init>(HtmlSafeStringStepdefs.kt:11)
	... 59 more
  • Java 11
  • Kotlin 1.5.21
  • AssertK 0.22
  • Cucumber JVM 5.7.0

Demo repository here

Where I actually hit this was trying to wrap Tuple into Point and Vector value classes for Red Rocket.

@hakanai hakanai changed the title Step definition fails to instantiate Step definition fails to instantiate if parameter type is a Kotlin value class Jul 17, 2021
@mpkorstanje
Copy link
Contributor

mpkorstanje commented Jul 17, 2021

There is an unrelated mistake in the regex of the parameter type of your reproducer. So to make this discussion easier I will be using this parameter type declaration to trigger the exception:

package com.example.bug

import io.cucumber.java8.En

class HtmlSafeStringStepdefs : En {
    init {
        ParameterType("html_safe_string", "html_safe\\(\"(.*)\"\\)") { string: String ->
             HtmlSafeString(string) 
        }
    }
}

And this is essentially the same as using a lambda reference:

package com.example.bug

import io.cucumber.java8.En

class HtmlSafeStringStepdefs: En {
    var htmlSafeString: HtmlSafeString? = null

    init {
        ParameterType("html_safe_string", "html_safe\\(\"(.*)\"\\)", ::HtmlSafeString)
    }
}

When declaring a ParameterType Cucumber expects the lambda argument to be a subclass of ParameterDefinitionBody.A1<T> with an accept method.

Because A1<T> is generic the JVM will add in bridge methods (see this excellent explanation why ) so that both the method accept(String) : Object and accept(String): String exist.

And we want to get the non-bridge accept(String): String because we need the non-generic type arguments from the methods signature later on.

Unfortunately this gets a bit complicated when the target is a method reference to a value class. Because the class is inlined the real method has been name mangled into accept-IDPvPPfA(String): String while the bridge method remains untouched.

As such you have the following options.

  1. Do not use inlined classes.
  2. Add extra braces around the lambda to avoid a method reference to the inlined value class.
package com.example.bug

import io.cucumber.java8.En

class HtmlSafeStringStepdefs : En {
    init {
        ParameterType("html_safe_string", "html_safe\\(\".*\"\\)") { string: String ->
            {
                HtmlSafeString(string)
            }
        }
    }
}
  1. Submit a PR that changes the behaviour of AbstractGlueDefinition such that instead of filtering the accept methods on the bodyClass it sorts the methods and picks the most preferred candidate.

@mpkorstanje mpkorstanje added the ⚡ enhancement Request for new functionality label Jul 17, 2021
@mpkorstanje mpkorstanje changed the title Step definition fails to instantiate if parameter type is a Kotlin value class [java8] support method references to Kotlin value class Jul 17, 2021
@mpkorstanje mpkorstanje changed the title [java8] support method references to Kotlin value class [java8] support lambda references to Kotlin value class Jul 17, 2021
@hakanai
Copy link
Author

hakanai commented Jul 17, 2021

Option 3 seems a bit tricky so I might be going with option 2. Edit: Scratch that, option 2 doesn't work for me.

Also, I see there is wind of cucumber-java8 being deprecated. Is the replacement available? Maybe it doesn't have the issue.

@mpkorstanje
Copy link
Contributor

We're considering a lambda based implementation in #2279. It'd need someone willing to implement it.

@mpkorstanje
Copy link
Contributor

With cucumber-java8 being considered for deprecation I do not think it is worth fixing this.

@mpkorstanje mpkorstanje added the 🙅 wontfix This will not be worked on label Oct 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🙅 wontfix This will not be worked on ⚡ enhancement Request for new functionality
Projects
None yet
Development

No branches or pull requests

2 participants