From 5b381e7a042044699763e55299baaba13041dfd2 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 25 Nov 2025 16:27:06 +0100 Subject: [PATCH 1/2] Support manual glue class registration --- .../io/cucumber/core/backend/Backend.java | 16 ++++++- .../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, 121 insertions(+), 22 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 676fd43cbc..5e78d4d4cf 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 @@ -4,6 +4,7 @@ import java.net.URI; import java.util.List; +import java.util.Set; @API(status = API.Status.STABLE) public interface Backend { @@ -15,7 +16,20 @@ public interface Backend { * @param glue Glue that provides the steps to be executed. * @param gluePaths The locations for the glue to be loaded. */ - void loadGlue(Glue glue, List gluePaths); + 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 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 df24f6b0de..f174f75cfb 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 @@ -30,6 +30,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; @@ -125,6 +126,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 795d74b946..674619c39e 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 @@ -14,6 +14,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<>(); @@ -150,6 +153,11 @@ public List getGlue() { return unmodifiableList(glue); } + @Override + public Set getGlueClasses() { + return unmodifiableSet(glueClasses); + } + @Override public boolean isDryRun() { return dryRun; @@ -191,6 +199,11 @@ void setGlue(List parsedGlue) { glue.addAll(parsedGlue); } + void setGlueClasses(Set parsedGlue) { + glueClasses.clear(); + glueClasses.addAll(parsedGlue); + } + @Override public List getFeaturePaths() { return unmodifiableList(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 58b6792bc5..84a9498098 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 @@ -12,6 +12,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; @@ -22,6 +23,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 List parsedRerunPaths = null; private Integer parsedThreads = null; @@ -59,6 +61,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; @@ -128,6 +136,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 6d8c1ffe11..f89ac2c5eb 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 (ClassNotFoundException | NoClassDefFoundError 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 0fe0ffb807..12758a9d17 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 @@ -5,7 +5,9 @@ import io.cucumber.core.snippets.SnippetType; import java.net.URI; +import java.util.Collections; import java.util.List; +import java.util.Set; public interface Options { @@ -19,4 +21,7 @@ public interface Options { 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 97de369009..f2cfa71cfc 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 3db7dd30b0..596f7253ff 100644 --- a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java +++ b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java @@ -12,7 +12,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; @@ -33,20 +36,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 b03dd28bc0a236fec15e4c5e0feefaef96ed7aa9 Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Tue, 25 Nov 2025 16:47:47 +0100 Subject: [PATCH 2/2] 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 596f7253ff..f56916a45e 100644 --- a/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java +++ b/cucumber-java8/src/main/java/io/cucumber/java8/Java8Backend.java @@ -53,6 +53,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()) @@ -64,7 +65,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());