88import java .io .InputStream ;
99import java .lang .reflect .Constructor ;
1010import java .lang .reflect .InvocationTargetException ;
11+ import java .lang .reflect .Method ;
1112import java .lang .reflect .Modifier ;
1213import java .nio .file .Files ;
1314import java .nio .file .Path ;
@@ -89,6 +90,9 @@ public class JunitTestRunner {
8990 public static final DotName QUARKUS_TEST = DotName .createSimple ("io.quarkus.test.junit.QuarkusTest" );
9091 public static final DotName QUARKUS_MAIN_TEST = DotName .createSimple ("io.quarkus.test.junit.main.QuarkusMainTest" );
9192 public static final DotName QUARKUS_INTEGRATION_TEST = DotName .createSimple ("io.quarkus.test.junit.QuarkusIntegrationTest" );
93+ public static final DotName QUARKUS_COMPONENT_TEST = DotName .createSimple ("io.quarkus.test.component.QuarkusComponentTest" );
94+ public static final DotName QUARKUS_COMPONENT_TEST_EXTENSION = DotName
95+ .createSimple ("io.quarkus.test.component.QuarkusComponentTestExtension" );
9296 public static final DotName TEST_PROFILE = DotName .createSimple ("io.quarkus.test.junit.TestProfile" );
9397 public static final DotName TEST = DotName .createSimple (Test .class .getName ());
9498 public static final DotName REPEATED_TEST = DotName .createSimple (RepeatedTest .class .getName ());
@@ -99,6 +103,7 @@ public class JunitTestRunner {
99103 public static final DotName NESTED = DotName .createSimple (Nested .class .getName ());
100104 private static final String ARCHUNIT_FIELDSOURCE_FQCN = "com.tngtech.archunit.junit.FieldSource" ;
101105 public static final String FACADE_CLASS_LOADER_NAME = "io.quarkus.test.junit.classloading.FacadeClassLoader" ;
106+ private static final String TEST_DISCOVERY_PROPERTY = "quarkus.continuous-tests-discovery" ;
102107
103108 private final long runId ;
104109 private final DevModeContext .ModuleInfo moduleInfo ;
@@ -568,6 +573,9 @@ private DiscoveryResult discoverTestClasses() {
568573 //for now this is out of scope, we are just going to do annotation based discovery
569574 //we will need to fix this sooner rather than later though
570575
576+ // Set the system property that is used for QuarkusComponentTest
577+ System .setProperty (TEST_DISCOVERY_PROPERTY , "true" );
578+
571579 if (moduleInfo .getTest ().isEmpty ()) {
572580 return DiscoveryResult .EMPTY ;
573581 }
@@ -613,6 +621,22 @@ private DiscoveryResult discoverTestClasses() {
613621 }
614622 }
615623
624+ Set <String > quarkusComponentTestClasses = new HashSet <>();
625+ for (AnnotationInstance a : index .getAnnotations (QUARKUS_COMPONENT_TEST )) {
626+ DotName name = a .target ().asClass ().name ();
627+ quarkusComponentTestClasses .add (name .toString ());
628+ for (ClassInfo subclass : index .getAllKnownSubclasses (name )) {
629+ quarkusComponentTestClasses .add (subclass .name ().toString ());
630+ }
631+ }
632+ for (ClassInfo clazz : index .getKnownUsers (QUARKUS_COMPONENT_TEST_EXTENSION )) {
633+ DotName name = clazz .name ();
634+ quarkusComponentTestClasses .add (name .toString ());
635+ for (ClassInfo subclass : index .getAllKnownSubclasses (name )) {
636+ quarkusComponentTestClasses .add (subclass .name ().toString ());
637+ }
638+ }
639+
616640 // 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
617641 // 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
618642
@@ -676,7 +700,8 @@ private DiscoveryResult discoverTestClasses() {
676700 for (DotName testClass : allTestClasses ) {
677701 String name = testClass .toString ();
678702 if (integrationTestClasses .contains (name )
679- || quarkusTestClasses .contains (name )) {
703+ || quarkusTestClasses .contains (name )
704+ || quarkusComponentTestClasses .contains (name )) {
680705 continue ;
681706 }
682707 var enclosing = enclosingClasses .get (testClass );
@@ -705,11 +730,14 @@ private DiscoveryResult discoverTestClasses() {
705730 // if we didn't find any test classes, let's return early
706731 // Make sure you also update the logic for the non-empty case above if you adjust this part
707732 if (testType == TestType .ALL ) {
708- if (unitTestClasses .isEmpty () && quarkusTestClasses .isEmpty ()) {
733+ if (unitTestClasses .isEmpty ()
734+ && quarkusTestClasses .isEmpty ()
735+ && quarkusComponentTestClasses .isEmpty ()) {
709736 return DiscoveryResult .EMPTY ;
710737 }
711738 } else if (testType == TestType .UNIT ) {
712- if (unitTestClasses .isEmpty ()) {
739+ if (unitTestClasses .isEmpty ()
740+ && quarkusComponentTestClasses .isEmpty ()) {
713741 return DiscoveryResult .EMPTY ;
714742 }
715743 } else if (quarkusTestClasses .isEmpty ()) {
@@ -784,8 +812,9 @@ public String apply(Class<?> aClass) {
784812 return testProfile .value ().asClass ().name ().toString () + "$$" + aClass .getName ();
785813 }
786814 }));
815+
787816 QuarkusClassLoader cl = null ;
788- if (!unitTestClasses .isEmpty ()) {
817+ if (!unitTestClasses .isEmpty () || ! quarkusComponentTestClasses . isEmpty () ) {
789818 //we need to work the unit test magic
790819 //this is a lot more complex
791820 //we need to transform the classes to make the tracing magic work
@@ -810,6 +839,7 @@ public String apply(Class<?> aClass) {
810839 cl = testApplication .createDeploymentClassLoader ();
811840 deploymentClassLoader = cl ;
812841 cl .reset (Collections .emptyMap (), transformedClasses );
842+
813843 for (String i : unitTestClasses ) {
814844 try {
815845 utClasses .add (cl .loadClass (i ));
@@ -820,6 +850,30 @@ public String apply(Class<?> aClass) {
820850 }
821851 }
822852
853+ if (!quarkusComponentTestClasses .isEmpty ()) {
854+ try {
855+ // We use the deployment class loader to load the test class
856+ Class <?> qcfcClazz = cl .loadClass ("io.quarkus.test.component.QuarkusComponentFacadeClassLoaders" );
857+ Constructor <?> c = qcfcClazz .getConstructor (Class .class , Set .class );
858+ Method getClassLoader = qcfcClazz .getMethod ("getClassLoader" , String .class , ClassLoader .class );
859+ for (String componentTestClass : quarkusComponentTestClasses ) {
860+ try {
861+ Class <?> testClass = cl .loadClass (componentTestClass );
862+ Object ecl = c .newInstance (testClass , classesToTransform );
863+ ClassLoader excl = (ClassLoader ) getClassLoader .invoke (ecl , componentTestClass , cl );
864+ utClasses .add (excl .loadClass (componentTestClass ));
865+ } catch (Exception e ) {
866+ log .debug (e );
867+ log .warnf ("Failed to load component test class %s, it will not be executed this run." ,
868+ componentTestClass );
869+ }
870+ }
871+ } catch (ClassNotFoundException | IllegalArgumentException
872+ | SecurityException | NoSuchMethodException e ) {
873+ log .warn (
874+ "Failed to load QuarkusComponentFacadeClassLoaders, component test classes will not be executed this run." );
875+ }
876+ }
823877 }
824878
825879 if (classLoaderToClose != null ) {
@@ -832,6 +886,9 @@ public String apply(Class<?> aClass) {
832886 }
833887 }
834888
889+ // Unset the system property that is used for QuarkusComponentTest
890+ System .clearProperty (TEST_DISCOVERY_PROPERTY );
891+
835892 // Make sure you also update the logic for the empty case above if you adjust this part
836893 if (testType == TestType .ALL ) {
837894 //run unit style tests first
0 commit comments