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

UNC path rejected for @QuarkusTest #31368

Closed
saver-r opened this issue Feb 23, 2023 · 15 comments
Closed

UNC path rejected for @QuarkusTest #31368

saver-r opened this issue Feb 23, 2023 · 15 comments
Labels
area/testing env/windows Impacts Windows machines kind/bug Something isn't working triage/wontfix This will not be worked on

Comments

@saver-r
Copy link

saver-r commented Feb 23, 2023

Describe the bug

Setup:

  • Project sources are located under //wsl2/Ubuntu 20.04/projects/myproject network share in Windows.
  • Eclipse is running on the local system under Windows.

When I run a test file from Eclipse (Run > Run JUnit test)

Expected behavior

the test case should run under windows.

Actual behavior

The test case outputs an error and throws an exception:

BasicLoggingEnabler failed to retrieve config: java.lang.IllegalArgumentException: URI authority component has undefined host

java.lang.RuntimeException: java.lang.IllegalArgumentException: URI authority component has undefined host
	at io.quarkus.test.junit.QuarkusTestExtension.throwBootFailureException(QuarkusTestExtension.java:625)
	at io.quarkus.test.junit.QuarkusTestExtension.interceptTestClassConstructor(QuarkusTestExtension.java:696)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.api.extension.InvocationInterceptor.interceptTestClassConstructor(InvocationInterceptor.java:73)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
	at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
	at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:62)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestClassConstructor(ClassBasedTestDescriptor.java:363)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateTestClass(ClassBasedTestDescriptor.java:310)
	at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.instantiateTestClass(ClassTestDescriptor.java:79)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:286)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$4(ClassBasedTestDescriptor.java:278)
	at java.base/java.util.Optional.orElseGet(Optional.java:364)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$5(ClassBasedTestDescriptor.java:277)
	at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:31)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:105)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:104)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:68)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$2(NodeTestTask.java:123)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:123)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:90)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
	at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:147)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:127)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:90)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:55)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:102)
	at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:54)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:95)
	at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:91)
	at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:60)
	at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:98)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:529)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:756)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:452)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Caused by: java.lang.IllegalArgumentException: URI authority component has undefined host
	at java.base/sun.nio.fs.WindowsUriSupport.fromUri(WindowsUriSupport.java:140)
	at java.base/sun.nio.fs.WindowsFileSystemProvider.getPath(WindowsFileSystemProvider.java:98)
	at java.base/java.nio.file.Path.of(Path.java:203)
	at java.base/java.nio.file.Paths.get(Paths.java:98)
	at io.quarkus.runtime.util.ClassPathUtils.toLocalPath(ClassPathUtils.java:224)
	at io.quarkus.test.common.PathTestHelper.toPath(PathTestHelper.java:283)
	at io.quarkus.test.common.PathTestHelper.getTestClassesLocation(PathTestHelper.java:146)
	at io.quarkus.test.junit.AbstractJvmQuarkusTestExtension.createAugmentor(AbstractJvmQuarkusTestExtension.java:58)
	at io.quarkus.test.junit.QuarkusTestExtension.doJavaStart(QuarkusTestExtension.java:217)
	at io.quarkus.test.junit.QuarkusTestExtension.ensureStarted(QuarkusTestExtension.java:592)
	at io.quarkus.test.junit.QuarkusTestExtension.beforeAll(QuarkusTestExtension.java:640)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeBeforeAllCallbacks$12(ClassBasedTestDescriptor.java:395)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeBeforeAllCallbacks(ClassBasedTestDescriptor.java:395)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:211)
	at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:84)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:148)
	... 36 more

How to Reproduce?

  1. Copy or have any quarkus project on a Windows network share (The project doesn't matter, but it must contain at least one @QuarkusTest, it also doesn't have to be a WSL2 share, any share will do)
  2. Install Eclipse (e. g. 2022-12) on localhost under Windows.
  3. Set Window > Preferences: Installed JRE: Duplicate JRE, then edit: Set "Default JVM arguments" to: -Djava.util.logging.manager=org.jboss.logmanager.LogManager -Dmaven.home=<path-to-your-maven-installation>, and mark as default JRE.
  4. Import quarkus project using UNC path for sources. The sources must be used in place, i. e. not copied to localhost. Do not store the .project file on the network share, otherwise Eclipse explodes.
  5. In Project Explorer right-click quarkus test case and select Run As > JUnit Test.

Output of uname -a or ver

Microsoft Windows [Version 10.0.19045.2486]

Output of java -version

openjdk version "17.0.5" 2022-10-18

GraalVM version (if different from Java)

No response

Quarkus version or git rev

2.14.3.Final

Build tool (ie. output of mvnw --version or gradlew --version)

Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)

Additional information

Analysis

When @QuarkusTest loads a test class, it invokes PathTestHelper.getTestClassesLocation(Class<?> testClass).

The utility method correctly determines the full URL to the test class, e.g.

file://wsl2/Ubuntu 20.04/projects/myproject/src/test/java/my/test/project/TestTestcase.class

i.e. the authority component is empty (correct), the path component starts with a double slash (correct).

Then, on line 146, it invokes

        Path path = toPath(resource);

The toPath function fails when it invokes ClassPathUtils.toLocalPath which uses

            return Paths.get(url.toURI());

to convert the correct URL into an incorrect URI whose authority part is wsl2 and whose path is /Ubuntu 20.04/projects/myproject/src/test/java/my/test/project/TestTestcase.class

As file URLs must not have an authority part, the URI is rejected by java.nio.file.Path.get.

Suggested Solution

Instead of using toURI in ClassPathUtils.toLocalPath, the URI should be constructed directly.

    public static Path toLocalPath(final URL url) {
        try {
            return Paths.get(new URI(url.getProtocol(), url.getAuthority(), url.getPath(), url.getQuery(), url.getRef()));
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Failed to translate " + url + " to local path", e);
        }
    }

In this case, the path is fully preserved, and the failure checks remain in place if the URL erroneously contained an authority part.

@saver-r saver-r added the kind/bug Something isn't working label Feb 23, 2023
@quarkus-bot quarkus-bot bot added env/windows Impacts Windows machines triage/needs-triage labels Feb 23, 2023
@geoand
Copy link
Contributor

geoand commented Feb 23, 2023

Thanks for the analysis!

Would you to provide a Pull Request?

@saver-r
Copy link
Author

saver-r commented Feb 28, 2023

ATM I lack time for setting up an environment. I'll get back to this as soon as possible.

@geoand
Copy link
Contributor

geoand commented Feb 28, 2023

👌🏼

@geoand
Copy link
Contributor

geoand commented Feb 28, 2023

Suggested Solution

Instead of using toURI in ClassPathUtils.toLocalPath, the URI should be constructed directly.

   public static Path toLocalPath(final URL url) {
       try {
           return Paths.get(new URI(url.getProtocol(), url.getAuthority(), url.getPath(), url.getQuery(), url.getRef()));
       } catch (URISyntaxException e) {
           throw new IllegalArgumentException("Failed to translate " + url + " to local path", e);
       }
   }

@dmlloyd @aloubyansky does this solution seem liike the best way to go to you? I admit I have never worked with UNC Paths and I was not able to find any definiteve information...

@dmlloyd
Copy link
Member

dmlloyd commented Mar 1, 2023

Yes, I think this is an OK fix. It's a bit weird that the URL->URI conversion is not reliable, but this is definitely the cleanest way around at (assuming it works).

@geoand
Copy link
Contributor

geoand commented Mar 1, 2023

Thanks @dmlloyd

@geoand
Copy link
Contributor

geoand commented Mar 8, 2023

Interesting enough, the change above makes one of our tests fail:

BannerProcessorTest.checkQuarkusCoreBannerOnFilesystemWithSpecialCharacters:39 » IllegalArgument Bad escape

The even weirder thing is that

url.toURI().equals(new URI(url.getProtocol(), url.getAuthority(), url.getPath(), url.getQuery(), url.getRef()))

is true.

@dmlloyd
Copy link
Member

dmlloyd commented Mar 8, 2023

What is giving the error?

@geoand
Copy link
Contributor

geoand commented Mar 8, 2023

The stacktrace is:

[ERROR] io.quarkus.deployment.pkg.steps.BannerProcessorTest.checkQuarkusCoreBannerOnFilesystemWithSpecialCharacters(Path)  Time elapsed: 0.046 s  <<< ERROR!
java.lang.IllegalArgumentException: Bad escape
        at java.base/sun.nio.fs.UnixUriUtils.fromUri(UnixUriUtils.java:88)
        at java.base/sun.nio.fs.UnixFileSystemProvider.getPath(UnixFileSystemProvider.java:102)
        at java.base/java.nio.file.Path.of(Path.java:203)
        at java.base/java.nio.file.Paths.get(Paths.java:98)
        at io.quarkus.runtime.util.ClassPathUtils.toLocalPath(ClassPathUtils.java:228)
        at io.quarkus.runtime.util.ClassPathUtils.processAsPath(ClassPathUtils.java:144)
        at io.quarkus.deployment.steps.BannerProcessor.isQuarkusCoreBanner(BannerProcessor.java:112)
        at io.quarkus.deployment.pkg.steps.BannerProcessorTest$MyBannerProcessor.test(BannerProcessorTest.java:21)
        at io.quarkus.deployment.pkg.steps.BannerProcessorTest.checkQuarkusCoreBannerOnFilesystemWithSpecialCharacters(BannerProcessorTest.java:39)

The URL being passed in is something like: jar:file:///home/gandrian/projects/redhat/quarkus/core/deployment/target/junit17083816038911712922/Descărcări971222298415387479/BannerProcessorTest.jar!/.

However I see that the test takes a Path that is a ZipPath with a toString() of "/", converts to a URL using path.toUri().toURL() and then that is code that URL is eventually passed to

return Paths.get(new URI(url.getProtocol(), url.getAuthority(), url.getPath(), url.getQuery(), url.getRef()));

down the line.

I assume something is getting lost in translation?

@dmlloyd
Copy link
Member

dmlloyd commented Mar 9, 2023

Working backwards, I can see that UnixUriUtils.java:88 is a failure which occurs when a URI raw path character is either the NUL character '\0' or outside the range of ASCII >= 0x80. So, we can deduce that the URI's raw path had such a character (probably one of the ă).

Taking this farther, consider the following cases for Path.of(uri):

  • uri = URI.create("file:///ābç")
    • uri.getPath() is /ābç
    • uri.getRawPath() is /ābç (same string)
    • Path.of(uri) gives Exception java.lang.IllegalArgumentException: Bad escape
    • uri.toASCIIString() is file:///%C4%81b%C3%A7 (we'll want this for our next experiment)
  • uri = URI.create("file:///%C4%81b%C3%A7")
    • uri.getPath() is still /ābç
    • uri.getRawPath() is /%C4%81b%C3%A7
    • Path.of(uri) works and gives /ābç

OK, so now we know that Path.of(uri) wants an URL-encoded URI path and isn't getting one. Possible places where things get mixed up:

  • in MyBannerProcessor: return this.isQuarkusCoreBanner(path.toUri().toURL()); - the ZipPath is being converted into a URI and then a URL; is the URI properly escaped? Is the URL?
  • in ClassPathUtils#processAsPath, we're doing a few things that might go wrong:
    • Getting the file component, which might not be escaped on line 139-ish
    • Creating a new URL from a substring of that file on line 143-ish

I'd look at these places to see where the escapes get lost.

@geoand
Copy link
Contributor

geoand commented Mar 9, 2023

So, we can deduce that the URI's raw path had such a character (probably one of the ă)

Right, that's what I saw with quick debugging as well.

I'd look at these places to see where the escapes get lost.

Thanks a ton for the hints @dmlloyd!

@geoand
Copy link
Contributor

geoand commented Mar 10, 2023

I am looking at this a little more and it seems like the problem is actually not encoding related (or at least, not only encoding related.

For the test we are talking about, the difference between

Paths.get(url.toURI())

and

Paths.get(new URI(url.getProtocol(), url.getAuthority(), url.getPath(), url.getQuery(), url.getRef()))

boils down to the former's toString() representation starting with file:/ while the latter starts with file:///. The former does not trigger the use of getRawPath

(as the JDK does: if (!uri.toString().startsWith("file:///")) return new File(uri).toPath();)

and that is why it passes.

@geoand
Copy link
Contributor

geoand commented Mar 10, 2023

I can get path this particular error in the test by ASCII encoding the URI, but then we run into another problem that the ZipFileSystem won't open because it supposedly does not exist.

At this point I think I'm just gonna give up

@geoand
Copy link
Contributor

geoand commented Jun 27, 2023

I am going to close this as it will be hard to fix for very very little gain

@geoand geoand closed this as not planned Won't fix, can't repro, duplicate, stale Jun 27, 2023
@geoand geoand added the triage/wontfix This will not be worked on label Jun 27, 2023
@dmlloyd
Copy link
Member

dmlloyd commented Aug 29, 2024

This might be fixed as a side-effect of #42248.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/testing env/windows Impacts Windows machines kind/bug Something isn't working triage/wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

3 participants