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 extends StepdefBody> clazz, Class extends StepdefBody> 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