74
74
import io .quarkus .deployment .util .IoUtil ;
75
75
import io .quarkus .dev .console .QuarkusConsole ;
76
76
import io .quarkus .dev .testing .TracingHandler ;
77
+ import io .quarkus .util .GlobUtil ;
77
78
78
79
/**
79
80
* This class is responsible for running a single run of JUnit tests.
@@ -105,6 +106,7 @@ public class JunitTestRunner {
105
106
private final Set <String > excludeTags ;
106
107
private final Pattern include ;
107
108
private final Pattern exclude ;
109
+ private final String specificSelection ;
108
110
private final List <String > includeEngines ;
109
111
private final List <String > excludeEngines ;
110
112
private final boolean failingTestsOnly ;
@@ -126,6 +128,7 @@ public JunitTestRunner(Builder builder) {
126
128
this .excludeTags = new HashSet <>(builder .excludeTags );
127
129
this .include = builder .include ;
128
130
this .exclude = builder .exclude ;
131
+ this .specificSelection = builder .specificSelection ;
129
132
this .includeEngines = builder .includeEngines ;
130
133
this .excludeEngines = builder .excludeEngines ;
131
134
this .failingTestsOnly = builder .failingTestsOnly ;
@@ -167,7 +170,15 @@ public FilterResult apply(TestDescriptor testDescriptor) {
167
170
} else if (!excludeTags .isEmpty ()) {
168
171
launchBuilder .filters (TagFilter .excludeTags (new ArrayList <>(excludeTags )));
169
172
}
170
- if (include != null ) {
173
+ if (specificSelection != null ) {
174
+ if (specificSelection .startsWith ("maven:" )) {
175
+ launchBuilder .filters (new MavenSpecificSelectionFilter (specificSelection .substring ("maven:" .length ())));
176
+ } else if (specificSelection .startsWith ("gradle:" )) {
177
+ launchBuilder .filters (new GradleSpecificSelectionFilter (specificSelection .substring ("gradle:" .length ())));
178
+ } else {
179
+ log .error ("Unknown specific selection, ignoring: " + specificSelection );
180
+ }
181
+ } else if (include != null ) {
171
182
launchBuilder .filters (new RegexFilter (false , include ));
172
183
} else if (exclude != null ) {
173
184
launchBuilder .filters (new RegexFilter (true , exclude ));
@@ -436,10 +447,10 @@ private static List<String> toTagList(TestIdentifier testIdentifier) {
436
447
private Class <?> getTestClassFromSource (Optional <TestSource > optionalTestSource ) {
437
448
if (optionalTestSource .isPresent ()) {
438
449
var testSource = optionalTestSource .get ();
439
- if (testSource instanceof ClassSource ) {
440
- return (( ClassSource ) testSource ) .getJavaClass ();
441
- } else if (testSource instanceof MethodSource ) {
442
- return (( MethodSource ) testSource ) .getJavaClass ();
450
+ if (testSource instanceof ClassSource classSource ) {
451
+ return classSource .getJavaClass ();
452
+ } else if (testSource instanceof MethodSource methodSource ) {
453
+ return methodSource .getJavaClass ();
443
454
} else if (testSource .getClass ().getName ().equals (ARCHUNIT_FIELDSOURCE_FQCN )) {
444
455
try {
445
456
return (Class <?>) testSource .getClass ().getMethod ("getJavaClass" ).invoke (testSource );
@@ -775,6 +786,7 @@ static class Builder {
775
786
private List <String > excludeTags = Collections .emptyList ();
776
787
private Pattern include ;
777
788
private Pattern exclude ;
789
+ private String specificSelection ;
778
790
private List <String > includeEngines = Collections .emptyList ();
779
791
private List <String > excludeEngines = Collections .emptyList ();
780
792
private boolean failingTestsOnly ;
@@ -844,6 +856,11 @@ public Builder setExclude(Pattern exclude) {
844
856
return this ;
845
857
}
846
858
859
+ public Builder setSpecificSelection (String specificSelection ) {
860
+ this .specificSelection = specificSelection ;
861
+ return this ;
862
+ }
863
+
847
864
public Builder setIncludeEngines (List <String > includeEngines ) {
848
865
this .includeEngines = includeEngines ;
849
866
return this ;
@@ -880,9 +897,8 @@ private RegexFilter(boolean exclude, Pattern pattern) {
880
897
@ Override
881
898
public FilterResult apply (TestDescriptor testDescriptor ) {
882
899
if (testDescriptor .getSource ().isPresent ()) {
883
- if (testDescriptor .getSource ().get () instanceof MethodSource ) {
884
- MethodSource methodSource = (MethodSource ) testDescriptor .getSource ().get ();
885
- String name = methodSource .getJavaClass ().getName ();
900
+ if (testDescriptor .getSource ().get () instanceof MethodSource methodSource ) {
901
+ String name = methodSource .getClassName ();
886
902
if (pattern .matcher (name ).matches ()) {
887
903
return FilterResult .includedIf (!exclude );
888
904
}
@@ -893,6 +909,232 @@ public FilterResult apply(TestDescriptor testDescriptor) {
893
909
}
894
910
}
895
911
912
+ // https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#test
913
+ // org.apache.maven.surefire.api.testset.TestListResolver
914
+ // org.apache.maven.surefire.api.testset.ResolvedTest
915
+ private static class MavenSpecificSelectionFilter implements PostDiscoveryFilter {
916
+ private final Matcher [] excludes ;
917
+ private final Matcher [] includes ;
918
+
919
+ MavenSpecificSelectionFilter (String selection ) {
920
+ List <Matcher > excludes = new ArrayList <>();
921
+ List <Matcher > includes = new ArrayList <>();
922
+
923
+ if (selection != null ) {
924
+ for (String item : selection .split ("," )) {
925
+ item = item .trim ();
926
+ if (item .isEmpty () || "!" .equals (item ) || "#" .equals (item )) {
927
+ continue ;
928
+ }
929
+ List <Matcher > list ;
930
+ if (item .startsWith ("!" )) {
931
+ list = excludes ;
932
+ item = item .substring (1 );
933
+ } else {
934
+ list = includes ;
935
+ }
936
+
937
+ int hashIndex = item .indexOf ('#' );
938
+ if (hashIndex == 0 ) {
939
+ List <Pattern > methods = extractMethodPatterns (item .substring (hashIndex + 1 ));
940
+ list .add (new MethodMatcher (methods .toArray (new Pattern [0 ])));
941
+ } else if (hashIndex > 0 ) {
942
+ String classPattern = adjustClassGlob (item .substring (0 , hashIndex ));
943
+ if (hashIndex == item .length () - 1 ) {
944
+ list .add (new ClassMatcher (globToPattern (classPattern )));
945
+ } else {
946
+ List <Pattern > methods = extractMethodPatterns (item .substring (hashIndex + 1 ));
947
+ list .add (new ClassAndMethodMatcher (globToPattern (classPattern ), methods .toArray (new Pattern [0 ])));
948
+ }
949
+ } else {
950
+ String classPattern = adjustClassGlob (item );
951
+ list .add (new ClassMatcher (globToPattern (classPattern )));
952
+ }
953
+ }
954
+ }
955
+
956
+ this .excludes = excludes .toArray (new Matcher [0 ]);
957
+ this .includes = includes .toArray (new Matcher [0 ]);
958
+ }
959
+
960
+ private static List <Pattern > extractMethodPatterns (String methodGlobs ) {
961
+ List <Pattern > result = new ArrayList <>();
962
+ for (String methodGlob : methodGlobs .split ("\\ +" )) {
963
+ methodGlob = methodGlob .trim ();
964
+ if (!methodGlob .isEmpty ()) {
965
+ result .add (globToPattern (methodGlob ));
966
+ }
967
+ }
968
+ return result ;
969
+ }
970
+
971
+ private static String adjustClassGlob (String classGlob ) {
972
+ if (classGlob .startsWith ("**/" )) {
973
+ classGlob = classGlob .substring ("**/" .length ());
974
+ }
975
+ if (classGlob .endsWith (".java" )) {
976
+ classGlob = classGlob .substring (0 , classGlob .length () - ".java" .length ());
977
+ } else if (classGlob .endsWith (".class" )) {
978
+ classGlob = classGlob .substring (0 , classGlob .length () - ".class" .length ());
979
+ } else if (classGlob .endsWith (".*" )) {
980
+ classGlob = classGlob .substring (0 , classGlob .length () - ".*" .length ());
981
+ }
982
+ return "**/" + classGlob .replace ('.' , '/' );
983
+ }
984
+
985
+ private static Pattern globToPattern (String glob ) {
986
+ return Pattern .compile (GlobUtil .toRegexPattern (glob ));
987
+ }
988
+
989
+ @ Override
990
+ public FilterResult apply (TestDescriptor testDescriptor ) {
991
+ if (testDescriptor .getSource ().isPresent ()
992
+ && testDescriptor .getSource ().get () instanceof MethodSource methodSource ) {
993
+ String className = methodSource .getClassName ().replace ('.' , '/' );
994
+ String methodName = methodSource .getMethodName ();
995
+ for (Matcher exclude : excludes ) {
996
+ if (exclude .matches (className , methodName )) {
997
+ return FilterResult .excluded (null );
998
+ }
999
+ }
1000
+ for (Matcher include : includes ) {
1001
+ if (include .matches (className , methodName )) {
1002
+ return FilterResult .included (null );
1003
+ }
1004
+ }
1005
+ return FilterResult .excluded (null );
1006
+ }
1007
+ return FilterResult .included ("not a method" );
1008
+ }
1009
+
1010
+ private interface Matcher {
1011
+ boolean matches (String className , String methodName );
1012
+ }
1013
+
1014
+ private record ClassMatcher (Pattern classPattern ) implements Matcher {
1015
+ @ Override
1016
+ public boolean matches (String className , String methodName ) {
1017
+ return classPattern .matcher (className ).matches ();
1018
+ }
1019
+ }
1020
+
1021
+ private record MethodMatcher (Pattern [] methodPatterns ) implements Matcher {
1022
+ @ Override
1023
+ public boolean matches (String className , String methodName ) {
1024
+ for (Pattern methodPattern : methodPatterns ) {
1025
+ if (methodPattern .matcher (methodName ).matches ()) {
1026
+ return true ;
1027
+ }
1028
+ }
1029
+ return false ;
1030
+ }
1031
+ }
1032
+
1033
+ private record ClassAndMethodMatcher (Pattern classPattern , Pattern [] methodPatterns ) implements Matcher {
1034
+ @ Override
1035
+ public boolean matches (String className , String methodName ) {
1036
+ if (classPattern .matcher (className ).matches ()) {
1037
+ for (Pattern methodPattern : methodPatterns ) {
1038
+ if (methodPattern .matcher (methodName ).matches ()) {
1039
+ return true ;
1040
+ }
1041
+ }
1042
+ }
1043
+ return false ;
1044
+ }
1045
+ }
1046
+ }
1047
+
1048
+ // https://docs.gradle.org/current/userguide/java_testing.html#test_filtering
1049
+ // org.gradle.api.internal.tasks.testing.filter.TestSelectionMatcher
1050
+ // org.gradle.api.internal.tasks.testing.filter.TestSelectionMatcher.TestPattern
1051
+ private static class GradleSpecificSelectionFilter implements PostDiscoveryFilter {
1052
+ // these 2 arrays always have the same length
1053
+ private final Pattern [] includes ;
1054
+ private final boolean [] simpleNames ;
1055
+
1056
+ GradleSpecificSelectionFilter (String selection ) {
1057
+ List <Pattern > includes = new ArrayList <>();
1058
+ List <Boolean > simpleNames = new ArrayList <>();
1059
+
1060
+ if (selection != null ) {
1061
+ for (String item : selection .split ("," )) {
1062
+ item = item .trim ();
1063
+ if (item .isEmpty ()) {
1064
+ continue ;
1065
+ }
1066
+
1067
+ includes .add (parsePattern (item ));
1068
+ simpleNames .add (Character .isUpperCase (item .charAt (0 )));
1069
+ }
1070
+ }
1071
+
1072
+ this .includes = includes .toArray (new Pattern [0 ]);
1073
+ this .simpleNames = new boolean [simpleNames .size ()];
1074
+ for (int i = 0 ; i < simpleNames .size (); i ++) {
1075
+ this .simpleNames [i ] = simpleNames .get (i );
1076
+ }
1077
+ }
1078
+
1079
+ private static Pattern parsePattern (String item ) {
1080
+ StringBuilder result = new StringBuilder ();
1081
+ int start = 0 ;
1082
+ int current = 0 ;
1083
+ while (current < item .length ()) {
1084
+ if (item .charAt (current ) == '*' ) {
1085
+ if (current > start ) {
1086
+ String part = item .substring (start , current );
1087
+ result .append (Pattern .quote (part ));
1088
+ }
1089
+ result .append (".*" );
1090
+ start = current + 1 ;
1091
+ }
1092
+ current ++;
1093
+ }
1094
+ if (current > start ) {
1095
+ String part = item .substring (start , current );
1096
+ result .append (Pattern .quote (part ));
1097
+ }
1098
+ return Pattern .compile (result .toString ());
1099
+ }
1100
+
1101
+ @ Override
1102
+ public FilterResult apply (TestDescriptor testDescriptor ) {
1103
+ if (testDescriptor .getSource ().isPresent ()
1104
+ && testDescriptor .getSource ().get () instanceof MethodSource methodSource ) {
1105
+ String className = methodSource .getClassName ();
1106
+ String methodName = methodSource .getMethodName ();
1107
+ String classAndMethodName = className + "." + methodName ;
1108
+
1109
+ String simpleClassName = className ;
1110
+ String simpleClassAndMethodName = classAndMethodName ;
1111
+
1112
+ // using simple names is common, so let's just precompute that unconditionally
1113
+ int lastDot = className .lastIndexOf ('.' );
1114
+ if (lastDot >= 0 && lastDot < className .length () - 1 ) {
1115
+ simpleClassName = className .substring (lastDot + 1 );
1116
+ simpleClassAndMethodName = simpleClassName + "." + methodName ;
1117
+ }
1118
+
1119
+ for (int i = 0 ; i < includes .length ; i ++) {
1120
+ String testedClassName = className ;
1121
+ String testedClassAndMethodName = classAndMethodName ;
1122
+ if (simpleNames [i ]) {
1123
+ testedClassName = simpleClassName ;
1124
+ testedClassAndMethodName = simpleClassAndMethodName ;
1125
+ }
1126
+
1127
+ Pattern include = includes [i ];
1128
+ if (include .matcher (testedClassAndMethodName ).matches () || include .matcher (testedClassName ).matches ()) {
1129
+ return FilterResult .included (null );
1130
+ }
1131
+ }
1132
+ return FilterResult .excluded (null );
1133
+ }
1134
+ return FilterResult .included ("not a method" );
1135
+ }
1136
+ }
1137
+
896
1138
/**
897
1139
* filter for tests that are currently failing.
898
1140
* <p>
@@ -904,10 +1146,8 @@ private class CurrentlyFailingFilter implements PostDiscoveryFilter {
904
1146
@ Override
905
1147
public FilterResult apply (TestDescriptor testDescriptor ) {
906
1148
if (testDescriptor .getSource ().isPresent ()) {
907
- if (testDescriptor .getSource ().get () instanceof MethodSource ) {
908
- MethodSource methodSource = (MethodSource ) testDescriptor .getSource ().get ();
909
-
910
- String name = methodSource .getJavaClass ().getName ();
1149
+ if (testDescriptor .getSource ().get () instanceof MethodSource methodSource ) {
1150
+ String name = methodSource .getClassName ();
911
1151
Map <UniqueId , TestResult > results = testState .getCurrentResults ().get (name );
912
1152
if (results == null ) {
913
1153
return FilterResult .included ("new test" );
0 commit comments