[SUREFIRE-1234] Allow to configure JVM for tests by referencing a toolchain entry

diff --git a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
index b26e981..112de82 100644
--- a/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
+++ b/maven-surefire-common/src/main/java/org/apache/maven/plugin/surefire/AbstractSurefireMojo.java
@@ -97,6 +97,7 @@
 import javax.annotation.Nonnull;
 import java.io.File;
 import java.io.IOException;
+import java.lang.reflect.Method;
 import java.math.BigDecimal;
 import java.nio.file.Files;
 import java.nio.file.Paths;
@@ -141,6 +142,8 @@
 import static org.apache.maven.surefire.booter.SystemUtils.toJdkVersionFromReleaseFile;
 import static org.apache.maven.surefire.suite.RunResult.failure;
 import static org.apache.maven.surefire.suite.RunResult.noTestsRun;
+import static org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray;
+import static org.apache.maven.surefire.util.ReflectionUtils.tryGetMethod;
 
 /**
  * Abstract base class for running tests using Surefire.
@@ -765,6 +768,42 @@
     private String[] dependenciesToScan;
 
     /**
+     * <p>
+     *     Allow for configuration of the test jvm via maven toolchains.
+     *     This permits a configuration where the project is built with one jvm and tested with another.
+     *     This is similar to {@link #jvm}, but avoids hardcoding paths.
+     *     The two parameters are mutually exclusive (jvm wins)
+     * </p>
+     *
+     * <p>Examples:</p>
+     * (see <a href="https://maven.apache.org/guides/mini/guide-using-toolchains.html">
+     *     Guide to Toolchains</a> for more info)
+     *
+     * <pre>
+     * {@code
+     *    <configuration>
+     *        ...
+     *        <jdkToolchain>
+     *            <version>1.11</version>
+     *        </jdkToolchain>
+     *    </configuration>
+     *
+     *    <configuration>
+     *        ...
+     *        <jdkToolchain>
+     *            <version>1.8</version>
+     *            <vendor>zulu</vendor>
+     *        </jdkToolchain>
+     *    </configuration>
+     *    }
+     * </pre>
+     *
+     * @since 3.0.0-M5 and Maven 3.3.x
+     */
+    @Parameter
+    private Map<String, String> jdkToolchain;
+
+    /**
      *
      */
     @Component
@@ -909,7 +948,49 @@
         return consoleLogger;
     }
 
-    private void setupStuff()
+    private static <T extends ToolchainManager> Toolchain getToolchainMaven33x( Class<T> toolchainManagerType,
+                                                                                T toolchainManager,
+                                                                                MavenSession session,
+                                                                                Map<String, String> toolchainArgs )
+        throws MojoFailureException
+    {
+        Method getToolchainsMethod =
+            tryGetMethod( toolchainManagerType, "getToolchains", MavenSession.class, String.class, Map.class );
+        if ( getToolchainsMethod != null )
+        {
+            //noinspection unchecked
+            List<Toolchain> tcs = (List<Toolchain>) invokeMethodWithArray( toolchainManager,
+                getToolchainsMethod, session, "jdk", toolchainArgs );
+            if ( tcs.isEmpty() )
+            {
+                throw new MojoFailureException(
+                    "Requested toolchain specification did not match any configured toolchain: " + toolchainArgs );
+            }
+            return tcs.get( 0 );
+        }
+        return null;
+    }
+
+    //TODO remove the part with ToolchainManager lookup once we depend on
+    //3.0.9 (have it as prerequisite). Define as regular component field then.
+    private Toolchain getToolchain() throws MojoFailureException
+    {
+        Toolchain tc = null;
+
+        if ( getJdkToolchain() != null )
+        {
+            tc = getToolchainMaven33x( ToolchainManager.class, getToolchainManager(), getSession(), getJdkToolchain() );
+        }
+
+        if ( tc == null )
+        {
+            tc = getToolchainManager().getToolchainFromBuildContext( "jdk", getSession() );
+        }
+
+        return tc;
+    }
+
+    private void setupStuff() throws MojoFailureException
     {
         surefireDependencyResolver = new SurefireDependencyResolver( getRepositorySystem(),
                 getConsoleLogger(), getLocalRepository(),
@@ -925,7 +1006,7 @@
 
         if ( getToolchainManager() != null )
         {
-            toolchain = getToolchainManager().getToolchainFromBuildContext( "jdk", getSession() );
+            toolchain = getToolchain();
         }
     }
 
@@ -3865,6 +3946,16 @@
         SurefireHelper.logDebugOrCliShowErrors( s, getConsoleLogger(), cli );
     }
 
+    public Map<String, String> getJdkToolchain()
+    {
+        return jdkToolchain;
+    }
+
+    public void setJdkToolchain( Map<String, String> jdkToolchain )
+    {
+        this.jdkToolchain = jdkToolchain;
+    }
+
     public String getTempDir()
     {
         return tempDir;
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoToolchainsTest.java b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoToolchainsTest.java
new file mode 100644
index 0000000..7589718
--- /dev/null
+++ b/maven-surefire-common/src/test/java/org/apache/maven/plugin/surefire/AbstractSurefireMojoToolchainsTest.java
@@ -0,0 +1,176 @@
+package org.apache.maven.plugin.surefire;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.toolchain.Toolchain;
+import org.apache.maven.toolchain.ToolchainManager;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static junit.framework.TestCase.assertNull;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.powermock.api.mockito.PowerMockito.mock;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+import static org.powermock.api.mockito.PowerMockito.when;
+import static org.powermock.reflect.Whitebox.invokeMethod;
+
+/**
+ * Test for {@link AbstractSurefireMojo}. jdkToolchain parameter
+ */
+@RunWith( PowerMockRunner.class )
+@PrepareForTest( {AbstractSurefireMojo.class} )
+@PowerMockIgnore( {"org.jacoco.agent.rt.*", "com.vladium.emma.rt.*"} )
+public class AbstractSurefireMojoToolchainsTest
+{
+
+    /**
+     * Ensure that we use the toolchain found by getToolchainMaven33x()
+     * when the jdkToolchain parameter is set.
+     */
+    @Test
+    public void shouldCallMaven33xMethodWhenSpecSet() throws Exception
+    {
+        AbstractSurefireMojoTest.Mojo mojo = new AbstractSurefireMojoTest.Mojo();
+        Toolchain expectedFromMaven33Method = mock( Toolchain.class );
+        MockToolchainManager toolchainManager = new MockToolchainManager( null, null );
+        mojo.setToolchainManager( toolchainManager );
+        mojo.setJdkToolchain( singletonMap( "version", "1.8" ) );
+
+        mockStatic( AbstractSurefireMojo.class );
+        when(
+            AbstractSurefireMojo.class,
+            "getToolchainMaven33x",
+            ToolchainManager.class,
+            toolchainManager,
+            mojo.getSession(), mojo.getJdkToolchain() ).thenReturn( expectedFromMaven33Method );
+        Toolchain actual = invokeMethod( mojo, "getToolchain" );
+        assertThat( actual )
+            .isSameAs( expectedFromMaven33Method );
+    }
+
+    /**
+     * Ensure that we use the toolchain from build context when
+     * no jdkToolchain map is configured in mojo parameters.
+     * getToolchain() returns the main maven toolchain from the build context
+     */
+    @Test
+    public void shouldFallthroughToBuildContextWhenNoSpecSet() throws Exception
+    {
+        AbstractSurefireMojoTest.Mojo mojo = new AbstractSurefireMojoTest.Mojo();
+        Toolchain expectedFromContext = mock( Toolchain.class );
+        Toolchain expectedFromSpec = mock( Toolchain.class ); //ensure it still behaves correctly even if not null
+        mojo.setToolchainManager( new MockToolchainManager( expectedFromSpec, expectedFromContext ) );
+        Toolchain actual = invokeMethod( mojo, "getToolchain" );
+        assertThat( actual )
+            .isSameAs( expectedFromContext );
+    }
+
+    @Test
+    public void shouldReturnNoToolchainInMaven32() throws Exception
+    {
+        Toolchain toolchain = invokeMethod( AbstractSurefireMojo.class,
+            "getToolchainMaven33x",
+            MockToolchainManagerMaven32.class,
+            new MockToolchainManagerMaven32( null ),
+            mock( MavenSession.class ),
+            emptyMap() );
+        assertNull( toolchain );
+    }
+
+    @Test( expected = MojoFailureException.class )
+    public void shouldThrowMaven33xToolchain() throws Exception
+    {
+        invokeMethod(
+            AbstractSurefireMojo.class,
+            "getToolchainMaven33x",
+            MockToolchainManager.class,
+            new MockToolchainManager( null, null ),
+            mock( MavenSession.class ),
+            emptyMap() );
+    }
+
+    @Test
+    public void shouldGetMaven33xToolchain() throws Exception
+    {
+        Toolchain expected = mock( Toolchain.class );
+        Toolchain actual = invokeMethod(
+            AbstractSurefireMojo.class,
+            "getToolchainMaven33x",
+            MockToolchainManager.class,
+            new MockToolchainManager( expected, null ),
+            mock( MavenSession.class ),
+            emptyMap() );
+
+        assertThat( actual )
+            .isSameAs( expected );
+    }
+
+    /**
+     * Mocks a ToolchainManager
+     */
+    public static final class MockToolchainManager extends MockToolchainManagerMaven32
+    {
+        private final Toolchain specToolchain;
+
+        public MockToolchainManager( Toolchain specToolchain, Toolchain buildContextToolchain )
+        {
+            super( buildContextToolchain );
+            this.specToolchain = specToolchain;
+        }
+
+        public List<Toolchain> getToolchains( MavenSession session, String type, Map<String, String> requirements )
+        {
+            return specToolchain == null ? Collections.<Toolchain>emptyList() : singletonList( specToolchain );
+        }
+    }
+
+    /**
+     * Mocks an older version that does not implement getToolchains()
+     * returns provided toolchain
+     */
+    public static class MockToolchainManagerMaven32 implements ToolchainManager
+    {
+
+        private final Toolchain buildContextToolchain;
+
+        public MockToolchainManagerMaven32( Toolchain buildContextToolchain )
+        {
+            this.buildContextToolchain = buildContextToolchain;
+        }
+
+        @Override
+        public Toolchain getToolchainFromBuildContext( String type, MavenSession context )
+        {
+            return buildContextToolchain;
+        }
+    }
+}
diff --git a/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java b/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java
index 22bf702..36425ed 100644
--- a/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java
+++ b/maven-surefire-common/src/test/java/org/apache/maven/surefire/JUnit4SuiteTest.java
@@ -25,6 +25,7 @@
 import junit.framework.TestSuite;
 import org.apache.maven.plugin.surefire.AbstractSurefireMojoJava7PlusTest;
 import org.apache.maven.plugin.surefire.AbstractSurefireMojoTest;
+import org.apache.maven.plugin.surefire.AbstractSurefireMojoToolchainsTest;
 import org.apache.maven.plugin.surefire.CommonReflectorTest;
 import org.apache.maven.plugin.surefire.MojoMocklessTest;
 import org.apache.maven.plugin.surefire.SurefireHelperTest;
@@ -97,6 +98,7 @@
         suite.addTest( new JUnit4TestAdapter( JarManifestForkConfigurationTest.class ) );
         suite.addTest( new JUnit4TestAdapter( ModularClasspathForkConfigurationTest.class ) );
         suite.addTest( new JUnit4TestAdapter( AbstractSurefireMojoJava7PlusTest.class ) );
+        suite.addTest( new JUnit4TestAdapter( AbstractSurefireMojoToolchainsTest.class ) );
         suite.addTest( new JUnit4TestAdapter( ScannerUtilTest.class ) );
         suite.addTest( new JUnit4TestAdapter( MojoMocklessTest.class ) );
         suite.addTest( new JUnit4TestAdapter( ForkClientTest.class ) );
diff --git a/maven-surefire-plugin/src/site/apt/examples/toolchains.apt.vm b/maven-surefire-plugin/src/site/apt/examples/toolchains.apt.vm
new file mode 100644
index 0000000..891f8b1
--- /dev/null
+++ b/maven-surefire-plugin/src/site/apt/examples/toolchains.apt.vm
@@ -0,0 +1,56 @@
+ ------
+ Using Maven Toolchains
+ ------
+ Akom <akom>
+ ------
+ 2020-04-17
+ ------
+
+~~ Licensed to the Apache Software Foundation (ASF) under one
+~~ or more contributor license agreements.  See the NOTICE file
+~~ distributed with this work for additional information
+~~ regarding copyright ownership.  The ASF licenses this file
+~~ to you under the Apache License, Version 2.0 (the
+~~ "License"); you may not use this file except in compliance
+~~ with the License.  You may obtain a copy of the License at
+~~
+~~   http://www.apache.org/licenses/LICENSE-2.0
+~~
+~~ Unless required by applicable law or agreed to in writing,
+~~ software distributed under the License is distributed on an
+~~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+~~ KIND, either express or implied.  See the License for the
+~~ specific language governing permissions and limitations
+~~ under the License.
+
+~~ NOTE: For help with the syntax of this file, see:
+~~ http://maven.apache.org/doxia/references/apt-format.html
+
+    For general information about Maven Toolchains, see
+    {{{https://maven.apache.org/guides/mini/guide-using-toolchains.html}Guide to Using Toolchains}}
+
+
+Using Maven Toolchains with ${thisPlugin}.
+
+    By default, if the pom configures the toolchains plugin as specified in the aforementioned
+    guide, ${thisPlugin} will launch the test jvm using the main toolchain
+    configured in Maven.
+
+    In some cases, it may be desirable to compile and test using different jvms.
+    While the <<<jvm>>> option can achieve this, it requires hardcoding system-specific paths.
+    Configuration option <<<jdkToolchain>>> can be used to supply an alternate toolchain specification.
+
+* Configuring a different jvm for running tests using toolchains
+
++---+
+<configuration>
+    [...]
+    <jdkToolchain>
+        <version>1.11</version>
+        <vendor>zulu</vendor>
+    </jdkToolchain>
+    [...]
+</configuration>
++---+
+
+         The above example assumes that your toolchains.xml contains a valid entry with these values.
diff --git a/maven-surefire-plugin/src/site/markdown/java9.md b/maven-surefire-plugin/src/site/markdown/java9.md
index 4ba2567..17157ab 100644
--- a/maven-surefire-plugin/src/site/markdown/java9.md
+++ b/maven-surefire-plugin/src/site/markdown/java9.md
@@ -125,3 +125,5 @@
     </plugin>
 
 Now you can run the build with tests on the top of Java 9.
+
+Also see the [full documentation for surefire toolchains](examples/toolchains.html) configuration options.
diff --git a/maven-surefire-plugin/src/site/site.xml b/maven-surefire-plugin/src/site/site.xml
index 2cec467..cbee436 100644
--- a/maven-surefire-plugin/src/site/site.xml
+++ b/maven-surefire-plugin/src/site/site.xml
@@ -62,6 +62,7 @@
       <item name="Shutdown of Forked JVM" href="examples/shutdown.html"/>
       <item name="Run tests with Java 9" href="java9.html"/>
       <item name="Run tests in Docker" href="docker.html"/>
+      <item name="Run tests in a different JVM using toolchains" href="examples/toolchains.html"/>
     </menu>
   </body>
 </project>