From 4d737c2b99dd41020d6434ec86fa4e4edfcd38e6 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 25 Nov 2025 16:27:06 +0100 Subject: [PATCH 01/14] Support manual glue class registration --- .../io/cucumber/core/backend/Backend.java | 12 +++++ .../cucumber/core/cli/CommandlineOptions.java | 2 + .../options/CommandlineOptionsParser.java | 4 ++ .../cucumber/core/options/RuntimeOptions.java | 13 ++++++ .../core/options/RuntimeOptionsBuilder.java | 12 +++++ .../core/resource/ClasspathScanner.java | 2 +- .../java/io/cucumber/core/runner/Options.java | 5 ++ .../java/io/cucumber/core/runner/Runner.java | 7 ++- .../java/io/cucumber/java/JavaBackend.java | 36 ++++++++++++--- .../java/io/cucumber/java8/Java8Backend.java | 46 +++++++++++++++---- 10 files changed, 118 insertions(+), 21 deletions(-) diff --git a/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java b/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java index a4787b2560..aad2cfaa32 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java +++ b/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java @@ -5,6 +5,7 @@ import java.net.URI; import java.util.List; +import java.util.Set; @API(status = API.Status.STABLE) public interface Backend { @@ -20,6 +21,17 @@ default void loadGlue(Glue glue, List gluePaths) { } + /** + * Invoked once before all features. This is where steps and hooks should be + * loaded. + * + * @param glue Glue that provides the steps to be executed. + * @param glueClassNames The classes of glue to be loaded. + */ + default void loadGlueClasses(Glue glue, Set glueClassNames) { + // TODO: Refactor out a request object. + } + /** * Invoked before a new scenario starts. Implementations should do any * necessary setup of new, isolated state here. Additional scenario scoped diff --git a/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java b/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java index 3b97f40c0b..2de52ed7ff 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java +++ b/cucumber-core/src/main/java/io/cucumber/core/cli/CommandlineOptions.java @@ -35,6 +35,8 @@ public final class CommandlineOptions { public static final String GLUE = "--glue"; public static final String GLUE_SHORT = "-g"; + public static final String GLUE_CLASS = "--glue-class"; + public static final String TAGS = "--tags"; public static final String TAGS_SHORT = "-t"; diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java index 9eb16fceea..70b66a092e 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/CommandlineOptionsParser.java @@ -32,6 +32,7 @@ import static io.cucumber.core.cli.CommandlineOptions.DRY_RUN; import static io.cucumber.core.cli.CommandlineOptions.DRY_RUN_SHORT; import static io.cucumber.core.cli.CommandlineOptions.GLUE; +import static io.cucumber.core.cli.CommandlineOptions.GLUE_CLASS; import static io.cucumber.core.cli.CommandlineOptions.GLUE_SHORT; import static io.cucumber.core.cli.CommandlineOptions.HELP; import static io.cucumber.core.cli.CommandlineOptions.HELP_SHORT; @@ -128,6 +129,9 @@ private RuntimeOptionsBuilder parse(List args) { String gluePath = removeArgFor(arg, args); URI parse = GluePath.parse(gluePath); parsedOptions.addGlue(parse); + } else if (arg.equals(GLUE_CLASS)) { + String glueClassName = removeArgFor(arg, args); + parsedOptions.addGlueClass(glueClassName); } else if (arg.equals(TAGS) || arg.equals(TAGS_SHORT)) { parsedOptions.addTagFilter(TagExpressionParser.parse(removeArgFor(arg, args))); } else if (arg.equals(PUBLISH)) { diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java index 6952b1133b..c4c88668b2 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java @@ -15,6 +15,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -30,6 +31,7 @@ import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableMap; +import static java.util.Collections.unmodifiableSet; public final class RuntimeOptions implements io.cucumber.core.feature.Options, @@ -40,6 +42,7 @@ public final class RuntimeOptions implements io.cucumber.core.eventbus.Options { private final List glue = new ArrayList<>(); + private final Set glueClasses = new HashSet<>(); private final List tagExpressions = new ArrayList<>(); private final List nameFilters = new ArrayList<>(); private final List featurePaths = new ArrayList<>(); @@ -151,6 +154,11 @@ public List getGlue() { return unmodifiableList(glue); } + @Override + public Set getGlueClasses() { + return unmodifiableSet(glueClasses); + } + @Override public boolean isDryRun() { return dryRun; @@ -192,6 +200,11 @@ void setGlue(List parsedGlue) { glue.addAll(parsedGlue); } + void setGlueClasses(Set parsedGlue) { + glueClasses.clear(); + glueClasses.addAll(parsedGlue); + } + @Override public List getFeaturePaths() { return featurePaths.stream() diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java index 46fd9404e0..6c89709c04 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java @@ -13,6 +13,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; @@ -23,6 +24,7 @@ public final class RuntimeOptionsBuilder { private final List parsedNameFilters = new ArrayList<>(); private final List parsedFeaturePaths = new ArrayList<>(); private final List parsedGlue = new ArrayList<>(); + private final Set parsedGlueClasses = new HashSet<>(); private final List plugins = new ArrayList<>(); private @Nullable List parsedRerunPaths = null; private @Nullable Integer parsedThreads = null; @@ -60,6 +62,12 @@ public RuntimeOptionsBuilder addGlue(URI glue) { return this; } + public RuntimeOptionsBuilder addGlueClass(String glueClassName) { + // TODO: Support Class ? + parsedGlueClasses.add(glueClassName); + return this; + } + public RuntimeOptionsBuilder addNameFilter(Pattern pattern) { this.parsedNameFilters.add(pattern); return this; @@ -129,6 +137,10 @@ public RuntimeOptions build(RuntimeOptions runtimeOptions) { runtimeOptions.setGlue(this.parsedGlue); } + if (!this.parsedGlueClasses.isEmpty()) { + runtimeOptions.setGlueClasses(this.parsedGlueClasses); + } + runtimeOptions.addPlugins(this.plugins); if (parsedObjectFactoryClass != null) { diff --git a/cucumber-core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java b/cucumber-core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java index 9b3e31781f..c165e06252 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java +++ b/cucumber-core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java @@ -101,7 +101,7 @@ private Function> processClassFiles( }; } - private Optional> safelyLoadClass(String fqn) { + public Optional> safelyLoadClass(String fqn) { try { return Optional.ofNullable(getClassLoader().loadClass(fqn)); } catch (Throwable e) { diff --git a/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java b/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java index 4fb2406dfa..b1073ba0db 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java @@ -6,7 +6,9 @@ import org.jspecify.annotations.Nullable; import java.net.URI; +import java.util.Collections; import java.util.List; +import java.util.Set; public interface Options { @@ -22,4 +24,7 @@ public interface Options { @Nullable Class getUuidGeneratorClass(); + default Set getGlueClasses() { + return Collections.emptySet(); + } } diff --git a/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java b/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java index bfd00e39fc..b877d44bc8 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java @@ -19,7 +19,6 @@ import io.cucumber.plugin.event.SnippetsSuggestedEvent; import io.cucumber.plugin.event.SnippetsSuggestedEvent.Suggestion; -import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -50,11 +49,11 @@ public Runner( this.backends = backends; this.glue = new CachingGlue(bus); this.objectFactory = objectFactory; - List gluePaths = runnerOptions.getGlue(); - log.debug(() -> "Loading glue from " + gluePaths); + log.debug(() -> "Loading glue from " + runnerOptions.getGlue()); for (Backend backend : backends) { log.debug(() -> "Loading glue for backend " + backend.getClass().getName()); - backend.loadGlue(this.glue, gluePaths); + backend.loadGlue(this.glue, runnerOptions.getGlue()); + backend.loadGlueClasses(this.glue, runnerOptions.getGlueClasses()); } } diff --git a/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java b/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java index 6236643db6..1be8d6470b 100644 --- a/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java +++ b/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java @@ -11,7 +11,10 @@ import java.net.URI; import java.util.Collection; import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; +import java.util.stream.Collectors; import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME; import static io.cucumber.java.MethodScanner.scan; @@ -30,18 +33,39 @@ final class JavaBackend implements Backend { @Override public void loadGlue(Glue glue, List gluePaths) { + loadGlueClassesImpl(glue, scanForClasses(gluePaths)); + } + + @Override + public void loadGlueClasses(Glue glue, Set glueClassNames) { + Set> glueClasses = glueClassNames.stream() + .map(classFinder::safelyLoadClass) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + + loadGlueClassesImpl(glue, glueClasses); + } + + private void loadGlueClassesImpl(Glue glue, Set> glueClasses) { GlueAdaptor glueAdaptor = new GlueAdaptor(lookup, glue); + glueClasses.forEach(aGlueClass -> processClass(aGlueClass, glueAdaptor)); + } - gluePaths.stream() + private Set> scanForClasses(List gluePaths) { + return gluePaths.stream() .filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme())) .map(ClasspathSupport::packageName) .map(classFinder::scanForClassesInPackage) .flatMap(Collection::stream) - .distinct() - .forEach(aGlueClass -> scan(aGlueClass, (method, annotation) -> { - container.addClass(method.getDeclaringClass()); - glueAdaptor.addDefinition(method, annotation); - })); + .collect(Collectors.toSet()); + } + + private void processClass(Class aGlueClass, GlueAdaptor glueAdaptor) { + scan(aGlueClass, (method, annotation) -> { + container.addClass(method.getDeclaringClass()); + glueAdaptor.addDefinition(method, annotation); + }); } @Override diff --git a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java index 5d56b37e08..bdaaa93688 100644 --- a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java +++ b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java @@ -13,7 +13,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; +import java.util.stream.Collectors; import static io.cucumber.java8.LambdaGlueRegistry.CLOSED; import static java.util.Objects.requireNonNull; @@ -35,20 +38,43 @@ final class Java8Backend implements Backend { @Override public void loadGlue(Glue glue, List gluePaths) { + loadGlueClassesImpl(glue, scanForClasses(gluePaths)); + } + + @Override + public void loadGlueClasses(Glue glue, Set glueClassNames) { + Set> glueClasses = glueClassNames.stream() + .map(classFinder::safelyLoadClass) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + + loadGlueClassesImpl(glue, glueClasses); + } + + private void loadGlueClassesImpl(Glue glue, Set> glueClasses) { this.glue = new ClosureAwareGlueRegistry(glue); - // Scan for Java8 style glue (lambdas) - gluePaths.stream() + glueClasses.stream() + .filter(aClass -> !LambdaGlue.class.equals(aClass) && LambdaGlue.class.isAssignableFrom(aClass)) + .map(aClass -> (Class) aClass.asSubclass(LambdaGlue.class)) + .filter(glueClass -> !glueClass.isInterface()) + .filter(glueClass -> glueClass.getConstructors().length > 0) + .forEach(this::processClass); + } + + private Set> scanForClasses(List gluePaths) { + return gluePaths.stream() .filter(gluePath -> ClasspathSupport.CLASSPATH_SCHEME.equals(gluePath.getScheme())) .map(ClasspathSupport::packageName) - .map(basePackageName -> classFinder.scanForSubClassesInPackage(basePackageName, LambdaGlue.class)) + // Scan for Java8 style glue (lambdas) + .map(classFinder::scanForClassesInPackage) .flatMap(Collection::stream) - .filter(glueClass -> !glueClass.isInterface()) - .filter(glueClass -> glueClass.getConstructors().length > 0) - .distinct() - .forEach(glueClass -> { - container.addClass(glueClass); - lambdaGlueClasses.add(glueClass); - }); + .collect(Collectors.toSet()); + } + + private void processClass(Class glueClass) { + container.addClass(glueClass); + lambdaGlueClasses.add(glueClass); } @Override From a721d45183794b084316e095d9d34cb71ec3639f Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 25 Nov 2025 16:47:47 +0100 Subject: [PATCH 02/14] Touchups --- .../src/main/java/io/cucumber/java8/Java8Backend.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java index bdaaa93688..4b7d28d844 100644 --- a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java +++ b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java @@ -55,6 +55,7 @@ public void loadGlueClasses(Glue glue, Set glueClassNames) { private void loadGlueClassesImpl(Glue glue, Set> glueClasses) { this.glue = new ClosureAwareGlueRegistry(glue); glueClasses.stream() + // Filter Java8 style glue (lambdas) .filter(aClass -> !LambdaGlue.class.equals(aClass) && LambdaGlue.class.isAssignableFrom(aClass)) .map(aClass -> (Class) aClass.asSubclass(LambdaGlue.class)) .filter(glueClass -> !glueClass.isInterface()) @@ -66,7 +67,6 @@ private Set> scanForClasses(List gluePaths) { return gluePaths.stream() .filter(gluePath -> ClasspathSupport.CLASSPATH_SCHEME.equals(gluePath.getScheme())) .map(ClasspathSupport::packageName) - // Scan for Java8 style glue (lambdas) .map(classFinder::scanForClassesInPackage) .flatMap(Collection::stream) .collect(Collectors.toSet()); From f740e6e969c008df1f3ccbfa4f2e5faeecea3940 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Thu, 18 Dec 2025 15:51:21 +0100 Subject: [PATCH 03/14] Work in progress --- .../java/io/cucumber/core/options/Constants.java | 10 ++++++++++ .../core/options/CucumberPropertiesParser.java | 6 ++++++ .../io/cucumber/core/options/RuntimeOptions.java | 2 +- .../core/options/RuntimeOptionsBuilder.java | 1 - .../cucumber/core/resource/ClasspathScanner.java | 8 ++++++++ .../resources/io/cucumber/core/options/USAGE.txt | 16 +++++++++++++--- .../main/java/io/cucumber/java/JavaBackend.java | 5 +---- .../java/io/cucumber/java8/Java8Backend.java | 5 +---- 8 files changed, 40 insertions(+), 13 deletions(-) diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java b/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java index 637c79edea..3a0203408c 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java @@ -118,6 +118,16 @@ public final class Constants { */ public static final String GLUE_PROPERTY_NAME = "cucumber.glue"; + /** + * Property name to set the glue classes: {@value} + *

+ * A comma separated list fully qualified class names e.g.: + * {@code com.example.app.Stepdefinitions}. + * + * @see io.cucumber.core.feature.GluePath + */ + public static final String GLUE_CLASSES_PROPERTY_NAME = "cucumber.glue-classes"; + /** * Property name used to select a specific object factory implementation: * {@value} diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java index 6ea80ffde0..edb0b08be7 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberPropertiesParser.java @@ -20,6 +20,7 @@ import static io.cucumber.core.options.Constants.FEATURES_PROPERTY_NAME; import static io.cucumber.core.options.Constants.FILTER_NAME_PROPERTY_NAME; import static io.cucumber.core.options.Constants.FILTER_TAGS_PROPERTY_NAME; +import static io.cucumber.core.options.Constants.GLUE_CLASSES_PROPERTY_NAME; import static io.cucumber.core.options.Constants.GLUE_PROPERTY_NAME; import static io.cucumber.core.options.Constants.OBJECT_FACTORY_PROPERTY_NAME; import static io.cucumber.core.options.Constants.OPTIONS_PROPERTY_NAME; @@ -92,6 +93,11 @@ public RuntimeOptionsBuilder parse(CucumberPropertiesProvider properties) { splitAndMap(GluePath::parse), builder::addGlue); + parseAll(properties, + GLUE_CLASSES_PROPERTY_NAME, + splitAndMap(identity()), + builder::addGlueClass); + parse(properties, OBJECT_FACTORY_PROPERTY_NAME, ObjectFactoryParser::parseObjectFactory, diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java index c4c88668b2..a178abae72 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java @@ -79,7 +79,7 @@ void addDefaultSummaryPrinter() { } void addDefaultGlueIfAbsent() { - if (glue.isEmpty()) { + if (glue.isEmpty() && glueClasses.isEmpty()) { glue.add(rootPackageUri()); } } diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java index 6c89709c04..de77b615a3 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptionsBuilder.java @@ -63,7 +63,6 @@ public RuntimeOptionsBuilder addGlue(URI glue) { } public RuntimeOptionsBuilder addGlueClass(String glueClassName) { - // TODO: Support Class ? parsedGlueClasses.add(glueClassName); return this; } diff --git a/cucumber-core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java b/cucumber-core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java index c165e06252..ef7a90a865 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java +++ b/cucumber-core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java @@ -112,6 +112,14 @@ public Optional> safelyLoadClass(String fqn) { return Optional.empty(); } + public Class loadClass(String fqn) { + try { + return getClassLoader().loadClass(fqn); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + throw new IllegalArgumentException("Could not to load class '" + fqn + "'", e); + } + } + public List> scanForClassesInPackage(String packageName) { return scanForClassesInPackage(packageName, aClass -> true); } diff --git a/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt b/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt index a46b2d4e9f..6768765c49 100644 --- a/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt +++ b/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt @@ -7,9 +7,16 @@ Options: -g, --glue PATH Package to load glue code (step definitions, hooks and plugins) from - e.g: com.example.app. When not - provided Cucumber will search the - classpath. + e.g: com.example.app. + When neither glue, nor glue classes + are provided Cucumber will search + the classpath. + + --glue-class FQCN A fully qualified glue class name + e.g: com.example.app.StepDefinitions. + When neither glue, nor glue classes + are provided Cucumber will search + the classpath. -p, --plugin PLUGIN[:[PATH|[URI [OPTIONS]]] Register a plugin. @@ -147,6 +154,9 @@ cucumber.filter.tags= # a cucumber tag expression. cucumber.glue= # comma separated package names. # example: com.example.glue +cucumber.glue-classes= # comma separated class names. + # example: com.example.glue.StepDefinitions + cucumber.plugin= # comma separated plugin strings. # example: pretty, json:path/to/report.json diff --git a/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java b/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java index 1be8d6470b..fd0dc9ef2c 100644 --- a/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java +++ b/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java @@ -11,7 +11,6 @@ import java.net.URI; import java.util.Collection; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -39,9 +38,7 @@ public void loadGlue(Glue glue, List gluePaths) { @Override public void loadGlueClasses(Glue glue, Set glueClassNames) { Set> glueClasses = glueClassNames.stream() - .map(classFinder::safelyLoadClass) - .filter(Optional::isPresent) - .map(Optional::get) + .map(classFinder::loadClass) .collect(Collectors.toSet()); loadGlueClassesImpl(glue, glueClasses); diff --git a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java index 4b7d28d844..64d35e4651 100644 --- a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java +++ b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -44,9 +43,7 @@ public void loadGlue(Glue glue, List gluePaths) { @Override public void loadGlueClasses(Glue glue, Set glueClassNames) { Set> glueClasses = glueClassNames.stream() - .map(classFinder::safelyLoadClass) - .filter(Optional::isPresent) - .map(Optional::get) + .map(classFinder::loadClass) .collect(Collectors.toSet()); loadGlueClassesImpl(glue, glueClasses); From 2fc052d8ce3a6cfa711725b8a4928e17e65fe810 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 28 Dec 2025 19:42:20 +0100 Subject: [PATCH 04/14] Implement for JUnit Platform --- cucumber-core/README.md | 4 ++-- .../main/java/io/cucumber/core/options/Constants.java | 2 +- .../main/resources/io/cucumber/core/options/USAGE.txt | 2 +- cucumber-junit-platform-engine/README.md | 3 +++ .../io/cucumber/junit/platform/engine/Constants.java | 10 ++++++++++ .../junit/platform/engine/CucumberConfiguration.java | 11 +++++++++++ 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/cucumber-core/README.md b/cucumber-core/README.md index 67fe0a8dc0..89976ccecf 100644 --- a/cucumber-core/README.md +++ b/cucumber-core/README.md @@ -47,8 +47,8 @@ cucumber.filter.tags= # a cucumber tag expression. # example: @Cucumber and not (@Gherkin or @Zucchini) cucumber.glue= # comma separated package names. - # example: com.example.glue - + # example: com.example.StepDefinitionsA, com.example.StepDefinitionsB + cucumber.plugin= # comma separated plugin strings. # example: pretty, json:path/to/report.json # example: com.example.MyCustomPlugin:path/to/report.xml diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java b/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java index 3a0203408c..1634c0b093 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/Constants.java @@ -122,7 +122,7 @@ public final class Constants { * Property name to set the glue classes: {@value} *

* A comma separated list fully qualified class names e.g.: - * {@code com.example.app.Stepdefinitions}. + * {@code com.example.StepDefinitionsA, com.example.StepDefinitionsB}. * * @see io.cucumber.core.feature.GluePath */ diff --git a/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt b/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt index 6768765c49..cab0512552 100644 --- a/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt +++ b/cucumber-core/src/main/resources/io/cucumber/core/options/USAGE.txt @@ -155,7 +155,7 @@ cucumber.glue= # comma separated package names. # example: com.example.glue cucumber.glue-classes= # comma separated class names. - # example: com.example.glue.StepDefinitions + # example: com.example.StepDefinitionsA, com.example.StepDefinitionsB cucumber.plugin= # comma separated plugin strings. # example: pretty, json:path/to/report.json diff --git a/cucumber-junit-platform-engine/README.md b/cucumber-junit-platform-engine/README.md index e23a65fca5..8eb01f0d9b 100644 --- a/cucumber-junit-platform-engine/README.md +++ b/cucumber-junit-platform-engine/README.md @@ -467,6 +467,9 @@ cucumber.filter.tags= # a cucumber tag cucumber.glue= # comma separated package names. # example: com.example.glue +cucumber.glue-classes= # comma separated class names. + # example: com.example.StepDefinitionsA, com.example.StepDefinitionsB + cucumber.junit-platform.discovery.as-root-engine # true or false # default: true # enable discovery when used as a root engine. diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java index 0fb3961ba1..5e191bc4dd 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java @@ -131,6 +131,16 @@ public final class Constants { */ public static final String GLUE_PROPERTY_NAME = io.cucumber.core.options.Constants.GLUE_PROPERTY_NAME; + /** + * Property name to set the glue classes: {@value} + *

+ * A comma separated list fully qualified class names e.g.: + * {@code com.example.StepDefinitionsA, com.example.StepDefinitionsB}. + * + * @see io.cucumber.core.feature.GluePath + */ + public static final String GLUE_CLASSES_PROPERTY_NAME = io.cucumber.core.options.Constants.GLUE_CLASSES_PROPERTY_NAME; + /** * Property name used to configure the naming strategy: {@value} *

diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java index c6ea7e9d49..d5762079ec 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java @@ -40,6 +40,7 @@ import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.FILTER_NAME_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.FILTER_TAGS_PROPERTY_NAME; +import static io.cucumber.junit.platform.engine.Constants.GLUE_CLASSES_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.JUNIT_PLATFORM_NAMING_STRATEGY_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.OBJECT_FACTORY_PROPERTY_NAME; @@ -150,6 +151,16 @@ public List getGlue() { .collect(Collectors.toList()); } + @Override + public Set getGlueClasses() { + return configurationParameters + .get(GLUE_CLASSES_PROPERTY_NAME, s -> Arrays.asList(s.split(","))) + .orElse(Collections.emptyList()) + .stream() + .map(String::trim) + .collect(Collectors.toSet()); + } + @Override public boolean isDryRun() { return configurationParameters From be1b4dd657490075730382c222e28631707fe775 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 28 Dec 2025 20:25:52 +0100 Subject: [PATCH 05/14] Implement for TestNG --- .../CucumberOptionsAnnotationParser.java | 20 ++++++++++++++--- .../CucumberOptionsAnnotationParserTest.java | 2 +- .../io/cucumber/testng/CucumberOptions.java | 22 +++++++++++++++++++ .../testng/TestNGCucumberOptionsProvider.java | 10 +++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java index dc21e6f983..013a25f15b 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/CucumberOptionsAnnotationParser.java @@ -107,25 +107,31 @@ private void addSnippets(CucumberOptions options, RuntimeOptionsBuilder args) { } private void addGlue(CucumberOptions options, RuntimeOptionsBuilder args) { - boolean hasExtraGlue = options.extraGlue().length > 0; - boolean hasGlue = options.glue().length > 0; + boolean hasExtraGlue = options.extraGlue().length > 0 || options.extraGlueGlasses().length > 0; + boolean hasGlue = options.glue().length > 0 || options.glueGlasses().length > 0; if (hasExtraGlue && hasGlue) { - throw new CucumberException("glue and extraGlue cannot be specified at the same time"); + throw new CucumberException("glue(Classes) and extraGlue(Classes) cannot be specified at the same time"); } + Class[] glueClasses = {}; String[] gluePaths = {}; if (hasExtraGlue) { gluePaths = options.extraGlue(); + glueClasses = options.extraGlueGlasses(); } if (hasGlue) { gluePaths = options.glue(); + glueClasses = options.glueGlasses(); overridingGlueSpecified = true; } for (String glue : gluePaths) { args.addGlue(GluePath.parse(glue)); } + for (Class glueClass : glueClasses) { + args.addGlueClass(glueClass.getName()); + } } private void addFeatures(CucumberOptions options, RuntimeOptionsBuilder args) { @@ -195,8 +201,16 @@ public interface CucumberOptions { String[] glue(); + default Class[] glueGlasses() { + return new Class[0]; + } + String[] extraGlue(); + default Class[] extraGlueGlasses() { + return new Class[0]; + } + String tags(); String[] plugin(); diff --git a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java index 63b20fea9b..1f11ad4c8d 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/options/CucumberOptionsAnnotationParserTest.java @@ -250,7 +250,7 @@ void cannot_create_with_glue_and_extra_glue() { Executable testMethod = () -> parser().parse(ClassWithGlueAndExtraGlue.class).build(); CucumberException actualThrown = assertThrows(CucumberException.class, testMethod); assertThat("Unexpected exception message", actualThrown.getMessage(), - is(equalTo("glue and extraGlue cannot be specified at the same time"))); + is(equalTo("glue(Classes) and extraGlue(Classes) cannot be specified at the same time"))); } @Test diff --git a/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java b/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java index 62ea3ac8f7..a59e8a6087 100644 --- a/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java +++ b/cucumber-testng/src/main/java/io/cucumber/testng/CucumberOptions.java @@ -70,6 +70,18 @@ */ String[] glue() default {}; + /** + * Classes with glue code. + *

+ * When no glue is provided, Cucumber will use the package of the annotated + * class. For example, if the annotated class is + * {@code com.example.RunCucumber} then glue is assumed to be located in + * {@code com.example}. + * + * @return list of class names + */ + Class[] glueClasses() default {}; + /** * Package to load additional glue code (step definitions, hooks and * plugins) from. E.g: {@code com.example.app} @@ -81,6 +93,16 @@ */ String[] extraGlue() default {}; + /** + * Addtional classes with glue code. + *

+ * These classes are used in addition to the default described in + * {@code #glue}. + * + * @return list of class names + */ + Class[] extraGlueClasses() default {}; + /** * Only run scenarios tagged with tags matching * Tag Expression. diff --git a/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java b/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java index 6de2890a4b..a035d7bcce 100644 --- a/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java +++ b/cucumber-testng/src/main/java/io/cucumber/testng/TestNGCucumberOptionsProvider.java @@ -57,11 +57,21 @@ public String[] glue() { return annotation.glue(); } + @Override + public Class[] glueGlasses() { + return annotation.glueClasses(); + } + @Override public String[] extraGlue() { return annotation.extraGlue(); } + @Override + public Class[] extraGlueGlasses() { + return annotation.extraGlueClasses(); + } + @Override public String tags() { return annotation.tags(); From d218f883955344dbb006395620161e9ca73867af Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Wed, 18 Mar 2026 14:49:56 +0100 Subject: [PATCH 06/14] Extract some sort of request object --- .../io/cucumber/core/backend/Backend.java | 16 +++-- .../core/backend/ConfigurationParameters.java | 26 +++++++++ .../core/backend/GlueDiscoveryRequest.java | 11 ++++ .../core/backend/GlueDiscoverySelector.java | 34 +++++++++++ .../cucumber/core/options/RuntimeOptions.java | 6 ++ .../java/io/cucumber/core/runner/Options.java | 3 + .../java/io/cucumber/core/runner/Runner.java | 3 +- .../java/io/cucumber/java/JavaBackend.java | 58 ++++++------------- 8 files changed, 111 insertions(+), 46 deletions(-) create mode 100644 cucumber-core/src/main/java/io/cucumber/core/backend/ConfigurationParameters.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java create mode 100644 cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoverySelector.java diff --git a/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java b/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java index aad2cfaa32..7a55566a1e 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java +++ b/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java @@ -1,11 +1,11 @@ package io.cucumber.core.backend; +import io.cucumber.core.backend.GlueDiscoverySelector.UriGlueDiscoverySelector; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; import java.net.URI; import java.util.List; -import java.util.Set; @API(status = API.Status.STABLE) public interface Backend { @@ -16,7 +16,9 @@ public interface Backend { * * @param glue Glue that provides the steps to be executed. * @param gluePaths The locations for the glue to be loaded. + * @deprecated use {@link #loadGlue(Glue, GlueDiscoveryRequest)} instead. */ + @Deprecated default void loadGlue(Glue glue, List gluePaths) { } @@ -25,11 +27,15 @@ default void loadGlue(Glue glue, List gluePaths) { * Invoked once before all features. This is where steps and hooks should be * loaded. * - * @param glue Glue that provides the steps to be executed. - * @param glueClassNames The classes of glue to be loaded. + * @param glue Glue that provides the steps to be executed. + * @param glueDiscoveryRequest The glue discovery request */ - default void loadGlueClasses(Glue glue, Set glueClassNames) { - // TODO: Refactor out a request object. + default void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { + List uris = glueDiscoveryRequest.getSelectorsByType(UriGlueDiscoverySelector.class) + .stream() + .map(UriGlueDiscoverySelector::getUri) + .toList(); + loadGlue(glue, uris); } /** diff --git a/cucumber-core/src/main/java/io/cucumber/core/backend/ConfigurationParameters.java b/cucumber-core/src/main/java/io/cucumber/core/backend/ConfigurationParameters.java new file mode 100644 index 0000000000..2d55d5f07f --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/backend/ConfigurationParameters.java @@ -0,0 +1,26 @@ +package io.cucumber.core.backend; + +import io.cucumber.core.exception.CucumberException; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; +import java.util.function.Function; + +import static java.util.Objects.requireNonNull; + +public interface ConfigurationParameters { + + Optional get(String key); + + default Optional get(String key, Function transformer) { + requireNonNull(transformer); + Function safeTransformer = (input) -> { + try { + return transformer.apply(input); + } catch (Exception e) { + throw new CucumberException("Could not transform configuration parameter '%s' and value '%s'".formatted(key, input), e); + } + }; + return get(key).map(safeTransformer); + } +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java b/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java new file mode 100644 index 0000000000..af1b7753f7 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java @@ -0,0 +1,11 @@ +package io.cucumber.core.backend; + +import java.util.List; + +public interface GlueDiscoveryRequest { + + List getSelectorsByType(Class selectorType); + + ConfigurationParameters getConfigurationParameters(); + +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoverySelector.java b/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoverySelector.java new file mode 100644 index 0000000000..e179513764 --- /dev/null +++ b/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoverySelector.java @@ -0,0 +1,34 @@ +package io.cucumber.core.backend; + +import java.net.URI; +import java.util.Objects; + +public interface GlueDiscoverySelector { + + class UriGlueDiscoverySelector implements GlueDiscoverySelector { + + private final URI uri; + + public UriGlueDiscoverySelector(URI uri) { + this.uri = Objects.requireNonNull(uri); + } + + public URI getUri() { + return uri; + } + } + + class ClassGlueDiscoverySelector implements GlueDiscoverySelector { + + private final String className; + + public ClassGlueDiscoverySelector(String className) { + this.className = className; + } + + public String getClassName() { + return className; + } + } + +} diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java index a178abae72..1bdf9b85a4 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java @@ -1,5 +1,6 @@ package io.cucumber.core.options; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.feature.FeatureWithLines; @@ -159,6 +160,11 @@ public Set getGlueClasses() { return unmodifiableSet(glueClasses); } + @Override + public GlueDiscoveryRequest getGlueDiscoveryRequest() { + return // TODO: Continue + } + @Override public boolean isDryRun() { return dryRun; diff --git a/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java b/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java index b1073ba0db..c5083fa565 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java @@ -1,5 +1,6 @@ package io.cucumber.core.runner; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.snippets.SnippetType; @@ -27,4 +28,6 @@ public interface Options { default Set getGlueClasses() { return Collections.emptySet(); } + + GlueDiscoveryRequest getGlueDiscoveryRequest(); } diff --git a/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java b/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java index b877d44bc8..25fd8559e1 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java @@ -52,8 +52,7 @@ public Runner( log.debug(() -> "Loading glue from " + runnerOptions.getGlue()); for (Backend backend : backends) { log.debug(() -> "Loading glue for backend " + backend.getClass().getName()); - backend.loadGlue(this.glue, runnerOptions.getGlue()); - backend.loadGlueClasses(this.glue, runnerOptions.getGlueClasses()); + backend.loadGlue(this.glue, runnerOptions.getGlueDiscoveryRequest()); } } diff --git a/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java b/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java index fd0dc9ef2c..bec5b61ad8 100644 --- a/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java +++ b/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java @@ -3,17 +3,16 @@ import io.cucumber.core.backend.Backend; import io.cucumber.core.backend.Container; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; +import io.cucumber.core.backend.GlueDiscoverySelector; import io.cucumber.core.backend.Lookup; import io.cucumber.core.backend.Snippet; import io.cucumber.core.resource.ClasspathScanner; import io.cucumber.core.resource.ClasspathSupport; -import java.net.URI; import java.util.Collection; -import java.util.List; -import java.util.Set; import java.util.function.Supplier; -import java.util.stream.Collectors; +import java.util.stream.Stream; import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME; import static io.cucumber.java.MethodScanner.scan; @@ -31,49 +30,30 @@ final class JavaBackend implements Backend { } @Override - public void loadGlue(Glue glue, List gluePaths) { - loadGlueClassesImpl(glue, scanForClasses(gluePaths)); - } - - @Override - public void loadGlueClasses(Glue glue, Set glueClassNames) { - Set> glueClasses = glueClassNames.stream() - .map(classFinder::loadClass) - .collect(Collectors.toSet()); - - loadGlueClassesImpl(glue, glueClasses); - } - - private void loadGlueClassesImpl(Glue glue, Set> glueClasses) { - GlueAdaptor glueAdaptor = new GlueAdaptor(lookup, glue); - glueClasses.forEach(aGlueClass -> processClass(aGlueClass, glueAdaptor)); - } - - private Set> scanForClasses(List gluePaths) { - return gluePaths.stream() + public void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { + Stream> classesFromUris = glueDiscoveryRequest.getSelectorsByType(GlueDiscoverySelector.UriGlueDiscoverySelector.class) + .stream() + .map(GlueDiscoverySelector.UriGlueDiscoverySelector::getUri) .filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme())) .map(ClasspathSupport::packageName) .map(classFinder::scanForClassesInPackage) - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - } + .flatMap(Collection::stream); - private void processClass(Class aGlueClass, GlueAdaptor glueAdaptor) { - scan(aGlueClass, (method, annotation) -> { - container.addClass(method.getDeclaringClass()); - glueAdaptor.addDefinition(method, annotation); - }); - } + Stream> classNames = glueDiscoveryRequest.getSelectorsByType(GlueDiscoverySelector.ClassGlueDiscoverySelector.class) + .stream() + .map(GlueDiscoverySelector.ClassGlueDiscoverySelector::getClassName) + .map(classFinder::loadClass); - @Override - public void buildWorld() { + GlueAdaptor glueAdaptor = new GlueAdaptor(lookup, glue); + Stream.concat(classesFromUris, classNames) + .distinct() + .forEach(aGlueClass -> scan(aGlueClass, (method, annotation) -> { + container.addClass(method.getDeclaringClass()); + glueAdaptor.addDefinition(method, annotation); + })); } - @Override - public void disposeWorld() { - - } @Override public Snippet getSnippet() { From 569c2a0d9df6100343f073ebb7dbc96ae6334cb4 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Wed, 25 Mar 2026 20:45:29 +0100 Subject: [PATCH 07/14] WIP --- .../compatibility/CompatibilityTest.java | 2 + .../io/cucumber/core/backend/Backend.java | 11 ++-- .../core/backend/ConfigurationParameters.java | 26 -------- .../core/backend/GlueDiscoveryRequest.java | 5 +- .../core/backend/GlueDiscoverySelector.java | 34 ---------- .../cucumber/core/options/RuntimeOptions.java | 14 +---- .../java/io/cucumber/core/runner/Options.java | 8 +-- .../java/io/cucumber/core/runner/Runner.java | 22 ++++++- .../cucumber/core/runner/HookOrderTest.java | 12 ++-- .../io/cucumber/core/runner/HookTest.java | 5 +- .../io/cucumber/core/runner/RunnerTest.java | 23 ++++--- .../core/runner/TestRunnerSupplier.java | 19 ------ .../io/cucumber/core/runtime/RuntimeTest.java | 6 +- .../core/runtime/StubBackendSupplier.java | 4 +- .../java/io/cucumber/java/JavaBackend.java | 10 +-- .../io/cucumber/java/JavaBackendTest.java | 62 ++++++++++++++++--- .../engine/CucumberConfiguration.java | 4 +- 17 files changed, 119 insertions(+), 148 deletions(-) delete mode 100644 cucumber-core/src/main/java/io/cucumber/core/backend/ConfigurationParameters.java delete mode 100644 cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoverySelector.java diff --git a/compatibility/src/test/java/io/cucumber/compatibility/CompatibilityTest.java b/compatibility/src/test/java/io/cucumber/compatibility/CompatibilityTest.java index f70b8dfe5a..639cc46422 100644 --- a/compatibility/src/test/java/io/cucumber/compatibility/CompatibilityTest.java +++ b/compatibility/src/test/java/io/cucumber/compatibility/CompatibilityTest.java @@ -55,6 +55,8 @@ final class CompatibilityTest { private static final List unsupportedTestCases = Arrays.asList( // exception: not applicable "test.feature-run-exception", + // exception: Cucumber JVM does not support named hooks + "hooks-named", // exception: Cucumber executes all hooks, // but skipped hooks can skip a scenario "hooks-skipped", diff --git a/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java b/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java index 7a55566a1e..fa6ef90540 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java +++ b/cucumber-core/src/main/java/io/cucumber/core/backend/Backend.java @@ -1,6 +1,5 @@ package io.cucumber.core.backend; -import io.cucumber.core.backend.GlueDiscoverySelector.UriGlueDiscoverySelector; import org.apiguardian.api.API; import org.jspecify.annotations.Nullable; @@ -14,9 +13,10 @@ public interface Backend { * Invoked once before all features. This is where steps and hooks should be * loaded. * - * @param glue Glue that provides the steps to be executed. - * @param gluePaths The locations for the glue to be loaded. - * @deprecated use {@link #loadGlue(Glue, GlueDiscoveryRequest)} instead. + * @param glue Glue that provides the steps to be executed. + * @param gluePaths The locations for the glue to be loaded. + * @deprecated use {@link #loadGlue(Glue, GlueDiscoveryRequest)} + * instead. */ @Deprecated default void loadGlue(Glue glue, List gluePaths) { @@ -31,9 +31,8 @@ default void loadGlue(Glue glue, List gluePaths) { * @param glueDiscoveryRequest The glue discovery request */ default void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { - List uris = glueDiscoveryRequest.getSelectorsByType(UriGlueDiscoverySelector.class) + List uris = glueDiscoveryRequest.getGlue() .stream() - .map(UriGlueDiscoverySelector::getUri) .toList(); loadGlue(glue, uris); } diff --git a/cucumber-core/src/main/java/io/cucumber/core/backend/ConfigurationParameters.java b/cucumber-core/src/main/java/io/cucumber/core/backend/ConfigurationParameters.java deleted file mode 100644 index 2d55d5f07f..0000000000 --- a/cucumber-core/src/main/java/io/cucumber/core/backend/ConfigurationParameters.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.cucumber.core.backend; - -import io.cucumber.core.exception.CucumberException; -import org.jspecify.annotations.Nullable; - -import java.util.Optional; -import java.util.function.Function; - -import static java.util.Objects.requireNonNull; - -public interface ConfigurationParameters { - - Optional get(String key); - - default Optional get(String key, Function transformer) { - requireNonNull(transformer); - Function safeTransformer = (input) -> { - try { - return transformer.apply(input); - } catch (Exception e) { - throw new CucumberException("Could not transform configuration parameter '%s' and value '%s'".formatted(key, input), e); - } - }; - return get(key).map(safeTransformer); - } -} diff --git a/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java b/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java index af1b7753f7..599ae9aaf7 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java +++ b/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java @@ -1,11 +1,12 @@ package io.cucumber.core.backend; +import java.net.URI; import java.util.List; public interface GlueDiscoveryRequest { - List getSelectorsByType(Class selectorType); + List getGlue(); - ConfigurationParameters getConfigurationParameters(); + List getGlueClassNames(); } diff --git a/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoverySelector.java b/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoverySelector.java deleted file mode 100644 index e179513764..0000000000 --- a/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoverySelector.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.cucumber.core.backend; - -import java.net.URI; -import java.util.Objects; - -public interface GlueDiscoverySelector { - - class UriGlueDiscoverySelector implements GlueDiscoverySelector { - - private final URI uri; - - public UriGlueDiscoverySelector(URI uri) { - this.uri = Objects.requireNonNull(uri); - } - - public URI getUri() { - return uri; - } - } - - class ClassGlueDiscoverySelector implements GlueDiscoverySelector { - - private final String className; - - public ClassGlueDiscoverySelector(String className) { - this.className = className; - } - - public String getClassName() { - return className; - } - } - -} diff --git a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java index 1bdf9b85a4..56179b85b7 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java +++ b/cucumber-core/src/main/java/io/cucumber/core/options/RuntimeOptions.java @@ -1,6 +1,5 @@ package io.cucumber.core.options; -import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.feature.FeatureWithLines; @@ -16,7 +15,6 @@ import java.net.URI; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -32,7 +30,6 @@ import static java.util.Collections.singletonList; import static java.util.Collections.unmodifiableList; import static java.util.Collections.unmodifiableMap; -import static java.util.Collections.unmodifiableSet; public final class RuntimeOptions implements io.cucumber.core.feature.Options, @@ -43,7 +40,7 @@ public final class RuntimeOptions implements io.cucumber.core.eventbus.Options { private final List glue = new ArrayList<>(); - private final Set glueClasses = new HashSet<>(); + private final List glueClasses = new ArrayList<>(); private final List tagExpressions = new ArrayList<>(); private final List nameFilters = new ArrayList<>(); private final List featurePaths = new ArrayList<>(); @@ -156,13 +153,8 @@ public List getGlue() { } @Override - public Set getGlueClasses() { - return unmodifiableSet(glueClasses); - } - - @Override - public GlueDiscoveryRequest getGlueDiscoveryRequest() { - return // TODO: Continue + public List getGlueClasses() { + return unmodifiableList(glueClasses); } @Override diff --git a/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java b/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java index c5083fa565..5c55901b42 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runner/Options.java @@ -1,15 +1,12 @@ package io.cucumber.core.runner; -import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.eventbus.UuidGenerator; import io.cucumber.core.snippets.SnippetType; import org.jspecify.annotations.Nullable; import java.net.URI; -import java.util.Collections; import java.util.List; -import java.util.Set; public interface Options { @@ -25,9 +22,6 @@ public interface Options { @Nullable Class getUuidGeneratorClass(); - default Set getGlueClasses() { - return Collections.emptySet(); - } + List getGlueClasses(); - GlueDiscoveryRequest getGlueDiscoveryRequest(); } diff --git a/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java b/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java index 25fd8559e1..df1520b7a2 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java +++ b/cucumber-core/src/main/java/io/cucumber/core/runner/Runner.java @@ -3,6 +3,7 @@ import io.cucumber.core.backend.Backend; import io.cucumber.core.backend.CucumberBackendException; import io.cucumber.core.backend.CucumberInvocationTargetException; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.backend.StaticHookDefinition; import io.cucumber.core.eventbus.EventBus; @@ -19,6 +20,7 @@ import io.cucumber.plugin.event.SnippetsSuggestedEvent; import io.cucumber.plugin.event.SnippetsSuggestedEvent.Suggestion; +import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -49,10 +51,9 @@ public Runner( this.backends = backends; this.glue = new CachingGlue(bus); this.objectFactory = objectFactory; - log.debug(() -> "Loading glue from " + runnerOptions.getGlue()); for (Backend backend : backends) { log.debug(() -> "Loading glue for backend " + backend.getClass().getName()); - backend.loadGlue(this.glue, runnerOptions.getGlueDiscoveryRequest()); + backend.loadGlue(this.glue, new RunnerGlueDiscoveryRequest(runnerOptions)); } } @@ -243,4 +244,21 @@ private List generateSnippetsForStep(Step step) { .collect(toList()); } + private static class RunnerGlueDiscoveryRequest implements GlueDiscoveryRequest { + private final Options runnerOptions; + + RunnerGlueDiscoveryRequest(Options runnerOptions) { + this.runnerOptions = runnerOptions; + } + + @Override + public List getGlue() { + return runnerOptions.getGlue(); + } + + @Override + public List getGlueClassNames() { + return runnerOptions.getGlueClasses(); + } + } } diff --git a/cucumber-core/src/test/java/io/cucumber/core/runner/HookOrderTest.java b/cucumber-core/src/test/java/io/cucumber/core/runner/HookOrderTest.java index b28cb20d5e..bbd20c9a16 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runner/HookOrderTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runner/HookOrderTest.java @@ -1,6 +1,7 @@ package io.cucumber.core.runner; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.HookDefinition; import io.cucumber.core.backend.StubStepDefinition; import io.cucumber.core.eventbus.EventBus; @@ -13,7 +14,6 @@ import org.mockito.ArgumentMatchers; import org.mockito.InOrder; -import java.net.URI; import java.time.Clock; import java.util.ArrayList; import java.util.List; @@ -42,7 +42,7 @@ void before_hooks_execute_in_order() { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addStepDefinition(new StubStepDefinition("pattern1")); for (HookDefinition hook : hooks) { glue.addBeforeHook(hook); @@ -81,7 +81,7 @@ void before_step_hooks_execute_in_order() { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addStepDefinition(stepDefinition); for (HookDefinition hook : hooks) { glue.addBeforeStepHook(hook); @@ -108,7 +108,7 @@ void after_hooks_execute_in_reverse_order() { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addStepDefinition(stepDefinition); for (HookDefinition hook : hooks) { glue.addAfterHook(hook); @@ -135,7 +135,7 @@ void after_step_hooks_execute_in_reverse_order() { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addStepDefinition(stepDefinition); for (HookDefinition hook : hooks) { glue.addAfterStepHook(hook); @@ -163,7 +163,7 @@ void hooks_order_across_many_backends() { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addStepDefinition(stepDefinition); for (HookDefinition hook : backend1Hooks) { diff --git a/cucumber-core/src/test/java/io/cucumber/core/runner/HookTest.java b/cucumber-core/src/test/java/io/cucumber/core/runner/HookTest.java index 7320825743..bd02eafefb 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runner/HookTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runner/HookTest.java @@ -2,6 +2,7 @@ import io.cucumber.core.backend.Backend; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.HookDefinition; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.eventbus.EventBus; @@ -56,7 +57,7 @@ void after_hooks_execute_before_objects_are_disposed() { Glue glue = invocation.getArgument(0); glue.addBeforeHook(hook); return null; - }).when(backend).loadGlue(any(Glue.class), ArgumentMatchers.anyList()); + }).when(backend).loadGlue(any(Glue.class), any(GlueDiscoveryRequest.class)); Runner runner = new Runner(bus, Collections.singleton(backend), objectFactory, runtimeOptions); @@ -81,7 +82,7 @@ void hook_throws_exception_with_name_when_tag_expression_is_invalid() { Glue glue = invocation.getArgument(0); glue.addBeforeHook(hook); return null; - }).when(backend).loadGlue(any(Glue.class), ArgumentMatchers.anyList()); + }).when(backend).loadGlue(any(Glue.class), any(GlueDiscoveryRequest.class)); RuntimeException e = assertThrows(RuntimeException.class, () -> new Runner(bus, Collections.singleton(backend), objectFactory, diff --git a/cucumber-core/src/test/java/io/cucumber/core/runner/RunnerTest.java b/cucumber-core/src/test/java/io/cucumber/core/runner/RunnerTest.java index d8f81c2f1a..d93f46b317 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runner/RunnerTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runner/RunnerTest.java @@ -2,6 +2,7 @@ import io.cucumber.core.backend.Backend; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.HookDefinition; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.backend.StaticHookDefinition; @@ -17,9 +18,7 @@ import org.mockito.ArgumentMatchers; import org.mockito.InOrder; -import java.net.URI; import java.time.Clock; -import java.util.List; import java.util.UUID; import static java.util.Collections.emptyList; @@ -61,7 +60,7 @@ void hooks_execute_inside_world_and_around_world() { glue.addBeforeHook(beforeHook); glue.addAfterHook(afterHook); return null; - }).when(backend).loadGlue(any(Glue.class), ArgumentMatchers.anyList()); + }).when(backend).loadGlue(any(Glue.class), any(GlueDiscoveryRequest.class)); Runner runner = new Runner(bus, singletonList(backend), objectFactory, runtimeOptions); runner.runBeforeAllHooks(); @@ -108,7 +107,7 @@ void steps_are_skipped_after_failure() { doThrow(new RuntimeException("Boom")).when(failingBeforeHook).execute(ArgumentMatchers.any()); TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addBeforeHook(failingBeforeHook); glue.addStepDefinition(stepDefinition); } @@ -148,7 +147,7 @@ public void execute(Object[] args) { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addAfterHook(afterStepHook); glue.addStepDefinition(stepDefinition); } @@ -171,7 +170,7 @@ void aftersteps_executed_for_passed_step() { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addAfterHook(afteStepHook1); glue.addAfterHook(afteStepHook2); glue.addStepDefinition(stepDefinition); @@ -196,7 +195,7 @@ void hooks_execute_also_after_failure() { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addBeforeHook(failingBeforeHook); glue.addBeforeHook(beforeHook); glue.addAfterHook(afterHook); @@ -219,7 +218,7 @@ void all_static_hooks_execute_also_after_failure() { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addBeforeAllHook(beforeAllHook); glue.addBeforeAllHook(failingBeforeAllHook); } @@ -239,7 +238,7 @@ void steps_are_executed() { Pickle pickleMatchingStepDefinitions = createPickleMatchingStepDefinitions(stepDefinition); TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addStepDefinition(stepDefinition); } }; @@ -254,7 +253,7 @@ void steps_are_not_executed_on_dry_run() { RuntimeOptions runtimeOptions = new RuntimeOptionsBuilder().setDryRun().build(); TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addStepDefinition(stepDefinition); } }; @@ -277,7 +276,7 @@ void hooks_not_executed_in_dry_run_mode() { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addBeforeAllHook(beforeAllHook); glue.addAfterAllHook(afterAllHook); glue.addBeforeHook(beforeHook); @@ -308,7 +307,7 @@ void scenario_hooks_not_executed_for_empty_pickles() { TestRunnerSupplier runnerSupplier = new TestRunnerSupplier(bus, runtimeOptions) { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addBeforeHook(beforeHook); glue.addAfterHook(afterHook); glue.addBeforeStepHook(beforeStepHook); diff --git a/cucumber-core/src/test/java/io/cucumber/core/runner/TestRunnerSupplier.java b/cucumber-core/src/test/java/io/cucumber/core/runner/TestRunnerSupplier.java index 54ce826ebc..af4d162171 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runner/TestRunnerSupplier.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runner/TestRunnerSupplier.java @@ -1,7 +1,6 @@ package io.cucumber.core.runner; import io.cucumber.core.backend.Backend; -import io.cucumber.core.backend.Glue; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.backend.Snippet; import io.cucumber.core.eventbus.EventBus; @@ -9,9 +8,6 @@ import io.cucumber.core.runtime.RunnerSupplier; import io.cucumber.core.snippets.TestSnippet; -import java.net.URI; -import java.util.List; - import static java.util.Collections.singleton; @SuppressWarnings("NullAway") // TODO: Use Assert @@ -25,21 +21,6 @@ protected TestRunnerSupplier(EventBus bus, RuntimeOptions runtimeOptions) { this.runtimeOptions = runtimeOptions; } - @Override - public void loadGlue(Glue glue, List gluePaths) { - - } - - @Override - public void buildWorld() { - - } - - @Override - public void disposeWorld() { - - } - @Override public Snippet getSnippet() { return new TestSnippet(); diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java index 3e63bb528c..71078c8d6c 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/RuntimeTest.java @@ -2,6 +2,7 @@ import io.cucumber.core.backend.CucumberBackendException; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.HookDefinition; import io.cucumber.core.backend.ParameterInfo; import io.cucumber.core.backend.ScenarioScoped; @@ -40,7 +41,6 @@ import org.junit.jupiter.api.function.Executable; import org.mockito.ArgumentCaptor; -import java.net.URI; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; @@ -428,7 +428,7 @@ void should_fail_on_exception_invoking_after_all_hook() { BackendSupplier backendSupplier = new TestBackendSupplier() { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { glue.addAfterAllHook(mockedStaticHookDefinition); } }; @@ -512,7 +512,7 @@ void generates_events_for_glue_and_scenario_scoped_glue() { private @Nullable Glue glue; @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { this.glue = glue; glue.addStepDefinition(mockedStepDefinition); } diff --git a/cucumber-core/src/test/java/io/cucumber/core/runtime/StubBackendSupplier.java b/cucumber-core/src/test/java/io/cucumber/core/runtime/StubBackendSupplier.java index 7ddc35b37e..881de128c3 100644 --- a/cucumber-core/src/test/java/io/cucumber/core/runtime/StubBackendSupplier.java +++ b/cucumber-core/src/test/java/io/cucumber/core/runtime/StubBackendSupplier.java @@ -2,13 +2,13 @@ import io.cucumber.core.backend.Backend; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.HookDefinition; import io.cucumber.core.backend.Snippet; import io.cucumber.core.backend.StaticHookDefinition; import io.cucumber.core.backend.StepDefinition; import io.cucumber.core.snippets.TestSnippet; -import java.net.URI; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -68,7 +68,7 @@ public StubBackendSupplier( public Collection get() { return Collections.singletonList(new Backend() { @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest request) { beforeAll.forEach(glue::addBeforeAllHook); before.forEach(glue::addBeforeHook); beforeStep.forEach(glue::addBeforeStepHook); diff --git a/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java b/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java index bec5b61ad8..40cfa16704 100644 --- a/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java +++ b/cucumber-java/src/main/java/io/cucumber/java/JavaBackend.java @@ -4,7 +4,6 @@ import io.cucumber.core.backend.Container; import io.cucumber.core.backend.Glue; import io.cucumber.core.backend.GlueDiscoveryRequest; -import io.cucumber.core.backend.GlueDiscoverySelector; import io.cucumber.core.backend.Lookup; import io.cucumber.core.backend.Snippet; import io.cucumber.core.resource.ClasspathScanner; @@ -31,22 +30,20 @@ final class JavaBackend implements Backend { @Override public void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { - Stream> classesFromUris = glueDiscoveryRequest.getSelectorsByType(GlueDiscoverySelector.UriGlueDiscoverySelector.class) + Stream> glueClasses = glueDiscoveryRequest.getGlue() .stream() - .map(GlueDiscoverySelector.UriGlueDiscoverySelector::getUri) .filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme())) .map(ClasspathSupport::packageName) .map(classFinder::scanForClassesInPackage) .flatMap(Collection::stream); - Stream> classNames = glueDiscoveryRequest.getSelectorsByType(GlueDiscoverySelector.ClassGlueDiscoverySelector.class) + Stream> explicitClasses = glueDiscoveryRequest.getGlueClassNames() .stream() - .map(GlueDiscoverySelector.ClassGlueDiscoverySelector::getClassName) .map(classFinder::loadClass); GlueAdaptor glueAdaptor = new GlueAdaptor(lookup, glue); - Stream.concat(classesFromUris, classNames) + Stream.concat(glueClasses, explicitClasses) .distinct() .forEach(aGlueClass -> scan(aGlueClass, (method, annotation) -> { container.addClass(method.getDeclaringClass()); @@ -54,7 +51,6 @@ public void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { })); } - @Override public Snippet getSnippet() { return new JavaSnippet(); diff --git a/cucumber-java/src/test/java/io/cucumber/java/JavaBackendTest.java b/cucumber-java/src/test/java/io/cucumber/java/JavaBackendTest.java index ca710541fb..c824f1d120 100644 --- a/cucumber-java/src/test/java/io/cucumber/java/JavaBackendTest.java +++ b/cucumber-java/src/test/java/io/cucumber/java/JavaBackendTest.java @@ -1,6 +1,7 @@ package io.cucumber.java; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.core.backend.StepDefinition; import io.cucumber.java.steps.Steps; @@ -17,7 +18,7 @@ import static java.lang.Thread.currentThread; import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; +import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; @@ -47,23 +48,43 @@ void createBackend() { @Test void finds_step_definitions_by_classpath_url() { - backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/java/steps"))); + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest(URI.create("classpath:io/cucumber/java/steps")); + backend.loadGlue(glue, request); + backend.buildWorld(); + verify(factory).addClass(Steps.class); + } + + @Test + void finds_step_definitions_by_class_name() { + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest(Steps.class.getName()); + backend.loadGlue(glue, request); backend.buildWorld(); verify(factory).addClass(Steps.class); } @Test void finds_step_definitions_once_by_classpath_url() { - backend.loadGlue(glue, - asList(URI.create("classpath:io/cucumber/java/steps"), URI.create("classpath:io/cucumber/java/steps"))); + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest(URI.create("classpath:io/cucumber/java/steps"), + URI.create("classpath:io/cucumber/java/steps")); + backend.loadGlue(glue, request); + backend.buildWorld(); + verify(factory, times(1)).addClass(Steps.class); + } + + @Test + void finds_step_definitions_once_by_class_name() { + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest(Steps.class.getName(), Steps.class.getName()); + backend.loadGlue(glue, request); backend.buildWorld(); verify(factory, times(1)).addClass(Steps.class); } @Test void detects_subclassed_glue_and_throws_exception() { - Executable testMethod = () -> backend.loadGlue(glue, asList(URI.create("classpath:io/cucumber/java/steps"), - URI.create("classpath:io/cucumber/java/incorrectlysubclassedsteps"))); + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest( + URI.create("classpath:io/cucumber/java/steps"), + URI.create("classpath:io/cucumber/java/incorrectlysubclassedsteps")); + Executable testMethod = () -> backend.loadGlue(glue, request); InvalidMethodException expectedThrown = assertThrows(InvalidMethodException.class, testMethod); assertThat(expectedThrown.getMessage(), is(equalTo( "You're not allowed to extend classes that define Step Definitions or hooks. class io.cucumber.java.incorrectlysubclassedsteps.SubclassesSteps extends class io.cucumber.java.steps.Steps"))); @@ -71,7 +92,9 @@ void detects_subclassed_glue_and_throws_exception() { @Test void detects_repeated_annotations() { - backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/java/repeatable"))); + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest( + URI.create("classpath:io/cucumber/java/repeatable")); + backend.loadGlue(glue, request); verify(glue, times(2)).addStepDefinition(stepDefinition.capture()); List patterns = stepDefinition.getAllValues() @@ -82,4 +105,29 @@ void detects_repeated_annotations() { } + private static final class TestGlueDiscoveryRequest implements GlueDiscoveryRequest { + private final List gluePaths; + private final List glueClassNames; + + TestGlueDiscoveryRequest(URI... gluePaths) { + this.gluePaths = List.of(gluePaths); + this.glueClassNames = emptyList(); + } + + TestGlueDiscoveryRequest(String... glueClassNames) { + this.gluePaths = emptyList(); + this.glueClassNames = List.of(glueClassNames); + } + + @Override + public List getGlue() { + return gluePaths; + } + + @Override + public List getGlueClassNames() { + return glueClassNames; + } + } + } diff --git a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java index d5762079ec..64b12ced93 100644 --- a/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java +++ b/cucumber-junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/CucumberConfiguration.java @@ -152,13 +152,13 @@ public List getGlue() { } @Override - public Set getGlueClasses() { + public List getGlueClasses() { return configurationParameters .get(GLUE_CLASSES_PROPERTY_NAME, s -> Arrays.asList(s.split(","))) .orElse(Collections.emptyList()) .stream() .map(String::trim) - .collect(Collectors.toSet()); + .collect(Collectors.toList()); } @Override From 93bff563d56a7f97295da9af62eef26b1cb5f65e Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 29 Mar 2026 14:11:10 +0200 Subject: [PATCH 08/14] WIP --- .../java/io/cucumber/guice/GuiceBackend.java | 35 ++++------- .../io/cucumber/guice/GuiceBackendTest.java | 60 ++++++++++++++++--- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/cucumber-guice/src/main/java/io/cucumber/guice/GuiceBackend.java b/cucumber-guice/src/main/java/io/cucumber/guice/GuiceBackend.java index 97696ba5a9..6beb6e6fea 100644 --- a/cucumber-guice/src/main/java/io/cucumber/guice/GuiceBackend.java +++ b/cucumber-guice/src/main/java/io/cucumber/guice/GuiceBackend.java @@ -3,15 +3,13 @@ import io.cucumber.core.backend.Backend; import io.cucumber.core.backend.Container; import io.cucumber.core.backend.Glue; -import io.cucumber.core.backend.Snippet; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.resource.ClasspathScanner; import io.cucumber.core.resource.ClasspathSupport; -import org.jspecify.annotations.Nullable; -import java.net.URI; import java.util.Collection; -import java.util.List; import java.util.function.Supplier; +import java.util.stream.Stream; import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME; @@ -26,30 +24,21 @@ final class GuiceBackend implements Backend { } @Override - public void loadGlue(Glue glue, List gluePaths) { - gluePaths.stream() + public void loadGlue(Glue glue, GlueDiscoveryRequest discoveryRequest) { + Stream> glueClasses = discoveryRequest.getGlue() + .stream() .filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme())) .map(ClasspathSupport::packageName) .map(classFinder::scanForClassesInPackage) - .flatMap(Collection::stream) - .filter(InjectorSource.class::isAssignableFrom) - .distinct() - .forEach(container::addClass); - } + .flatMap(Collection::stream); - @Override - public void buildWorld() { + Stream> explicitClasses = discoveryRequest.getGlueClassNames() + .stream() + .map(classFinder::loadClass); - } - - @Override - public void disposeWorld() { - - } - - @Override - public @Nullable Snippet getSnippet() { - return null; + Stream.concat(glueClasses, explicitClasses) + .distinct() + .forEach(container::addClass); } } diff --git a/cucumber-guice/src/test/java/io/cucumber/guice/GuiceBackendTest.java b/cucumber-guice/src/test/java/io/cucumber/guice/GuiceBackendTest.java index 55410bcbe9..4333539648 100644 --- a/cucumber-guice/src/test/java/io/cucumber/guice/GuiceBackendTest.java +++ b/cucumber-guice/src/test/java/io/cucumber/guice/GuiceBackendTest.java @@ -2,6 +2,7 @@ import io.cucumber.core.backend.BackendProviderService; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.guice.integration.YourInjectorSource; import org.junit.jupiter.api.Test; @@ -9,11 +10,11 @@ import org.mockito.junit.jupiter.MockitoSettings; import java.net.URI; +import java.util.List; import java.util.function.Supplier; import static java.lang.Thread.currentThread; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; +import static java.util.Collections.emptyList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -36,22 +37,36 @@ class GuiceBackendTest { @Test void finds_injector_source_impls_by_classpath_url() { GuiceBackend backend = new GuiceBackend(factory, classLoader); - backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/guice/integration"))); + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest( + URI.create("classpath:io/cucumber/guice/integration")); + backend.loadGlue(glue, request); verify(factory).addClass(YourInjectorSource.class); } @Test void finds_injector_source_impls_once_by_classpath_url() { GuiceBackend backend = new GuiceBackend(factory, classLoader); - backend.loadGlue(glue, asList(URI.create("classpath:io/cucumber/guice/integration"), - URI.create("classpath:io/cucumber/guice/integration"))); + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest( + URI.create("classpath:io/cucumber/guice/integration"), + URI.create("classpath:io/cucumber/guice/integration")); + backend.loadGlue(glue, request); verify(factory, times(1)).addClass(YourInjectorSource.class); } + @Test + void finds_injector_source_impls_by_classname() { + GuiceBackend backend = new GuiceBackend(factory, classLoader); + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest(YourInjectorSource.class.getName()); + backend.loadGlue(glue, request); + verify(factory).addClass(YourInjectorSource.class); + } + @Test void world_and_snippet_methods_do_nothing() { GuiceBackend backend = new GuiceBackend(factory, classLoader); - backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/guice/integration"))); + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest( + URI.create("classpath:io/cucumber/guice/integration")); + backend.loadGlue(glue, request); backend.buildWorld(); backend.disposeWorld(); assertThat(backend.getSnippet(), is(nullValue())); @@ -61,15 +76,17 @@ void world_and_snippet_methods_do_nothing() { @SuppressWarnings("NullAway") void doesnt_save_anything_in_glue() { GuiceBackend backend = new GuiceBackend(factory, classLoader); - backend.loadGlue(null, singletonList(URI.create("classpath:io/cucumber/guice/integration"))); + TestGlueDiscoveryRequest request = new TestGlueDiscoveryRequest( + URI.create("classpath:io/cucumber/guice/integration")); + backend.loadGlue(null, request); verify(factory).addClass(YourInjectorSource.class); } @Test @SuppressWarnings("NullAway") - void list_of_uris_cant_be_null() { + void request_cant_be_null() { GuiceBackend backend = new GuiceBackend(factory, classLoader); - assertThrows(NullPointerException.class, () -> backend.loadGlue(glue, null)); + assertThrows(NullPointerException.class, () -> backend.loadGlue(glue, (GlueDiscoveryRequest) null)); } @Test @@ -78,4 +95,29 @@ void backend_service_creates_backend() { assertThat(backendProviderService.create(factory, factory, classLoader), is(notNullValue())); } + private static final class TestGlueDiscoveryRequest implements GlueDiscoveryRequest { + private final List gluePaths; + private final List glueClassNames; + + TestGlueDiscoveryRequest(URI... gluePaths) { + this.gluePaths = List.of(gluePaths); + this.glueClassNames = emptyList(); + } + + TestGlueDiscoveryRequest(String... glueClassNames) { + this.gluePaths = emptyList(); + this.glueClassNames = List.of(glueClassNames); + } + + @Override + public List getGlue() { + return gluePaths; + } + + @Override + public List getGlueClassNames() { + return glueClassNames; + } + } + } From 214ece96370a01b13e7b06b696c9b4a5cbb31243 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 29 Mar 2026 14:17:23 +0200 Subject: [PATCH 09/14] WIP --- .../java/io/cucumber/java8/Java8Backend.java | 42 +++++++---------- .../io/cucumber/java8/Java8BackendTest.java | 47 +++++++++++++++++-- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java index 64d35e4651..181ba96536 100644 --- a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java +++ b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java @@ -3,20 +3,20 @@ import io.cucumber.core.backend.Backend; import io.cucumber.core.backend.Container; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.Lookup; import io.cucumber.core.backend.Snippet; import io.cucumber.core.resource.ClasspathScanner; import io.cucumber.core.resource.ClasspathSupport; import org.jspecify.annotations.Nullable; -import java.net.URI; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Set; import java.util.function.Supplier; -import java.util.stream.Collectors; +import java.util.stream.Stream; +import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME; import static io.cucumber.java8.LambdaGlueRegistry.CLOSED; import static java.util.Objects.requireNonNull; @@ -36,23 +36,22 @@ final class Java8Backend implements Backend { } @Override - public void loadGlue(Glue glue, List gluePaths) { - loadGlueClassesImpl(glue, scanForClasses(gluePaths)); - } - - @Override - public void loadGlueClasses(Glue glue, Set glueClassNames) { - Set> glueClasses = glueClassNames.stream() - .map(classFinder::loadClass) - .collect(Collectors.toSet()); + public void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { + Stream> glueClasses = glueDiscoveryRequest.getGlue() + .stream() + .filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme())) + .map(ClasspathSupport::packageName) + .map(classFinder::scanForClassesInPackage) + .flatMap(Collection::stream); - loadGlueClassesImpl(glue, glueClasses); - } + Stream> explicitClasses = glueDiscoveryRequest.getGlueClassNames() + .stream() + .map(classFinder::loadClass); - private void loadGlueClassesImpl(Glue glue, Set> glueClasses) { this.glue = new ClosureAwareGlueRegistry(glue); - glueClasses.stream() - // Filter Java8 style glue (lambdas) + + Stream.concat(glueClasses, explicitClasses) + .distinct() .filter(aClass -> !LambdaGlue.class.equals(aClass) && LambdaGlue.class.isAssignableFrom(aClass)) .map(aClass -> (Class) aClass.asSubclass(LambdaGlue.class)) .filter(glueClass -> !glueClass.isInterface()) @@ -60,15 +59,6 @@ private void loadGlueClassesImpl(Glue glue, Set> glueClasses) { .forEach(this::processClass); } - private Set> scanForClasses(List gluePaths) { - return gluePaths.stream() - .filter(gluePath -> ClasspathSupport.CLASSPATH_SCHEME.equals(gluePath.getScheme())) - .map(ClasspathSupport::packageName) - .map(classFinder::scanForClassesInPackage) - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - } - private void processClass(Class glueClass) { container.addClass(glueClass); lambdaGlueClasses.add(glueClass); diff --git a/cucumber-java8/src/test/java/io/cucumber/java8/Java8BackendTest.java b/cucumber-java8/src/test/java/io/cucumber/java8/Java8BackendTest.java index 2c56c8d88d..e72c53d6c2 100644 --- a/cucumber-java8/src/test/java/io/cucumber/java8/Java8BackendTest.java +++ b/cucumber-java8/src/test/java/io/cucumber/java8/Java8BackendTest.java @@ -1,6 +1,7 @@ package io.cucumber.java8; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.java8.steps.Steps; import org.junit.jupiter.api.BeforeEach; @@ -9,10 +10,10 @@ import org.mockito.junit.jupiter.MockitoSettings; import java.net.URI; +import java.util.List; import static java.lang.Thread.currentThread; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; +import static java.util.Collections.emptyList; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -34,17 +35,53 @@ void createBackend() { @Test void finds_step_definitions_by_classpath_url() { - backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/java8/steps"))); + TestGlueDiscoveryRequest glueDiscoveryRequest = new TestGlueDiscoveryRequest( + URI.create("classpath:io/cucumber/java8/steps")); + backend.loadGlue(glue, glueDiscoveryRequest); + backend.buildWorld(); + verify(factory).addClass(Steps.class); + } + + @Test + void finds_step_definitions_by_class_name() { + TestGlueDiscoveryRequest glueDiscoveryRequest = new TestGlueDiscoveryRequest(Steps.class.getName()); + backend.loadGlue(glue, glueDiscoveryRequest); backend.buildWorld(); verify(factory).addClass(Steps.class); } @Test void finds_step_definitions_once_by_classpath_url() { - backend.loadGlue(glue, - asList(URI.create("classpath:io/cucumber/java8/steps"), URI.create("classpath:io/cucumber/java8/steps"))); + TestGlueDiscoveryRequest glueDiscoveryRequest = new TestGlueDiscoveryRequest( + URI.create("classpath:io/cucumber/java8/steps"), + URI.create("classpath:io/cucumber/java8/steps")); + backend.loadGlue(glue, glueDiscoveryRequest); backend.buildWorld(); verify(factory, times(1)).addClass(Steps.class); } + private static final class TestGlueDiscoveryRequest implements GlueDiscoveryRequest { + private final List gluePaths; + private final List glueClassNames; + + TestGlueDiscoveryRequest(URI... gluePaths) { + this.gluePaths = List.of(gluePaths); + this.glueClassNames = emptyList(); + } + + TestGlueDiscoveryRequest(String... glueClassNames) { + this.gluePaths = emptyList(); + this.glueClassNames = List.of(glueClassNames); + } + + @Override + public List getGlue() { + return gluePaths; + } + + @Override + public List getGlueClassNames() { + return glueClassNames; + } + } } From 34bfbbe7ec023d44c9249d9d8c543d10cb79d03c Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 29 Mar 2026 14:25:52 +0200 Subject: [PATCH 10/14] WIP --- .../java/io/cucumber/java8/Java8Backend.java | 2 +- .../cucumber/picocontainer/PicoBackend.java | 32 +++++-------- .../picocontainer/PicoBackendTest.java | 46 ++++++++++++++++--- 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java index 181ba96536..8c330e31d9 100644 --- a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java +++ b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java @@ -51,11 +51,11 @@ public void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { this.glue = new ClosureAwareGlueRegistry(glue); Stream.concat(glueClasses, explicitClasses) - .distinct() .filter(aClass -> !LambdaGlue.class.equals(aClass) && LambdaGlue.class.isAssignableFrom(aClass)) .map(aClass -> (Class) aClass.asSubclass(LambdaGlue.class)) .filter(glueClass -> !glueClass.isInterface()) .filter(glueClass -> glueClass.getConstructors().length > 0) + .distinct() .forEach(this::processClass); } diff --git a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java index 1b6566514a..5c4c19aab9 100644 --- a/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java +++ b/cucumber-picocontainer/src/main/java/io/cucumber/picocontainer/PicoBackend.java @@ -3,15 +3,13 @@ import io.cucumber.core.backend.Backend; import io.cucumber.core.backend.Container; import io.cucumber.core.backend.Glue; -import io.cucumber.core.backend.Snippet; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.resource.ClasspathScanner; import io.cucumber.core.resource.ClasspathSupport; -import org.jspecify.annotations.Nullable; -import java.net.URI; import java.util.Collection; -import java.util.List; import java.util.function.Supplier; +import java.util.stream.Stream; import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME; @@ -26,12 +24,19 @@ final class PicoBackend implements Backend { } @Override - public void loadGlue(Glue glue, List gluePaths) { - gluePaths.stream() + public void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { + Stream> glueClasses = glueDiscoveryRequest.getGlue() + .stream() .filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme())) .map(ClasspathSupport::packageName) .map(classFinder::scanForClassesInPackage) - .flatMap(Collection::stream) + .flatMap(Collection::stream); + + Stream> explicitClasses = glueDiscoveryRequest.getGlueClassNames() + .stream() + .map(classFinder::loadClass); + + Stream.concat(glueClasses, explicitClasses) .filter(PicoBackend::hasCucumberPicoProvider) .distinct() .forEach(container::addClass); @@ -41,17 +46,4 @@ private static boolean hasCucumberPicoProvider(Class clazz) { return clazz.isAnnotationPresent(CucumberPicoProvider.class); } - @Override - public void buildWorld() { - } - - @Override - public void disposeWorld() { - } - - @Override - public @Nullable Snippet getSnippet() { - return null; - } - } diff --git a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java index 3165692baa..fe58749d83 100644 --- a/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java +++ b/cucumber-picocontainer/src/test/java/io/cucumber/picocontainer/PicoBackendTest.java @@ -1,6 +1,7 @@ package io.cucumber.picocontainer; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.picocontainer.annotationconfig.DatabaseConnectionProvider; import io.cucumber.picocontainer.annotationconfig.ExamplePicoConfiguration; @@ -12,10 +13,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.net.URI; +import java.util.List; import static java.lang.Thread.currentThread; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; +import static java.util.Collections.emptyList; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -39,7 +40,7 @@ void createBackend() { @Test void considers_but_does_not_add_annotated_configuration() { backend.loadGlue(glue, - singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); + new TestGlueDiscoveryRequest(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); backend.buildWorld(); verify(factory, never()).addClass(ExamplePicoConfiguration.class); } @@ -47,16 +48,24 @@ void considers_but_does_not_add_annotated_configuration() { @Test void adds_unnested_provider_classes() { backend.loadGlue(glue, - singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); + new TestGlueDiscoveryRequest(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); backend.buildWorld(); verify(factory).addClass(UrlToUriProvider.class); verify(factory).addClass(DatabaseConnectionProvider.class); } + @Test + void adds_unnested_provider_classes_from_class_name() { + backend.loadGlue(glue, + new TestGlueDiscoveryRequest(UrlToUriProvider.class.getName())); + backend.buildWorld(); + verify(factory).addClass(UrlToUriProvider.class); + } + @Test void adds_nested_provider_classes() { backend.loadGlue(glue, - singletonList(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); + new TestGlueDiscoveryRequest(URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); backend.buildWorld(); verify(factory).addClass(ExamplePicoConfiguration.NestedUrlProvider.class); verify(factory).addClass(ExamplePicoConfiguration.NestedUrlConnectionProvider.class); @@ -64,7 +73,7 @@ void adds_nested_provider_classes() { @Test void finds_configured_classes_only_once_when_scanning_twice() { - backend.loadGlue(glue, asList( + backend.loadGlue(glue, new TestGlueDiscoveryRequest( URI.create("classpath:io/cucumber/picocontainer/annotationconfig"), URI.create("classpath:io/cucumber/picocontainer/annotationconfig"))); backend.buildWorld(); @@ -75,4 +84,29 @@ void finds_configured_classes_only_once_when_scanning_twice() { verify(factory, times(1)).addClass(DatabaseConnectionProvider.class); } + private static final class TestGlueDiscoveryRequest implements GlueDiscoveryRequest { + private final List gluePaths; + private final List glueClassNames; + + TestGlueDiscoveryRequest(URI... gluePaths) { + this.gluePaths = List.of(gluePaths); + this.glueClassNames = emptyList(); + } + + TestGlueDiscoveryRequest(String... glueClassNames) { + this.gluePaths = emptyList(); + this.glueClassNames = List.of(glueClassNames); + } + + @Override + public List getGlue() { + return gluePaths; + } + + @Override + public List getGlueClassNames() { + return glueClassNames; + } + } + } From 39c5f1dcadbc6d89c85f681fd102b1a11f5550b5 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 29 Mar 2026 14:30:20 +0200 Subject: [PATCH 11/14] WIP --- .../io/cucumber/spring/SpringBackend.java | 17 +++++-- .../io/cucumber/spring/SpringBackendTest.java | 48 ++++++++++++++++--- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/cucumber-spring/src/main/java/io/cucumber/spring/SpringBackend.java b/cucumber-spring/src/main/java/io/cucumber/spring/SpringBackend.java index 6ae0ba571e..1a17386f16 100644 --- a/cucumber-spring/src/main/java/io/cucumber/spring/SpringBackend.java +++ b/cucumber-spring/src/main/java/io/cucumber/spring/SpringBackend.java @@ -3,14 +3,14 @@ import io.cucumber.core.backend.Backend; import io.cucumber.core.backend.Container; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.resource.ClasspathScanner; import io.cucumber.core.resource.ClasspathSupport; import java.lang.reflect.Modifier; -import java.net.URI; import java.util.Collection; -import java.util.List; import java.util.function.Supplier; +import java.util.stream.Stream; import static io.cucumber.core.resource.ClasspathSupport.CLASSPATH_SCHEME; @@ -25,12 +25,19 @@ final class SpringBackend implements Backend { } @Override - public void loadGlue(Glue glue, List gluePaths) { - gluePaths.stream() + public void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { + Stream> glueClasses = glueDiscoveryRequest.getGlue() + .stream() .filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme())) .map(ClasspathSupport::packageName) .map(classFinder::scanForClassesInPackage) - .flatMap(Collection::stream) + .flatMap(Collection::stream); + + Stream> explicitClasses = glueDiscoveryRequest.getGlueClassNames() + .stream() + .map(classFinder::loadClass); + + Stream.concat(glueClasses, explicitClasses) .filter(SpringFactory::hasCucumberContextConfiguration) .filter(this::checkIfOfClassTypeAndNotAbstract) .distinct() diff --git a/cucumber-spring/src/test/java/io/cucumber/spring/SpringBackendTest.java b/cucumber-spring/src/test/java/io/cucumber/spring/SpringBackendTest.java index 8bc6e015e5..dddcde77ad 100644 --- a/cucumber-spring/src/test/java/io/cucumber/spring/SpringBackendTest.java +++ b/cucumber-spring/src/test/java/io/cucumber/spring/SpringBackendTest.java @@ -1,6 +1,7 @@ package io.cucumber.spring; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.ObjectFactory; import io.cucumber.spring.annotationconfig.AnnotationContextConfiguration; import io.cucumber.spring.cucumbercontextconfigannotation.AbstractWithComponentAnnotation; @@ -12,10 +13,10 @@ import org.mockito.junit.jupiter.MockitoSettings; import java.net.URI; +import java.util.List; import static java.lang.Thread.currentThread; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; +import static java.util.Collections.emptyList; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -37,14 +38,22 @@ void createBackend() { @Test void finds_annotation_context_configuration_by_classpath_url() { - backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/spring/annotationconfig"))); + backend.loadGlue(glue, + new TestGlueDiscoveryRequest(URI.create("classpath:io/cucumber/spring/annotationconfig"))); + backend.buildWorld(); + verify(factory).addClass(AnnotationContextConfiguration.class); + } + + @Test + void finds_annotation_context_configuration_by_classname() { + backend.loadGlue(glue, new TestGlueDiscoveryRequest(AnnotationContextConfiguration.class.getName())); backend.buildWorld(); verify(factory).addClass(AnnotationContextConfiguration.class); } @Test void finds_annotaiton_context_configuration_once_by_classpath_url() { - backend.loadGlue(glue, asList( + backend.loadGlue(glue, new TestGlueDiscoveryRequest( URI.create("classpath:io/cucumber/spring/annotationconfig"), URI.create("classpath:io/cucumber/spring/annotationconfig"))); backend.buildWorld(); @@ -53,7 +62,7 @@ void finds_annotaiton_context_configuration_once_by_classpath_url() { @Test void ignoresAbstractClassWithCucumberContextConfiguration() { - backend.loadGlue(glue, singletonList( + backend.loadGlue(glue, new TestGlueDiscoveryRequest( URI.create("classpath:io/cucumber/spring/cucumbercontextconfigannotation"))); backend.buildWorld(); verify(factory, times(0)).addClass(AbstractWithComponentAnnotation.class); @@ -61,7 +70,7 @@ void ignoresAbstractClassWithCucumberContextConfiguration() { @Test void ignoresInterfaceWithCucumberContextConfiguration() { - backend.loadGlue(glue, singletonList( + backend.loadGlue(glue, new TestGlueDiscoveryRequest( URI.create("classpath:io/cucumber/spring/cucumbercontextconfigannotation"))); backend.buildWorld(); verify(factory, times(0)).addClass(AnnotatedInterface.class); @@ -69,10 +78,35 @@ void ignoresInterfaceWithCucumberContextConfiguration() { @Test void considersClassWithCucumberContextConfigurationMetaAnnotation() { - backend.loadGlue(glue, singletonList( + backend.loadGlue(glue, new TestGlueDiscoveryRequest( URI.create("classpath:io/cucumber/spring/cucumbercontextconfigannotation"))); backend.buildWorld(); verify(factory, times(1)).addClass(WithMetaAnnotation.class); } + private static final class TestGlueDiscoveryRequest implements GlueDiscoveryRequest { + private final List gluePaths; + private final List glueClassNames; + + TestGlueDiscoveryRequest(URI... gluePaths) { + this.gluePaths = List.of(gluePaths); + this.glueClassNames = emptyList(); + } + + TestGlueDiscoveryRequest(String... glueClassNames) { + this.gluePaths = emptyList(); + this.glueClassNames = List.of(glueClassNames); + } + + @Override + public List getGlue() { + return gluePaths; + } + + @Override + public List getGlueClassNames() { + return glueClassNames; + } + } + } From 5d7203beb84e3506413059927aff7287889c0fbe Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 29 Mar 2026 14:32:09 +0200 Subject: [PATCH 12/14] WIP --- .../junit/platform/engine/StubBackendProviderService.java | 4 ++-- .../java/io/cucumber/junit/StubBackendProviderService.java | 4 ++-- .../java/io/cucumber/testng/StubBackendProviderService.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/StubBackendProviderService.java b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/StubBackendProviderService.java index 795f6fcfc4..0fb2019cc3 100644 --- a/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/StubBackendProviderService.java +++ b/cucumber-junit-platform-engine/src/test/java/io/cucumber/junit/platform/engine/StubBackendProviderService.java @@ -4,13 +4,13 @@ import io.cucumber.core.backend.BackendProviderService; import io.cucumber.core.backend.Container; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.Lookup; import io.cucumber.core.backend.ParameterInfo; import io.cucumber.core.backend.Snippet; import io.cucumber.core.backend.StepDefinition; import java.lang.reflect.Type; -import java.net.URI; import java.text.MessageFormat; import java.util.Collections; import java.util.List; @@ -34,7 +34,7 @@ public static final class StubBackend implements Backend { } @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { glue.addStepDefinition(createStepDefinition("a single scenario")); glue.addStepDefinition(createStepDefinition("it is executed")); glue.addStepDefinition(createStepDefinition("nothing else happens")); diff --git a/cucumber-junit/src/test/java/io/cucumber/junit/StubBackendProviderService.java b/cucumber-junit/src/test/java/io/cucumber/junit/StubBackendProviderService.java index a7d006b3c1..bab205ec9c 100644 --- a/cucumber-junit/src/test/java/io/cucumber/junit/StubBackendProviderService.java +++ b/cucumber-junit/src/test/java/io/cucumber/junit/StubBackendProviderService.java @@ -4,6 +4,7 @@ import io.cucumber.core.backend.BackendProviderService; import io.cucumber.core.backend.Container; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.HookDefinition; import io.cucumber.core.backend.Lookup; import io.cucumber.core.backend.ParameterInfo; @@ -13,7 +14,6 @@ import io.cucumber.core.backend.TestCaseState; import java.lang.reflect.Type; -import java.net.URI; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; @@ -41,7 +41,7 @@ public static final class StubBackend implements Backend { } @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { glue.addStepDefinition(createStepDefinition("first step")); glue.addStepDefinition(createStepDefinition("second step")); glue.addStepDefinition(createStepDefinition("third step")); diff --git a/cucumber-testng/src/test/java/io/cucumber/testng/StubBackendProviderService.java b/cucumber-testng/src/test/java/io/cucumber/testng/StubBackendProviderService.java index bd1590d3e9..ae320d9343 100644 --- a/cucumber-testng/src/test/java/io/cucumber/testng/StubBackendProviderService.java +++ b/cucumber-testng/src/test/java/io/cucumber/testng/StubBackendProviderService.java @@ -4,13 +4,13 @@ import io.cucumber.core.backend.BackendProviderService; import io.cucumber.core.backend.Container; import io.cucumber.core.backend.Glue; +import io.cucumber.core.backend.GlueDiscoveryRequest; import io.cucumber.core.backend.Lookup; import io.cucumber.core.backend.ParameterInfo; import io.cucumber.core.backend.Snippet; import io.cucumber.core.backend.StepDefinition; import java.lang.reflect.Type; -import java.net.URI; import java.text.MessageFormat; import java.util.Collections; import java.util.List; @@ -34,7 +34,7 @@ private static final class StubBackend implements Backend { } @Override - public void loadGlue(Glue glue, List gluePaths) { + public void loadGlue(Glue glue, GlueDiscoveryRequest glueDiscoveryRequest) { glue.addStepDefinition(createStepDefinition("a scenario")); glue.addStepDefinition(createStepDefinition("a scenario outline")); glue.addStepDefinition(createStepDefinition("it is executed")); From 9aa6b59c04a99a353f9149d2288c1f71ce4b1d8b Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 29 Mar 2026 14:35:15 +0200 Subject: [PATCH 13/14] WIP --- .../cucumber/core/backend/GlueDiscoveryRequest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java b/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java index 599ae9aaf7..9992560d33 100644 --- a/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java +++ b/cucumber-core/src/main/java/io/cucumber/core/backend/GlueDiscoveryRequest.java @@ -3,10 +3,23 @@ import java.net.URI; import java.util.List; +/** + * A request to a Backend implementation to discover glue classes. + */ public interface GlueDiscoveryRequest { + /** + * Returns a list of (typically classpath) URIs for glue discovery. + * + * @return a list of (typically classpath) URIs for glue discovery. + */ List getGlue(); + /** + * Returns a list class names to consider for glue discovery. + * + * @return a list class names to consider for glue discovery. + */ List getGlueClassNames(); } From ed76a2ac0626156b3fed62afc9659e03857bb741 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Sun, 29 Mar 2026 14:40:05 +0200 Subject: [PATCH 14/14] WIP --- .../test/java/io/cucumber/compatibility/CompatibilityTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/compatibility/src/test/java/io/cucumber/compatibility/CompatibilityTest.java b/compatibility/src/test/java/io/cucumber/compatibility/CompatibilityTest.java index 639cc46422..f70b8dfe5a 100644 --- a/compatibility/src/test/java/io/cucumber/compatibility/CompatibilityTest.java +++ b/compatibility/src/test/java/io/cucumber/compatibility/CompatibilityTest.java @@ -55,8 +55,6 @@ final class CompatibilityTest { private static final List unsupportedTestCases = Arrays.asList( // exception: not applicable "test.feature-run-exception", - // exception: Cucumber JVM does not support named hooks - "hooks-named", // exception: Cucumber executes all hooks, // but skipped hooks can skip a scenario "hooks-skipped",