2
2
3
3
import static io .quarkus .commons .classloading .ClassLoaderHelper .fromClassNameToResourceName ;
4
4
5
+ import java .io .Closeable ;
6
+ import java .io .File ;
5
7
import java .io .IOException ;
6
8
import java .io .InputStream ;
9
+ import java .lang .reflect .Constructor ;
10
+ import java .lang .reflect .InvocationTargetException ;
7
11
import java .lang .reflect .Modifier ;
8
12
import java .nio .file .Files ;
9
13
import java .nio .file .Path ;
22
26
import java .util .Set ;
23
27
import java .util .concurrent .LinkedBlockingDeque ;
24
28
import java .util .concurrent .atomic .AtomicReference ;
25
- import java .util .function .Consumer ;
26
29
import java .util .function .Function ;
27
30
import java .util .regex .Pattern ;
28
31
import java .util .stream .Collectors ;
74
77
import io .quarkus .deployment .util .IoUtil ;
75
78
import io .quarkus .dev .console .QuarkusConsole ;
76
79
import io .quarkus .dev .testing .TracingHandler ;
80
+ import io .quarkus .logging .Log ;
77
81
import io .quarkus .util .GlobUtil ;
78
82
79
83
/**
@@ -94,6 +98,7 @@ public class JunitTestRunner {
94
98
public static final DotName TESTABLE = DotName .createSimple (Testable .class .getName ());
95
99
public static final DotName NESTED = DotName .createSimple (Nested .class .getName ());
96
100
private static final String ARCHUNIT_FIELDSOURCE_FQCN = "com.tngtech.archunit.junit.FieldSource" ;
101
+ public static final String FACADE_CLASS_LOADER_NAME = "io.quarkus.test.junit.classloading.FacadeClassLoader" ;
97
102
private final long runId ;
98
103
private final DevModeContext .ModuleInfo moduleInfo ;
99
104
private final CuratedApplication testApplication ;
@@ -114,6 +119,12 @@ public class JunitTestRunner {
114
119
115
120
private volatile boolean testsRunning = false ;
116
121
private volatile boolean aborted ;
122
+ private QuarkusClassLoader deploymentClassLoader ;
123
+
124
+ // A stable classloader for loading support classes, which can see more than the CL used to load this class
125
+ private static QuarkusClassLoader firstDeploymentClassLoader ;
126
+
127
+ // private static ClassLoader classLoaderForLoadingTests;
117
128
118
129
public JunitTestRunner (Builder builder ) {
119
130
this .runId = builder .runId ;
@@ -140,12 +151,15 @@ public Runnable prepare() {
140
151
long start = System .currentTimeMillis ();
141
152
ClassLoader old = Thread .currentThread ().getContextClassLoader ();
142
153
QuarkusClassLoader tcl = testApplication .createDeploymentClassLoader ();
154
+ deploymentClassLoader = tcl ;
155
+ if (firstDeploymentClassLoader == null ) {
156
+ firstDeploymentClassLoader = deploymentClassLoader ;
157
+ }
143
158
LogCapturingOutputFilter logHandler = new LogCapturingOutputFilter (testApplication , true , true ,
144
- TestSupport .instance ().get ()::isDisplayTestOutput );
159
+ TestSupport .instance ()
160
+ .get ()::isDisplayTestOutput );
161
+ // TODO do we want to do this setting of the TCCL? I think it just makes problems?
145
162
Thread .currentThread ().setContextClassLoader (tcl );
146
- Consumer currentTestAppConsumer = (Consumer ) tcl .loadClass (CurrentTestApplication .class .getName ())
147
- .getDeclaredConstructor ().newInstance ();
148
- currentTestAppConsumer .accept (testApplication );
149
163
150
164
Set <UniqueId > allDiscoveredIds = new HashSet <>();
151
165
Set <UniqueId > dynamicIds = new HashSet <>();
@@ -407,7 +421,6 @@ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry e
407
421
}
408
422
} finally {
409
423
try {
410
- currentTestAppConsumer .accept (null );
411
424
TracingHandler .setTracingHandler (null );
412
425
QuarkusConsole .removeOutputFilter (logHandler );
413
426
Thread .currentThread ().setContextClassLoader (old );
@@ -587,7 +600,10 @@ private DiscoveryResult discoverTestClasses() {
587
600
Set <String > quarkusTestClasses = new HashSet <>();
588
601
for (var a : Arrays .asList (QUARKUS_TEST , QUARKUS_MAIN_TEST )) {
589
602
for (AnnotationInstance i : index .getAnnotations (a )) {
590
- DotName name = i .target ().asClass ().name ();
603
+
604
+ DotName name = i .target ()
605
+ .asClass ()
606
+ .name ();
591
607
quarkusTestClasses .add (name .toString ());
592
608
for (ClassInfo clazz : index .getAllKnownSubclasses (name )) {
593
609
if (!integrationTestClasses .contains (clazz .name ().toString ())) {
@@ -597,6 +613,37 @@ private DiscoveryResult discoverTestClasses() {
597
613
}
598
614
}
599
615
616
+ // The FacadeClassLoader approach of loading test classes with the classloader we will use to run them can only work for `@QuarkusTest` and not main or integration tests
617
+ // Most logic in the JUnitRunner counts main tests as quarkus tests, so do a (mildly irritating) special pass to get the ones which are strictly @QuarkusTest
618
+
619
+ Set <String > quarkusTestClassesForFacadeClassLoader = new HashSet <>();
620
+ for (var a : Arrays .asList (QUARKUS_TEST )) {
621
+ for (AnnotationInstance i : index .getAnnotations (a )) {
622
+ DotName name = i .target ()
623
+ .asClass ()
624
+ .name ();
625
+ quarkusTestClassesForFacadeClassLoader .add (name .toString ());
626
+ for (ClassInfo clazz : index .getAllKnownSubclasses (name )) {
627
+ if (!integrationTestClasses .contains (clazz .name ()
628
+ .toString ())) {
629
+ quarkusTestClassesForFacadeClassLoader .add (clazz .name ()
630
+ .toString ());
631
+ }
632
+ }
633
+ }
634
+ }
635
+
636
+ Map <String , String > profiles = new HashMap <>();
637
+
638
+ for (AnnotationInstance i : index .getAnnotations (TEST_PROFILE )) {
639
+
640
+ DotName name = i .target ()
641
+ .asClass ()
642
+ .name ();
643
+ // We could do the value as a class, but it wouldn't be in the right classloader
644
+ profiles .put (name .toString (), i .value ().asString ());
645
+ }
646
+
600
647
Set <DotName > allTestAnnotations = collectTestAnnotations (index );
601
648
Set <DotName > allTestClasses = new HashSet <>();
602
649
Map <DotName , DotName > enclosingClasses = new HashMap <>();
@@ -651,13 +698,55 @@ private DiscoveryResult discoverTestClasses() {
651
698
652
699
List <Class <?>> itClasses = new ArrayList <>();
653
700
List <Class <?>> utClasses = new ArrayList <>();
701
+
702
+ ClassLoader classLoaderForLoadingTests ;
703
+ Closeable classLoaderToClose = null ;
704
+ ClassLoader orig = Thread .currentThread ().getContextClassLoader ();
705
+ try {
706
+ // JUnitTestRunner is loaded with an augmentation classloader which does not have visibility of FacadeClassLoader, but the deployment classloader can see it
707
+ // We need a consistent classloader or we leak curated applications, so use a static classloader we stashed away
708
+ Class fclClazz = firstDeploymentClassLoader .loadClass (FACADE_CLASS_LOADER_NAME );
709
+ Constructor constructor = fclClazz .getConstructor (ClassLoader .class , boolean .class , CuratedApplication .class ,
710
+ Map .class ,
711
+ Set .class ,
712
+ String .class );
713
+
714
+ // Passing in the test classes is necessary because in dev mode getAnnotations() on the class returns an empty array, for some reason (plus it saves rediscovery effort)
715
+ String classPath = moduleInfo .getMain ()
716
+ .getClassesPath () + File .pathSeparator + moduleInfo .getTest ().get ().getClassesPath ();
717
+ classLoaderForLoadingTests = (ClassLoader ) constructor .newInstance (Thread .currentThread ()
718
+ .getContextClassLoader (), true , testApplication , profiles , quarkusTestClassesForFacadeClassLoader ,
719
+ classPath );
720
+ // We only want to close classloaders if they're facade loaders we made, so squirrel away an instance to close on this path
721
+ classLoaderToClose = (Closeable ) classLoaderForLoadingTests ;
722
+
723
+ Thread .currentThread ()
724
+ .setContextClassLoader (classLoaderForLoadingTests );
725
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException
726
+ | InvocationTargetException e ) {
727
+ // This is fine, and usually just means that test-framework/junit5 isn't one of the project dependencies
728
+ // In that case, fallback to loading classes as we normally would, using a TCCL
729
+ Log .debug (
730
+ "Could not load class for FacadeClassLoader. This might be because quarkus-junit5 is not on the project classpath: "
731
+ + e );
732
+ Log .debug (e );
733
+ classLoaderForLoadingTests = Thread .currentThread ()
734
+ .getContextClassLoader ();
735
+ }
736
+
654
737
for (String i : quarkusTestClasses ) {
655
738
try {
656
- itClasses .add (Thread .currentThread ().getContextClassLoader ().loadClass (i ));
657
- } catch (ClassNotFoundException e ) {
739
+ // We could load these classes directly, since we know the profile and we have a handy interception point;
740
+ // but we need to signal to the downstream interceptor that it shouldn't interfere with the classloading
741
+ // While we're doing that, we may as well share the classloading logic
742
+ itClasses .add (classLoaderForLoadingTests .loadClass (i ));
743
+ } catch (Exception e ) {
744
+ Log .debug (e );
658
745
log .warnf (
659
746
"Failed to load test class %s (possibly as it was added after the test run started), it will not be executed this run." ,
660
747
i );
748
+ } finally {
749
+ // TODO should we do this? Thread.currentThread().setContextClassLoader(old);
661
750
}
662
751
}
663
752
itClasses .sort (Comparator .comparing (new Function <Class <?>, String >() {
@@ -676,8 +765,9 @@ public String apply(Class<?> aClass) {
676
765
//we need to work the unit test magic
677
766
//this is a lot more complex
678
767
//we need to transform the classes to make the tracing magic work
679
- QuarkusClassLoader deploymentClassLoader = ( QuarkusClassLoader ) Thread . currentThread (). getContextClassLoader ();
768
+
680
769
Set <String > classesToTransform = new HashSet <>(deploymentClassLoader .getReloadableClassNames ());
770
+ // this won't be the right classloader for some profiles, but that is ok because it's only for vanilla tests
681
771
Map <String , byte []> transformedClasses = new HashMap <>();
682
772
for (String i : classesToTransform ) {
683
773
try {
@@ -694,6 +784,7 @@ public String apply(Class<?> aClass) {
694
784
}
695
785
}
696
786
cl = testApplication .createDeploymentClassLoader ();
787
+ deploymentClassLoader = cl ;
697
788
cl .reset (Collections .emptyMap (), transformedClasses );
698
789
for (String i : unitTestClasses ) {
699
790
try {
@@ -706,6 +797,17 @@ public String apply(Class<?> aClass) {
706
797
}
707
798
708
799
}
800
+
801
+ if (classLoaderToClose != null ) {
802
+ try {
803
+ classLoaderToClose .close ();
804
+ // Don't leave a closed classloader as the TCCL
805
+ Thread .currentThread ().setContextClassLoader (orig );
806
+ } catch (IOException e ) {
807
+ throw new RuntimeException (e );
808
+ }
809
+ }
810
+
709
811
if (testType == TestType .ALL ) {
710
812
//run unit style tests first
711
813
//before the quarkus tests have started
@@ -806,6 +908,7 @@ public Builder setTestType(TestType testType) {
806
908
return this ;
807
909
}
808
910
911
+ // TODO we now ignore what gets set here and make our own, how to handle that?
809
912
public Builder setTestApplication (CuratedApplication testApplication ) {
810
913
this .testApplication = testApplication ;
811
914
return this ;
0 commit comments