Skip to content

Commit 0158029

Browse files
committed
[Java] Allow specifying individual classes in the glue option (#1707)
1 parent 8114aed commit 0158029

File tree

18 files changed

+175
-34
lines changed

18 files changed

+175
-34
lines changed

core/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ cucumber.filter.tags= # a cucumber tag expression.
4343
# only scenarios with matching tags are executed.
4444
# example: @Cucumber and not (@Gherkin or @Zucchini)
4545
46-
cucumber.glue= # comma separated package names.
47-
# example: com.example.glue
46+
cucumber.glue= # comma separated package or class names.
47+
# example: com.example.glue,com.example.features.SomeFeature
4848
4949
cucumber.plugin= # comma separated plugin strings.
5050
# example: pretty, json:path/to/report.json

core/src/main/java/io/cucumber/core/options/Constants.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ public final class Constants {
103103
/**
104104
* Property name to set the glue path: {@value}
105105
* <p>
106-
* A comma separated list of a classpath uri or package name e.g.:
107-
* {@code com.example.app.steps}.
106+
* A comma separated list of a classpath uri or a package or a class name
107+
* e.g.:
108+
* {@code com.example.app.steps,com.example.app.features.SomeFeatureSteps}.
108109
*
109110
* @see io.cucumber.core.feature.GluePath
110111
*/

core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java

+21-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.net.URI;
77
import java.nio.file.Path;
88
import java.util.ArrayList;
9+
import java.util.Arrays;
910
import java.util.Collection;
1011
import java.util.List;
1112
import java.util.Optional;
@@ -38,6 +39,15 @@ public ClasspathScanner(Supplier<ClassLoader> classLoaderSupplier) {
3839
this.classLoaderSupplier = classLoaderSupplier;
3940
}
4041

42+
public <T> List<Class<? extends T>> scanForSubClasses(String packageOrClassName, Class<T> parentClass) {
43+
Optional<Class<?>> classFromName = safelyLoadClass(packageOrClassName, false);
44+
45+
return classFromName.isPresent() && !parentClass.equals(classFromName.get())
46+
&& parentClass.isAssignableFrom(classFromName.get())
47+
? Arrays.asList((Class<? extends T>) classFromName.get())
48+
: scanForSubClassesInPackage(packageOrClassName, parentClass);
49+
}
50+
4151
public <T> List<Class<? extends T>> scanForSubClassesInPackage(String packageName, Class<T> parentClass) {
4252
return scanForClassesInPackage(packageName, isSubClassOf(parentClass))
4353
.stream()
@@ -96,17 +106,19 @@ private Function<Path, Consumer<Path>> processClassFiles(
96106
) {
97107
return baseDir -> classFile -> {
98108
String fqn = determineFullyQualifiedClassName(baseDir, basePackageName, classFile);
99-
safelyLoadClass(fqn)
109+
safelyLoadClass(fqn, true)
100110
.filter(classFilter)
101111
.ifPresent(classConsumer);
102112
};
103113
}
104114

105-
private Optional<Class<?>> safelyLoadClass(String fqn) {
115+
private Optional<Class<?>> safelyLoadClass(String fqn, boolean logWarning) {
106116
try {
107117
return Optional.ofNullable(getClassLoader().loadClass(fqn));
108118
} catch (ClassNotFoundException | NoClassDefFoundError e) {
109-
log.warn(e, () -> "Failed to load class '" + fqn + "'.\n" + classPathScanningExplanation());
119+
if (logWarning) {
120+
log.warn(e, () -> "Failed to load class '" + fqn + "'.\n" + classPathScanningExplanation());
121+
}
110122
}
111123
return Optional.empty();
112124
}
@@ -115,4 +127,10 @@ public List<Class<?>> scanForClassesInPackage(String packageName) {
115127
return scanForClassesInPackage(packageName, NULL_FILTER);
116128
}
117129

130+
public List<Class<?>> getClasses(String packageOrClassName) {
131+
Optional<Class<?>> classFromName = safelyLoadClass(packageOrClassName, false);
132+
return classFromName.isPresent() ? Arrays.asList(classFromName.get())
133+
: scanForClassesInPackage(packageOrClassName, NULL_FILTER);
134+
}
135+
118136
}

core/src/test/java/io/cucumber/core/feature/GluePathTest.java

+28
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ void can_parse_absolute_path_form() {
7070
() -> assertThat(uri.getSchemeSpecificPart(), is("/com/example/app")));
7171
}
7272

73+
@Test
74+
void can_parse_absolute_path_form_class() {
75+
URI uri = GluePath.parse("/com/example/app/Steps");
76+
77+
assertAll(
78+
() -> assertThat(uri.getScheme(), is("classpath")),
79+
() -> assertThat(uri.getSchemeSpecificPart(), is("/com/example/app/Steps")));
80+
}
81+
7382
@Test
7483
void can_parse_package_form() {
7584
URI uri = GluePath.parse("com.example.app");
@@ -79,6 +88,15 @@ void can_parse_package_form() {
7988
() -> assertThat(uri.getSchemeSpecificPart(), is("/com/example/app")));
8089
}
8190

91+
@Test
92+
void can_parse_package_form_class() {
93+
URI uri = GluePath.parse("com.example.app.Steps");
94+
95+
assertAll(
96+
() -> assertThat(uri.getScheme(), is("classpath")),
97+
() -> assertThat(uri.getSchemeSpecificPart(), is("/com/example/app/Steps")));
98+
}
99+
82100
@Test
83101
void glue_path_must_have_class_path_scheme() {
84102
Executable testMethod = () -> GluePath.parse("file:com/example/app");
@@ -105,6 +123,16 @@ void can_parse_windows_path_form() {
105123
() -> assertThat(uri.getSchemeSpecificPart(), is(equalTo("/com/example/app"))));
106124
}
107125

126+
@Test
127+
@EnabledOnOs(OS.WINDOWS)
128+
void can_parse_windows_path_form_class() {
129+
URI uri = GluePath.parse("com\\example\\app\\Steps");
130+
131+
assertAll(
132+
() -> assertThat(uri.getScheme(), is("classpath")),
133+
() -> assertThat(uri.getSchemeSpecificPart(), is(equalTo("/com/example/app/Steps"))));
134+
}
135+
108136
@Test
109137
@EnabledOnOs(OS.WINDOWS)
110138
void absolute_windows_path_form_is_not_valid() {

core/src/test/java/io/cucumber/core/resource/ClasspathScannerTest.java

+62-11
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,16 @@
55
import io.cucumber.core.resource.test.ExampleClass;
66
import io.cucumber.core.resource.test.ExampleInterface;
77
import io.cucumber.core.resource.test.OtherClass;
8-
import org.hamcrest.CoreMatchers;
9-
import org.hamcrest.Matchers;
108
import org.junit.jupiter.api.AfterEach;
119
import org.junit.jupiter.api.BeforeEach;
1210
import org.junit.jupiter.api.Test;
13-
import org.mockito.Mockito;
1411

1512
import java.io.IOException;
16-
import java.net.URI;
1713
import java.net.URL;
1814
import java.net.URLConnection;
1915
import java.net.URLStreamHandler;
20-
import java.util.Arrays;
21-
import java.util.Collections;
22-
import java.util.Enumeration;
2316
import java.util.List;
24-
import java.util.Vector;
25-
import java.util.logging.Level;
26-
import java.util.logging.LogRecord;
2717

28-
import static java.util.Arrays.asList;
2918
import static java.util.Collections.enumeration;
3019
import static java.util.Collections.singletonList;
3120
import static org.hamcrest.MatcherAssert.assertThat;
@@ -70,6 +59,38 @@ void scanForSubClassesInNonExistingPackage() {
7059
assertThat(classes, empty());
7160
}
7261

62+
@Test
63+
void scanForSubClassesWhenPackage() {
64+
List<Class<? extends ExampleInterface>> classes = scanner.scanForSubClasses(
65+
"io.cucumber.core.resource.test",
66+
ExampleInterface.class);
67+
68+
assertThat(classes, contains(ExampleClass.class));
69+
}
70+
71+
@Test
72+
void scanForSubClassesWhenClass() {
73+
List<Class<? extends ExampleInterface>> classes = scanner.scanForSubClasses(
74+
"io.cucumber.core.resource.test.ExampleClass",
75+
ExampleInterface.class);
76+
77+
assertThat(classes, contains(ExampleClass.class));
78+
}
79+
80+
@Test
81+
void scanForSubClassesWhenNonExistingPackage() {
82+
List<Class<? extends ExampleInterface>> classes = scanner
83+
.scanForSubClasses("io.cucumber.core.resource.does.not.exist", ExampleInterface.class);
84+
assertThat(classes, empty());
85+
}
86+
87+
@Test
88+
void scanForSubClassesWhenNonExistingClass() {
89+
List<Class<? extends ExampleInterface>> classes = scanner
90+
.scanForSubClasses("io.cucumber.core.resource.test.NonExistentClass", ExampleInterface.class);
91+
assertThat(classes, empty());
92+
}
93+
7394
@Test
7495
void scanForClassesInPackage() {
7596
List<Class<?>> classes = scanner.scanForClassesInPackage("io.cucumber.core.resource.test");
@@ -104,4 +125,34 @@ protected URLConnection openConnection(URL u) {
104125
containsString("Failed to find resources for 'bundle-resource:com/cucumber/bundle'"));
105126
}
106127

128+
@Test
129+
void getClassesWhenPackage() {
130+
List<Class<?>> classes = scanner.getClasses("io.cucumber.core.resource.test");
131+
132+
assertThat(classes, containsInAnyOrder(
133+
ExampleClass.class,
134+
ExampleInterface.class,
135+
OtherClass.class));
136+
137+
}
138+
139+
@Test
140+
void getClassesWhenNonExistingPackage() {
141+
List<Class<?>> classes = scanner.getClasses("io.cucumber.core.resource.does.not.exist");
142+
assertThat(classes, empty());
143+
}
144+
145+
@Test
146+
void getClassesWhenClass() {
147+
List<Class<?>> classes = scanner.getClasses("io.cucumber.core.resource.test.ExampleClass");
148+
149+
assertThat(classes, contains(ExampleClass.class));
150+
151+
}
152+
153+
@Test
154+
void getClassesWhenNonExistingClass() {
155+
List<Class<?>> classes = scanner.getClasses("io.cucumber.core.resource.test.NonExistentClass");
156+
assertThat(classes, empty());
157+
}
107158
}

guice/src/main/java/io/cucumber/guice/GuiceBackend.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public void loadGlue(Glue glue, List<URI> gluePaths) {
2929
gluePaths.stream()
3030
.filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme()))
3131
.map(ClasspathSupport::packageName)
32-
.map(classFinder::scanForClassesInPackage)
32+
.map(classFinder::getClasses)
3333
.flatMap(Collection::stream)
3434
.filter(InjectorSource.class::isAssignableFrom)
3535
.forEach(container::addClass);

guice/src/test/java/io/cucumber/guice/GuiceBackendTest.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,19 @@ class GuiceBackendTest {
3333
private ObjectFactory factory;
3434

3535
@Test
36-
void finds_injector_source_impls_by_classpath_url() {
36+
void finds_injector_source_impls_by_package_classpath_url() {
3737
GuiceBackend backend = new GuiceBackend(factory, classLoader);
3838
backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/guice/integration")));
3939
verify(factory).addClass(YourInjectorSource.class);
4040
}
4141

42+
@Test
43+
void finds_injector_source_impls_by_class_classpath_url() {
44+
GuiceBackend backend = new GuiceBackend(factory, classLoader);
45+
backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/guice/integration/YourInjectorSource")));
46+
verify(factory).addClass(YourInjectorSource.class);
47+
}
48+
4249
@Test
4350
void world_and_snippet_methods_do_nothing() {
4451
GuiceBackend backend = new GuiceBackend(factory, classLoader);

java/src/main/java/io/cucumber/java/JavaBackend.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void loadGlue(Glue glue, List<URI> gluePaths) {
3535
gluePaths.stream()
3636
.filter(gluePath -> CLASSPATH_SCHEME.equals(gluePath.getScheme()))
3737
.map(ClasspathSupport::packageName)
38-
.map(classFinder::scanForClassesInPackage)
38+
.map(classFinder::getClasses)
3939
.flatMap(Collection::stream)
4040
.forEach(aGlueClass -> scan(aGlueClass, (method, annotation) -> {
4141
container.addClass(method.getDeclaringClass());

java/src/test/java/io/cucumber/java/JavaBackendTest.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.cucumber.core.backend.Glue;
44
import io.cucumber.core.backend.ObjectFactory;
55
import io.cucumber.core.backend.StepDefinition;
6+
import io.cucumber.java.individualclasssteps.StepsTwo;
67
import io.cucumber.java.steps.Steps;
78
import org.junit.jupiter.api.BeforeEach;
89
import org.junit.jupiter.api.Test;
@@ -48,9 +49,11 @@ void createBackend() {
4849

4950
@Test
5051
void finds_step_definitions_by_classpath_url() {
51-
backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/java/steps")));
52+
backend.loadGlue(glue, asList(URI.create("classpath:io/cucumber/java/steps"),
53+
URI.create("classpath:io/cucumber/java/individualclasssteps/StepsTwo")));
5254
backend.buildWorld();
5355
verify(factory).addClass(Steps.class);
56+
verify(factory).addClass(StepsTwo.class);
5457
}
5558

5659
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.cucumber.java.individualclasssteps;
2+
3+
import io.cucumber.java.en.Given;
4+
5+
public class StepsTwo {
6+
7+
@Given("test")
8+
public void test() {
9+
10+
}
11+
12+
}

java8/src/main/java/io/cucumber/java8/Java8Backend.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public void loadGlue(Glue glue, List<URI> gluePaths) {
4444
gluePaths.stream()
4545
.filter(gluePath -> ClasspathSupport.CLASSPATH_SCHEME.equals(gluePath.getScheme()))
4646
.map(ClasspathSupport::packageName)
47-
.map(basePackageName -> classFinder.scanForSubClassesInPackage(basePackageName, LambdaGlue.class))
47+
.map(basePackageName -> classFinder.scanForSubClasses(basePackageName, LambdaGlue.class))
4848
.flatMap(Collection::stream)
4949
.filter(glueClass -> !glueClass.isInterface())
5050
.filter(glueClass -> glueClass.getConstructors().length > 0)

java8/src/test/java/io/cucumber/java8/Java8BackendTest.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.cucumber.core.backend.Glue;
44
import io.cucumber.core.backend.ObjectFactory;
5+
import io.cucumber.java8.individualclasssteps.StepsTwo;
56
import io.cucumber.java8.steps.Steps;
67
import org.junit.jupiter.api.BeforeEach;
78
import org.junit.jupiter.api.Test;
@@ -10,9 +11,9 @@
1011
import org.mockito.junit.jupiter.MockitoExtension;
1112

1213
import java.net.URI;
14+
import java.util.Arrays;
1315

1416
import static java.lang.Thread.currentThread;
15-
import static java.util.Collections.singletonList;
1617
import static org.mockito.Mockito.verify;
1718

1819
@ExtendWith({ MockitoExtension.class })
@@ -33,9 +34,11 @@ void createBackend() {
3334

3435
@Test
3536
void finds_step_definitions_by_classpath_url() {
36-
backend.loadGlue(glue, singletonList(URI.create("classpath:io/cucumber/java8/steps")));
37+
backend.loadGlue(glue, Arrays.asList(URI.create("classpath:io/cucumber/java8/steps"),
38+
URI.create("classpath:io/cucumber/java8/individualclasssteps/StepsTwo")));
3739
backend.buildWorld();
3840
verify(factory).addClass(Steps.class);
41+
verify(factory).addClass(StepsTwo.class);
3942
}
4043

4144
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.cucumber.java8.individualclasssteps;
2+
3+
import io.cucumber.java8.En;
4+
5+
public class StepsTwo implements En {
6+
7+
public StepsTwo() {
8+
9+
Given("another test", () -> {
10+
11+
});
12+
13+
}
14+
15+
}

junit-platform-engine/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,8 @@ cucumber.filter.tags= # a cucumber tag e
295295
# JUnit 5 prefer using JUnit 5s discovery request filters
296296
# or JUnit 5 tag expressions instead.
297297
298-
cucumber.glue= # comma separated package names.
299-
# example: com.example.glue
298+
cucumber.glue= # comma separated package or class names.
299+
# example: com.example.glue,com.example.features.SomeFeature
300300
301301
cucumber.junit-platform.naming-strategy= # long or short.
302302
# default: short

junit-platform-engine/src/main/java/io/cucumber/junit/platform/engine/Constants.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ public final class Constants {
7878
/**
7979
* Property name to set the glue path: {@value}
8080
* <p>
81-
* A comma separated list of a classpath uri or package name e.g.:
82-
* {@code com.example.app.steps}.
81+
* A comma separated list of a classpath uri or a package or a class name
82+
* e.g.:
83+
* {@code com.example.app.steps,com.example.app.features.SomeFeatureSteps}.
8384
*
8485
* @see io.cucumber.core.feature.GluePath
8586
*/

junit/src/main/java/io/cucumber/junit/CucumberOptions.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@
3636
String[] features() default {};
3737

3838
/**
39-
* Package to load glue code (step definitions, hooks and plugins) from.
40-
* E.g: {@code com.example.app}
39+
* Packages or classes to load glue code (step definitions, hooks and
40+
* plugins) from. E.g:
41+
* {@code com.example.app, com.example.app.features.SomeFeatureSteps}
4142
* <p>
4243
* When no glue is provided, Cucumber will use the package of the annotated
4344
* class. For example, if the annotated class is

0 commit comments

Comments
 (0)