From 9b2f989b28aa885fbad5c11fe9e20b97f2affe8f Mon Sep 17 00:00:00 2001 From: Roman Rozenshteyn Date: Sun, 7 Nov 2021 08:56:11 -0800 Subject: [PATCH 1/2] [Java] Allow non-private classes to contain hooks and allow hook annotations in non-public methods (#2370) --- .../java/io/cucumber/java/MethodScanner.java | 32 ++++- .../io/cucumber/java/MethodScannerTest.java | 113 +++++++++++++++++- 2 files changed, 139 insertions(+), 6 deletions(-) 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/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") From 0158029962852af48c72f03db20516ebb03029dd Mon Sep 17 00:00:00 2001 From: Roman Rozenshteyn Date: Mon, 17 Jan 2022 09:25:51 -0800 Subject: [PATCH 2/2] [Java] Allow specifying individual classes in the glue option (cucumber#1707) --- core/README.md | 4 +- .../io/cucumber/core/options/Constants.java | 5 +- .../core/resource/ClasspathScanner.java | 24 +++++- .../cucumber/core/feature/GluePathTest.java | 28 +++++++ .../core/resource/ClasspathScannerTest.java | 73 ++++++++++++++++--- .../java/io/cucumber/guice/GuiceBackend.java | 2 +- .../io/cucumber/guice/GuiceBackendTest.java | 9 ++- .../java/io/cucumber/java/JavaBackend.java | 2 +- .../io/cucumber/java/JavaBackendTest.java | 5 +- .../java/individualclasssteps/StepsTwo.java | 12 +++ .../java/io/cucumber/java8/Java8Backend.java | 2 +- .../io/cucumber/java8/Java8BackendTest.java | 7 +- .../java8/individualclasssteps/StepsTwo.java | 15 ++++ junit-platform-engine/README.md | 4 +- .../junit/platform/engine/Constants.java | 5 +- .../io/cucumber/junit/CucumberOptions.java | 5 +- .../io/cucumber/spring/SpringBackend.java | 2 +- .../io/cucumber/testng/CucumberOptions.java | 5 +- 18 files changed, 175 insertions(+), 34 deletions(-) create mode 100644 java/src/test/java/io/cucumber/java/individualclasssteps/StepsTwo.java create mode 100644 java8/src/test/java/io/cucumber/java8/individualclasssteps/StepsTwo.java 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) 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/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/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