Skip to content

Commit 0ef73b6

Browse files
committed
Fix test class location search for bytecode enhanced test classes
1 parent 0061772 commit 0ef73b6

File tree

1 file changed

+76
-24
lines changed

1 file changed

+76
-24
lines changed

test-framework/common/src/main/java/io/quarkus/test/common/PathTestHelper.java

+76-24
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
import io.quarkus.bootstrap.app.CuratedApplication;
1919
import io.quarkus.bootstrap.classloading.ClassPathElement;
2020
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
21+
import io.quarkus.bootstrap.model.ApplicationModel;
2122
import io.quarkus.bootstrap.workspace.ArtifactSources;
2223
import io.quarkus.bootstrap.workspace.SourceDir;
2324
import io.quarkus.bootstrap.workspace.WorkspaceModule;
2425
import io.quarkus.commons.classloading.ClassLoaderHelper;
26+
import io.quarkus.maven.dependency.DependencyFlags;
2527
import io.quarkus.paths.PathTree;
2628
import io.quarkus.paths.PathVisit;
2729
import io.quarkus.runtime.util.ClassPathUtils;
@@ -32,6 +34,7 @@
3234
public final class PathTestHelper {
3335
private static final String TARGET = "target";
3436
private static final Map<String, String> TEST_TO_MAIN_DIR_FRAGMENTS = new HashMap<>();
37+
private static final List<String> TEST_DIRS;
3538

3639
static {
3740
//region Eclipse
@@ -131,6 +134,7 @@ public final class PathTestHelper {
131134
}
132135
});
133136
}
137+
TEST_DIRS = List.of(TEST_TO_MAIN_DIR_FRAGMENTS.keySet().toArray(new String[0]));
134138
}
135139

136140
private PathTestHelper() {
@@ -143,32 +147,53 @@ private PathTestHelper() {
143147
* @return directory or JAR containing the test class
144148
*/
145149
public static Path getTestClassesLocation(Class<?> testClass) {
146-
String classFileName = testClass.getName().replace('.', File.separatorChar) + ".class";
147-
URL resource = testClass.getClassLoader().getResource(fromClassNameToResourceName(testClass.getName()));
148-
150+
final String classFileName = fromClassNameToResourceName(testClass.getName());
151+
URL resource = testClass.getClassLoader().getResource(classFileName);
149152
if (resource == null) {
150153
throw new IllegalStateException(
151-
"Could not find resource: " + testClass.getName() + " using class loader " + testClass.getClassLoader());
154+
"Could not find resource " + classFileName + " using class loader " + testClass.getClassLoader());
152155
}
153156
if (resource.getProtocol().equals("jar")) {
154157
try {
155158
resource = URI.create(resource.getFile().substring(0, resource.getFile().indexOf('!'))).toURL();
156159
return toPath(resource);
157160
} catch (MalformedURLException e) {
158-
throw new RuntimeException("Failed to resolve the location of the JAR containing " + testClass, e);
161+
throw new RuntimeException("Failed to resolve the location of the JAR containing " + classFileName, e);
162+
}
163+
}
164+
if (resource.getProtocol().equals("quarkus")) {
165+
// This is a bytecode enhanced class, the original class is either in the application module or a dependency
166+
final ApplicationModel appModel = ((QuarkusClassLoader) testClass.getClassLoader()).getCuratedApplication()
167+
.getApplicationModel();
168+
169+
Path testLocation = getTestClassesDirOrNull(classFileName, appModel.getApplicationModule());
170+
if (testLocation != null) {
171+
return testLocation;
172+
}
173+
174+
// JARs containing tests will most of the time be direct test scoped dependencies (e.g. Quarkus platform testsuite).
175+
// Look among the direct dependencies first to optimize for the majority of cases.
176+
// Depending on the amount of dependencies and their ordering, could be ~15-20 times faster.
177+
testLocation = getTestClassLocationFromDepsOrNull(classFileName, appModel,
178+
DependencyFlags.DIRECT | DependencyFlags.RUNTIME_CP);
179+
if (testLocation != null) {
180+
return testLocation;
159181
}
160-
} else if (resource.getProtocol().equals("quarkus")) {
161-
// This is loaded with a quarkus classloader, so we can (sort of) ask it directly
162-
QuarkusClassLoader qcl = (QuarkusClassLoader) testClass.getClassLoader();
163-
return getTestClassesLocation(testClass, qcl.getCuratedApplication());
182+
// Look among all the runtime dependencies. We need a more efficient way to handle this case.
183+
// I don't think we have a test for this case.
184+
testLocation = getTestClassLocationFromDepsOrNull(classFileName, appModel, DependencyFlags.RUNTIME_CP);
185+
if (testLocation != null) {
186+
return testLocation;
187+
}
188+
throw new RuntimeException("Failed to locate " + classFileName + " among the application dependencies");
164189
}
165190
Path path = toPath(resource);
166191
path = path.getRoot().resolve(path.subpath(0, path.getNameCount() - Path.of(classFileName).getNameCount()));
167192

168-
if (!isInTestDir(resource) && !path.getParent().getFileName().toString().equals(TARGET)) {
193+
if (!isInTestDir(path) && !path.getParent().getFileName().toString().equals(TARGET)) {
169194
final StringBuilder msg = new StringBuilder();
170195
msg.append("The test class ").append(testClass.getName()).append(" is not located in any of the directories ");
171-
var i = TEST_TO_MAIN_DIR_FRAGMENTS.keySet().iterator();
196+
var i = TEST_DIRS.iterator();
172197
msg.append(i.next());
173198
while (i.hasNext()) {
174199
msg.append(", ").append(i.next());
@@ -178,12 +203,43 @@ public static Path getTestClassesLocation(Class<?> testClass) {
178203
return path;
179204
}
180205

206+
/**
207+
* Looks for a resource among the dependencies with specific flags. The method will return the first dependency
208+
* providing the resource of null, if none of the dependencies provide the resource.
209+
*
210+
* @param classFileName classpath resource name
211+
* @param appModel application model
212+
* @param depFlags dependency flags
213+
* @return the first dependency containing the resource or null, if none of the matching dependencies provide the resource
214+
*/
215+
private static Path getTestClassLocationFromDepsOrNull(String classFileName, ApplicationModel appModel, int depFlags) {
216+
for (var d : appModel.getDependencies(depFlags)) {
217+
final Path root = d.getContentTree().apply(classFileName, PathTestHelper::getRootOrNull);
218+
if (root != null) {
219+
return root;
220+
}
221+
}
222+
return null;
223+
}
224+
181225
public static Path getTestClassesLocation(Class<?> requiredTestClass, CuratedApplication curatedApplication) {
182-
final WorkspaceModule module = curatedApplication.getApplicationModel().getAppArtifact().getWorkspaceModule();
226+
final Path testClassesDir = getTestClassesDirOrNull(
227+
ClassLoaderHelper.fromClassNameToResourceName(requiredTestClass.getName()),
228+
curatedApplication.getApplicationModel().getApplicationModule());
229+
return testClassesDir != null ? testClassesDir : getTestClassesLocation(requiredTestClass);
183230

231+
}
232+
233+
/**
234+
* Looks for a resource in the output directories of a workspace module.
235+
* If a directory containing the resource could not be found, the method will return null.
236+
*
237+
* @param testClassFileName classpath resource
238+
* @param module workspace module
239+
* @return output directory containing the resource or null, in case the resource could not be found
240+
*/
241+
private static Path getTestClassesDirOrNull(String testClassFileName, WorkspaceModule module) {
184242
ArtifactSources testSources = module.getTestSources();
185-
final String testClassFileName = ClassLoaderHelper
186-
.fromClassNameToResourceName(requiredTestClass.getName());
187243
if (testSources != null) {
188244
PathTree paths = testSources.getOutputTree();
189245
var testClassesDir = paths.apply(testClassFileName, PathTestHelper::getRootOrNull);
@@ -202,21 +258,18 @@ public static Path getTestClassesLocation(Class<?> requiredTestClass, CuratedApp
202258
}
203259
}
204260
}
205-
206261
// If we got to this point, fall back to the filesystem search
207262
// This happens for maven source set scenarios
208263
// TODO getSourceClassifiers() should return the source sets in the maven case, but currently does not - see BuildIT.testCustomTestSourceSets test
209-
return getTestClassesLocation(requiredTestClass);
210-
264+
return null;
211265
}
212266

213267
private static Path getRootOrNull(PathVisit visit) {
214268
if (visit == null) {
215269
// this path does not exist in this path tree
216270
return null;
217-
} else {
218-
return visit.getRoot();
219271
}
272+
return visit.getRoot();
220273
}
221274

222275
public static void validateTestDir(Class<?> requiredTestClass, Path testClassesDir, WorkspaceModule module) {
@@ -261,7 +314,7 @@ public static Path getAppClassLocationForTestLocation(Path testClassLocationPath
261314
// we should replace only the last occurrence of the fragment
262315
final int i = testClassLocation.lastIndexOf(e.getKey());
263316
final StringBuilder buf = new StringBuilder(testClassLocation.length());
264-
buf.append(testClassLocation.substring(0, i)).append(e.getValue());
317+
buf.append(testClassLocation, 0, i).append(e.getValue());
265318
if (i + e.getKey().length() + 1 < testClassLocation.length()) {
266319
buf.append(testClassLocation.substring(i + e.getKey().length()));
267320
}
@@ -362,10 +415,9 @@ public static boolean isTestClass(String className, ClassLoader classLoader, Pat
362415
return testLocation.equals(Path.of(path));
363416
}
364417

365-
private static boolean isInTestDir(URL resource) {
366-
String path = toPath(resource).toString();
367-
return TEST_TO_MAIN_DIR_FRAGMENTS.keySet().stream()
368-
.anyMatch(path::contains);
418+
private static boolean isInTestDir(Path resource) {
419+
final String path = resource.toString();
420+
return TEST_DIRS.stream().anyMatch(path::contains);
369421
}
370422

371423
private static Path toPath(URL resource) {

0 commit comments

Comments
 (0)