Skip to content

Commit

Permalink
[Core] Log warning when resources could not be loaded (#2235)
Browse files Browse the repository at this point in the history
Fixes: #2212, #2229
  • Loading branch information
mpkorstanje authored Feb 12, 2021
1 parent 3fd0746 commit e0ad566
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Fixed
* [Core] Pass class loader to ServiceLoader.load invocations ([#2220](https://github.com/cucumber/cucumber-jvm/issues/2220) M.P. Korstanje)
* [Core] Log warnings when classes or resource could not be loaded ([#2235](https://github.com/cucumber/cucumber-jvm/issues/2235) M.P. Korstanje)

## [6.9.1] (2020-12-14)

Expand Down
20 changes: 13 additions & 7 deletions core/src/main/java/io/cucumber/core/resource/ClasspathScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.function.Predicate;
import java.util.function.Supplier;

import static io.cucumber.core.resource.ClasspathSupport.classPathScanningExplanation;
import static io.cucumber.core.resource.ClasspathSupport.determineFullyQualifiedClassName;
import static io.cucumber.core.resource.ClasspathSupport.getUrisForPackage;
import static io.cucumber.core.resource.ClasspathSupport.requireValidPackageName;
Expand Down Expand Up @@ -95,16 +96,21 @@ private Function<Path, Consumer<Path>> processClassFiles(
) {
return baseDir -> classFile -> {
String fqn = determineFullyQualifiedClassName(baseDir, basePackageName, classFile);
try {
Optional.of(getClassLoader().loadClass(fqn))
.filter(classFilter)
.ifPresent(classConsumer);
} catch (ClassNotFoundException | NoClassDefFoundError e) {
log.debug(e, () -> "Failed to load class " + fqn);
}
safelyLoadClass(fqn)
.filter(classFilter)
.ifPresent(classConsumer);
};
}

private Optional<Class<?>> safelyLoadClass(String fqn) {
try {
return Optional.ofNullable(getClassLoader().loadClass(fqn));
} catch (ClassNotFoundException | NoClassDefFoundError e) {
log.warn(e, () -> "Failed to load class '" + fqn + "'.\n" + classPathScanningExplanation());
}
return Optional.empty();
}

public List<Class<?>> scanForClassesInPackage(String packageName) {
return scanForClassesInPackage(packageName, NULL_FILTER);
}
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/io/cucumber/core/resource/ClasspathSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,14 @@ public static URI rootPackageUri() {
return URI.create(CLASSPATH_SCHEME_PREFIX + RESOURCE_SEPARATOR_CHAR);
}

public static String classPathScanningExplanation() {
return "By default Cucumber scans the entire classpath for step definitions.\n" +
"You can restrict this by configuring the glue path.\n" +
"\n" +
"Examples:\n" +
" - @CucumberOptions(glue = \"com.example.application\")\n" +
" - src/test/resources/junit-platform.properties cucumber.glue=com.example.application\n" +
" - src/test/resources/cucumber.properties cucumber.glue=com.example.application\n";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private static CloseablePath handleJarUriScheme(URI uri) throws IOException, URI

private static CucumberException nestedJarEntriesAreUnsupported(URI uri) {
return new CucumberException("" +
"The resource " + uri + " is located in a nested jar.\n" +
"The resource '" + uri + "' is located in a nested jar.\n" +
"\n" +
"This typically happens when trying to run Cucumber inside a Spring Boot Executable Jar.\n" +
"Cucumber currently doesn't support classpath scanning in nested jars.\n" +
Expand Down
5 changes: 5 additions & 0 deletions core/src/main/java/io/cucumber/core/resource/PathScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
Expand All @@ -22,10 +23,14 @@

class PathScanner {

private static final Logger log = LoggerFactory.getLogger(PathScanner.class);

void findResourcesForUri(URI baseUri, Predicate<Path> filter, Function<Path, Consumer<Path>> consumer) {
try (CloseablePath closeablePath = open(baseUri)) {
Path baseDir = closeablePath.getPath();
findResourcesForPath(baseDir, filter, consumer);
} catch (FileSystemNotFoundException e) {
log.warn(e, () -> "Failed to find resources for '" + baseUri + "'");
} catch (IOException | URISyntaxException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
package io.cucumber.core.resource;

import io.cucumber.core.logging.LogRecordListener;
import io.cucumber.core.logging.LoggerFactory;
import io.cucumber.core.resource.test.ExampleClass;
import io.cucumber.core.resource.test.ExampleInterface;
import io.cucumber.core.resource.test.OtherClass;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.LogRecord;

import static java.util.Arrays.asList;
import static java.util.Collections.enumeration;
import static java.util.Collections.singletonList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class ClasspathScannerTest {

private final ClasspathScanner scanner = new ClasspathScanner(
ClasspathScannerTest.class::getClassLoader);

private LogRecordListener logRecordListener;

@BeforeEach
void setup() {
logRecordListener = new LogRecordListener();
LoggerFactory.addListener(logRecordListener);
}

@AfterEach
void tearDown() {
LoggerFactory.removeListener(logRecordListener);
}

@Test
void scanForSubClassesInPackage() {
List<Class<? extends ExampleInterface>> classes = scanner.scanForSubClassesInPackage("io.cucumber",
Expand Down Expand Up @@ -49,4 +86,21 @@ void scanForClassesInNonExistingPackage() {
assertThat(classes, empty());
}

@Test
void scanForResourcesInUnsupportedFileSystem() throws IOException {
ClassLoader classLoader = mock(ClassLoader.class);
ClasspathScanner scanner = new ClasspathScanner(() -> classLoader);
URLStreamHandler handler = new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) {
return null;
}
};
URL resourceUrl = new URL(null, "bundle-resource:com/cucumber/bundle", handler);
when(classLoader.getResources("com/cucumber/bundle")).thenReturn(enumeration(singletonList(resourceUrl)));
assertThat(scanner.scanForClassesInPackage("com.cucumber.bundle"), empty());
assertThat(logRecordListener.getLogRecords().get(0).getMessage(),
containsString("Failed to find resources for 'bundle-resource:com/cucumber/bundle'"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ void scanForResourcesDirectory() {
new File("src/test/resources/io/cucumber/core/resource/test/spaces in name resource.txt").toURI()));
}

@Test
void shouldThrowIfForResourcesPathNotExist() {
File file = new File("src/test/resources/io/cucumber/core/does/not/exist");
assertThrows(IllegalArgumentException.class, () -> resourceScanner.scanForResourcesPath(file.toPath()));
}

@Test
@DisabledOnOs(value = OS.WINDOWS,
disabledReason = "Only works if repository is explicitly cloned activated symlinks and " +
Expand Down
18 changes: 17 additions & 1 deletion java/src/main/java/io/cucumber/java/MethodScanner.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package io.cucumber.java;

import io.cucumber.core.logging.Logger;
import io.cucumber.core.logging.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.function.BiConsumer;

import static io.cucumber.core.resource.ClasspathSupport.classPathScanningExplanation;
import static io.cucumber.java.InvalidMethodException.createInvalidMethodException;

final class MethodScanner {

private static final Logger log = LoggerFactory.getLogger(MethodScanner.class);

private MethodScanner() {
}

Expand All @@ -21,11 +27,21 @@ static void scan(Class<?> aClass, BiConsumer<Method, Annotation> consumer) {
if (!isInstantiable(aClass)) {
return;
}
for (Method method : aClass.getMethods()) {
for (Method method : safelyGetMethods(aClass)) {
scan(consumer, aClass, method);
}
}

private static Method[] safelyGetMethods(Class<?> aClass) {
try {
return aClass.getMethods();
} catch (NoClassDefFoundError e) {
log.warn(e,
() -> "Failed to load methods of class '" + aClass.getName() + "'.\n" + classPathScanningExplanation());
}
return new Method[0];
}

private static boolean isInstantiable(Class<?> clazz) {
boolean isNonStaticInnerClass = !Modifier.isStatic(clazz.getModifiers()) && clazz.getEnclosingClass() != null;
return Modifier.isPublic(clazz.getModifiers()) && !Modifier.isAbstract(clazz.getModifiers())
Expand Down

0 comments on commit e0ad566

Please sign in to comment.