From e0da6b1485123acebd919e4b586a37c2a52d6236 Mon Sep 17 00:00:00 2001 From: Jean Bisutti Date: Mon, 19 Oct 2026 23:42:02 +0200 Subject: [PATCH] Fix global annotations ignored when the class path contains a plus sign --- .../quickperf/QuickPerfUserConfigClasses.java | 3 +- .../QuickPerfUserConfigClassesTest.java | 108 ++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 core/src/test/java/org/quickperf/QuickPerfUserConfigClassesTest.java diff --git a/core/src/main/java/org/quickperf/QuickPerfUserConfigClasses.java b/core/src/main/java/org/quickperf/QuickPerfUserConfigClasses.java index 40fe1da9..e071fc1c 100644 --- a/core/src/main/java/org/quickperf/QuickPerfUserConfigClasses.java +++ b/core/src/main/java/org/quickperf/QuickPerfUserConfigClasses.java @@ -47,7 +47,8 @@ private List findDirectoriesForPackage() throws IOException { List dirs = new ArrayList<>(); while (resources.hasMoreElements()) { URL resource = resources.nextElement(); - String classPath = URLDecoder.decode(resource.getPath(), "UTF-8"); + // URLDecoder would decode + to a space, but in a URL path + is a plus sign + String classPath = URLDecoder.decode(resource.getPath().replace("+", "%2B"), "UTF-8"); File file = new File(classPath); dirs.add(file); } diff --git a/core/src/test/java/org/quickperf/QuickPerfUserConfigClassesTest.java b/core/src/test/java/org/quickperf/QuickPerfUserConfigClassesTest.java new file mode 100644 index 00000000..b3f8dc4a --- /dev/null +++ b/core/src/test/java/org/quickperf/QuickPerfUserConfigClassesTest.java @@ -0,0 +1,108 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * Copyright 2019-2022 the original author or authors. + */ +package org.quickperf; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QuickPerfUserConfigClassesTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + public static class UserConfigClass {} + + @Test public void + should_find_a_user_config_class_in_a_directory_whose_path_contains_a_plus_sign() throws Exception { + + // GIVEN + File classPathDirectory = createClassPathDirectoryContainingUserConfigClass("my+project"); + // file:/.../my+project/, as with -cp, Gradle, and the JVM forked by QuickPerf + URL classPathUrl = classPathDirectory.toURI().toURL(); + + // WHEN + Class[] classes = findClassesWithClassPath(classPathUrl); + + // THEN + assertThat(classes).containsExactly(UserConfigClass.class); + + } + + @Test public void + should_find_a_user_config_class_when_the_plus_sign_of_the_directory_url_is_encoded() throws Exception { + + // GIVEN + File classPathDirectory = createClassPathDirectoryContainingUserConfigClass("my+project"); + // file:/.../my%2Bproject/, as with the manifest-only JAR of Maven Surefire 3 on Windows + URL classPathUrl = new URL(classPathDirectory.toURI().toString().replace("+", "%2B")); + + // WHEN + Class[] classes = findClassesWithClassPath(classPathUrl); + + // THEN + assertThat(classes).containsExactly(UserConfigClass.class); + + } + + @Test public void + should_find_a_user_config_class_in_a_directory_whose_path_contains_a_space() throws Exception { + + // GIVEN + File classPathDirectory = createClassPathDirectoryContainingUserConfigClass("my project"); + // file:/.../my%20project/ + URL classPathUrl = classPathDirectory.toURI().toURL(); + + // WHEN + Class[] classes = findClassesWithClassPath(classPathUrl); + + // THEN + assertThat(classes).containsExactly(UserConfigClass.class); + + } + + private File createClassPathDirectoryContainingUserConfigClass(String directoryName) throws IOException { + File classPathDirectory = temporaryFolder.newFolder(directoryName); + String classFilePath = UserConfigClass.class.getName().replace('.', '/') + ".class"; + File classFile = new File(classPathDirectory, classFilePath); + classFile.getParentFile().mkdirs(); + try (InputStream classFileContent = UserConfigClass.class.getClassLoader().getResourceAsStream(classFilePath)) { + Files.copy(classFileContent, classFile.toPath()); + } + return classPathDirectory; + } + + private Class[] findClassesWithClassPath(URL classPathUrl) throws Exception { + Thread currentThread = Thread.currentThread(); + ClassLoader contextClassLoader = currentThread.getContextClassLoader(); + // Without a parent, the class loader only finds the org/quickperf directory of classPathUrl + URLClassLoader classLoader = new URLClassLoader(new URL[] {classPathUrl}, null); + currentThread.setContextClassLoader(classLoader); + try { + return QuickPerfUserConfigClasses.INSTANCE.findClasses(); + } finally { + currentThread.setContextClassLoader(contextClassLoader); + classLoader.close(); + } + } + +}