1818import io .quarkus .bootstrap .app .CuratedApplication ;
1919import io .quarkus .bootstrap .classloading .ClassPathElement ;
2020import io .quarkus .bootstrap .classloading .QuarkusClassLoader ;
21+ import io .quarkus .bootstrap .model .ApplicationModel ;
2122import io .quarkus .bootstrap .workspace .ArtifactSources ;
2223import io .quarkus .bootstrap .workspace .SourceDir ;
2324import io .quarkus .bootstrap .workspace .WorkspaceModule ;
2425import io .quarkus .commons .classloading .ClassLoaderHelper ;
26+ import io .quarkus .maven .dependency .DependencyFlags ;
2527import io .quarkus .paths .PathTree ;
2628import io .quarkus .paths .PathVisit ;
2729import io .quarkus .runtime .util .ClassPathUtils ;
3234public 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