Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests not found via classpath scanning within nested JAR in Spring Boot JAR #1724

Closed
ferstl opened this issue Jan 10, 2019 · 22 comments · Fixed by #3606
Closed

Tests not found via classpath scanning within nested JAR in Spring Boot JAR #1724

ferstl opened this issue Jan 10, 2019 · 22 comments · Fixed by #3606

Comments

@ferstl
Copy link

ferstl commented Jan 10, 2019

We are providing a test suite for our applications as Spring Boot application. The test suite is basically a set of regular JUnit 5 tests that are launched in the Spring Boot application.
The application is packaged as Spring Boot JAR (created with the spring-boot-maven-plugin).

We do now have the problem, that the tests are not found when we are using DiscoverySelectors.selectPackage() and run the application as Spring Boot JAR. Everything works fine, when we select the tests by classname. Executing the application within an IDE works fine as well.

The problem seems to be in ClasspathScanner#findClassesForPath()). Files.walkFileTree() does not work with nested JAR files. A similar problem was already reported in #399.

Steps to reproduce

I created a project on GitHub to reproduce this issue. When the Application is run within the IDE, everything works fine. When it is executed as Spring Boot JAR, the test is not found.

Context

  • Used versions (Jupiter/Vintage/Platform): junit-bom 5.3.2
  • Build Tool/IDE: Maven, OpenJDK11, IntelliJ
  • Spring Boot: 2.1.1.RELEASE
@ferstl ferstl changed the title Tests not found when within a Spring Boot JAR Tests not found within a Spring Boot JAR Jan 10, 2019
@sbrannen sbrannen added this to the 5.4 M2 milestone Jan 10, 2019
@sbrannen
Copy link
Member

Thanks for raising the issue.

I've tentatively slated this for 5.4 M2 for the purpose of team discussion.

@sbrannen sbrannen changed the title Tests not found within a Spring Boot JAR Tests not found via classpath scanning within nested JAR Jan 10, 2019
@sormuras sormuras modified the milestones: 5.4 M2, 5.4 Backlog Jan 11, 2019
@sormuras sormuras self-assigned this Jun 30, 2019
@sormuras sormuras modified the milestones: 5.6 Backlog, 5.6 M1 Jun 30, 2019
@sormuras
Copy link
Member

sormuras commented Jun 30, 2019

Today I stumbled over the same problems. Thanks for creating a demo project @ferstl!

I'll a) push an integration test showing the issue is even applicable when test classes are jarred (not nested) and b) tackle the underlying missing feature/enhancement after 5.5 GA is released.

@marcphilipp marcphilipp modified the milestones: 5.6 M1, General Backlog Jul 12, 2019
@marcphilipp
Copy link
Member

Team Decision: Move to General Backlog to see whether this use case is relevant for other users as well.

@Oleg3n
Copy link

Oleg3n commented Jul 24, 2019

Yes, this problem is relevant for other users. Please fix it.

@jbduncan
Copy link
Contributor

jbduncan commented Jul 25, 2019

If this issue is ever addressed, I wonder if it would be worth including ClassGraph, shadowing it, and using it instead of ClasspathScanner. 🤔

@sbrannen
Copy link
Member

Interesting idea, @jbduncan!

I didn't realize that ClassGraph had explicit support for Spring Boot's proprietary JAR structure until now.

Jarfiles within jarfiles (to unlimited nesting depth), e.g.
project.jar!/BOOT-INF/lib/dependency.jar, as required by Spring-Boot, JBoss, and Felix classloaders, and probably others.

@sbrannen sbrannen changed the title Tests not found via classpath scanning within nested JAR Tests not found via classpath scanning within nested JAR in Spring Boot JAR Jul 25, 2019
@marcphilipp
Copy link
Member

I've spiked implementing ClasspathScanner using ClassGraph and it passes all the tests:
9000a80

The build is currently broken due to some error in our module compilation setup:
https://scans.gradle.com/s/z2kzsnwxcrt7y/console-log?task=:junit-jupiter:compileModule

@sormuras Let's take a look at that when you're back.

@sormuras
Copy link
Member

sormuras commented Aug 4, 2019

Why so late? 🤓

Looks like the ClassGraph JAR is not available when compiling the modules. Neither on the module-path, nor on the class-path. Will look into it ... soon.

@ValTimof
Copy link

ValTimof commented Oct 3, 2019

Hello
I have faced with this issue triying to run my tests from spring boot jar.
I use gradle bootJar task and place my tests to BOOT-INF/classes as spring using this

    from(sourceSets.test.output) {
        into 'BOOT-INF/classes'
    }

I can see that the problem is in CloseablePath.create(baseUri) method where you split the path by "!" symbol. It returns "BOOT-INF/classes" instead of "BOOT-INF/classes/com.target.package.name"

The reason is that full path in spring boot jar contains two "!" signs like this
"jar:file:/C:/Users/IdeaProjects/build/libs/application.jar!/BOOT-INF/classes!/com/target/package/name"

I decided to use -c and pass full class name and it works. But won't be handy if I have a lot of tests

@Jmyeluri
Copy link

@epam-valerii is correct, the baseDir is incorrect so the subPackage variable in determineSubpackageName is returning the full package instead of '' like it supposed to. @marcphilipp any update on this issue? It still has not been fixed in 5.5.2.

@marcphilipp
Copy link
Member

The team decision from above to wait for additional interest still stands.

@ValTimof
Copy link

I've found a workaround.
Gradle Shadow plugin could be used instead of bootJar task for not to produce nested Jars.

spring-projects/spring-boot#1828 (comment)

With configuration below Spring Boot features work alongside with JUnit Console Launcher --select-package option

import com.github.jengelman.gradle.plugins.shadow.transformers.*
shadowJar {
    zip64 true
    
    manifest {
        attributes 'Implementation-Title': 'Testing Jar File',
                'Main-Class': 'org.junit.platform.console.ConsoleLauncher'
    }
    from sourceSets.test.output
    configurations = [project.configurations.testRuntimeClasspath]
    exclude '**/Log4j2Plugins.dat'

    // Required for Spring
    mergeServiceFiles()
    append 'META-INF/spring.handlers'
    append 'META-INF/spring.schemas'
    append 'META-INF/spring.tooling'
    transform(PropertiesFileTransformer) {
        paths = ['META-INF/spring.factories' ]
        mergeStrategy = "append"
    }
}

@rossdanderson
Copy link

Same issue here, currently declaring every class via LauncherDiscoveryRequestBuilder

@adrian-skybaker
Copy link

Hit this issue for integration tests run using JUnit 5 that are packaged up for a CI/CD pipeline as a Spring Boot application.

@jimonthebarn
Copy link

jimonthebarn commented Nov 11, 2020

Just chiming in here. We ran into this issue as well. Our motivation for this is to run E2E-tests within a cluster by deploying a Spring Boot Application executing the tests using the JUnit5 platform launcher.

@dkroehan
Copy link

Same issue here. Any update on this issue?

@marcphilipp
Copy link
Member

No, but someone could pick up where I left off above: #1724 (comment) 🙂

@marcphilipp marcphilipp removed this from the General Backlog milestone Jun 19, 2021
@newcron
Copy link

newcron commented Jun 15, 2022

hey there, also stumbled upon it. same issue: trying to package dependencies via the spring boot fat jar packager to run integration tests in a kubernetes cluster.

I know, the problem does not exist when using other build plugins (e.g. maven shade plugin or assembly plugin). However, these plugins add quite a bit to the build time. The spring plugin is literally over 20x faster when assembling a fat jar. (that's because it does not extract and add all files to the target archive but just adds the archives itself)

@elekktrisch
Copy link

elekktrisch commented Jan 24, 2023

Looks like the following could be a workaround:

  1. Add a marker-annotation with runtime retention to all test-classes
  2. Use ClassPathScanningCandidateComponentProvider with an AnnotationTypeFilter
  3. Use DiscoverySelectors.selectClass(Class.forName(candidate.getBeanClassName()))

That way at least in our environment it seems to pick up all annotated test-classes.

E.g.:

  private static List<ClassSelector> selectorsFor(Class<? extends Annotation> annotationType) {
    ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
    scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));

    return scanner.findCandidateComponents("ch/corporateapi/corapi/testing/systemtest").stream()
        .map(c -> c.getBeanClassName())
        .filter(Objects::nonNull)
        .map(className -> DiscoverySelectors.selectClass(classFor(className)))
        .toList();
  }

  @NotNull
  private static Class<?> classFor(String className) {
    try {
      return Class.forName(className);
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

after that, we can use LauncherDiscoveryRequestBuilder.request().selectors(selectorsFor(MyTest.class));

@gte811i
Copy link

gte811i commented Feb 2, 2023

Yes. Please fix. I spent 2 days finding a solution to this problem of shadow jar not working with Spring Boot. BootJar not bringing in test classes, not getting any shadowjar to run the platform-console.

I can confirm @valeratimofeev solution works. That will create a fat jar (make sure to run the compileTestJava first) that when executing -jar junit-platform-console-standalone-1.9.2.jar -cp yourshadow.jar -c your.test.Class it will execute.

What a pain.

@mpkorstanje
Copy link
Contributor

mpkorstanje commented Dec 17, 2023

Please note the Spring Boot 3.2.0 release notes:

Nested Jar Support
The underlying code that supports Spring Boot’s "Uber Jar" loading has been rewritten now that we no longer need to support Java 8. The updated code makes use of a new URL format which is more compliant with JDK expectations. The previous URL format of jar:file:/dir/myjar.jar:BOOT-INF/lib/nested.jar!/com/example/MyClass.class has been replaced with jar:nested:/dir/myjar.jar/!BOOT-INF/lib/nested.jar!/com/example/MyClass.class. The updated code also makes use of java.lang.ref.Cleaner (which was part of JDK 9) for resource management.

Ignoring the typos in the example uri, this means that java's FileSystem API can now be used to read Spring Boots jar:nested URIs. There does appear to be a minor problem in that JUnit 5 does not parse URI's correctly.

2023-12-17T09:29:36.063+01:00  WARN 8361 --- [           main] o.j.p.commons.util.ClasspathScanner      : Error scanning files for URI jar:nested:/home/mpkorstanje/Projects/mpkorstanje/junit-5-spring-boot-3.2.0/build/libs/junit-5-spring-boot-3.2.0-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/com/example

java.lang.IllegalArgumentException: 'path' must contain '/!'
        at org.springframework.boot.loader.net.protocol.nested.NestedLocation.parse(NestedLocation.java:98) ~[junit-5-spring-boot-3.2.0-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.net.protocol.nested.NestedLocation.fromUri(NestedLocation.java:89) ~[junit-5-spring-boot-3.2.0-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.nio.file.NestedFileSystemProvider.getPath(NestedFileSystemProvider.java:88) ~[junit-5-spring-boot-3.2.0-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at java.base/java.nio.file.Path.of(Path.java:208) ~[na:na]
        at java.base/java.nio.file.Paths.get(Paths.java:98) ~[na:na]
        at jdk.zipfs/jdk.nio.zipfs.ZipFileSystemProvider.uriToPath(ZipFileSystemProvider.java:76) ~[jdk.zipfs:na]
        at jdk.zipfs/jdk.nio.zipfs.ZipFileSystemProvider.newFileSystem(ZipFileSystemProvider.java:98) ~[jdk.zipfs:na]
        at java.base/java.nio.file.FileSystems.newFileSystem(FileSystems.java:339) ~[na:na]
        at java.base/java.nio.file.FileSystems.newFileSystem(FileSystems.java:288) ~[na:na]
        at org.junit.platform.commons.util.CloseablePath$ManagedFileSystem.<init>(CloseablePath.java:92) ~[junit-platform-commons-1.10.1.jar!/:1.10.1]
        at org.junit.platform.commons.util.CloseablePath.lambda$createForJarFileSystem$3(CloseablePath.java:63) ~[junit-platform-commons-1.10.1.jar!/:1.10.1]

Which happens because JUnit 5 assumes the jar uri is flat:

static CloseablePath create(URI uri, FileSystemProvider fileSystemProvider) throws URISyntaxException {
if (JAR_URI_SCHEME.equals(uri.getScheme())) {
String[] parts = uri.toString().split(JAR_URI_SEPARATOR);
String jarUri = parts[0];
String jarEntry = parts[1];
return createForJarFileSystem(new URI(jarUri), fileSystem -> fileSystem.getPath(jarEntry),
fileSystemProvider);
}

While it in fact it is recursive jar:<url>!/[<entry>] and so the last, not the first !/ should be used.

	private static final String JAR_URI_SEPARATOR = "!/";


        String uriString = uri.toString();
        int lastJarUriSeparator = uriString.lastIndexOf(JAR_URI_SEPARATOR);
        String jarUri = uriString.substring(0, lastJarUriSeparator);
        String jarEntry = uriString.substring(lastJarUriSeparator + 1);

Worth noting that this will only work when scanning inside BOOT-INF/classes as spring-projects/spring-boot#38595 will prevent scanning BOOT-INF/lib for now.

mpkorstanje added a commit to mpkorstanje/junit5 that referenced this issue Dec 17, 2023
Jar uris follow the format[1]:

```
jar:<url>!/[<entry>]
```

So splitting should be done on the last `!/` rather than the first.

Fixes: junit-team#1724 for Spring Boot 3.2 and later.

 1. https://docs.oracle.com/javase/8/docs/api/java/net/JarURLConnection.html
mpkorstanje added a commit to mpkorstanje/junit5 that referenced this issue Dec 17, 2023
Jar uris follow the format[1]:

```
jar:<url>!/[<entry>]
```

So splitting should be done on the last `!/` rather than the first.

Fixes: junit-team#1724 for Spring Boot 3.2 and later.

 1. https://docs.oracle.com/javase/8/docs/api/java/net/JarURLConnection.html
mpkorstanje added a commit to mpkorstanje/junit5 that referenced this issue Dec 17, 2023
Jar uris follow the format[1]:

```
jar:<url>!/[<entry>]
```

So splitting should be done on the last `!/` rather than the first.

Fixes: junit-team#1724 for Spring Boot 3.2 and later.

 1. https://docs.oracle.com/javase/8/docs/api/java/net/JarURLConnection.html
@mpkorstanje
Copy link
Contributor

With a relatively small change scanning the BOOT-INF/classes would also be possible.

if (JAR_URI_SCHEME.equals(uri.getScheme())) {
	String uriString = uri.toString();

	if (uriString.startsWith("jar:file:") && uriString.contains("!/BOOT-INF/classes")) {
		// Parsing jar:file:<file>!/BOOT-INF/classes!/[<entry>]
		String[] parts = uri.toString().split("!");
		String jarUri = parts[0];
		String jarEntry = parts[1];
		String subEntry = parts[2];
		return createForJarFileSystem(new URI(jarUri), fileSystem -> fileSystem.getPath(jarEntry + subEntry), fileSystemProvider);
	}

	// Parsing: jar:<url>!/[<entry>]
	int lastJarUriSeparator = uriString.lastIndexOf(JAR_URI_SEPARATOR);
	String jarUri = uriString.substring(0, lastJarUriSeparator);
	String jarEntry = uriString.substring(lastJarUriSeparator + 1);
	return createForJarFileSystem(new URI(jarUri), fileSystem -> fileSystem.getPath(jarEntry), fileSystemProvider);
}

marcphilipp pushed a commit that referenced this issue Feb 25, 2024
Jar uris follow the format [1]:

```
jar:<url>!/[<entry>]
```

So splitting should be done on the last `!/` rather than the first.

Fixes: #1724 for Spring Boot 3.2 and later.

 1. https://docs.oracle.com/javase/8/docs/api/java/net/JarURLConnection.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.