diff --git a/History.md b/History.md index eaf9317504..765e4360b8 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,6 @@ ## [2.0.0-SNAPSHOT](https://github.com/cucumber/cucumber-jvm/compare/v1.2.5...master) (In Git) +* [Java8, Kotlin Java8] Support java 8 method references ([#1140](https://github.com/cucumber/cucumber-jvm/pull/1140) M.P. Korstanje) * [Core] Show explicit error message when field name missed in table header ([#1014](https://github.com/cucumber/cucumber-jvm/pull/1014) Mykola Gurov) * [Examples] Properly quit selenium in webbit examples ([#1146](https://github.com/cucumber/cucumber-jvm/pull/1146) Alberto Scotto) * [JUnit] Use AssumptionFailed to mark scenarios/steps as skipped ([#1142](https://github.com/cucumber/cucumber-jvm/pull/1142) Björn Rasmusson) diff --git a/java8/pom.xml b/java8/pom.xml index 5ad77a4a29..88f6b37a20 100644 --- a/java8/pom.xml +++ b/java8/pom.xml @@ -28,11 +28,6 @@ junit test - - org.mockito - mockito-all - test - net.sourceforge.cobertura cobertura diff --git a/java8/src/main/java/cucumber/runtime/java8/ConstantPoolTypeIntrospector.java b/java8/src/main/java/cucumber/runtime/java8/ConstantPoolTypeIntrospector.java index 63a11deaf2..7f8aafc590 100644 --- a/java8/src/main/java/cucumber/runtime/java8/ConstantPoolTypeIntrospector.java +++ b/java8/src/main/java/cucumber/runtime/java8/ConstantPoolTypeIntrospector.java @@ -1,17 +1,22 @@ package cucumber.runtime.java8; -import cucumber.runtime.CucumberException; +import static java.lang.Class.forName; +import static java.lang.System.arraycopy; +import static jdk.internal.org.objectweb.asm.Type.getObjectType; + import cucumber.api.java8.StepdefBody; +import cucumber.runtime.CucumberException; import cucumber.runtime.java.TypeIntrospector; import sun.reflect.ConstantPool; import java.lang.reflect.Method; import java.lang.reflect.Type; -import java.util.Arrays; -import java.util.List; public class ConstantPoolTypeIntrospector implements TypeIntrospector { private static final Method Class_getConstantPool; + private static final int REFERENCE_CLASS = 0; + private static final int REFERENCE_METHOD = 1; + private static final int REFERENCE_ARGUMENT_TYPES = 2; static { try { @@ -26,37 +31,72 @@ public class ConstantPoolTypeIntrospector implements TypeIntrospector { @Override public Type[] getGenericTypes(Class clazz, Class interfac3) throws Exception { - ConstantPool constantPool = (ConstantPool) Class_getConstantPool.invoke(clazz); - String typeString = getLambdaTypeString(constantPool); - int typeParameterCount = interfac3.getTypeParameters().length; - jdk.internal.org.objectweb.asm.Type[] argumentTypes = jdk.internal.org.objectweb.asm.Type.getArgumentTypes(typeString); + final ConstantPool constantPool = (ConstantPool) Class_getConstantPool.invoke(clazz); + final String[] member = getMemberReference(constantPool); + final int parameterCount = interfac3.getTypeParameters().length; + + // Kotlin lambda expression without arguments or closure variables + if (member[REFERENCE_METHOD].equals("INSTANCE")) { + return handleKotlinInstance(); + } + + final jdk.internal.org.objectweb.asm.Type[] argumentTypes = jdk.internal.org.objectweb.asm.Type.getArgumentTypes(member[REFERENCE_ARGUMENT_TYPES]); + + // If we are one parameter short, this is a + // - Reference to an instance method of an arbitrary object of a particular type + if (parameterCount - 1 == argumentTypes.length) { + return handleMethodReferenceToObjectOfType(member[REFERENCE_CLASS], handleLambda(argumentTypes, parameterCount - 1)); + } + // If we are not short on parameters this either + // - Reference to a static method + // - Reference to an instance method of a particular object + // - Reference to a constructor + // - A lambda expression + // We can all treat these as lambda's for figuring out the types. + return handleLambda(argumentTypes, parameterCount); + } + + private static Type[] handleMethodReferenceToObjectOfType(String containingType, Type[] methodArgumentTypes) throws ClassNotFoundException { + Type[] containingTypeAndMethodArgumentTypes = new Type[methodArgumentTypes.length + 1]; + containingTypeAndMethodArgumentTypes[0] = forName(getObjectType(containingType).getClassName()); + arraycopy(methodArgumentTypes, 0, containingTypeAndMethodArgumentTypes, 1, methodArgumentTypes.length); + return containingTypeAndMethodArgumentTypes; + } + + private static Type[] handleLambda(jdk.internal.org.objectweb.asm.Type[] argumentTypes, int typeParameterCount) throws ClassNotFoundException { + if (argumentTypes.length < typeParameterCount) { + throw new CucumberException(String.format("Expected at least %s arguments but found only %s", typeParameterCount, argumentTypes.length)); + } + // Only look at the N last arguments to the lambda static method, since the first ones might be variables // who only pass in the states of closed variables - List interestingArgumentTypes = Arrays.asList(argumentTypes) - .subList(argumentTypes.length - typeParameterCount, argumentTypes.length); + jdk.internal.org.objectweb.asm.Type[] interestingArgumentTypes = new jdk.internal.org.objectweb.asm.Type[typeParameterCount]; + arraycopy(argumentTypes, argumentTypes.length - typeParameterCount, interestingArgumentTypes, 0, typeParameterCount); Type[] typeArguments = new Type[typeParameterCount]; for (int i = 0; i < typeParameterCount; i++) { - typeArguments[i] = Class.forName(interestingArgumentTypes.get(i).getClassName()); + typeArguments[i] = forName(interestingArgumentTypes[i].getClassName()); } return typeArguments; } - private String getLambdaTypeString(ConstantPool constantPool) { + private static Type[] handleKotlinInstance() { + return new Type[0]; + } + + private static String[] getMemberReference(ConstantPool constantPool) { int size = constantPool.getSize(); - String[] memberRef = null; // find last element in constantPool with valid memberRef // - previously always at size-2 index but changed with JDK 1.8.0_60 for (int i = size - 1; i > -1; i--) { try { - memberRef = constantPool.getMemberRefInfoAt(i); - return memberRef[2]; + return constantPool.getMemberRefInfoAt(i); } catch (IllegalArgumentException e) { // eat error; null entry at ConstantPool index? } } - throw new CucumberException("Couldn't find memberRef."); + throw new CucumberException("Couldn't find memberRef."); } } diff --git a/java8/src/test/java/cucumber/runtime/java8/test/LambdaStepdefs.java b/java8/src/test/java/cucumber/runtime/java8/test/LambdaStepdefs.java index e0a72012f3..8596f182e5 100644 --- a/java8/src/test/java/cucumber/runtime/java8/test/LambdaStepdefs.java +++ b/java8/src/test/java/cucumber/runtime/java8/test/LambdaStepdefs.java @@ -1,17 +1,20 @@ package cucumber.runtime.java8.test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + import cucumber.api.DataTable; import cucumber.api.Scenario; import cucumber.api.java8.En; import java.util.List; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotSame; - public class LambdaStepdefs implements En { private static LambdaStepdefs lastInstance; + private final int outside = 41; + public LambdaStepdefs() { Before((Scenario scenario) -> { assertNotSame(this, lastInstance); @@ -41,33 +44,72 @@ public LambdaStepdefs() { assertEquals("hello", localState); }); - int localInt = 1; Given("^A statement with a simple match$", () -> { - assertEquals(2, localInt+1); + assertTrue(true); + }); + + int localInt = 1; + Given("^A statement with a scoped argument$", () -> { + assertEquals(2, localInt + 1); + assertEquals(42, outside + 1); }); Given("^I will give you (\\d+) and ([\\d\\.]+) and (\\w+) and (\\d+)$", (Integer a, Float b, String c, Integer d) - -> { + -> { assertEquals((Integer) 1, a); assertEquals((Float) 2.2f, b); assertEquals("three", c); assertEquals((Integer) 4, d); }); - Given("^A lambda that declares an exception$", this::methodThatDeclaresException); + Given("^A method reference that declares an exception$", this::methodThatDeclaresException); + Given("^A method reference with an argument (\\d+)$", this::methodWithAnArgument); + Given("^A constructor reference with an argument (.*)$", Contact::new); + Given("^A static method reference with an argument (\\d+)$", LambdaStepdefs::staticMethodWithAnArgument); + + Given("^A method reference to an arbitrary object of a particular type (\\d+)$", Contact::call); + Given("^A method reference to an arbitrary object of a particular type (.*) with argument (.*)$", Contact::update); + } private void methodThatDeclaresException() throws Throwable { } + private void methodWithAnArgument(Integer cuckes) throws Throwable { + assertEquals(42, cuckes.intValue()); + } + + public static void staticMethodWithAnArgument(Integer cuckes) throws Throwable { + assertEquals(42, cuckes.intValue()); + } private void hookWithArgs(Scenario scenario) throws Throwable { } - public static class Person { String first; String last; } + + public static class Contact { + + private final String number; + + public Contact(String number){ + this.number = number; + assertEquals("42", number); + } + + public void call(){ + assertEquals("42", number); + } + + public void update(String number){ + assertEquals("42", this.number); + assertEquals("314", number); + } + } + + } diff --git a/java8/src/test/resources/cucumber/runtime/java8/test/java8.feature b/java8/src/test/resources/cucumber/runtime/java8/test/java8.feature index 57c4e64dcb..ba51718aee 100644 --- a/java8/src/test/resources/cucumber/runtime/java8/test/java8.feature +++ b/java8/src/test/resources/cucumber/runtime/java8/test/java8.feature @@ -10,6 +10,7 @@ Feature: Java8 Scenario: Parameterless lambdas Given A statement with a simple match + Given A statement with a scoped argument Scenario: Multi-param lambdas Given I will give you 1 and 2.2 and three and 4 @@ -20,5 +21,10 @@ Feature: Java8 | Aslak | Hellesøy | | Donald | Duck | - Scenario: using lambdas with exceptions - Given A lambda that declares an exception + Scenario: using method references + Given A method reference that declares an exception + Given A method reference with an argument 42 + Given A static method reference with an argument 42 + Given A constructor reference with an argument 42 + Given A method reference to an arbitrary object of a particular type 42 + Given A method reference to an arbitrary object of a particular type 42 with argument 314 diff --git a/kotlin-java8/README.md b/kotlin-java8/README.md new file mode 100644 index 0000000000..bbccc6852d --- /dev/null +++ b/kotlin-java8/README.md @@ -0,0 +1,6 @@ +Java 8 Bindings for Kotlin +========================== + +This module only runs tests. + +You can use `cucumber-java` or `cucumber-java8` directly in Kotlin. diff --git a/kotlin-java8/pom.xml b/kotlin-java8/pom.xml new file mode 100644 index 0000000000..d163ad31c4 --- /dev/null +++ b/kotlin-java8/pom.xml @@ -0,0 +1,93 @@ + + 4.0.0 + + + io.cucumber + cucumber-jvm + ../pom.xml + 2.0.0-SNAPSHOT + + + cucumber-kotlin-java8 + jar + Cucumber-JVM: Kotlin Java8 + + + 1.1.2-2 + + + + + io.cucumber + cucumber-java8 + + + io.cucumber + cucumber-junit + test + + + junit + junit + test + + + net.sourceforge.cobertura + cobertura + test + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + test + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + compile + + compile + + + + test-compile + + test-compile + + + + + + maven-jar-plugin + + true + + + + maven-install-plugin + + true + + + + maven-deploy-plugin + + true + + + + + + diff --git a/kotlin-java8/src/test/kotlin/cucumber/runtime/kotlin/test/LambdaStepdefs.kt b/kotlin-java8/src/test/kotlin/cucumber/runtime/kotlin/test/LambdaStepdefs.kt new file mode 100644 index 0000000000..3949031987 --- /dev/null +++ b/kotlin-java8/src/test/kotlin/cucumber/runtime/kotlin/test/LambdaStepdefs.kt @@ -0,0 +1,55 @@ +package cucumber.runtime.kotlin.test; + +import cucumber.api.DataTable +import cucumber.api.Scenario +import cucumber.api.java8.En +import org.junit.Assert.* + +var lastInstance : LambdaStepdefs? = null + +class LambdaStepdefs : En { + + init { + Before { scenario: Scenario -> + assertNotSame(this, lastInstance) + lastInstance = this + } + + Given("^this data table:$") { peopleTable: DataTable -> + val people = peopleTable.asList(Person::class.java) + assertEquals("Aslak", people[0].first) + assertEquals("Hellesøy", people[0].last) + } + + val alreadyHadThisManyCukes = 1 + Given("^I have (\\d+) cukes in my belly$") { n: Long -> + assertEquals(1, alreadyHadThisManyCukes) + assertEquals(42L, n) + } + + val localState = "hello" + Then("^I really have (\\d+) cukes in my belly") { i: Int -> + assertEquals(42, i) + assertEquals("hello", localState) + } + + Given("^A statement with a body expression$") { assertTrue(true) } + + Given("^A statement with a simple match$", { -> assertTrue(true) }) + + val localInt = 1 + Given("^A statement with a scoped argument$", { assertEquals(2, localInt + 1) }) + + Given("^I will give you (\\d+) and ([\\d\\.]+) and (\\w+) and (\\d+)$") { a: Int, b: Float, c: String, d: Int -> + assertEquals(1, a) + assertEquals(2.2f, b) + assertEquals("three", c) + assertEquals(4, d) + } + } + + class Person { + internal var first: String? = null + internal var last: String? = null + } +} diff --git a/kotlin-java8/src/test/kotlin/cucumber/runtime/kotlin/test/RunCukesTest.kt b/kotlin-java8/src/test/kotlin/cucumber/runtime/kotlin/test/RunCukesTest.kt new file mode 100644 index 0000000000..77e63fff7f --- /dev/null +++ b/kotlin-java8/src/test/kotlin/cucumber/runtime/kotlin/test/RunCukesTest.kt @@ -0,0 +1,8 @@ +package cucumber.runtime.kotlin.test + +import cucumber.api.junit.Cucumber +import org.junit.runner.RunWith + +@RunWith(Cucumber::class) +class RunCukesTest { +} diff --git a/kotlin-java8/src/test/resources/cucumber/runtime/kotlin/test/kotlin.feature b/kotlin-java8/src/test/resources/cucumber/runtime/kotlin/test/kotlin.feature new file mode 100644 index 0000000000..e9b47a62d0 --- /dev/null +++ b/kotlin-java8/src/test/resources/cucumber/runtime/kotlin/test/kotlin.feature @@ -0,0 +1,25 @@ +Feature: Kotlin + + Scenario: use the API with Java8 style + Given I have 42 cukes in my belly + Then I really have 42 cukes in my belly + + Scenario: another scenario which should have isolated state + Given I have 42 cukes in my belly + And something that isn't defined + + Scenario: Parameterless lambdas + Given A statement with a simple match + Given A statement with a scoped argument + + Scenario: I can use body expressions + Given A statement with a body expression + + Scenario: Multi-param lambdas + Given I will give you 1 and 2.2 and three and 4 + + Scenario: use a table + Given this data table: + | first | last | + | Aslak | Hellesøy | + | Donald | Duck | diff --git a/pom.xml b/pom.xml index e8e2859251..078de7b7f3 100644 --- a/pom.xml +++ b/pom.xml @@ -192,6 +192,16 @@ android-examples ${project.version} + + io.cucumber + cucumber-java8 + ${project.version} + + + io.cucumber + cucumber-kotlin-java8 + ${project.version} + @@ -581,6 +591,7 @@ java8 + kotlin-java8