|
23 | 23 | import java.io.Closeable; |
24 | 24 | import java.io.File; |
25 | 25 | import java.io.IOException; |
| 26 | +import java.nio.file.Files; |
| 27 | +import java.util.ArrayList; |
26 | 28 | import java.util.Collection; |
27 | 29 | import java.util.Collections; |
28 | 30 | import java.util.HashMap; |
| 31 | +import java.util.List; |
29 | 32 | import java.util.Map; |
30 | 33 | import java.util.Queue; |
31 | 34 | import java.util.Set; |
|
73 | 76 | import org.apache.maven.surefire.booter.StartupConfiguration; |
74 | 77 | import org.apache.maven.surefire.booter.SurefireBooterForkException; |
75 | 78 | import org.apache.maven.surefire.booter.SurefireExecutionException; |
| 79 | +import org.apache.maven.surefire.booter.TypeEncodedValue; |
76 | 80 | import org.apache.maven.surefire.extensions.EventHandler; |
77 | 81 | import org.apache.maven.surefire.extensions.ForkChannel; |
78 | 82 | import org.apache.maven.surefire.extensions.ForkNodeFactory; |
|
86 | 90 | import static java.lang.StrictMath.min; |
87 | 91 | import static java.lang.System.currentTimeMillis; |
88 | 92 | import static java.lang.Thread.currentThread; |
| 93 | +import static java.nio.charset.StandardCharsets.UTF_8; |
89 | 94 | import static java.util.Collections.addAll; |
90 | 95 | import static java.util.UUID.randomUUID; |
91 | 96 | import static java.util.concurrent.Executors.newScheduledThreadPool; |
|
109 | 114 | import static org.apache.maven.surefire.api.util.internal.StringUtils.NL; |
110 | 115 | import static org.apache.maven.surefire.booter.SystemPropertyManager.writePropertiesFile; |
111 | 116 | import static org.apache.maven.surefire.booter.SystemUtils.pidOf; |
| 117 | +import static org.apache.maven.surefire.booter.SystemUtils.toJdkHomeFromJre; |
112 | 118 | import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.addShutDownHook; |
113 | 119 | import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.removeShutdownHook; |
114 | 120 |
|
@@ -342,8 +348,8 @@ private RunResult runSuitesForkOnceMultiple(SurefireProperties effectiveSystemPr |
342 | 348 |
|
343 | 349 | Queue<String> tests = new ConcurrentLinkedQueue<>(); |
344 | 350 |
|
345 | | - for (Class<?> clazz : getSuitesIterator()) { |
346 | | - tests.add(clazz.getName()); |
| 351 | + for (String testClassName : getTestClassNames(effectiveSystemProps)) { |
| 352 | + tests.add(testClassName); |
347 | 353 | } |
348 | 354 |
|
349 | 355 | Queue<TestProvidingInputStream> forks = new ConcurrentLinkedQueue<>(); |
@@ -408,7 +414,8 @@ private RunResult runSuitesForkPerTestSet(SurefireProperties effectiveSystemProp |
408 | 414 | addShutDownHook(shutdown); |
409 | 415 | int failFastCount = providerConfiguration.getSkipAfterFailureCount(); |
410 | 416 | 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) |
412 | 419 | .filter(fork -> !forkConfiguration.getPluginPlatform().isShutdown()) |
413 | 420 | .map(testSet -> (Callable<RunResult>) () -> { |
414 | 421 | int forkNumber = drawNumber(); |
@@ -513,6 +520,29 @@ private RunResult fork( |
513 | 520 | ForkNodeFactory forkNodeFactory, |
514 | 521 | boolean readTestsFromInStream) |
515 | 522 | 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 { |
516 | 546 | CloseableCloser closer = new CloseableCloser(forkNumber, commandReader); |
517 | 547 | final String tempDir; |
518 | 548 | final File surefireProperties; |
@@ -542,7 +572,8 @@ private RunResult fork( |
542 | 572 | readTestsFromInStream, |
543 | 573 | pluginPid, |
544 | 574 | forkNumber, |
545 | | - connectionString); |
| 575 | + connectionString, |
| 576 | + discoverTestsOutputFile); |
546 | 577 |
|
547 | 578 | if (effectiveSystemProperties != null) { |
548 | 579 | SurefireProperties filteredProperties = |
@@ -720,11 +751,130 @@ private Iterable<Class<?>> getSuitesIterator() throws SurefireBooterForkExceptio |
720 | 751 | startupConfiguration, providerConfiguration, unifiedClassLoader, reporterFactory); |
721 | 752 | SurefireProvider surefireProvider = providerFactory.createProvider(false); |
722 | 753 | 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); |
723 | 762 | } catch (SurefireExecutionException e) { |
724 | 763 | throw new SurefireBooterForkException("Unable to create classloader to find test suites", e); |
725 | 764 | } |
726 | 765 | } |
727 | 766 |
|
| 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 | + |
728 | 878 | private static Thread createImmediateShutdownHookThread( |
729 | 879 | final TestLessInputStreamBuilder builder, final Shutdown shutdownType) { |
730 | 880 | return SHUTDOWN_HOOK_THREAD_FACTORY.newThread(new Runnable() { |
|
0 commit comments