diff --git a/core/README.md b/core/README.md
index cbb329c542..d5fd4979f8 100644
--- a/core/README.md
+++ b/core/README.md
@@ -43,8 +43,8 @@ cucumber.filter.tags= # a cucumber tag expression.
# only scenarios with matching tags are executed.
# example: @Cucumber and not (@Gherkin or @Zucchini)
-cucumber.glue= # comma separated package names.
- # example: com.example.glue
+cucumber.glue= # comma separated package or class names.
+ # example: com.example.glue,com.example.features.SomeFeature
cucumber.plugin= # comma separated plugin strings.
# example: pretty, json:path/to/report.json
diff --git a/core/src/main/java/io/cucumber/core/options/Constants.java b/core/src/main/java/io/cucumber/core/options/Constants.java
index b11f0ae895..c20bde5390 100644
--- a/core/src/main/java/io/cucumber/core/options/Constants.java
+++ b/core/src/main/java/io/cucumber/core/options/Constants.java
@@ -103,8 +103,9 @@ public final class Constants {
/**
* Property name to set the glue path: {@value}
*
- * A comma separated list of a classpath uri or package name e.g.:
- * {@code com.example.app.steps}.
+ * A comma separated list of a classpath uri or a package or a class name
+ * e.g.:
+ * {@code com.example.app.steps,com.example.app.features.SomeFeatureSteps}.
*
* @see io.cucumber.core.feature.GluePath
*/
diff --git a/core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java b/core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java
index efcc7a7026..84ad1fdc72 100644
--- a/core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java
+++ b/core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java
@@ -6,6 +6,7 @@
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@@ -38,6 +39,15 @@ public ClasspathScanner(Supplier classLoaderSupplier) {
this.classLoaderSupplier = classLoaderSupplier;
}
+ public List> scanForSubClasses(String packageOrClassName, Class parentClass) {
+ Optional> classFromName = safelyLoadClass(packageOrClassName, false);
+
+ return classFromName.isPresent() && !parentClass.equals(classFromName.get())
+ && parentClass.isAssignableFrom(classFromName.get())
+ ? Arrays.asList((Class extends T>) classFromName.get())
+ : scanForSubClassesInPackage(packageOrClassName, parentClass);
+ }
+
public List> scanForSubClassesInPackage(String packageName, Class parentClass) {
return scanForClassesInPackage(packageName, isSubClassOf(parentClass))
.stream()
@@ -96,17 +106,19 @@ private Function> processClassFiles(
) {
return baseDir -> classFile -> {
String fqn = determineFullyQualifiedClassName(baseDir, basePackageName, classFile);
- safelyLoadClass(fqn)
+ safelyLoadClass(fqn, true)
.filter(classFilter)
.ifPresent(classConsumer);
};
}
- private Optional> safelyLoadClass(String fqn) {
+ private Optional> safelyLoadClass(String fqn, boolean logWarning) {
try {
return Optional.ofNullable(getClassLoader().loadClass(fqn));
} catch (ClassNotFoundException | NoClassDefFoundError e) {
- log.warn(e, () -> "Failed to load class '" + fqn + "'.\n" + classPathScanningExplanation());
+ if (logWarning) {
+ log.warn(e, () -> "Failed to load class '" + fqn + "'.\n" + classPathScanningExplanation());
+ }
}
return Optional.empty();
}
@@ -115,4 +127,10 @@ public List> scanForClassesInPackage(String packageName) {
return scanForClassesInPackage(packageName, NULL_FILTER);
}
+ public List> getClasses(String packageOrClassName) {
+ Optional> classFromName = safelyLoadClass(packageOrClassName, false);
+ return classFromName.isPresent() ? Arrays.asList(classFromName.get())
+ : scanForClassesInPackage(packageOrClassName, NULL_FILTER);
+ }
+
}
diff --git a/core/src/test/java/io/cucumber/core/feature/GluePathTest.java b/core/src/test/java/io/cucumber/core/feature/GluePathTest.java
index 1134446f1b..21be3a3f9c 100644
--- a/core/src/test/java/io/cucumber/core/feature/GluePathTest.java
+++ b/core/src/test/java/io/cucumber/core/feature/GluePathTest.java
@@ -70,6 +70,15 @@ void can_parse_absolute_path_form() {
() -> assertThat(uri.getSchemeSpecificPart(), is("/com/example/app")));
}
+ @Test
+ void can_parse_absolute_path_form_class() {
+ URI uri = GluePath.parse("/com/example/app/Steps");
+
+ assertAll(
+ () -> assertThat(uri.getScheme(), is("classpath")),
+ () -> assertThat(uri.getSchemeSpecificPart(), is("/com/example/app/Steps")));
+ }
+
@Test
void can_parse_package_form() {
URI uri = GluePath.parse("com.example.app");
@@ -79,6 +88,15 @@ void can_parse_package_form() {
() -> assertThat(uri.getSchemeSpecificPart(), is("/com/example/app")));
}
+ @Test
+ void can_parse_package_form_class() {
+ URI uri = GluePath.parse("com.example.app.Steps");
+
+ assertAll(
+ () -> assertThat(uri.getScheme(), is("classpath")),
+ () -> assertThat(uri.getSchemeSpecificPart(), is("/com/example/app/Steps")));
+ }
+
@Test
void glue_path_must_have_class_path_scheme() {
Executable testMethod = () -> GluePath.parse("file:com/example/app");
@@ -105,6 +123,16 @@ void can_parse_windows_path_form() {
() -> assertThat(uri.getSchemeSpecificPart(), is(equalTo("/com/example/app"))));
}
+ @Test
+ @EnabledOnOs(OS.WINDOWS)
+ void can_parse_windows_path_form_class() {
+ URI uri = GluePath.parse("com\\example\\app\\Steps");
+
+ assertAll(
+ () -> assertThat(uri.getScheme(), is("classpath")),
+ () -> assertThat(uri.getSchemeSpecificPart(), is(equalTo("/com/example/app/Steps"))));
+ }
+
@Test
@EnabledOnOs(OS.WINDOWS)
void absolute_windows_path_form_is_not_valid() {
diff --git a/core/src/test/java/io/cucumber/core/resource/ClasspathScannerTest.java b/core/src/test/java/io/cucumber/core/resource/ClasspathScannerTest.java
index 1b2ce6ea60..e234375ffb 100644
--- a/core/src/test/java/io/cucumber/core/resource/ClasspathScannerTest.java
+++ b/core/src/test/java/io/cucumber/core/resource/ClasspathScannerTest.java
@@ -5,27 +5,16 @@
import io.cucumber.core.resource.test.ExampleClass;
import io.cucumber.core.resource.test.ExampleInterface;
import io.cucumber.core.resource.test.OtherClass;
-import org.hamcrest.CoreMatchers;
-import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
import java.io.IOException;
-import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Enumeration;
import java.util.List;
-import java.util.Vector;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
-import static java.util.Arrays.asList;
import static java.util.Collections.enumeration;
import static java.util.Collections.singletonList;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -70,6 +59,38 @@ void scanForSubClassesInNonExistingPackage() {
assertThat(classes, empty());
}
+ @Test
+ void scanForSubClassesWhenPackage() {
+ List> classes = scanner.scanForSubClasses(
+ "io.cucumber.core.resource.test",
+ ExampleInterface.class);
+
+ assertThat(classes, contains(ExampleClass.class));
+ }
+
+ @Test
+ void scanForSubClassesWhenClass() {
+ List> classes = scanner.scanForSubClasses(
+ "io.cucumber.core.resource.test.ExampleClass",
+ ExampleInterface.class);
+
+ assertThat(classes, contains(ExampleClass.class));
+ }
+
+ @Test
+ void scanForSubClassesWhenNonExistingPackage() {
+ List> classes = scanner
+ .scanForSubClasses("io.cucumber.core.resource.does.not.exist", ExampleInterface.class);
+ assertThat(classes, empty());
+ }
+
+ @Test
+ void scanForSubClassesWhenNonExistingClass() {
+ List> classes = scanner
+ .scanForSubClasses("io.cucumber.core.resource.test.NonExistentClass", ExampleInterface.class);
+ assertThat(classes, empty());
+ }
+
@Test
void scanForClassesInPackage() {
List> classes = scanner.scanForClassesInPackage("io.cucumber.core.resource.test");
@@ -104,4 +125,34 @@ protected URLConnection openConnection(URL u) {
containsString("Failed to find resources for 'bundle-resource:com/cucumber/bundle'"));
}
+ @Test
+ void getClassesWhenPackage() {
+ List> classes = scanner.getClasses("io.cucumber.core.resource.test");
+
+ assertThat(classes, containsInAnyOrder(
+ ExampleClass.class,
+ ExampleInterface.class,
+ OtherClass.class));
+
+ }
+
+ @Test
+ void getClassesWhenNonExistingPackage() {
+ List> classes = scanner.getClasses("io.cucumber.core.resource.does.not.exist");
+ assertThat(classes, empty());
+ }
+
+ @Test
+ void getClassesWhenClass() {
+ List> classes = scanner.getClasses("io.cucumber.core.resource.test.ExampleClass");
+
+ assertThat(classes, contains(ExampleClass.class));
+
+ }
+
+ @Test
+ void getClassesWhenNonExistingClass() {
+ List> classes = scanner.getClasses("io.cucumber.core.resource.test.NonExistentClass");
+ assertThat(classes, empty());
+ }
}
diff --git a/guice/src/main/java/io/cucumber/guice/GuiceBackend.java b/guice/src/main/java/io/cucumber/guice/GuiceBackend.java
index 7f7c8dd36a..ee8d40ecc7 100644
--- a/guice/src/main/java/io/cucumber/guice/GuiceBackend.java
+++ b/guice/src/main/java/io/cucumber/guice/GuiceBackend.java
@@ -29,7 +29,7 @@ public void loadGlue(Glue glue, List gluePaths) {
gluePaths.stream()
.filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme()))
.map(ClasspathSupport::packageName)
- .map(classFinder::scanForClassesInPackage)
+ .map(classFinder::getClasses)
.flatMap(Collection::stream)
.filter(InjectorSource.class::isAssignableFrom)
.forEach(container::addClass);
diff --git a/guice/src/test/java/io/cucumber/guice/GuiceBackendTest.java b/guice/src/test/java/io/cucumber/guice/GuiceBackendTest.java
index 74317e3361..b2180d908b 100644
--- a/guice/src/test/java/io/cucumber/guice/GuiceBackendTest.java
+++ b/guice/src/test/java/io/cucumber/guice/GuiceBackendTest.java
@@ -33,12 +33,19 @@ class GuiceBackendTest {
private ObjectFactory factory;
@Test
- void finds_injector_source_impls_by_classpath_url() {
+ void finds_injector_source_impls_by_package_classpath_url() {
GuiceBackend backend = new GuiceBackend(factory, classLoader);
backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/guice/integration")));
verify(factory).addClass(YourInjectorSource.class);
}
+ @Test
+ void finds_injector_source_impls_by_class_classpath_url() {
+ GuiceBackend backend = new GuiceBackend(factory, classLoader);
+ backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/guice/integration/YourInjectorSource")));
+ verify(factory).addClass(YourInjectorSource.class);
+ }
+
@Test
void world_and_snippet_methods_do_nothing() {
GuiceBackend backend = new GuiceBackend(factory, classLoader);
diff --git a/java/src/main/java/io/cucumber/java/JavaBackend.java b/java/src/main/java/io/cucumber/java/JavaBackend.java
index ee9375fe82..11931e2222 100644
--- a/java/src/main/java/io/cucumber/java/JavaBackend.java
+++ b/java/src/main/java/io/cucumber/java/JavaBackend.java
@@ -35,7 +35,7 @@ public void loadGlue(Glue glue, List gluePaths) {
gluePaths.stream()
.filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme()))
.map(ClasspathSupport::packageName)
- .map(classFinder::scanForClassesInPackage)
+ .map(classFinder::getClasses)
.flatMap(Collection::stream)
.forEach(aGlueClass -> scan(aGlueClass, (method, annotation) -> {
container.addClass(method.getDeclaringClass());
diff --git a/java/src/main/java/io/cucumber/java/MethodScanner.java b/java/src/main/java/io/cucumber/java/MethodScanner.java
index e79e72fd66..9af32bd04f 100644
--- a/java/src/main/java/io/cucumber/java/MethodScanner.java
+++ b/java/src/main/java/io/cucumber/java/MethodScanner.java
@@ -5,11 +5,16 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
import static io.cucumber.core.resource.ClasspathSupport.classPathScanningExplanation;
import static io.cucumber.java.InvalidMethodException.createInvalidMethodException;
import static java.lang.reflect.Modifier.isAbstract;
+import static java.lang.reflect.Modifier.isPrivate;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;
@@ -29,12 +34,15 @@ static void scan(Class> aClass, BiConsumer consumer) {
if (!isInstantiable(aClass)) {
return;
}
- for (Method method : safelyGetMethods(aClass)) {
+ for (Method method : safelyGetPublicMethods(aClass)) {
+ scan(consumer, aClass, method);
+ }
+ for (Method method : safelyGetNonPublicMethods(aClass)) {
scan(consumer, aClass, method);
}
}
- private static Method[] safelyGetMethods(Class> aClass) {
+ private static Method[] safelyGetPublicMethods(Class> aClass) {
try {
return aClass.getMethods();
} catch (NoClassDefFoundError e) {
@@ -44,8 +52,26 @@ private static Method[] safelyGetMethods(Class> aClass) {
return new Method[0];
}
+ private static List safelyGetNonPublicMethods(Class> aClass) {
+ try {
+ return Arrays.stream(aClass.getDeclaredMethods())
+ .filter(MethodScanner::hasAcceptableModifiers)
+ .collect(Collectors.toList());
+ } catch (NoClassDefFoundError e) {
+ log.warn(e,
+ () -> "Failed to load methods of class '" + aClass.getName() + "'.\n" + classPathScanningExplanation());
+ }
+ return Collections.emptyList();
+ }
+
+ private static boolean hasAcceptableModifiers(Method aMethod) {
+ return !isPrivate(aMethod.getModifiers())
+ && !isPublic(aMethod.getModifiers())
+ && !isAbstract(aMethod.getModifiers());
+ }
+
private static boolean isInstantiable(Class> clazz) {
- return isPublic(clazz.getModifiers())
+ return !isPrivate(clazz.getModifiers())
&& !isAbstract(clazz.getModifiers())
&& (isStatic(clazz.getModifiers()) || clazz.getEnclosingClass() == null);
}
diff --git a/java/src/test/java/io/cucumber/java/JavaBackendTest.java b/java/src/test/java/io/cucumber/java/JavaBackendTest.java
index 9df7a5698f..c1f2d3dc82 100644
--- a/java/src/test/java/io/cucumber/java/JavaBackendTest.java
+++ b/java/src/test/java/io/cucumber/java/JavaBackendTest.java
@@ -3,6 +3,7 @@
import io.cucumber.core.backend.Glue;
import io.cucumber.core.backend.ObjectFactory;
import io.cucumber.core.backend.StepDefinition;
+import io.cucumber.java.individualclasssteps.StepsTwo;
import io.cucumber.java.steps.Steps;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -48,9 +49,11 @@ void createBackend() {
@Test
void finds_step_definitions_by_classpath_url() {
- backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/java/steps")));
+ backend.loadGlue(glue, asList(URI.create("classpath:io/cucumber/java/steps"),
+ URI.create("classpath:io/cucumber/java/individualclasssteps/StepsTwo")));
backend.buildWorld();
verify(factory).addClass(Steps.class);
+ verify(factory).addClass(StepsTwo.class);
}
@Test
diff --git a/java/src/test/java/io/cucumber/java/MethodScannerTest.java b/java/src/test/java/io/cucumber/java/MethodScannerTest.java
index c58e486e5c..c8b86f2aca 100644
--- a/java/src/test/java/io/cucumber/java/MethodScannerTest.java
+++ b/java/src/test/java/io/cucumber/java/MethodScannerTest.java
@@ -29,10 +29,45 @@ void createBackend() {
}
@Test
- void scan_finds_annotated_methods() throws NoSuchMethodException {
- Method method = BaseSteps.class.getMethod("m");
+ void scan_finds_annotated_methods_in_public_class() throws NoSuchMethodException {
+ Method publicMethod = BaseSteps.class.getMethod("m");
+ Method packagePrivateMethod = BaseSteps.class.getDeclaredMethod("n");
+ Method protectedMethod = BaseSteps.class.getDeclaredMethod("o");
MethodScanner.scan(BaseSteps.class, backend);
- assertThat(scanResult, contains(new SimpleEntry<>(method, method.getAnnotations()[0])));
+ assertThat(scanResult,
+ contains(new SimpleEntry<>(publicMethod, publicMethod.getAnnotations()[0]),
+ new SimpleEntry<>(packagePrivateMethod, packagePrivateMethod.getAnnotations()[0]),
+ new SimpleEntry<>(protectedMethod, protectedMethod.getAnnotations()[0])));
+ }
+
+ @Test
+ void scan_finds_annotated_methods_in_protected_class() throws NoSuchMethodException {
+ Method publicMethod = ProtectedSteps.class.getMethod("m");
+ Method packagePrivateMethod = ProtectedSteps.class.getDeclaredMethod("n");
+ Method protectedMethod = ProtectedSteps.class.getDeclaredMethod("o");
+ MethodScanner.scan(ProtectedSteps.class, backend);
+ assertThat(scanResult,
+ contains(new SimpleEntry<>(publicMethod, publicMethod.getAnnotations()[0]),
+ new SimpleEntry<>(packagePrivateMethod, packagePrivateMethod.getAnnotations()[0]),
+ new SimpleEntry<>(protectedMethod, protectedMethod.getAnnotations()[0])));
+ }
+
+ @Test
+ void scan_finds_annotated_methods_in_package_private_class() throws NoSuchMethodException {
+ Method publicMethod = PackagePrivateSteps.class.getMethod("m");
+ Method packagePrivateMethod = PackagePrivateSteps.class.getDeclaredMethod("n");
+ Method protectedMethod = PackagePrivateSteps.class.getDeclaredMethod("o");
+ MethodScanner.scan(PackagePrivateSteps.class, backend);
+ assertThat(scanResult,
+ contains(new SimpleEntry<>(publicMethod, publicMethod.getAnnotations()[0]),
+ new SimpleEntry<>(packagePrivateMethod, packagePrivateMethod.getAnnotations()[0]),
+ new SimpleEntry<>(protectedMethod, protectedMethod.getAnnotations()[0])));
+ }
+
+ @Test
+ void scan_ignores_private_class() {
+ MethodScanner.scan(PrivateSteps.class, backend);
+ assertThat(scanResult, empty());
}
@Test
@@ -70,6 +105,78 @@ public static class BaseSteps {
public void m() {
}
+ @Before
+ void n() {
+ }
+
+ @Before
+ protected void o() {
+ }
+
+ @Before
+ private void p() {
+ }
+
+ }
+
+ protected static class ProtectedSteps {
+
+ @Before
+ public void m() {
+ }
+
+ @Before
+ void n() {
+ }
+
+ @Before
+ protected void o() {
+ }
+
+ @Before
+ private void p() {
+ }
+
+ }
+
+ static class PackagePrivateSteps {
+
+ @Before
+ public void m() {
+ }
+
+ @Before
+ void n() {
+ }
+
+ @Before
+ protected void o() {
+ }
+
+ @Before
+ private void p() {
+ }
+
+ }
+
+ private static class PrivateSteps {
+
+ @Before
+ public void m() {
+ }
+
+ @Before
+ void n() {
+ }
+
+ @Before
+ protected void o() {
+ }
+
+ @Before
+ private void p() {
+ }
+
}
@SuppressWarnings("InnerClassMayBeStatic")
diff --git a/java/src/test/java/io/cucumber/java/individualclasssteps/StepsTwo.java b/java/src/test/java/io/cucumber/java/individualclasssteps/StepsTwo.java
new file mode 100644
index 0000000000..db162d00f3
--- /dev/null
+++ b/java/src/test/java/io/cucumber/java/individualclasssteps/StepsTwo.java
@@ -0,0 +1,12 @@
+package io.cucumber.java.individualclasssteps;
+
+import io.cucumber.java.en.Given;
+
+public class StepsTwo {
+
+ @Given("test")
+ public void test() {
+
+ }
+
+}
diff --git a/java8/src/main/java/io/cucumber/java8/Java8Backend.java b/java8/src/main/java/io/cucumber/java8/Java8Backend.java
index 1f46a48e9f..3fd100c77e 100644
--- a/java8/src/main/java/io/cucumber/java8/Java8Backend.java
+++ b/java8/src/main/java/io/cucumber/java8/Java8Backend.java
@@ -44,7 +44,7 @@ public void loadGlue(Glue glue, List gluePaths) {
gluePaths.stream()
.filter(gluePath -> ClasspathSupport.CLASSPATH_SCHEME.equals(gluePath.getScheme()))
.map(ClasspathSupport::packageName)
- .map(basePackageName -> classFinder.scanForSubClassesInPackage(basePackageName, LambdaGlue.class))
+ .map(basePackageName -> classFinder.scanForSubClasses(basePackageName, LambdaGlue.class))
.flatMap(Collection::stream)
.filter(glueClass -> !glueClass.isInterface())
.filter(glueClass -> glueClass.getConstructors().length > 0)
diff --git a/java8/src/test/java/io/cucumber/java8/Java8BackendTest.java b/java8/src/test/java/io/cucumber/java8/Java8BackendTest.java
index cbd0c5757a..b5a7135bb3 100644
--- a/java8/src/test/java/io/cucumber/java8/Java8BackendTest.java
+++ b/java8/src/test/java/io/cucumber/java8/Java8BackendTest.java
@@ -2,6 +2,7 @@
import io.cucumber.core.backend.Glue;
import io.cucumber.core.backend.ObjectFactory;
+import io.cucumber.java8.individualclasssteps.StepsTwo;
import io.cucumber.java8.steps.Steps;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -10,9 +11,9 @@
import org.mockito.junit.jupiter.MockitoExtension;
import java.net.URI;
+import java.util.Arrays;
import static java.lang.Thread.currentThread;
-import static java.util.Collections.singletonList;
import static org.mockito.Mockito.verify;
@ExtendWith({ MockitoExtension.class })
@@ -33,9 +34,11 @@ void createBackend() {
@Test
void finds_step_definitions_by_classpath_url() {
- backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/java8/steps")));
+ backend.loadGlue(glue, Arrays.asList(URI.create("classpath:io/cucumber/java8/steps"),
+ URI.create("classpath:io/cucumber/java8/individualclasssteps/StepsTwo")));
backend.buildWorld();
verify(factory).addClass(Steps.class);
+ verify(factory).addClass(StepsTwo.class);
}
}
diff --git a/java8/src/test/java/io/cucumber/java8/individualclasssteps/StepsTwo.java b/java8/src/test/java/io/cucumber/java8/individualclasssteps/StepsTwo.java
new file mode 100644
index 0000000000..0aa68b3e6e
--- /dev/null
+++ b/java8/src/test/java/io/cucumber/java8/individualclasssteps/StepsTwo.java
@@ -0,0 +1,15 @@
+package io.cucumber.java8.individualclasssteps;
+
+import io.cucumber.java8.En;
+
+public class StepsTwo implements En {
+
+ public StepsTwo() {
+
+ Given("another test", () -> {
+
+ });
+
+ }
+
+}
diff --git a/junit-platform-engine/README.md b/junit-platform-engine/README.md
index f06b647047..ab0a8f1cfc 100644
--- a/junit-platform-engine/README.md
+++ b/junit-platform-engine/README.md
@@ -295,8 +295,8 @@ cucumber.filter.tags= # a cucumber tag e
# JUnit 5 prefer using JUnit 5s discovery request filters
# or JUnit 5 tag expressions instead.
-cucumber.glue= # comma separated package names.
- # example: com.example.glue
+cucumber.glue= # comma separated package or class names.
+ # example: com.example.glue,com.example.features.SomeFeature
cucumber.junit-platform.naming-strategy= # long or short.
# default: short
diff --git a/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java b/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java
index d1edcddcf4..010262caec 100644
--- a/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java
+++ b/junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java
@@ -78,8 +78,9 @@ public final class Constants {
/**
* Property name to set the glue path: {@value}
*
- * A comma separated list of a classpath uri or package name e.g.:
- * {@code com.example.app.steps}.
+ * A comma separated list of a classpath uri or a package or a class name
+ * e.g.:
+ * {@code com.example.app.steps,com.example.app.features.SomeFeatureSteps}.
*
* @see io.cucumber.core.feature.GluePath
*/
diff --git a/junit/src/main/java/io/cucumber/junit/CucumberOptions.java b/junit/src/main/java/io/cucumber/junit/CucumberOptions.java
index cdc8ba9acf..9c167d5eac 100644
--- a/junit/src/main/java/io/cucumber/junit/CucumberOptions.java
+++ b/junit/src/main/java/io/cucumber/junit/CucumberOptions.java
@@ -36,8 +36,9 @@
String[] features() default {};
/**
- * Package to load glue code (step definitions, hooks and plugins) from.
- * E.g: {@code com.example.app}
+ * Packages or classes to load glue code (step definitions, hooks and
+ * plugins) from. E.g:
+ * {@code com.example.app, com.example.app.features.SomeFeatureSteps}
*
* When no glue is provided, Cucumber will use the package of the annotated
* class. For example, if the annotated class is
diff --git a/spring/src/main/java/io/cucumber/spring/SpringBackend.java b/spring/src/main/java/io/cucumber/spring/SpringBackend.java
index 8aadb97d08..6b4eae1983 100644
--- a/spring/src/main/java/io/cucumber/spring/SpringBackend.java
+++ b/spring/src/main/java/io/cucumber/spring/SpringBackend.java
@@ -29,7 +29,7 @@ public void loadGlue(Glue glue, List gluePaths) {
gluePaths.stream()
.filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme()))
.map(ClasspathSupport::packageName)
- .map(classFinder::scanForClassesInPackage)
+ .map(classFinder::getClasses)
.flatMap(Collection::stream)
.filter((Class clazz) -> clazz.getAnnotation(CucumberContextConfiguration.class) != null)
.forEach(container::addClass);
diff --git a/testng/src/main/java/io/cucumber/testng/CucumberOptions.java b/testng/src/main/java/io/cucumber/testng/CucumberOptions.java
index 84d6abd366..9e7f0987c0 100644
--- a/testng/src/main/java/io/cucumber/testng/CucumberOptions.java
+++ b/testng/src/main/java/io/cucumber/testng/CucumberOptions.java
@@ -36,8 +36,9 @@
String[] features() default {};
/**
- * Package to load glue code (step definitions, hooks and plugins) from.
- * E.g: {@code com.example.app}
+ * Packages or classes to load glue code (step definitions, hooks and
+ * plugins) from. E.g:
+ * {@code com.example.app, com.example.app.features.SomeFeatureSteps}
*
* When no glue is provided, Cucumber will use the package of the annotated
* class. For example, if the annotated class is