Skip to content

Commit c051938

Browse files
authored
Discover tests in a fork when a toolchain JDK is used (#3444)
* Discover tests in a fork when a toolchain JDK is used When forking has to list the tests first (forkCount > 1 or reuseForks=false), Surefire did it in Maven's JVM. If the tests were built for a newer JDK, that failed with UnsupportedClassVersionError. Now the listing runs in a short-lived fork using the toolchain JDK, so no test class is loaded in Maven's JVM. The extra fork only starts when the test JVM differs from Maven's. Fixes #2151 --------- Signed-off-by: Olivier Lamy <olamy@apache.org>
1 parent db75df8 commit c051938

14 files changed

Lines changed: 693 additions & 10 deletions

File tree

maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/BooterSerializer.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@
3737
import org.apache.maven.surefire.booter.ProcessCheckerType;
3838
import org.apache.maven.surefire.booter.ProviderConfiguration;
3939
import org.apache.maven.surefire.booter.StartupConfiguration;
40+
import org.apache.maven.surefire.booter.TypeEncodedValue;
4041

4142
import static org.apache.maven.plugin.surefire.SurefireHelper.replaceForkThreadsInPath;
4243
import static org.apache.maven.surefire.booter.AbstractPathConfiguration.CHILD_DELEGATION;
4344
import static org.apache.maven.surefire.booter.AbstractPathConfiguration.CLASSPATH;
4445
import static org.apache.maven.surefire.booter.AbstractPathConfiguration.ENABLE_ASSERTIONS;
4546
import static org.apache.maven.surefire.booter.AbstractPathConfiguration.SUREFIRE_CLASSPATH;
47+
import static org.apache.maven.surefire.booter.BooterConstants.DISCOVER_TESTS_OUTPUT_FILE;
4648
import static org.apache.maven.surefire.booter.BooterConstants.EXCLUDES_PROPERTY_PREFIX;
4749
import static org.apache.maven.surefire.booter.BooterConstants.FAIL_FAST_COUNT;
4850
import static org.apache.maven.surefire.booter.BooterConstants.FORKTESTSET;
@@ -105,10 +107,12 @@ File serialize(
105107
boolean readTestsFromInStream,
106108
Long pid,
107109
int forkNumber,
108-
String forkNodeConnectionString)
110+
String forkNodeConnectionString,
111+
String discoverTestsOutputFile)
109112
throws IOException {
110113
SurefireProperties properties = new SurefireProperties(sourceProperties);
111114
properties.setNullableProperty(FORK_NODE_CONNECTION_STRING, forkNodeConnectionString);
115+
properties.setNullableProperty(DISCOVER_TESTS_OUTPUT_FILE, discoverTestsOutputFile);
112116
properties.setProperty(PLUGIN_PID, pid);
113117

114118
AbstractPathConfiguration cp = startupConfiguration.getClasspathConfiguration();
@@ -179,6 +183,10 @@ private static String getTypeEncoded(Object value) {
179183
if (value == null) {
180184
return null;
181185
}
186+
if (value instanceof TypeEncodedValue) {
187+
TypeEncodedValue encoded = (TypeEncodedValue) value;
188+
return encoded.getType() + "|" + encoded.getValue();
189+
}
182190
String valueToUse = value instanceof Class ? ((Class<?>) value).getName() : value.toString();
183191
return value.getClass().getName() + "|" + valueToUse;
184192
}

maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/booterclient/ForkStarter.java

Lines changed: 154 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
import java.io.Closeable;
2424
import java.io.File;
2525
import java.io.IOException;
26+
import java.nio.file.Files;
27+
import java.util.ArrayList;
2628
import java.util.Collection;
2729
import java.util.Collections;
2830
import java.util.HashMap;
31+
import java.util.List;
2932
import java.util.Map;
3033
import java.util.Queue;
3134
import java.util.Set;
@@ -73,6 +76,7 @@
7376
import org.apache.maven.surefire.booter.StartupConfiguration;
7477
import org.apache.maven.surefire.booter.SurefireBooterForkException;
7578
import org.apache.maven.surefire.booter.SurefireExecutionException;
79+
import org.apache.maven.surefire.booter.TypeEncodedValue;
7680
import org.apache.maven.surefire.extensions.EventHandler;
7781
import org.apache.maven.surefire.extensions.ForkChannel;
7882
import org.apache.maven.surefire.extensions.ForkNodeFactory;
@@ -86,6 +90,7 @@
8690
import static java.lang.StrictMath.min;
8791
import static java.lang.System.currentTimeMillis;
8892
import static java.lang.Thread.currentThread;
93+
import static java.nio.charset.StandardCharsets.UTF_8;
8994
import static java.util.Collections.addAll;
9095
import static java.util.UUID.randomUUID;
9196
import static java.util.concurrent.Executors.newScheduledThreadPool;
@@ -109,6 +114,7 @@
109114
import static org.apache.maven.surefire.api.util.internal.StringUtils.NL;
110115
import static org.apache.maven.surefire.booter.SystemPropertyManager.writePropertiesFile;
111116
import static org.apache.maven.surefire.booter.SystemUtils.pidOf;
117+
import static org.apache.maven.surefire.booter.SystemUtils.toJdkHomeFromJre;
112118
import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.addShutDownHook;
113119
import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.removeShutdownHook;
114120

@@ -342,8 +348,8 @@ private RunResult runSuitesForkOnceMultiple(SurefireProperties effectiveSystemPr
342348

343349
Queue<String> tests = new ConcurrentLinkedQueue<>();
344350

345-
for (Class<?> clazz : getSuitesIterator()) {
346-
tests.add(clazz.getName());
351+
for (String testClassName : getTestClassNames(effectiveSystemProps)) {
352+
tests.add(testClassName);
347353
}
348354

349355
Queue<TestProvidingInputStream> forks = new ConcurrentLinkedQueue<>();
@@ -408,7 +414,8 @@ private RunResult runSuitesForkPerTestSet(SurefireProperties effectiveSystemProp
408414
addShutDownHook(shutdown);
409415
int failFastCount = providerConfiguration.getSkipAfterFailureCount();
410416
AtomicInteger notifyForksToSkipTestsNow = new AtomicInteger(failFastCount);
411-
Collection<Future<RunResult>> results = stream(((Iterable<?>) getSuitesIterator()).spliterator(), false)
417+
Collection<Future<RunResult>> results = stream(
418+
getForkPerTestSetTestSets(effectiveSystemProps).spliterator(), false)
412419
.filter(fork -> !forkConfiguration.getPluginPlatform().isShutdown())
413420
.map(testSet -> (Callable<RunResult>) () -> {
414421
int forkNumber = drawNumber();
@@ -513,6 +520,29 @@ private RunResult fork(
513520
ForkNodeFactory forkNodeFactory,
514521
boolean readTestsFromInStream)
515522
throws SurefireBooterForkException {
523+
return fork(
524+
testSet,
525+
providerProperties,
526+
forkClient,
527+
effectiveSystemProperties,
528+
forkNumber,
529+
commandReader,
530+
forkNodeFactory,
531+
readTestsFromInStream,
532+
null);
533+
}
534+
535+
private RunResult fork(
536+
Object testSet,
537+
PropertiesWrapper providerProperties,
538+
ForkClient forkClient,
539+
SurefireProperties effectiveSystemProperties,
540+
int forkNumber,
541+
AbstractCommandReader commandReader,
542+
ForkNodeFactory forkNodeFactory,
543+
boolean readTestsFromInStream,
544+
String discoverTestsOutputFile)
545+
throws SurefireBooterForkException {
516546
CloseableCloser closer = new CloseableCloser(forkNumber, commandReader);
517547
final String tempDir;
518548
final File surefireProperties;
@@ -542,7 +572,8 @@ private RunResult fork(
542572
readTestsFromInStream,
543573
pluginPid,
544574
forkNumber,
545-
connectionString);
575+
connectionString,
576+
discoverTestsOutputFile);
546577

547578
if (effectiveSystemProperties != null) {
548579
SurefireProperties filteredProperties =
@@ -720,11 +751,130 @@ private Iterable<Class<?>> getSuitesIterator() throws SurefireBooterForkExceptio
720751
startupConfiguration, providerConfiguration, unifiedClassLoader, reporterFactory);
721752
SurefireProvider surefireProvider = providerFactory.createProvider(false);
722753
return surefireProvider.getSuites();
754+
} catch (UnsupportedClassVersionError e) {
755+
throw new SurefireBooterForkException(
756+
"Could not load the test classes to figure out which tests to run. This usually means the tests "
757+
+ "were compiled with a newer JDK than the one running Maven, and forking (forkCount greater "
758+
+ "than 1 or reuseForks=false) needs to list the tests using Maven's own JVM. Point the "
759+
+ "'jvm' parameter or a 'jdk' toolchain at a JDK that is at least as new as the one used to "
760+
+ "compile the tests.",
761+
e);
723762
} catch (SurefireExecutionException e) {
724763
throw new SurefireBooterForkException("Unable to create classloader to find test suites", e);
725764
}
726765
}
727766

767+
/**
768+
* Gives back the names of the test classes to run. If the tests run in a different JVM than Maven itself
769+
* (because a {@code jdk} toolchain or the {@code jvm} parameter is set), we ask a short-lived fork to list
770+
* them, so Maven never has to load test classes that may be compiled for a newer JDK. Otherwise we just
771+
* list them here, like before.
772+
*
773+
* @see <a href="https://github.com/apache/maven-surefire/issues/2151">Issue 2151</a>
774+
*/
775+
private List<String> getTestClassNames(SurefireProperties effectiveSystemProperties)
776+
throws SurefireBooterForkException {
777+
if (isForkJvmDifferentFromBuildJvm()) {
778+
return discoverTestClassNames(effectiveSystemProperties);
779+
}
780+
List<String> testClassNames = new ArrayList<>();
781+
for (Class<?> clazz : getSuitesIterator()) {
782+
testClassNames.add(clazz.getName());
783+
}
784+
return testClassNames;
785+
}
786+
787+
/**
788+
* Gives back the test sets for {@code fork-per-test-set} runs. When the tests run in a different JVM than Maven,
789+
* we discover the class names in a fork (see {@link #getTestClassNames}) and wrap each one as a
790+
* {@link TypeEncodedValue} of type {@link Class}, so every fork loads the class with its own JVM. Otherwise we
791+
* reuse the already-loaded {@link Class} instances, like before.
792+
*/
793+
private Iterable<?> getForkPerTestSetTestSets(SurefireProperties effectiveSystemProperties)
794+
throws SurefireBooterForkException {
795+
if (isForkJvmDifferentFromBuildJvm()) {
796+
return discoverTestClassNames(effectiveSystemProperties).stream()
797+
.map(testClassName -> new TypeEncodedValue(Class.class.getName(), testClassName))
798+
.collect(toList());
799+
}
800+
return getSuitesIterator();
801+
}
802+
803+
/**
804+
* @return {@code true} when the tests run in a different JDK than Maven (set through a {@code jdk} toolchain or the
805+
* {@code jvm} parameter), in which case we need a fork to list the test classes.
806+
*/
807+
private boolean isForkJvmDifferentFromBuildJvm() {
808+
File testsJdkHome = forkConfiguration.getJdkForTests().getJdkHome();
809+
return testsJdkHome != null && !testsJdkHome.equals(toJdkHomeFromJre());
810+
}
811+
812+
/**
813+
* Starts one short-lived fork with the configured test JVM that lists the test classes to run and writes their
814+
* names to a temporary file, then reads them back. The candidate class names already travel to the fork through
815+
* the provider properties (from the directory scan Maven did earlier), so no test class is loaded in Maven's JVM.
816+
*/
817+
private List<String> discoverTestClassNames(SurefireProperties effectiveSystemProperties)
818+
throws SurefireBooterForkException {
819+
File discoveryFile;
820+
try {
821+
discoveryFile =
822+
File.createTempFile("surefire-discovered-tests", ".txt", forkConfiguration.getTempDirectory());
823+
} catch (IOException e) {
824+
throw new SurefireBooterForkException("Cannot create temporary file for test discovery", e);
825+
}
826+
827+
TestLessInputStreamBuilder builder = new TestLessInputStreamBuilder();
828+
TestLessInputStream stream = builder.build();
829+
Thread shutdown = createImmediateShutdownHookThread(builder, providerConfiguration.getShutdown());
830+
ScheduledFuture<?> ping = triggerPingTimerForShutdown(builder);
831+
int forkNumber = drawNumber();
832+
PropertiesWrapper props = new PropertiesWrapper(providerConfiguration.getProviderProperties());
833+
try {
834+
addShutDownHook(shutdown);
835+
DefaultReporterFactory forkedReporterFactory =
836+
new DefaultReporterFactory(startupReportConfiguration, log, forkNumber);
837+
defaultReporterFactories.add(forkedReporterFactory);
838+
ForkClient forkClient = new ForkClient(forkedReporterFactory, stream, forkNumber);
839+
ForkNodeFactory node = forkConfiguration.getForkNodeFactory();
840+
if (!forkConfiguration.getPluginPlatform().isShutdown()) {
841+
fork(
842+
null,
843+
props,
844+
forkClient,
845+
effectiveSystemProperties,
846+
forkNumber,
847+
stream,
848+
node,
849+
false,
850+
discoveryFile.getAbsolutePath());
851+
}
852+
return readTestClassNames(discoveryFile);
853+
} finally {
854+
returnNumber(forkNumber);
855+
removeShutdownHook(shutdown);
856+
ping.cancel(true);
857+
builder.removeStream(stream);
858+
if (!forkConfiguration.isDebug() && !discoveryFile.delete()) {
859+
discoveryFile.deleteOnExit();
860+
}
861+
}
862+
}
863+
864+
private static List<String> readTestClassNames(File discoveryFile) throws SurefireBooterForkException {
865+
if (!discoveryFile.isFile()) {
866+
return Collections.emptyList();
867+
}
868+
try {
869+
return Files.readAllLines(discoveryFile.toPath(), UTF_8).stream()
870+
.map(String::trim)
871+
.filter(String::isEmpty)
872+
.collect(toList());
873+
} catch (IOException e) {
874+
throw new SurefireBooterForkException("Cannot read discovered tests from " + discoveryFile, e);
875+
}
876+
}
877+
728878
private static Thread createImmediateShutdownHookThread(
729879
final TestLessInputStreamBuilder builder, final Shutdown shutdownType) {
730880
return SHUTDOWN_HOOK_THREAD_FACTORY.newThread(new Runnable() {

maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerProviderConfigurationTest.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,15 @@ private ProviderConfiguration saveAndReload(
234234
test = "aTest";
235235
}
236236
final File propsTest = booterSerializer.serialize(
237-
props, booterConfiguration, testProviderConfiguration, test, readTestsFromInStream, 51L, 1, "pipe://1");
237+
props,
238+
booterConfiguration,
239+
testProviderConfiguration,
240+
test,
241+
readTestsFromInStream,
242+
51L,
243+
1,
244+
"pipe://1",
245+
null);
238246
BooterDeserializer booterDeserializer = new BooterDeserializer(new FileInputStream(propsTest));
239247
assertEquals("51", (Object) booterDeserializer.getPluginPid());
240248
assertEquals("pipe://1", booterDeserializer.getConnectionString());

maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/booterclient/BooterDeserializerStartupConfigurationTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ private StartupConfiguration saveAndReload(StartupConfiguration startupConfigura
165165
false,
166166
null,
167167
1,
168-
"tcp://localhost:63003");
168+
"tcp://localhost:63003",
169+
null);
169170
try (InputStream inputStream = Files.newInputStream(propsTest.toPath())) {
170171
BooterDeserializer booterDeserializer = new BooterDeserializer(inputStream);
171172
assertNull(booterDeserializer.getPluginPid());

0 commit comments

Comments
 (0)