/*
 * 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.
 */
package org.apache.maven.plugin.surefire;

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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.zip.ZipFile;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.DefaultArtifact;
import org.apache.maven.artifact.handler.ArtifactHandler;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugin.surefire.booterclient.ChecksumCalculator;
import org.apache.maven.plugin.surefire.booterclient.ClasspathForkConfiguration;
import org.apache.maven.plugin.surefire.booterclient.ForkConfiguration;
import org.apache.maven.plugin.surefire.booterclient.ForkStarter;
import org.apache.maven.plugin.surefire.booterclient.JarManifestForkConfiguration;
import org.apache.maven.plugin.surefire.booterclient.ModularClasspathForkConfiguration;
import org.apache.maven.plugin.surefire.booterclient.Platform;
import org.apache.maven.plugin.surefire.extensions.LegacyForkNodeFactory;
import org.apache.maven.plugin.surefire.extensions.SurefireConsoleOutputReporter;
import org.apache.maven.plugin.surefire.extensions.SurefireStatelessReporter;
import org.apache.maven.plugin.surefire.extensions.SurefireStatelessTestsetInfoReporter;
import org.apache.maven.plugin.surefire.log.PluginConsoleLogger;
import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
import org.apache.maven.plugin.surefire.util.DependencyScanner;
import org.apache.maven.plugin.surefire.util.DirectoryScanner;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.artifact.filter.PatternIncludesArtifactFilter;
import org.apache.maven.surefire.api.booter.ProviderParameterNames;
import org.apache.maven.surefire.api.booter.Shutdown;
import org.apache.maven.surefire.api.cli.CommandLineOption;
import org.apache.maven.surefire.api.report.ReporterConfiguration;
import org.apache.maven.surefire.api.suite.RunResult;
import org.apache.maven.surefire.api.testset.DirectoryScannerParameters;
import org.apache.maven.surefire.api.testset.RunOrderParameters;
import org.apache.maven.surefire.api.testset.TestArtifactInfo;
import org.apache.maven.surefire.api.testset.TestListResolver;
import org.apache.maven.surefire.api.testset.TestRequest;
import org.apache.maven.surefire.api.testset.TestSetFailedException;
import org.apache.maven.surefire.api.util.DefaultScanResult;
import org.apache.maven.surefire.api.util.RunOrder;
import org.apache.maven.surefire.booter.ClassLoaderConfiguration;
import org.apache.maven.surefire.booter.Classpath;
import org.apache.maven.surefire.booter.ClasspathConfiguration;
import org.apache.maven.surefire.booter.KeyValueSource;
import org.apache.maven.surefire.booter.ModularClasspath;
import org.apache.maven.surefire.booter.ModularClasspathConfiguration;
import org.apache.maven.surefire.booter.ProcessCheckerType;
import org.apache.maven.surefire.booter.ProviderConfiguration;
import org.apache.maven.surefire.booter.StartupConfiguration;
import org.apache.maven.surefire.booter.SurefireBooterForkException;
import org.apache.maven.surefire.booter.SurefireExecutionException;
import org.apache.maven.surefire.extensions.ForkNodeFactory;
import org.apache.maven.surefire.providerapi.ConfigurableProviderInfo;
import org.apache.maven.surefire.providerapi.ProviderDetector;
import org.apache.maven.surefire.providerapi.ProviderInfo;
import org.apache.maven.surefire.providerapi.ProviderRequirements;
import org.apache.maven.surefire.shared.utils.io.FileUtils;
import org.apache.maven.toolchain.DefaultToolchain;
import org.apache.maven.toolchain.Toolchain;
import org.apache.maven.toolchain.ToolchainManager;
import org.apache.maven.toolchain.java.DefaultJavaToolChain;
import org.codehaus.plexus.languages.java.jpms.JavaModuleDescriptor;
import org.codehaus.plexus.languages.java.jpms.LocationManager;
import org.codehaus.plexus.languages.java.jpms.ResolvePathRequest;
import org.codehaus.plexus.languages.java.jpms.ResolvePathResult;
import org.codehaus.plexus.languages.java.jpms.ResolvePathsRequest;
import org.codehaus.plexus.languages.java.jpms.ResolvePathsResult;
import org.codehaus.plexus.logging.Logger;

import static java.lang.Integer.parseInt;
import static java.util.Arrays.asList;
import static java.util.Collections.addAll;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.apache.maven.plugin.surefire.AbstractSurefireMojo.PluginFailureReason.COULD_NOT_RUN_DEFAULT_TESTS;
import static org.apache.maven.plugin.surefire.AbstractSurefireMojo.PluginFailureReason.COULD_NOT_RUN_SPECIFIED_TESTS;
import static org.apache.maven.plugin.surefire.AbstractSurefireMojo.PluginFailureReason.NONE;
import static org.apache.maven.plugin.surefire.SurefireDependencyResolver.isWithinVersionSpec;
import static org.apache.maven.plugin.surefire.SurefireHelper.replaceThreadNumberPlaceholders;
import static org.apache.maven.plugin.surefire.util.DependencyScanner.filter;
import static org.apache.maven.surefire.api.booter.ProviderParameterNames.EXCLUDE_JUNIT5_ENGINES_PROP;
import static org.apache.maven.surefire.api.booter.ProviderParameterNames.INCLUDE_JUNIT5_ENGINES_PROP;
import static org.apache.maven.surefire.api.suite.RunResult.failure;
import static org.apache.maven.surefire.api.suite.RunResult.noTestsRun;
import static org.apache.maven.surefire.api.util.ReflectionUtils.invokeMethodWithArray;
import static org.apache.maven.surefire.api.util.ReflectionUtils.tryGetMethod;
import static org.apache.maven.surefire.booter.Classpath.emptyClasspath;
import static org.apache.maven.surefire.booter.SystemUtils.endsWithJavaPath;
import static org.apache.maven.surefire.booter.SystemUtils.isBuiltInJava9AtLeast;
import static org.apache.maven.surefire.booter.SystemUtils.isJava9AtLeast;
import static org.apache.maven.surefire.booter.SystemUtils.toJdkHomeFromJvmExec;
import static org.apache.maven.surefire.booter.SystemUtils.toJdkVersionFromReleaseFile;
import static org.apache.maven.surefire.shared.lang3.JavaVersion.JAVA_RECENT;
import static org.apache.maven.surefire.shared.lang3.StringUtils.substringBeforeLast;
import static org.apache.maven.surefire.shared.lang3.SystemUtils.IS_OS_WINDOWS;
import static org.apache.maven.surefire.shared.utils.StringUtils.capitalizeFirstLetter;
import static org.apache.maven.surefire.shared.utils.StringUtils.isEmpty;
import static org.apache.maven.surefire.shared.utils.StringUtils.isNotBlank;
import static org.apache.maven.surefire.shared.utils.StringUtils.isNotEmpty;
import static org.apache.maven.surefire.shared.utils.StringUtils.split;
import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.addShutDownHook;
import static org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils.removeShutdownHook;

/**
 * Abstract base class for running tests using Surefire.
 *
 * @author Stephen Connolly
 * @version $Id: SurefirePlugin.java 945065 2010-05-17 10:26:22Z stephenc $
 */
public abstract class AbstractSurefireMojo extends AbstractMojo implements SurefireExecutionParameters {
    private static final Map<String, String> JAVA_9_MATCHER_OLD_NOTATION = singletonMap("version", "[1.9,)");
    private static final Map<String, String> JAVA_9_MATCHER = singletonMap("version", "[9,)");
    private static final Platform PLATFORM = new Platform();

    private final ClasspathCache classpathCache = new ClasspathCache();

    /**
     * Note: use the legacy system property <em>disableXmlReport</em> set to {@code true} to disable the report.
     */
    @Parameter
    private SurefireStatelessReporter statelessTestsetReporter;

    @Parameter
    private SurefireConsoleOutputReporter consoleOutputReporter;

    @Parameter
    private SurefireStatelessTestsetInfoReporter statelessTestsetInfoReporter;

    /**
     * Information about this plugin, mainly used to lookup this plugin's configuration from the currently executing
     * project.
     *
     * @since 2.12
     */
    @Parameter(defaultValue = "${plugin}", readonly = true, required = true)
    private PluginDescriptor pluginDescriptor;

    /**
     * Set this to "true" to skip running tests, but still compile them. Its use is NOT RECOMMENDED, but quite
     * convenient on occasion.<br>
     * Failsafe plugin deprecated the parameter {@code skipTests} and the parameter will be removed in
     * <i>Failsafe 3.0.0</i> as it is a source of conflicts between Failsafe and Surefire plugin.
     *
     * @since 2.4
     */
    @Parameter(property = "skipTests", defaultValue = "false")
    protected boolean skipTests;

    /**
     * This old parameter is just like {@code skipTests}, but bound to the old property "maven.test.skip.exec".
     *
     * @since 2.3
     * @deprecated Use skipTests instead.
     */
    @Deprecated
    @Parameter(property = "maven.test.skip.exec")
    protected boolean skipExec;

    /**
     * Set this to "true" to bypass unit tests entirely. Its use is NOT RECOMMENDED, especially if you enable it using
     * the "maven.test.skip" property, because maven.test.skip disables both running the tests and compiling the tests.
     * Consider using the {@code skipTests} parameter instead.
     */
    @Parameter(property = "maven.test.skip", defaultValue = "false")
    protected boolean skip;

    /**
     * The Maven Project Object.
     */
    @Parameter(defaultValue = "${project}", required = true, readonly = true)
    private MavenProject project;

    /**
     * The base directory of the project being tested. This can be obtained in your integration test via
     * System.getProperty("basedir").
     */
    @Parameter(defaultValue = "${basedir}", readonly = true, required = true)
    protected File basedir;

    /**
     * The directory containing generated test classes of the project being tested. This will be included at the
     * beginning of the test classpath.
     */
    @Parameter(defaultValue = "${project.build.testOutputDirectory}")
    protected File testClassesDirectory;

    /**
     * List of dependencies to exclude from the test classpath at runtime.
     * Each item is passed as pattern to {@link PatternIncludesArtifactFilter}.
     * The pattern is matched against the following artifact ids:
     * <ul>
     * <li>{@code groupId:artifactId} (Short ID)</li>
     * <li>{@code groupId:artifactId:type:classifier} (Dependency Conflict ID)</li>
     * <li>{@code groupId:artifactId:type:classifier:version} (Full ID)</li>
     * </ul>
     * The matching algorithm is described in detail in <a href="https://maven.apache.org/plugins/maven-assembly-plugin/advanced-descriptor-topics.html#advanced-artifact-matching-in-includes-and-excludes">Advanced Artifact-Matching</a> for the maven-assembly-plugin. This parameter behaves the same as the {@code excludes} pattern described there.
     * The dependency matching is applied to the project dependency IDs (including transitive ones) <i>after resolving</i>, i.e. excluding
     * one dependency will not exclude its transitive dependencies!
     * @since 2.6
     */
    @Parameter(property = "maven.test.dependency.excludes")
    private String[] classpathDependencyExcludes;

    /**
     * A dependency scope to exclude from the test classpath at runtime. The scope should be one of the scopes defined by
     * org.apache.maven.artifact.Artifact. This includes the following:
     * <br>
     * <ul>
     * <li><i>compile</i> - system, provided, compile
     * <li><i>runtime</i> - compile, runtime
     * <li><i>compile+runtime</i> - system, provided, compile, runtime
     * <li><i>runtime+system</i> - system, compile, runtime
     * <li><i>test</i> - system, provided, compile, runtime, test
     * </ul>
     *
     * @since 2.6
     */
    @Parameter(defaultValue = "")
    private String classpathDependencyScopeExclude;

    /**
     * Additional elements to be appended to the test classpath at runtime.
     * Each element must be a file system path to a JAR file or a directory containing classes.
     * No wildcards are allowed here.
     *
     * @since 2.4
     */
    @Parameter(property = "maven.test.additionalClasspath")
    private String[] additionalClasspathElements;

    /**
     * Additional Maven dependencies to be added to the test classpath at runtime.
     * Each element supports the parametrization like documented in <a href="https://maven.apache.org/pom.html#dependencies">POM Reference: Dependencies</a>.
     * <p>
     * Those dependencies are automatically collected (i.e. have their full dependency tree calculated) and then all underlying artifacts are resolved from the repository (including their transitive dependencies).
     * Afterwards the resolved artifacts are filtered to only contain {@code compile} and {@code runtime} scoped ones and appended to the test classpath at runtime
     * (after the ones from {@link #additionalClasspathElements}).
     * <p>
     * The following differences to regular project dependency resolving apply:
     * <ul>
     * <li>The dependency management from the project is not taken into account.</li>
     * <li>Conflicts between the different items and the project dependencies are not resolved.</li>
     * <li>Only external dependencies (outside the current Maven reactor) are supported.</li>
     * </ul>
     *
     * @since 3.2
     */
    @Parameter(property = "maven.test.additionalClasspathDependencies")
    private List<Dependency> additionalClasspathDependencies;

    /**
     * The test source directory containing test class sources.
     * Important <b>only</b> for TestNG HTML reports.
     *
     * @since 2.2
     */
    @Parameter(defaultValue = "${project.build.testSourceDirectory}")
    private File testSourceDirectory;

    /**
     * List of System properties to pass to a provider.
     *
     * @deprecated Use systemPropertyVariables instead.
     */
    @Deprecated
    @Parameter
    private Properties systemProperties;

    /**
     * List of System properties to pass to a provider.
     *
     * @since 2.5
     */
    @Parameter
    private Map<String, String> systemPropertyVariables;

    /**
     * List of properties for configuring all TestNG related configurations. This is the new preferred method of
     * configuring TestNG.
     *
     * @since 2.4
     */
    @Parameter
    private Properties properties;

    /**
     * Map of plugin artifacts.
     */
    @Parameter(property = "plugin.artifactMap", required = true, readonly = true)
    private Map<String, Artifact> pluginArtifactMap;

    /**
     * Map of project artifacts.
     */
    @Parameter(property = "project.artifactMap", readonly = true, required = true)
    private Map<String, Artifact> projectArtifactMap;

    /**
     * Add custom text into report filename: TEST-testClassName-reportNameSuffix.xml,
     * testClassName-reportNameSuffix.txt and testClassName-reportNameSuffix-output.txt.
     * File TEST-testClassName-reportNameSuffix.xml has changed attributes 'testsuite'--'name'
     * and 'testcase'--'classname' - reportNameSuffix is added to the attribute value.
     */
    @Parameter(property = "surefire.reportNameSuffix", defaultValue = "")
    private String reportNameSuffix;

    /**
     * Set this to "true" to redirect the unit test standard output to a file (found in
     * reportsDirectory/testName-output.txt).
     *
     * @since 2.3
     */
    @Parameter(property = "maven.test.redirectTestOutputToFile", defaultValue = "false")
    private boolean redirectTestOutputToFile;

    /**
     * Set this to "true" to cause a failure if there are no tests to run. Defaults to "false".
     *
     * @since 2.4
     */
    @Parameter(property = "failIfNoTests", defaultValue = "false")
    private boolean failIfNoTests;

    /**
     * Relative path to <i>temporary-surefire-boot</i> directory containing internal Surefire temporary files.
     * <br>
     * The <i>temporary-surefire-boot</i> directory is <i>project.build.directory</i> on most platforms or
     * <i>system default temporary-directory</i> specified by the system property {@code java.io.tmpdir}
     * on Windows (see <a href="https://issues.apache.org/jira/browse/SUREFIRE-1400">SUREFIRE-1400</a>).
     * <br>
     * It is deleted after the test set has completed.
     *
     * @since 2.20
     */
    @Parameter(property = "tempDir", defaultValue = "surefire")
    private String tempDir;

    /**
     * Option to specify the jvm (or path to the java executable) to use with the forking options. For the default, the
     * jvm will be a new instance of the same VM as the one used to run Maven. JVM settings are not inherited from
     * MAVEN_OPTS.
     *
     * @since 2.1
     */
    @Parameter(property = "jvm")
    private String jvm;

    /**
     * Arbitrary JVM options to set on the command line.
     * <br>
     * <br>
     * Since the Version 2.17 using an alternate syntax for {@code argLine}, <b>@{...}</b> allows late replacement
     * of properties when the plugin is executed, so properties that have been modified by other plugins will be picked
     * up correctly.
     * See the Frequently Asked Questions page with more details:<br>
     * <a href="http://maven.apache.org/surefire/maven-surefire-plugin/faq.html">
     *     http://maven.apache.org/surefire/maven-surefire-plugin/faq.html</a>
     * <br>
     * <a href="http://maven.apache.org/surefire/maven-failsafe-plugin/faq.html">
     *     http://maven.apache.org/surefire/maven-failsafe-plugin/faq.html</a>
     *
     * @since 2.1
     */
    @Parameter(property = "argLine")
    private String argLine;

    /**
     * Additional environment variables to set on the command line.
     *
     * @since 2.1.3
     */
    @Parameter
    private Map<String, String> environmentVariables = new HashMap<>();

    /**
     * Command line working directory.
     *
     * @since 2.1.3
     */
    @Parameter(property = "basedir")
    private File workingDirectory;

    /**
     * When false it makes tests run using the standard classloader delegation instead of the default Maven isolated
     * classloader. Only used when forking ({@code forkCount} is greater than zero).<br>
     * Setting it to false helps with some problems caused by conflicts between xml parsers in the classpath and the
     * Java 5 provider parser.
     *
     * @since 2.1
     */
    @Parameter(property = "childDelegation", defaultValue = "false")
    private boolean childDelegation;

    /**
     * (TestNG/JUnit47 provider with JUnit4.8+ only and JUnit5+ provider since 2.22.0) Groups/categories/tags for this
     * test. Only classes/methods/etc decorated with one of the groups/categories/tags specified here will be included
     * in test run, if specified.<br>
     * For JUnit4 tests, this parameter forces the use of the 4.7 provider. For JUnit5 tests, this parameter forces
     * the use of the JUnit platform provider.<br>
     * This parameter is ignored if the {@code suiteXmlFiles} parameter is specified.<br>
     * Since version 2.18.1 and JUnit 4.12, the {@code @Category} annotation type is automatically inherited from
     * superclasses, see {@code @java.lang.annotation.Inherited}. Make sure that test class inheritance still makes
     * sense together with {@code @Category} annotation of the JUnit 4.12 or higher appeared in superclass.
     *
     * @since 2.2
     */
    @Parameter(property = "groups")
    private String groups;

    /**
     * (TestNG/JUnit47 provider with JUnit4.8+ only and JUnit5+ provider since 2.22.0) Excluded groups/categories/tags.
     * Any methods/classes/etc with one of the groups/categories/tags specified in this list will specifically not be
     * run.<br>
     * For JUnit4, this parameter forces the use of the 4.7 provider. For JUnit5, this parameter forces the use of the
     * JUnit platform provider.<br>
     * This parameter is ignored if the {@code suiteXmlFiles} parameter is specified.<br>
     * Since version 2.18.1 and JUnit 4.12, the {@code @Category} annotation type is automatically inherited from
     * superclasses, see {@code @java.lang.annotation.Inherited}. Make sure that test class inheritance still makes
     * sense together with {@code @Category} annotation of the JUnit 4.12 or higher appeared in superclass.
     *
     * @since 2.2
     */
    @Parameter(property = "excludedGroups")
    private String excludedGroups;

    /**
     * Allows you to specify the name of the JUnit artifact. If not set, {@code junit:junit} will be used.
     *
     * @since 2.3.1
     */
    @Parameter(property = "junitArtifactName", defaultValue = "junit:junit")
    private String junitArtifactName;

    /**
     * Allows you to specify the name of the TestNG artifact. If not set, {@code org.testng:testng} will be used.
     *
     * @since 2.3.1
     */
    @Parameter(property = "testNGArtifactName", defaultValue = "org.testng:testng")
    private String testNGArtifactName;

    /**
     * (TestNG/JUnit 4.7 provider) The attribute thread-count allows you to specify how many threads should be
     * allocated for this execution. Only makes sense to use in conjunction with the {@code parallel} parameter.
     *
     * @since 2.2
     */
    @Parameter(property = "threadCount")
    private int threadCount;

    /**
     * Option to specify the number of VMs to fork in parallel in order to execute the tests. When terminated with "C",
     * the number part is multiplied with the number of CPU cores. Floating point value are only accepted together with
     * "C". If set to "0", no VM is forked and all tests are executed within the main process.<br>
     * <br>
     * Example values: "1.5C", "4"<br>
     * <br>
     * The system properties and the {@code argLine} of the forked processes may contain the place holder string
     * <code>${surefire.forkNumber}</code>, which is replaced with a fixed number for each of the parallel forks,
     * ranging from <b>1</b> to the effective value of {@code forkCount} times the maximum number of parallel
     * Surefire executions in maven parallel builds, i.e. the effective value of the <b>-T</b> command line
     * argument of maven core.
     *
     * @since 2.14
     */
    @Parameter(property = "forkCount", defaultValue = "1")
    private String forkCount;

    /**
     * Indicates if forked VMs can be reused. If set to "false", a new VM is forked for each test class to be executed.
     * If set to "true", up to {@code forkCount} VMs will be forked and then reused to execute all tests.
     *
     * @since 2.13
     */
    @Parameter(property = "reuseForks", defaultValue = "true")
    private boolean reuseForks;

    /**
     * (JUnit 4.7 provider) Indicates that threadCount, threadCountSuites, threadCountClasses, threadCountMethods
     * are per cpu core.
     *
     * @since 2.5
     */
    @Parameter(property = "perCoreThreadCount", defaultValue = "true")
    private boolean perCoreThreadCount;

    /**
     * (JUnit 4.7 provider) Indicates that the thread pool will be unlimited. The {@code parallel} parameter and
     * the actual number of classes/methods will decide. Setting this to "true" effectively disables
     * {@code perCoreThreadCount} and {@code threadCount}. Defaults to "false".
     *
     * @since 2.5
     */
    @Parameter(property = "useUnlimitedThreads", defaultValue = "false")
    private boolean useUnlimitedThreads;

    /**
     * (TestNG provider) When you use the parameter {@code parallel}, TestNG will try to run all your test methods
     * in separate threads, except for methods that depend on each other, which will be run in the same thread in order
     * to respect their order of execution.  Supports two values: {@code classes} or {@code methods}.
     * <br>
     * (JUnit 4.7 provider) Supports values {@code classes}, {@code methods}, {@code both} to run
     * in separate threads been controlled by {@code threadCount}.
     * <br>
     * <br>
     * Since version 2.16 (JUnit 4.7 provider), the value {@code both} is <strong>DEPRECATED</strong>.
     * Use {@code classesAndMethods} instead.
     * <br>
     * <br>
     * Since version 2.16 (JUnit 4.7 provider), additional vales are available:
     * <br>
     * {@code suites}, {@code suitesAndClasses}, {@code suitesAndMethods}, {@code classesAndMethods}, {@code all}.
     * <br>
     * By default, Surefire does not execute tests in parallel. You can set the parameter {@code parallel} to
     * {@code none} to explicitly disable parallel execution (e.g. when disabling parallel execution in special Maven
     * profiles when executing coverage analysis).
     *
     * @since 2.2
     */
    @Parameter(property = "parallel")
    private String parallel;

    /**
     * (JUnit 4.7 / provider only) The thread counts do not exceed the number of parallel suite, class runners and
     * average number of methods per class if set to <strong>true</strong>.
     * <br>
     * True by default.
     *
     * @since 2.17
     */
    @Parameter(property = "parallelOptimized", defaultValue = "true")
    private boolean parallelOptimized;

    /**
     * (JUnit 4.7 provider) This attribute allows you to specify the concurrency in test suites, i.e.:
     * <ul>
     *  <li>number of concurrent suites if {@code threadCount} is 0 or unspecified</li>
     *  <li>limited suites concurrency if {@code useUnlimitedThreads} is set to <strong>true</strong></li>
     *  <li>if {@code threadCount} and certain thread-count parameters are &gt; 0 for {@code parallel}, the
     *  concurrency is computed from ratio. For instance {@code parallel=all} and the ratio between
     *      {@code threadCountSuites}:{@code threadCountClasses}:{@code threadCountMethods} is
     *      <b>2</b>:3:5, there is 20% of {@code threadCount} which appeared in concurrent suites.</li>
     * </ul>
     *
     * Only makes sense to use in conjunction with the {@code parallel} parameter.
     * The default value <b>0</b> behaves same as unspecified one.
     *
     * @since 2.16
     */
    @Parameter(property = "threadCountSuites", defaultValue = "0")
    private int threadCountSuites;

    /**
     * (JUnit 4.7 provider) This attribute allows you to specify the concurrency in test classes, i.e.:
     * <ul>
     *  <li>number of concurrent classes if {@code threadCount} is 0 or unspecified</li>
     *  <li>limited classes concurrency if {@code useUnlimitedThreads} is set to <strong>true</strong></li>
     *  <li>if {@code threadCount} and certain thread-count parameters are &gt; 0 for {@code parallel}, the
     *  concurrency is computed from ratio. For instance {@code parallel=all} and the ratio between
     *      {@code threadCountSuites}:{@code threadCountClasses}:{@code threadCountMethods} is
     *      2:<b>3</b>:5, there is 30% of {@code threadCount} in concurrent classes.</li>
     *  <li>as in the previous case but without this leaf thread-count. Example: {@code parallel=suitesAndClasses},
     *  {@code threadCount=16}, {@code threadCountSuites=5}, {@code threadCountClasses} is unspecified leaf, the number
     *  of concurrent classes is varying from &gt;= 11 to 14 or 15. The {@code threadCountSuites} become
     *  given number of threads.</li>
     * </ul>
     *
     * Only makes sense to use in conjunction with the {@code parallel} parameter.
     * The default value <b>0</b> behaves same as unspecified one.
     *
     * @since 2.16
     */
    @Parameter(property = "threadCountClasses", defaultValue = "0")
    private int threadCountClasses;

    /**
     * (JUnit 4.7 provider) This attribute allows you to specify the concurrency in test methods, i.e.:
     * <ul>
     * <li>number of concurrent methods if {@code threadCount} is 0 or unspecified</li>
     * <li>limited concurrency of methods if {@code useUnlimitedThreads} is set to <strong>true</strong></li>
     * <li>if {@code threadCount} and certain thread-count parameters are &gt; 0 for {@code parallel}, the
     * concurrency is computed from ratio. For instance parallel=all and the ratio between
     * {@code threadCountSuites}:{@code threadCountClasses}:{@code threadCountMethods} is 2:3:<b>5</b>,
     * there is 50% of {@code threadCount} which appears in concurrent methods.</li>
     * <li>as in the previous case but without this leaf thread-count. Example: {@code parallel=all},
     * {@code threadCount=16}, {@code threadCountSuites=2}, {@code threadCountClasses=3}, but {@code threadCountMethods}
     * is unspecified leaf, the number of concurrent methods is varying from &gt;= 11 to 14 or 15.
     * The {@code threadCountSuites} and {@code threadCountClasses} become given number of threads.</li>
     * </ul>
     * Only makes sense to use in conjunction with the {@code parallel} parameter. The default value <b>0</b>
     * behaves same as unspecified one.
     *
     * @since 2.16
     */
    @Parameter(property = "threadCountMethods", defaultValue = "0")
    private int threadCountMethods;

    /**
     * Whether to trim the stack trace in the reports to just the lines within the test, or show the full trace.
     *
     * @since 2.2
     */
    @Parameter(property = "trimStackTrace", defaultValue = "false")
    private boolean trimStackTrace;

    /**
     * Flag to disable the generation of report files in xml format.
     * Deprecated since 3.0.0-M4.
     * Instead use <em>disable</em> within {@code statelessTestsetReporter} since of 3.0.0-M6.
     * @since 2.2
     */
    @Deprecated // todo make readonly to handle system property
    @Parameter(property = "disableXmlReport", defaultValue = "false")
    private boolean disableXmlReport;

    /**
     * By default, Surefire enables JVM assertions for the execution of your test cases. To disable the assertions, set
     * this flag to "false".
     *
     * @since 2.3.1
     */
    @Parameter(property = "enableAssertions", defaultValue = "true")
    private boolean enableAssertions;

    /**
     * The current build session instance.
     */
    @Parameter(defaultValue = "${session}", required = true, readonly = true)
    private MavenSession session;

    @Component
    private Logger logger;

    /**
     * (TestNG only) Define the factory class used to create all test instances.
     *
     * @since 2.5
     */
    @Parameter(property = "objectFactory")
    private String objectFactory;

    /**
     * Parallel Maven Execution.
     */
    @Parameter(defaultValue = "${session.parallel}", readonly = true)
    private Boolean parallelMavenExecution;

    /**
     * Read-only parameter with value of Maven property <i>project.build.directory</i>.
     * @since 2.20
     */
    @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true)
    private File projectBuildDirectory;

    /**
     * List of dependencies to scan for test classes to include in the test run.
     * The child elements of this element must be &lt;dependency&gt; elements, and the
     * contents of each of these elements must be a string which follows the general form:
     *
     * <p>{@code groupId[:artifactId[:type[:classifier][:version]]]}</p>
     *
     * <p>The wildcard character <code>*</code> can be used within the sub parts of those composite identifiers to
     * do glob-like pattern matching. The classifier may be omitted when matching dependencies without a classifier.</p>
     *
     * <p>Examples:</p>
     *
     * <ul>
     *     <li>{@code group} or, equivalently, {@code group:*}</li>
     *     <li>{@code g*p:*rtifac*}</li>
     *     <li>{@code group:*:jar}</li>
     *     <li>{@code group:artifact:*:1.0.0} (no classifier)</li>
     *     <li>{@code group:*:test-jar:tests}</li>
     *     <li>{@code *:artifact:*:*:1.0.0}</li>
     * </ul>
     *
     * <p>Since version 2.22.0 you can scan for test classes from a project
     * dependency of your multi-module project.</p>
     *
     * <p>In versions before 3.0.0-M4, only <code>groupId:artifactId</code> is supported.</p>
     *
     * @since 2.15
     */
    @Parameter(property = "dependenciesToScan")
    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
    private ToolchainManager toolchainManager;

    @Component
    private LocationManager locationManager;

    @Component
    private ProviderDetector providerDetector;

    private Toolchain toolchain;

    private int effectiveForkCount = -1;

    protected abstract String getPluginName();

    protected abstract int getRerunFailingTestsCount();

    @Override
    public abstract List<String> getIncludes();

    public abstract File getIncludesFile();

    @Override
    public abstract void setIncludes(List<String> includes);

    public abstract File getExcludesFile();

    /**
     * Calls {@link #getSuiteXmlFiles()} as {@link List list}.
     * Never returns <code>null</code>.
     *
     * @return list of TestNG suite XML files provided by MOJO
     */
    protected abstract List<File> suiteXmlFiles();

    /**
     * @return {@code true} if {@link #getSuiteXmlFiles() suite-xml files array} is not empty.
     */
    protected abstract boolean hasSuiteXmlFiles();

    protected abstract String[] getExcludedEnvironmentVariables();

    public abstract File[] getSuiteXmlFiles();

    public abstract void setSuiteXmlFiles(File[] suiteXmlFiles);

    public abstract String getRunOrder();

    public abstract void setRunOrder(String runOrder);

    public abstract Long getRunOrderRandomSeed();

    public abstract void setRunOrderRandomSeed(Long runOrderRandomSeed);

    protected abstract void handleSummary(RunResult summary, Exception firstForkException)
            throws MojoExecutionException, MojoFailureException;

    protected abstract boolean isSkipExecution();

    protected abstract String[] getDefaultIncludes();

    protected abstract String getReportSchemaLocation();

    protected abstract boolean useModulePath();

    protected abstract void setUseModulePath(boolean useModulePath);

    protected abstract String getEnableProcessChecker();

    protected abstract ForkNodeFactory getForkNode();

    /**
     * This plugin MOJO artifact.
     *
     * @return non-null plugin artifact
     */
    protected Artifact getMojoArtifact() {
        return getPluginDescriptor().getPluginArtifact();
    }

    private String getDefaultExcludes() {
        return "**/*$*";
    }

    @Component(role = SurefireDependencyResolver.class)
    private SurefireDependencyResolver surefireDependencyResolver;

    private TestListResolver specificTests;

    private TestListResolver includedExcludedTests;

    private List<CommandLineOption> cli;

    private volatile PluginConsoleLogger consoleLogger;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        cli = commandLineOptions();
        // Stuff that should have been final
        setupStuff();
        Platform platform = PLATFORM.withJdkExecAttributesForTests(getEffectiveJvm());
        Thread shutdownThread = new Thread(platform::setShutdownState);
        addShutDownHook(shutdownThread);
        try {
            if (verifyParameters() && !hasExecutedBefore()) {
                DefaultScanResult scan = scanForTestClasses();
                if (!hasSuiteXmlFiles() && scan.isEmpty()) {
                    switch (getEffectiveFailIfNoTests()) {
                        case COULD_NOT_RUN_DEFAULT_TESTS:
                            throw new MojoFailureException(
                                    "No tests were executed!  (Set -DfailIfNoTests=false to ignore this error.)");
                        case COULD_NOT_RUN_SPECIFIED_TESTS:
                            throw new MojoFailureException("No tests matching pattern \""
                                    + getSpecificTests().toString()
                                    + "\" were executed! (Set "
                                    + "-D" + getPluginName()
                                    + ".failIfNoSpecifiedTests=false to ignore this error.)");
                        default:
                            handleSummary(noTestsRun(), null);
                            return;
                    }
                }
                logReportsDirectory();
                executeAfterPreconditionsChecked(scan, platform);
            }
        } finally {
            platform.clearShutdownState();
            removeShutdownHook(shutdownThread);
        }
    }

    void setLogger(Logger logger) {
        this.logger = logger;
    }

    void setSurefireDependencyResolver(SurefireDependencyResolver surefireDependencyResolver) {
        this.surefireDependencyResolver = surefireDependencyResolver;
    }

    @Nonnull
    protected final PluginConsoleLogger getConsoleLogger() {
        if (consoleLogger == null) {
            synchronized (this) {
                if (consoleLogger == null) {
                    consoleLogger = new PluginConsoleLogger(logger);
                }
            }
        }
        return consoleLogger;
    }

    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 =
                    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 {

        if (getBooterArtifact() == null) {
            throw new RuntimeException("Unable to locate surefire-booter in the list of plugin artifacts");
        }

        if (getToolchainManager() != null) {
            toolchain = getToolchain();
        }
    }

    @Nonnull
    private DefaultScanResult scanForTestClasses() throws MojoFailureException {
        DefaultScanResult scan = scanDirectories();
        DefaultScanResult scanDeps = scanDependencies();
        return scan.append(scanDeps);
    }

    private DefaultScanResult scanDirectories() throws MojoFailureException {
        DirectoryScanner scanner = new DirectoryScanner(getTestClassesDirectory(), getIncludedAndExcludedTests());
        return scanner.scan();
    }

    List<Artifact> getProjectTestArtifacts() {
        return project.getTestArtifacts();
    }

    DefaultScanResult scanDependencies() throws MojoFailureException {
        if (getDependenciesToScan() == null) {
            return null;
        } else {
            try {
                DefaultScanResult result = null;

                List<Artifact> dependenciesToScan = filter(getProjectTestArtifacts(), asList(getDependenciesToScan()));

                for (Artifact artifact : dependenciesToScan) {
                    String type = artifact.getType();
                    File out = artifact.getFile();
                    if (out == null
                            || !out.exists()
                            || !("jar".equals(type)
                                    || out.isDirectory()
                                    || out.getName().endsWith(".jar"))) {
                        continue;
                    }

                    if (out.isFile()) {
                        DependencyScanner scanner =
                                new DependencyScanner(singletonList(out), getIncludedAndExcludedTests());
                        result = result == null ? scanner.scan() : result.append(scanner.scan());
                    } else if (out.isDirectory()) {
                        DirectoryScanner scanner = new DirectoryScanner(out, getIncludedAndExcludedTests());
                        result = result == null ? scanner.scan() : result.append(scanner.scan());
                    }
                }

                return result;
            } catch (Exception e) {
                throw new MojoFailureException(e.getLocalizedMessage(), e);
            }
        }
    }

    boolean verifyParameters() throws MojoFailureException, MojoExecutionException {
        setProperties(new SurefireProperties(getProperties()));
        if (isSkipExecution()) {
            getConsoleLogger().info("Tests are skipped.");
            return false;
        }

        String jvmToUse = getJvm();
        if (toolchain != null) {
            getConsoleLogger().info("Toolchain in maven-" + getPluginName() + "-plugin: " + toolchain);
            if (jvmToUse != null) {
                getConsoleLogger().warning("Toolchains are ignored, 'jvm' parameter is set to " + jvmToUse);
            }
        }

        if (!getTestClassesDirectory().exists()
                && (getDependenciesToScan() == null || getDependenciesToScan().length == 0)) {
            if (getFailIfNoTests()) {
                throw new MojoFailureException("No tests to run!");
            }
            getConsoleLogger().info("No tests to run.");
        } else {
            ensureEnableProcessChecker();
            ensureWorkingDirectoryExists();
            ensureParallelRunningCompatibility();
            warnIfUselessUseSystemClassLoaderParameter();
            warnIfDefunctGroupsCombinations();
            warnIfRerunClashes();
            warnIfWrongShutdownValue();
            warnIfNotApplicableSkipAfterFailureCount();
            warnIfIllegalTempDir();
            warnIfForkCountIsZero();
            warnIfIllegalFailOnFlakeCount();
            printDefaultSeedIfNecessary();
        }
        return true;
    }

    private void warnIfForkCountIsZero() {
        if ("0".equals(getForkCount())) {
            getConsoleLogger()
                    .warning("The parameter forkCount should likely not be 0. Forking a JVM for tests "
                            + "improves test accuracy. Ensure to have a <forkCount> >= 1.");
        }
    }

    private void executeAfterPreconditionsChecked(@Nonnull DefaultScanResult scanResult, @Nonnull Platform platform)
            throws MojoExecutionException, MojoFailureException {
        TestClassPath testClasspath = generateTestClasspath();
        List<ProviderInfo> providers = createProviders(testClasspath);
        ResolvePathResultWrapper wrapper =
                findModuleDescriptor(platform.getJdkExecAttributesForTests().getJdkHome());

        RunResult current = noTestsRun();

        Exception firstForkException = null;
        for (ProviderInfo provider : providers) {
            try {
                current = current.aggregate(executeProvider(provider, scanResult, testClasspath, platform, wrapper));
            } catch (SurefireBooterForkException | SurefireExecutionException | TestSetFailedException e) {
                if (firstForkException == null) {
                    firstForkException = e;
                }
            }
        }

        if (firstForkException != null) {
            current = failure(current, firstForkException);
        }

        handleSummary(current, firstForkException);
    }

    protected List<ProviderInfo> createProviders(TestClassPath testClasspath) throws MojoExecutionException {
        Artifact junitDepArtifact = getJunitDepArtifact();
        return providerDetector.resolve(
                new DynamicProviderInfo(null),
                new JUnitPlatformProviderInfo(getJUnitPlatformRunnerArtifact(), getJUnit5Artifact(), testClasspath),
                new TestNgProviderInfo(getTestNgArtifact()),
                new JUnitCoreProviderInfo(getJunitArtifact(), junitDepArtifact),
                new JUnit4ProviderInfo(getJunitArtifact(), junitDepArtifact),
                new JUnit3ProviderInfo());
    }

    private SurefireProperties setupProperties() {
        SurefireProperties sysProps = null;
        try {
            sysProps = SurefireProperties.loadProperties(getSystemPropertiesFile());
        } catch (IOException e) {
            String msg = "The file '" + getSystemPropertiesFile().getAbsolutePath() + "' can't be read.";
            if (getConsoleLogger().isDebugEnabled()) {
                getConsoleLogger().debug(msg, e);
            } else {
                getConsoleLogger().warning(msg);
            }
        }

        SurefireProperties result = SurefireProperties.calculateEffectiveProperties(
                getSystemProperties(), getSystemPropertyVariables(), getUserProperties(), sysProps);

        result.setProperty("basedir", getBasedir().getAbsolutePath());
        result.setProperty("localRepository", getLocalRepositoryPath());
        if (isForking()) {
            for (Object o : result.propertiesThatCannotBeSetASystemProperties()) {
                if (getArgLine() == null || !getArgLine().contains("-D" + o + "=")) {
                    getConsoleLogger()
                            .warning(o + " cannot be set as system property, use <argLine>-D" + o
                                    + "=...</argLine> instead");
                }
            }
            for (Object systemPropertyMatchingArgLine : systemPropertiesMatchingArgLine(result)) {
                getConsoleLogger()
                        .warning("The system property "
                                + systemPropertyMatchingArgLine
                                + " is configured twice! "
                                + "The property appears in <argLine/> and any of <systemPropertyVariables/>, "
                                + "<systemProperties/> or user property.");
            }
        } else {
            result.setProperty("user.dir", getWorkingDirectory().getAbsolutePath());
        }

        if (getConsoleLogger().isDebugEnabled()) {
            showToLog(result, getConsoleLogger());
        }

        return result;
    }

    private Set<Object> systemPropertiesMatchingArgLine(SurefireProperties result) {
        Set<Object> intersection = new HashSet<>();
        if (isNotBlank(getArgLine())) {
            for (Object systemProperty : result.getStringKeySet()) {
                if (getArgLine().contains("-D" + systemProperty + "=")) {
                    intersection.add(systemProperty);
                }
            }

            Set<Object> ignored = result.propertiesThatCannotBeSetASystemProperties();
            intersection.removeAll(ignored);
        }
        return intersection;
    }

    private void showToLog(SurefireProperties props, ConsoleLogger log) {
        for (Object key : props.getStringKeySet()) {
            String value = props.getProperty((String) key);
            log.debug("Setting system property [" + key + "]=[" + value + "]");
        }
    }

    @Nonnull
    private RunResult executeProvider(
            @Nonnull ProviderInfo provider,
            @Nonnull DefaultScanResult scanResult,
            @Nonnull TestClassPath testClasspathWrapper,
            @Nonnull Platform platform,
            @Nonnull ResolvePathResultWrapper resolvedJavaModularityResult)
            throws MojoExecutionException, MojoFailureException, SurefireExecutionException,
                    SurefireBooterForkException, TestSetFailedException {
        getConsoleLogger().debug("Using the provider " + provider.getProviderName());
        SurefireProperties effectiveProperties = setupProperties();
        ClassLoaderConfiguration classLoaderConfiguration = getClassLoaderConfiguration();
        provider.addProviderProperties();
        RunOrderParameters runOrderParameters =
                new RunOrderParameters(getRunOrder(), getStatisticsFile(getConfigChecksum()), getRunOrderRandomSeed());

        if (isNotForking()) {
            Properties originalSystemProperties =
                    (Properties) System.getProperties().clone();
            try {
                createCopyAndReplaceForkNumPlaceholder(effectiveProperties, 1).copyToSystemProperties();
                getConsoleLogger().debug("Using in-process starter");
                InPluginVMSurefireStarter surefireStarter = createInprocessStarter(
                        provider,
                        classLoaderConfiguration,
                        runOrderParameters,
                        scanResult,
                        platform,
                        testClasspathWrapper);
                return surefireStarter.runSuitesInProcess(scanResult);
            } finally {
                System.setProperties(originalSystemProperties);
            }
        } else {
            ForkConfiguration forkConfiguration = createForkConfiguration(platform, resolvedJavaModularityResult);
            if (getConsoleLogger().isDebugEnabled()) {
                getConsoleLogger()
                        .debug("Using fork starter with configuration implementation "
                                + forkConfiguration.getClass().getName());
                showMap(getEnvironmentVariables(), "environment variable");
                showArray(getExcludedEnvironmentVariables(), "excluded environment variable");
            }

            Properties originalSystemProperties =
                    (Properties) System.getProperties().clone();
            ForkStarter forkStarter = null;
            try {
                forkStarter = createForkStarter(
                        provider,
                        forkConfiguration,
                        classLoaderConfiguration,
                        runOrderParameters,
                        getConsoleLogger(),
                        scanResult,
                        testClasspathWrapper,
                        platform,
                        resolvedJavaModularityResult);

                return forkStarter.run(effectiveProperties, scanResult);
            } catch (SurefireBooterForkException e) {
                forkStarter.killOrphanForks();
                throw e;
            } finally {
                System.setProperties(originalSystemProperties);
                cleanupForkConfiguration(forkConfiguration);
            }
        }
    }

    public static SurefireProperties createCopyAndReplaceForkNumPlaceholder(
            SurefireProperties effectiveSystemProperties, int threadNumber) {
        SurefireProperties filteredProperties = new SurefireProperties((KeyValueSource) effectiveSystemProperties);
        for (Entry<Object, Object> entry : effectiveSystemProperties.entrySet()) {
            if (entry.getValue() instanceof String) {
                String value = (String) entry.getValue();
                filteredProperties.put(entry.getKey(), replaceThreadNumberPlaceholders(value, threadNumber));
            }
        }
        return filteredProperties;
    }

    protected void cleanupForkConfiguration(ForkConfiguration forkConfiguration) {
        if (!getConsoleLogger().isDebugEnabled() && forkConfiguration != null) {
            File tempDirectory = forkConfiguration.getTempDirectory();
            try {
                FileUtils.deleteDirectory(tempDirectory);
            } catch (IOException e) {
                getConsoleLogger()
                        .warning("Could not delete temp directory " + tempDirectory + " because " + e.getMessage());
            }
        }
    }

    protected void logReportsDirectory() {
        logDebugOrCliShowErrors(capitalizeFirstLetter(getPluginName()) + " report directory: " + getReportsDirectory());
    }

    private boolean existsModuleDescriptor(ResolvePathResultWrapper resolvedJavaModularityResult) {
        return resolvedJavaModularityResult.getResolvePathResult() != null;
    }

    private ResolvePathResultWrapper findModuleDescriptor(File jdkHome) {
        ResolvePathResultWrapper test = findModuleDescriptor(jdkHome, getTestClassesDirectory(), false);
        return test.getResolvePathResult() == null ? findModuleDescriptor(jdkHome, getMainBuildPath(), true) : test;
    }

    private ResolvePathResultWrapper findModuleDescriptor(File jdkHome, File buildPath, boolean isMainDescriptor) {
        boolean isJpmsModule =
                buildPath.isDirectory() ? new File(buildPath, "module-info.class").exists() : isModule(buildPath);

        if (!isJpmsModule) {
            return new ResolvePathResultWrapper(null, isMainDescriptor);
        }

        try {
            ResolvePathRequest<?> request = ResolvePathRequest.ofFile(buildPath).setJdkHome(jdkHome);
            ResolvePathResult result = getLocationManager().resolvePath(request);
            boolean isEmpty = result.getModuleNameSource() == null;
            return new ResolvePathResultWrapper(isEmpty ? null : result, isMainDescriptor);
        } catch (Exception e) {
            return new ResolvePathResultWrapper(null, isMainDescriptor);
        }
    }

    private static boolean isModule(File jar) {
        try (ZipFile zip = new ZipFile(jar)) {
            return zip.getEntry("module-info.class") != null;
        } catch (IOException e) {
            return false;
        }
    }

    private boolean canExecuteProviderWithModularPath(
            @Nonnull Platform platform, @Nonnull ResolvePathResultWrapper resolvedJavaModularityResult) {
        return useModulePath()
                && platform.getJdkExecAttributesForTests().isJava9AtLeast()
                && existsModuleDescriptor(resolvedJavaModularityResult);
    }

    /**
     * Converts old TestNG configuration parameters over to new properties based configuration
     * method. (if any are defined the old way)
     */
    private void convertTestNGParameters() throws MojoExecutionException {
        if (this.getParallel() != null) {
            getProperties().setProperty(ProviderParameterNames.PARALLEL_PROP, this.getParallel());
        }
        convertGroupParameters();

        if (this.getThreadCount() > 0) {
            getProperties()
                    .setProperty(ProviderParameterNames.THREADCOUNT_PROP, Integer.toString(this.getThreadCount()));
        }
        if (this.getObjectFactory() != null) {
            getProperties().setProperty("objectfactory", this.getObjectFactory());
        }
        if (this.getTestClassesDirectory() != null) {
            getProperties()
                    .setProperty(
                            "testng.test.classpath", getTestClassesDirectory().getAbsolutePath());
        }

        Artifact testNgArtifact = getTestNgArtifact();
        if (testNgArtifact != null) {
            DefaultArtifactVersion defaultArtifactVersion = new DefaultArtifactVersion(testNgArtifact.getVersion());
            getProperties()
                    .setProperty(
                            "testng.configurator", getConfiguratorName(defaultArtifactVersion, getConsoleLogger()));
        }
    }

    private static String getConfiguratorName(ArtifactVersion version, PluginConsoleLogger log)
            throws MojoExecutionException {
        try {
            VersionRange range = VersionRange.createFromVersionSpec("[4.7,5.2)");
            if (range.containsVersion(version)) {
                return "org.apache.maven.surefire.testng.conf.TestNG4751Configurator";
            }
            range = VersionRange.createFromVersionSpec("[5.2,5.3)");
            if (range.containsVersion(version)) {
                return "org.apache.maven.surefire.testng.conf.TestNG52Configurator";
            }
            range = VersionRange.createFromVersionSpec("[5.3,5.10)");
            if (range.containsVersion(version)) {
                return "org.apache.maven.surefire.testng.conf.TestNGMapConfigurator";
            }
            range = VersionRange.createFromVersionSpec("[5.10,5.13)");
            if (range.containsVersion(version)) {
                return "org.apache.maven.surefire.testng.conf.TestNG510Configurator";
            }
            range = VersionRange.createFromVersionSpec("[5.13,5.14.1)");
            if (range.containsVersion(version)) {
                return "org.apache.maven.surefire.testng.conf.TestNG513Configurator";
            }
            range = VersionRange.createFromVersionSpec("[5.14.1,5.14.3)");
            if (range.containsVersion(version)) {
                log.warning("The 'reporter' or 'listener' may not work properly in TestNG 5.14.1 and 5.14.2.");
                return "org.apache.maven.surefire.testng.conf.TestNG5141Configurator";
            }
            range = VersionRange.createFromVersionSpec("[5.14.3,6.0)");
            if (range.containsVersion(version)) {
                if (version.equals(new DefaultArtifactVersion("[5.14.3,5.14.5]"))) {
                    throw new MojoExecutionException("TestNG 5.14.3-5.14.5 is not supported. "
                            + "System dependency org.testng:guice missed path.");
                }
                return "org.apache.maven.surefire.testng.conf.TestNG5143Configurator";
            }
            range = VersionRange.createFromVersionSpec("[6.0,7.4.0)");
            if (range.containsVersion(version)) {
                return "org.apache.maven.surefire.testng.conf.TestNG60Configurator";
            }
            range = VersionRange.createFromVersionSpec("[7.4.0,)");
            if (range.containsVersion(version)) {
                return "org.apache.maven.surefire.testng.conf.TestNG740Configurator";
            }

            throw new MojoExecutionException("Unknown TestNG version " + version);
        } catch (InvalidVersionSpecificationException invsex) {
            throw new MojoExecutionException("Bug in plugin. Please report it with the attached stacktrace", invsex);
        }
    }

    private void convertGroupParameters() {
        if (this.getExcludedGroups() != null) {
            getProperties().setProperty(ProviderParameterNames.TESTNG_EXCLUDEDGROUPS_PROP, this.getExcludedGroups());
        }
        if (this.getGroups() != null) {
            getProperties().setProperty(ProviderParameterNames.TESTNG_GROUPS_PROP, this.getGroups());
        }
    }

    private void convertJunitEngineParameters() {
        if (getIncludeJUnit5Engines() != null && getIncludeJUnit5Engines().length != 0) {
            getProperties().setProperty(INCLUDE_JUNIT5_ENGINES_PROP, join(getIncludeJUnit5Engines()));
        }

        if (getExcludeJUnit5Engines() != null && getExcludeJUnit5Engines().length != 0) {
            getProperties().setProperty(EXCLUDE_JUNIT5_ENGINES_PROP, join(getExcludeJUnit5Engines()));
        }
    }

    private static String join(String[] array) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0, length = array.length; i < length; i++) {
            stringBuilder.append(array[i]);
            if (i < length - 1) {
                stringBuilder.append(',');
            }
        }
        return stringBuilder.toString();
    }

    protected boolean isAnyConcurrencySelected() {
        return getParallel() != null && !getParallel().trim().isEmpty();
    }

    protected boolean isAnyGroupsSelected() {
        return this.getGroups() != null || this.getExcludedGroups() != null;
    }

    /**
     * Converts old JUnit configuration parameters over to new properties based configuration
     * method. (if any are defined the old way)
     */
    private void convertJunitCoreParameters() throws MojoExecutionException {
        checkThreadCountEntity(getThreadCountSuites(), "suites");
        checkThreadCountEntity(getThreadCountClasses(), "classes");
        checkThreadCountEntity(getThreadCountMethods(), "methods");

        String usedParallel = (getParallel() != null) ? getParallel() : "none";

        if (!"none".equals(usedParallel)) {
            checkNonForkedThreads(parallel);
        }

        getProperties().setProperty(ProviderParameterNames.PARALLEL_PROP, usedParallel);
        if (this.getThreadCount() > 0) {
            getProperties().setProperty(ProviderParameterNames.THREADCOUNT_PROP, Integer.toString(getThreadCount()));
        }
        getProperties().setProperty("perCoreThreadCount", Boolean.toString(getPerCoreThreadCount()));
        getProperties().setProperty("useUnlimitedThreads", Boolean.toString(getUseUnlimitedThreads()));
        getProperties()
                .setProperty(ProviderParameterNames.THREADCOUNTSUITES_PROP, Integer.toString(getThreadCountSuites()));
        getProperties()
                .setProperty(ProviderParameterNames.THREADCOUNTCLASSES_PROP, Integer.toString(getThreadCountClasses()));
        getProperties()
                .setProperty(ProviderParameterNames.THREADCOUNTMETHODS_PROP, Integer.toString(getThreadCountMethods()));
        getProperties()
                .setProperty(
                        ProviderParameterNames.PARALLEL_TIMEOUT_PROP,
                        Double.toString(getParallelTestsTimeoutInSeconds()));
        getProperties()
                .setProperty(
                        ProviderParameterNames.PARALLEL_TIMEOUTFORCED_PROP,
                        Double.toString(getParallelTestsTimeoutForcedInSeconds()));
        getProperties()
                .setProperty(ProviderParameterNames.PARALLEL_OPTIMIZE_PROP, Boolean.toString(isParallelOptimized()));

        String message = "parallel='" + usedParallel + '\''
                + ", perCoreThreadCount=" + getPerCoreThreadCount()
                + ", threadCount=" + getThreadCount()
                + ", useUnlimitedThreads=" + getUseUnlimitedThreads()
                + ", threadCountSuites=" + getThreadCountSuites()
                + ", threadCountClasses=" + getThreadCountClasses()
                + ", threadCountMethods=" + getThreadCountMethods()
                + ", parallelOptimized=" + isParallelOptimized();

        logDebugOrCliShowErrors(message);
    }

    private void checkNonForkedThreads(String parallel) throws MojoExecutionException {
        if ("suites".equals(parallel)) {
            if (!(getUseUnlimitedThreads() || getThreadCount() > 0 ^ getThreadCountSuites() > 0)) {
                throw new MojoExecutionException(
                        "Use threadCount or threadCountSuites > 0 or useUnlimitedThreads=true for parallel='suites'");
            }
            setThreadCountClasses(0);
            setThreadCountMethods(0);
        } else if ("classes".equals(parallel)) {
            if (!(getUseUnlimitedThreads() || getThreadCount() > 0 ^ getThreadCountClasses() > 0)) {
                throw new MojoExecutionException(
                        "Use threadCount or threadCountClasses > 0 or useUnlimitedThreads=true for parallel='classes'");
            }
            setThreadCountSuites(0);
            setThreadCountMethods(0);
        } else if ("methods".equals(parallel)) {
            if (!(getUseUnlimitedThreads() || getThreadCount() > 0 ^ getThreadCountMethods() > 0)) {
                throw new MojoExecutionException(
                        "Use threadCount or threadCountMethods > 0 or useUnlimitedThreads=true for parallel='methods'");
            }
            setThreadCountSuites(0);
            setThreadCountClasses(0);
        } else if ("suitesAndClasses".equals(parallel)) {
            if (!(getUseUnlimitedThreads()
                    || onlyThreadCount()
                    || getThreadCountSuites() > 0
                            && getThreadCountClasses() > 0
                            && getThreadCount() == 0
                            && getThreadCountMethods() == 0
                    || getThreadCount() > 0
                            && getThreadCountSuites() > 0
                            && getThreadCountClasses() > 0
                            && getThreadCountMethods() == 0
                    || getThreadCount() > 0
                            && getThreadCountSuites() > 0
                            && getThreadCount() > getThreadCountSuites()
                            && getThreadCountClasses() == 0
                            && getThreadCountMethods() == 0)) {
                throw new MojoExecutionException("Use useUnlimitedThreads=true, "
                        + "or only threadCount > 0, "
                        + "or (threadCountSuites > 0 and threadCountClasses > 0), "
                        + "or (threadCount > 0 and threadCountSuites > 0 and threadCountClasses > 0) "
                        + "or (threadCount > 0 and threadCountSuites > 0 and threadCount > threadCountSuites) "
                        + "for parallel='suitesAndClasses' or 'both'");
            }
            setThreadCountMethods(0);
        } else if ("suitesAndMethods".equals(parallel)) {
            if (!(getUseUnlimitedThreads()
                    || onlyThreadCount()
                    || getThreadCountSuites() > 0
                            && getThreadCountMethods() > 0
                            && getThreadCount() == 0
                            && getThreadCountClasses() == 0
                    || getThreadCount() > 0
                            && getThreadCountSuites() > 0
                            && getThreadCountMethods() > 0
                            && getThreadCountClasses() == 0
                    || getThreadCount() > 0
                            && getThreadCountSuites() > 0
                            && getThreadCount() > getThreadCountSuites()
                            && getThreadCountClasses() == 0
                            && getThreadCountMethods() == 0)) {
                throw new MojoExecutionException("Use useUnlimitedThreads=true, "
                        + "or only threadCount > 0, "
                        + "or (threadCountSuites > 0 and threadCountMethods > 0), "
                        + "or (threadCount > 0 and threadCountSuites > 0 and threadCountMethods > 0), "
                        + "or (threadCount > 0 and threadCountSuites > 0 and threadCount > threadCountSuites) "
                        + "for parallel='suitesAndMethods'");
            }
            setThreadCountClasses(0);
        } else if ("both".equals(parallel) || "classesAndMethods".equals(parallel)) {
            if (!(getUseUnlimitedThreads()
                    || onlyThreadCount()
                    || getThreadCountClasses() > 0
                            && getThreadCountMethods() > 0
                            && getThreadCount() == 0
                            && getThreadCountSuites() == 0
                    || getThreadCount() > 0
                            && getThreadCountClasses() > 0
                            && getThreadCountMethods() > 0
                            && getThreadCountSuites() == 0
                    || getThreadCount() > 0
                            && getThreadCountClasses() > 0
                            && getThreadCount() > getThreadCountClasses()
                            && getThreadCountSuites() == 0
                            && getThreadCountMethods() == 0)) {
                throw new MojoExecutionException("Use useUnlimitedThreads=true, "
                        + "or only threadCount > 0, "
                        + "or (threadCountClasses > 0 and threadCountMethods > 0), "
                        + "or (threadCount > 0 and threadCountClasses > 0 and threadCountMethods > 0), "
                        + "or (threadCount > 0 and threadCountClasses > 0 and threadCount > threadCountClasses) "
                        + "for parallel='both' or parallel='classesAndMethods'");
            }
            setThreadCountSuites(0);
        } else if ("all".equals(parallel)) {
            if (!(getUseUnlimitedThreads()
                    || onlyThreadCount()
                    || getThreadCountSuites() > 0 && getThreadCountClasses() > 0 && getThreadCountMethods() > 0
                    || getThreadCount() > 0
                            && getThreadCountSuites() > 0
                            && getThreadCountClasses() > 0
                            && getThreadCountMethods() == 0
                            && getThreadCount() > (getThreadCountSuites() + getThreadCountClasses()))) {
                throw new MojoExecutionException("Use useUnlimitedThreads=true, "
                        + "or only threadCount > 0, "
                        + "or (threadCountSuites > 0 and threadCountClasses > 0 and threadCountMethods > 0), "
                        + "or every thread-count is specified, "
                        + "or (threadCount > 0 and threadCountSuites > 0 and threadCountClasses > 0 "
                        + "and threadCount > threadCountSuites + threadCountClasses) "
                        + "for parallel='all'");
            }
        } else {
            throw new MojoExecutionException("Illegal parallel='" + parallel + "'");
        }
    }

    private boolean onlyThreadCount() {
        return getThreadCount() > 0
                && getThreadCountSuites() == 0
                && getThreadCountClasses() == 0
                && getThreadCountMethods() == 0;
    }

    private static void checkThreadCountEntity(int count, String entity) throws MojoExecutionException {
        if (count < 0) {
            throw new MojoExecutionException("parallel maven execution does not allow negative thread-count" + entity);
        }
    }

    private boolean isJunit47Compatible(Artifact artifact) {
        return isWithinVersionSpec(artifact, "[4.7,)");
    }

    private boolean isAnyJunit4(Artifact artifact) {
        return isWithinVersionSpec(artifact, "[4.0,)");
    }

    protected boolean isForking() {
        return 0 < getEffectiveForkCount();
    }

    private List<RunOrder> getRunOrders() {
        String runOrderString = getRunOrder();
        RunOrder[] runOrder = runOrderString == null ? RunOrder.DEFAULT : RunOrder.valueOfMulti(runOrderString);
        return asList(runOrder);
    }

    private boolean requiresRunHistory() {
        final List<RunOrder> runOrders = getRunOrders();
        return runOrders.contains(RunOrder.BALANCED) || runOrders.contains(RunOrder.FAILEDFIRST);
    }

    private PluginFailureReason getEffectiveFailIfNoTests() {
        if (isSpecificTestSpecified()) {
            return getFailIfNoSpecifiedTests() ? COULD_NOT_RUN_SPECIFIED_TESTS : NONE;
        } else {
            return getFailIfNoTests() ? COULD_NOT_RUN_DEFAULT_TESTS : NONE;
        }
    }

    private ProviderConfiguration createProviderConfiguration(RunOrderParameters runOrderParameters)
            throws MojoExecutionException, MojoFailureException {
        final ReporterConfiguration reporterConfiguration =
                new ReporterConfiguration(getReportsDirectory(), isTrimStackTrace());

        final Artifact testNgArtifact = getTestNgArtifact();
        final boolean isTestNg = testNgArtifact != null;
        final TestArtifactInfo testNg =
                isTestNg ? new TestArtifactInfo(testNgArtifact.getVersion(), testNgArtifact.getClassifier()) : null;
        final TestRequest testSuiteDefinition = new TestRequest(
                suiteXmlFiles(), getTestSourceDirectory(), getSpecificTests(), getRerunFailingTestsCount());

        final boolean actualFailIfNoTests;
        DirectoryScannerParameters directoryScannerParameters = null;
        if (hasSuiteXmlFiles() && !isSpecificTestSpecified()) {
            actualFailIfNoTests = getFailIfNoTests();
            if (!isTestNg) {
                throw new MojoExecutionException("suiteXmlFiles is configured, but there is no TestNG dependency");
            }
        } else {
            // @todo remove these three params and use DirectoryScannerParameters to pass into DirectoryScanner only
            // @todo or remove it in next major version :: 3.0
            // @todo remove deprecated methods in ProviderParameters => included|excluded|specificTests not needed here

            List<String> actualIncludes = getIncludeList(); // Collections.emptyList(); behaves same
            List<String> actualExcludes = getExcludeList(); // Collections.emptyList(); behaves same
            // Collections.emptyList(); behaves same
            List<String> specificTests = Collections.emptyList();

            directoryScannerParameters = new DirectoryScannerParameters(
                    getTestClassesDirectory(), actualIncludes, actualExcludes, specificTests, getRunOrder());
        }

        Map<String, String> providerProperties = toStringProperties(getProperties());

        return new ProviderConfiguration(
                directoryScannerParameters,
                runOrderParameters,
                reporterConfiguration,
                testNg, // Not really used in provider. Limited to de/serializer.
                testSuiteDefinition,
                providerProperties,
                null,
                false,
                cli,
                getSkipAfterFailureCount(),
                Shutdown.parameterOf(getShutdown()),
                getForkedProcessExitTimeoutInSeconds());
    }

    private static Map<String, String> toStringProperties(Properties properties) {
        Map<String, String> h = new ConcurrentHashMap<>(properties.size());
        for (Enumeration<?> e = properties.keys(); e.hasMoreElements(); ) {
            Object k = e.nextElement();
            Object v = properties.get(k);
            if (k.getClass() == String.class && v.getClass() == String.class) {
                h.put((String) k, (String) v);
            }
        }
        return h;
    }

    private File getStatisticsFile(String configurationHash) {
        return new File(getBasedir(), ".surefire-" + configurationHash);
    }

    private StartupConfiguration createStartupConfiguration(
            @Nonnull ProviderInfo provider,
            boolean isForking,
            @Nonnull ClassLoaderConfiguration classLoaderConfiguration,
            @Nonnull DefaultScanResult scanResult,
            @Nonnull TestClassPath testClasspathWrapper,
            @Nonnull Platform platform,
            @Nonnull ResolvePathResultWrapper resolvedJavaModularity)
            throws MojoExecutionException {
        try {
            if (isForking && canExecuteProviderWithModularPath(platform, resolvedJavaModularity)) {
                File jdkHome = platform.getJdkExecAttributesForTests().getJdkHome();
                return newStartupConfigWithModularPath(
                        classLoaderConfiguration,
                        provider,
                        resolvedJavaModularity,
                        scanResult,
                        jdkHome.getAbsolutePath(),
                        testClasspathWrapper);
            } else {
                return newStartupConfigWithClasspath(classLoaderConfiguration, provider, testClasspathWrapper);
            }
        } catch (IOException e) {
            throw new MojoExecutionException(e.getMessage(), e);
        }
    }

    private StartupConfiguration newStartupConfigWithClasspath(
            @Nonnull ClassLoaderConfiguration classLoaderConfiguration,
            @Nonnull ProviderInfo providerInfo,
            @Nonnull TestClassPath testClasspathWrapper)
            throws MojoExecutionException {
        Classpath testClasspath = testClasspathWrapper.toClasspath();
        Set<Artifact> providerArtifacts = providerInfo.getProviderClasspath();
        String providerName = providerInfo.getProviderName();
        Classpath providerClasspath = classpathCache.getCachedClassPath(providerName);
        if (providerClasspath == null) {
            providerClasspath = classpathCache.setCachedClasspath(providerName, providerArtifacts);
        }

        getConsoleLogger().debug(testClasspath.getLogMessage("test classpath:"));
        getConsoleLogger().debug(providerClasspath.getLogMessage("provider classpath:"));
        getConsoleLogger().debug(testClasspath.getCompactLogMessage("test(compact) classpath:"));
        getConsoleLogger().debug(providerClasspath.getCompactLogMessage("provider(compact) classpath:"));

        Artifact[] additionalInProcArtifacts = {
            getCommonArtifact(),
            getBooterArtifact(),
            getExtensionsArtifact(),
            getApiArtifact(),
            getSpiArtifact(),
            getLoggerApiArtifact(),
            getSurefireSharedUtilsArtifact()
        };
        Set<Artifact> inProcArtifacts = retainInProcArtifactsUnique(providerArtifacts, additionalInProcArtifacts);
        Classpath inProcClasspath = createInProcClasspath(providerClasspath, inProcArtifacts);
        getConsoleLogger().debug(inProcClasspath.getLogMessage("in-process classpath:"));
        getConsoleLogger().debug(inProcClasspath.getCompactLogMessage("in-process(compact) classpath:"));

        ClasspathConfiguration classpathConfiguration = new ClasspathConfiguration(
                testClasspath, providerClasspath, inProcClasspath, effectiveIsEnableAssertions(), isChildDelegation());
        ProviderRequirements forkRequirements = new ProviderRequirements(false, false, false);
        return new StartupConfiguration(
                providerName,
                classpathConfiguration,
                classLoaderConfiguration,
                ProcessCheckerType.toEnum(getEnableProcessChecker()),
                providerInfo.getJpmsArguments(forkRequirements));
    }

    private static Set<Artifact> retainInProcArtifactsUnique(
            Set<Artifact> providerArtifacts, Artifact... inPluginArtifacts) {
        Set<Artifact> result = new LinkedHashSet<>();
        for (Artifact inPluginArtifact : inPluginArtifacts) {
            boolean contains = false;
            for (Artifact providerArtifact : providerArtifacts) {
                if (hasGroupArtifactId(
                        providerArtifact.getGroupId(), providerArtifact.getArtifactId(), inPluginArtifact)) {
                    contains = true;
                    break;
                }
            }
            if (!contains) {
                result.add(inPluginArtifact);
            }
        }
        return result;
    }

    private static boolean hasGroupArtifactId(String groupId, String artifactId, Artifact artifact) {
        return groupId.equals(artifact.getGroupId()) && artifactId.equals(artifact.getArtifactId());
    }

    private static Classpath createInProcClasspath(Classpath providerClasspath, Set<Artifact> newArtifacts) {
        Classpath inprocClasspath = providerClasspath.clone();
        for (Artifact newArtifact : newArtifacts) {
            inprocClasspath =
                    inprocClasspath.addClassPathElementUrl(newArtifact.getFile().getAbsolutePath());
        }
        return inprocClasspath;
    }

    /**
     * For testing purposes - Mockito.
     * @return plexus component
     */
    private LocationManager getLocationManager() {
        return locationManager;
    }

    private StartupConfiguration newStartupConfigWithModularPath(
            @Nonnull ClassLoaderConfiguration classLoaderConfiguration,
            @Nonnull ProviderInfo providerInfo,
            @Nonnull ResolvePathResultWrapper moduleDescriptor,
            @Nonnull DefaultScanResult scanResult,
            @Nonnull String javaHome,
            @Nonnull TestClassPath testClasspathWrapper)
            throws MojoExecutionException, IOException {
        boolean isMainDescriptor = moduleDescriptor.isMainModuleDescriptor();
        JavaModuleDescriptor javaModuleDescriptor =
                moduleDescriptor.getResolvePathResult().getModuleDescriptor();
        SortedSet<String> packages = new TreeSet<>();

        Classpath testClasspath = testClasspathWrapper.toClasspath();
        Set<Artifact> providerArtifacts = providerInfo.getProviderClasspath();
        String providerName = providerInfo.getProviderName();
        Classpath providerClasspath = classpathCache.getCachedClassPath(providerName);
        if (providerClasspath == null) {
            providerClasspath = classpathCache.setCachedClasspath(providerName, providerArtifacts);
        }

        final ProviderRequirements providerRequirements;
        final Classpath testModulepath;
        if (isMainDescriptor) {
            providerRequirements = new ProviderRequirements(true, true, false);
            ResolvePathsRequest<String> req = ResolvePathsRequest.ofStrings(testClasspath.getClassPath())
                    .setIncludeAllProviders(true)
                    .setJdkHome(javaHome)
                    .setIncludeStatic(true)
                    .setModuleDescriptor(javaModuleDescriptor);

            ResolvePathsResult<String> result = getLocationManager().resolvePaths(req);
            for (Entry<String, Exception> entry : result.getPathExceptions().entrySet()) {
                // Probably JDK version < 9. Other known causes: passing a non-jar or a corrupted jar.
                getConsoleLogger().warning("Exception for '" + entry.getKey() + "'.", entry.getValue());
            }

            testClasspath = new Classpath(result.getClasspathElements());
            testModulepath = new Classpath(result.getModulepathElements().keySet());

            for (String className : scanResult.getClasses()) {
                packages.add(substringBeforeLast(className, "."));
            }
        } else {
            providerRequirements = new ProviderRequirements(true, false, true);
            testModulepath = testClasspath;
            testClasspath = emptyClasspath();
        }

        getConsoleLogger().debug("main module descriptor name: " + javaModuleDescriptor.name());

        ModularClasspath modularClasspath = new ModularClasspath(
                javaModuleDescriptor.name(),
                testModulepath.getClassPath(),
                packages,
                isMainDescriptor ? getTestClassesDirectory() : null,
                isMainDescriptor);

        Artifact[] additionalInProcArtifacts = {
            getCommonArtifact(),
            getBooterArtifact(),
            getExtensionsArtifact(),
            getApiArtifact(),
            getSpiArtifact(),
            getLoggerApiArtifact(),
            getSurefireSharedUtilsArtifact()
        };
        Set<Artifact> inProcArtifacts = retainInProcArtifactsUnique(providerArtifacts, additionalInProcArtifacts);
        Classpath inProcClasspath = createInProcClasspath(providerClasspath, inProcArtifacts);

        ModularClasspathConfiguration classpathConfiguration = new ModularClasspathConfiguration(
                modularClasspath,
                testClasspath,
                providerClasspath,
                inProcClasspath,
                effectiveIsEnableAssertions(),
                isChildDelegation());

        getConsoleLogger().debug(testClasspath.getLogMessage("test classpath:"));
        getConsoleLogger().debug(testModulepath.getLogMessage("test modulepath:"));
        getConsoleLogger().debug(providerClasspath.getLogMessage("provider classpath:"));
        getConsoleLogger().debug(testClasspath.getCompactLogMessage("test(compact) classpath:"));
        getConsoleLogger().debug(testModulepath.getCompactLogMessage("test(compact) modulepath:"));
        getConsoleLogger().debug(providerClasspath.getCompactLogMessage("provider(compact) classpath:"));
        getConsoleLogger().debug(inProcClasspath.getLogMessage("in-process classpath:"));
        getConsoleLogger().debug(inProcClasspath.getCompactLogMessage("in-process(compact) classpath:"));

        ProcessCheckerType processCheckerType = ProcessCheckerType.toEnum(getEnableProcessChecker());
        List<String[]> jpmsArgs = providerInfo.getJpmsArguments(providerRequirements);
        return new StartupConfiguration(
                providerName, classpathConfiguration, classLoaderConfiguration, processCheckerType, jpmsArgs);
    }

    private Artifact getCommonArtifact() {
        return getPluginArtifactMap().get("org.apache.maven.surefire:maven-surefire-common");
    }

    private Artifact getExtensionsArtifact() {
        return getPluginArtifactMap().get("org.apache.maven.surefire:surefire-extensions-api");
    }

    private Artifact getSpiArtifact() {
        return getPluginArtifactMap().get("org.apache.maven.surefire:surefire-extensions-spi");
    }

    private Artifact getApiArtifact() {
        return getPluginArtifactMap().get("org.apache.maven.surefire:surefire-api");
    }

    private Artifact getSurefireSharedUtilsArtifact() {
        return getPluginArtifactMap().get("org.apache.maven.surefire:surefire-shared-utils");
    }

    private Artifact getLoggerApiArtifact() {
        return getPluginArtifactMap().get("org.apache.maven.surefire:surefire-logger-api");
    }

    private Artifact getBooterArtifact() {
        return getPluginArtifactMap().get("org.apache.maven.surefire:surefire-booter");
    }

    private Artifact getShadefireArtifact() {
        return getPluginArtifactMap().get("org.apache.maven.surefire:surefire-shadefire");
    }

    private StartupReportConfiguration getStartupReportConfiguration(String configChecksum, boolean isForking) {
        SurefireStatelessReporter xmlReporter = statelessTestsetReporter == null
                ? new SurefireStatelessReporter(/*todo call def. constr.*/ isDisableXmlReport(), "3.0")
                : statelessTestsetReporter;

        xmlReporter.setDisable(isDisableXmlReport()); // todo change to Boolean in the version 3.0.0-M6

        SurefireConsoleOutputReporter outReporter =
                consoleOutputReporter == null ? new SurefireConsoleOutputReporter() : consoleOutputReporter;

        SurefireStatelessTestsetInfoReporter testsetReporter = statelessTestsetInfoReporter == null
                ? new SurefireStatelessTestsetInfoReporter()
                : statelessTestsetInfoReporter;

        return new StartupReportConfiguration(
                isUseFile(),
                isPrintSummary(),
                getReportFormat(),
                isRedirectTestOutputToFile(),
                getReportsDirectory(),
                isTrimStackTrace(),
                getReportNameSuffix(),
                getStatisticsFile(configChecksum),
                requiresRunHistory(),
                getRerunFailingTestsCount(),
                getReportSchemaLocation(),
                getEncoding(),
                isForking,
                xmlReporter,
                outReporter,
                testsetReporter);
    }

    private boolean isSpecificTestSpecified() {
        return isNotBlank(getTest());
    }

    @Nonnull
    private List<String> readListFromFile(@Nonnull final File file) {
        getConsoleLogger().debug("Reading list from: " + file);

        if (!file.exists()) {
            throw new RuntimeException("Failed to load list from file: " + file);
        }

        try {
            List<String> list = FileUtils.loadFile(file);

            if (getConsoleLogger().isDebugEnabled()) {
                getConsoleLogger().debug("List contents:");
                for (String entry : list) {
                    getConsoleLogger().debug("  " + entry);
                }
            }
            return list;
        } catch (IOException e) {
            throw new RuntimeException("Failed to load list from file: " + file, e);
        }
    }

    @Nonnull
    private List<String> getExcludedScanList() throws MojoFailureException {
        return getExcludeList(true);
    }

    @Nonnull
    private List<String> getExcludeList() throws MojoFailureException {
        return getExcludeList(false);
    }

    /**
     * Computes a merge list of test exclusions.
     * Used only in {@link #getExcludeList()} and {@link #getExcludedScanList()}.
     * @param asScanList true if dependency or directory scanner
     * @return list of patterns
     * @throws MojoFailureException if the excludes breaks a pattern format
     */
    @Nonnull
    private List<String> getExcludeList(boolean asScanList) throws MojoFailureException {
        List<String> excludes;
        if (isSpecificTestSpecified()) {
            excludes = Collections.emptyList();
        } else {
            excludes = new ArrayList<>();
            if (asScanList) {
                if (getExcludes() != null) {
                    excludes.addAll(getExcludes());
                }
                checkMethodFilterInIncludesExcludes(excludes);
            }

            if (getExcludesFile() != null) {
                excludes.addAll(readListFromFile(getExcludesFile()));
            }

            if (asScanList && excludes.isEmpty()) {
                excludes = Collections.singletonList(getDefaultExcludes());
            }
        }
        return filterNulls(excludes);
    }

    @Nonnull
    private List<String> getIncludedScanList() throws MojoFailureException {
        return getIncludeList(true);
    }

    @Nonnull
    private List<String> getIncludeList() throws MojoFailureException {
        return getIncludeList(false);
    }

    /**
     * Computes a merge list of test inclusions.
     * Used only in {@link #getIncludeList()} and {@link #getIncludedScanList()}.
     * @param asScanList true if dependency or directory scanner
     * @return list of patterns
     * @throws MojoFailureException if the includes breaks a pattern format
     */
    @Nonnull
    private List<String> getIncludeList(boolean asScanList) throws MojoFailureException {
        final List<String> includes = new ArrayList<>();
        if (isSpecificTestSpecified()) {
            addAll(includes, split(getTest(), ","));
        } else {
            if (asScanList) {
                if (getIncludes() != null) {
                    includes.addAll(getIncludes());
                }
                checkMethodFilterInIncludesExcludes(includes);
            }

            if (getIncludesFile() != null) {
                includes.addAll(readListFromFile(getIncludesFile()));
            }

            if (asScanList && includes.isEmpty()) {
                addAll(includes, getDefaultIncludes());
            }
        }

        return filterNulls(includes);
    }

    private void checkMethodFilterInIncludesExcludes(Iterable<String> patterns) throws MojoFailureException {
        for (String pattern : patterns) {
            if (pattern != null && pattern.contains("#")) {
                throw new MojoFailureException("Method filter prohibited in includes|excludes parameter: " + pattern);
            }
        }
    }

    private TestListResolver getIncludedAndExcludedTests() throws MojoFailureException {
        if (includedExcludedTests == null) {
            includedExcludedTests = new TestListResolver(getIncludedScanList(), getExcludedScanList());
            getConsoleLogger().debug("Resolved included and excluded patterns: " + includedExcludedTests);
        }
        return includedExcludedTests;
    }

    public TestListResolver getSpecificTests() throws MojoFailureException {
        if (specificTests == null) {
            specificTests = new TestListResolver(getIncludeList(), getExcludeList());
        }
        return specificTests;
    }

    @Nonnull
    private List<String> filterNulls(@Nonnull List<String> toFilter) {
        List<String> result = new ArrayList<>(toFilter.size());
        for (String item : toFilter) {
            if (item != null) {
                item = item.trim();
                if (!item.isEmpty()) {
                    result.add(item);
                }
            }
        }

        return result;
    }

    private Artifact getTestNgArtifact() throws MojoExecutionException {
        Artifact artifact = getProjectArtifactMap().get(getTestNGArtifactName());
        Artifact projectArtifact = project.getArtifact();
        String projectArtifactName = projectArtifact.getGroupId() + ":" + projectArtifact.getArtifactId();

        if (artifact != null) {
            VersionRange range = createVersionRange();
            if (!range.containsVersion(new DefaultArtifactVersion(artifact.getVersion()))) {
                throw new MojoExecutionException(
                        "TestNG support requires version 4.7 or above. You have declared version "
                                + artifact.getVersion());
            }
        } else if (projectArtifactName.equals(getTestNGArtifactName())) {
            artifact = projectArtifact;
        }

        return artifact;
    }

    private VersionRange createVersionRange() {
        try {
            return VersionRange.createFromVersionSpec("[4.7,)");
        } catch (InvalidVersionSpecificationException e) {
            throw new RuntimeException(e);
        }
    }

    private Artifact getJunitArtifact() {
        Artifact artifact = getProjectArtifactMap().get(getJunitArtifactName());
        Artifact projectArtifact = project.getArtifact();
        String projectArtifactName = projectArtifact.getGroupId() + ":" + projectArtifact.getArtifactId();

        if (artifact == null && projectArtifactName.equals(getJunitArtifactName())) {
            artifact = projectArtifact;
        }

        return artifact;
    }

    private Artifact getJunitDepArtifact() {
        return getProjectArtifactMap().get("junit:junit-dep");
    }

    private Artifact getJUnitPlatformRunnerArtifact() {
        return getProjectArtifactMap().get("org.junit.platform:junit-platform-runner");
    }

    private Artifact getJUnit5Artifact() {
        Artifact artifact = getPluginArtifactMap().get("org.junit.platform:junit-platform-engine");
        if (artifact == null) {
            return getProjectArtifactMap().get("org.junit.platform:junit-platform-commons");
        }

        return artifact;
    }

    private ForkStarter createForkStarter(
            @Nonnull ProviderInfo provider,
            @Nonnull ForkConfiguration forkConfiguration,
            @Nonnull ClassLoaderConfiguration classLoaderConfiguration,
            @Nonnull RunOrderParameters runOrderParameters,
            @Nonnull ConsoleLogger log,
            @Nonnull DefaultScanResult scanResult,
            @Nonnull TestClassPath testClasspathWrapper,
            @Nonnull Platform platform,
            @Nonnull ResolvePathResultWrapper resolvedJavaModularityResult)
            throws MojoExecutionException, MojoFailureException {
        StartupConfiguration startupConfiguration = createStartupConfiguration(
                provider,
                true,
                classLoaderConfiguration,
                scanResult,
                testClasspathWrapper,
                platform,
                resolvedJavaModularityResult);
        String configChecksum = getConfigChecksum();
        StartupReportConfiguration startupReportConfiguration = getStartupReportConfiguration(configChecksum, true);
        ProviderConfiguration providerConfiguration = createProviderConfiguration(runOrderParameters);
        return new ForkStarter(
                providerConfiguration,
                startupConfiguration,
                forkConfiguration,
                getForkedProcessTimeoutInSeconds(),
                startupReportConfiguration,
                log);
    }

    private InPluginVMSurefireStarter createInprocessStarter(
            @Nonnull ProviderInfo provider,
            @Nonnull ClassLoaderConfiguration classLoaderConfig,
            @Nonnull RunOrderParameters runOrderParameters,
            @Nonnull DefaultScanResult scanResult,
            @Nonnull Platform platform,
            @Nonnull TestClassPath testClasspathWrapper)
            throws MojoExecutionException, MojoFailureException {
        StartupConfiguration startupConfiguration = createStartupConfiguration(
                provider,
                false,
                classLoaderConfig,
                scanResult,
                testClasspathWrapper,
                platform,
                new ResolvePathResultWrapper(null, true));
        String configChecksum = getConfigChecksum();
        StartupReportConfiguration startupReportConfiguration = getStartupReportConfiguration(configChecksum, false);
        ProviderConfiguration providerConfiguration = createProviderConfiguration(runOrderParameters);
        return new InPluginVMSurefireStarter(
                startupConfiguration, providerConfiguration, startupReportConfiguration, getConsoleLogger(), platform);
    }

    // todo this is in separate method and can be better tested than whole method createForkConfiguration()
    @Nonnull
    private ForkNodeFactory getForkNodeFactory() {
        ForkNodeFactory forkNode = getForkNode();
        return forkNode == null ? new LegacyForkNodeFactory() : forkNode;
    }

    @Nonnull
    private ForkConfiguration createForkConfiguration(
            @Nonnull Platform platform, @Nonnull ResolvePathResultWrapper resolvedJavaModularityResult)
            throws MojoExecutionException {
        File tmpDir = getSurefireTempDir();

        Artifact shadeFire = getShadefireArtifact();

        Classpath bootClasspath = getArtifactClasspath(shadeFire != null ? shadeFire : getBooterArtifact());

        ForkNodeFactory forkNode = getForkNodeFactory();

        getConsoleLogger()
                .debug("Found implementation of fork node factory: "
                        + forkNode.getClass().getName());

        if (canExecuteProviderWithModularPath(platform, resolvedJavaModularityResult)) {
            return new ModularClasspathForkConfiguration(
                    bootClasspath,
                    tmpDir,
                    getEffectiveDebugForkedProcess(),
                    getWorkingDirectory() != null ? getWorkingDirectory() : getBasedir(),
                    getProject().getModel().getProperties(),
                    getArgLine(),
                    getEnvironmentVariables(),
                    getExcludedEnvironmentVariables(),
                    getConsoleLogger().isDebugEnabled(),
                    getEffectiveForkCount(),
                    reuseForks,
                    platform,
                    getConsoleLogger(),
                    forkNode);
        } else if (getClassLoaderConfiguration().isManifestOnlyJarRequestedAndUsable()) {
            return new JarManifestForkConfiguration(
                    bootClasspath,
                    tmpDir,
                    getEffectiveDebugForkedProcess(),
                    getWorkingDirectory() != null ? getWorkingDirectory() : getBasedir(),
                    getProject().getModel().getProperties(),
                    getArgLine(),
                    getEnvironmentVariables(),
                    getExcludedEnvironmentVariables(),
                    getConsoleLogger().isDebugEnabled(),
                    getEffectiveForkCount(),
                    reuseForks,
                    platform,
                    getConsoleLogger(),
                    forkNode);
        } else {
            return new ClasspathForkConfiguration(
                    bootClasspath,
                    tmpDir,
                    getEffectiveDebugForkedProcess(),
                    getWorkingDirectory() != null ? getWorkingDirectory() : getBasedir(),
                    getProject().getModel().getProperties(),
                    getArgLine(),
                    getEnvironmentVariables(),
                    getExcludedEnvironmentVariables(),
                    getConsoleLogger().isDebugEnabled(),
                    getEffectiveForkCount(),
                    reuseForks,
                    platform,
                    getConsoleLogger(),
                    forkNode);
        }
    }

    private void ensureEnableProcessChecker() throws MojoFailureException {
        if (!ProcessCheckerType.isValid(getEnableProcessChecker())) {
            throw new MojoFailureException("Unexpected value '"
                    + getEnableProcessChecker()
                    + "' in the configuration parameter 'enableProcessChecker'.");
        }
    }

    @SuppressWarnings("checkstyle:emptyblock")
    protected int getEffectiveForkCount() {
        if (effectiveForkCount < 0) {
            try {
                effectiveForkCount = convertWithCoreCount(forkCount);
            } catch (NumberFormatException ignored) {
            }

            if (effectiveForkCount < 0) {
                throw new IllegalArgumentException("Fork count " + forkCount.trim() + " is not a legal value.");
            }
        }

        return effectiveForkCount;
    }

    protected int convertWithCoreCount(String count) {
        String trimmed = count.trim();
        if (trimmed.endsWith("C")) {
            double multiplier = Double.parseDouble(trimmed.substring(0, trimmed.length() - 1));
            double calculated = multiplier * ((double) Runtime.getRuntime().availableProcessors());
            return calculated > 0d ? Math.max((int) calculated, 1) : 0;
        } else {
            return parseInt(trimmed);
        }
    }

    private String getEffectiveDebugForkedProcess() {
        String debugForkedProcess = getDebugForkedProcess();
        if ("true".equals(debugForkedProcess)) {
            return "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=localhost:5005";
        }
        return debugForkedProcess;
    }

    private JdkAttributes getEffectiveJvm() throws MojoFailureException {
        if (isNotEmpty(getJvm())) {
            File pathToJava = new File(getJvm()).getAbsoluteFile();
            if (!endsWithJavaPath(pathToJava.getPath())) {
                throw new MojoFailureException(
                        "Given path does not end with java executor \"" + pathToJava.getPath() + "\".");
            }

            if (!(pathToJava.isFile()
                    || "java".equals(pathToJava.getName())
                            && pathToJava.getParentFile().isDirectory())) {
                throw new MojoFailureException(
                        "Given path to java executor does not exist \"" + pathToJava.getPath() + "\".");
            }

            File jdkHome = toJdkHomeFromJvmExec(pathToJava.getPath());
            if (jdkHome == null) {
                getConsoleLogger().warning("Cannot determine JAVA_HOME of jvm exec path " + pathToJava);
            } else if (!getEnvironmentVariables().containsKey("JAVA_HOME")) {
                getEnvironmentVariables().put("JAVA_HOME", jdkHome.getAbsolutePath());
            }
            BigDecimal version = jdkHome == null ? null : toJdkVersionFromReleaseFile(jdkHome);
            boolean javaVersion9 = version == null ? isJava9AtLeast(pathToJava.getPath()) : isJava9AtLeast(version);
            return new JdkAttributes(pathToJava, jdkHome, javaVersion9);
        }

        if (toolchain != null) {
            String jvmToUse = toolchain.findTool("java");
            if (isNotEmpty(jvmToUse)) {
                boolean javaVersion9 = false;
                String jdkHome = null;

                if (toolchain instanceof DefaultToolchain) {
                    DefaultToolchain defaultToolchain = (DefaultToolchain) toolchain;
                    javaVersion9 = defaultToolchain.matchesRequirements(JAVA_9_MATCHER)
                            || defaultToolchain.matchesRequirements(JAVA_9_MATCHER_OLD_NOTATION);
                }

                if (toolchain instanceof DefaultJavaToolChain) {
                    DefaultJavaToolChain defaultJavaToolChain = (DefaultJavaToolChain) toolchain;
                    if (!getEnvironmentVariables().containsKey("JAVA_HOME")) {
                        jdkHome = defaultJavaToolChain.getJavaHome();
                        getEnvironmentVariables().put("JAVA_HOME", jdkHome);
                    }
                }

                if (!javaVersion9) {
                    javaVersion9 = isJava9AtLeast(jvmToUse);
                }

                return new JdkAttributes(
                        new File(jvmToUse),
                        jdkHome == null ? toJdkHomeFromJvmExec(jvmToUse) : new File(jdkHome),
                        javaVersion9);
            }
        }

        // use the same JVM as the one used to run Maven (the "java.home" one)
        String jvmToUse = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";
        getConsoleLogger().debug("Using JVM: " + jvmToUse + " with Java version " + JAVA_RECENT);

        return new JdkAttributes(jvmToUse, isBuiltInJava9AtLeast());
    }

    /**
     * Where surefire stores its own temp files
     *
     * @return A file pointing to the location of surefire's own temp files
     */
    File getSurefireTempDir() {
        File result = IS_OS_WINDOWS ? createSurefireBootDirectoryInTemp() : createSurefireBootDirectoryInBuild();
        try {
            File canonical = result.getCanonicalFile();
            if (!result.equals(canonical)) {
                getConsoleLogger().debug("Canonicalized tempDir path '" + result + "' to '" + canonical + "'");
            }
            return canonical;
        } catch (IOException e) {
            getConsoleLogger().error("Could not canonicalize tempDir path '" + result + "'", e);
        }
        return result;
    }

    /**
     * Operates on raw plugin parameters, not the "effective" values.
     *
     * @return The checksum
     */
    private String getConfigChecksum() {
        ChecksumCalculator checksum = new ChecksumCalculator();
        checksum.add(getPluginName());
        checksum.add(isSkipTests());
        checksum.add(isSkipExec());
        checksum.add(isSkip());
        checksum.add(getTestClassesDirectory());
        checksum.add(getMainBuildPath());
        checksum.add(getClasspathDependencyExcludes());
        checksum.add(getClasspathDependencyScopeExclude());
        checksum.add(getAdditionalClasspathElements());
        checksum.add(getReportsDirectory());
        checksum.add(getProjectBuildDirectory());
        checksum.add(getTestSourceDirectory());
        checksum.add(getTest());
        checksum.add(getIncludes());
        checksum.add(getSkipAfterFailureCount());
        checksum.add(getShutdown());
        checksum.add(getExcludes());
        checksum.add(getLocalRepositoryPath());
        checksum.add(getSystemProperties());
        checksum.add(getSystemPropertyVariables());
        checksum.add(getSystemPropertiesFile());
        checksum.add(getProperties());
        checksum.add(isPrintSummary());
        checksum.add(getReportFormat());
        checksum.add(getReportNameSuffix());
        checksum.add(isUseFile());
        checksum.add(isRedirectTestOutputToFile());
        checksum.add(getForkCount());
        checksum.add(isReuseForks());
        checksum.add(getJvm());
        checksum.add(getArgLine());
        checksum.add(getDebugForkedProcess());
        checksum.add(getForkedProcessTimeoutInSeconds());
        checksum.add(getParallelTestsTimeoutInSeconds());
        checksum.add(getParallelTestsTimeoutForcedInSeconds());
        checksum.add(getEnvironmentVariables());
        checksum.add(getExcludedEnvironmentVariables());
        checksum.add(getWorkingDirectory());
        checksum.add(isChildDelegation());
        checksum.add(getGroups());
        checksum.add(getExcludedGroups());
        checksum.add(getIncludeJUnit5Engines());
        checksum.add(getExcludeJUnit5Engines());
        checksum.add(getSuiteXmlFiles());
        checksum.add(getJunitArtifact());
        checksum.add(getTestNGArtifactName());
        checksum.add(getThreadCount());
        checksum.add(getThreadCountSuites());
        checksum.add(getThreadCountClasses());
        checksum.add(getThreadCountMethods());
        checksum.add(getPerCoreThreadCount());
        checksum.add(getUseUnlimitedThreads());
        checksum.add(getParallel());
        checksum.add(isParallelOptimized());
        checksum.add(isTrimStackTrace());
        checksum.add(isDisableXmlReport());
        checksum.add(isUseSystemClassLoader());
        checksum.add(isUseManifestOnlyJar());
        checksum.add(getEncoding());
        checksum.add(isEnableAssertions());
        checksum.add(getObjectFactory());
        checksum.add(getFailIfNoTests());
        checksum.add(getRunOrder());
        checksum.add(getDependenciesToScan());
        checksum.add(getForkedProcessExitTimeoutInSeconds());
        checksum.add(getRerunFailingTestsCount());
        checksum.add(getTempDir());
        checksum.add(useModulePath());
        checksum.add(getEnableProcessChecker());
        addPluginSpecificChecksumItems(checksum);
        return checksum.getSha1();
    }

    protected void addPluginSpecificChecksumItems(ChecksumCalculator checksum) {}

    protected boolean hasExecutedBefore() {
        // A tribute to Linus Torvalds
        String configChecksum = getConfigChecksum();
        @SuppressWarnings("unchecked")
        Map<String, String> pluginContext = getPluginContext();
        if (pluginContext.containsKey(configChecksum)) {
            getConsoleLogger()
                    .info("Skipping execution of surefire because it has already been run for this configuration");
            return true;
        }
        pluginContext.put(configChecksum, configChecksum);

        return false;
    }

    @Nonnull
    protected ClassLoaderConfiguration getClassLoaderConfiguration() {
        return isForking()
                ? new ClassLoaderConfiguration(isUseSystemClassLoader(), isUseManifestOnlyJar())
                : new ClassLoaderConfiguration(false, false);
    }

    /**
     * Generates the test classpath.
     *
     * @return the classpath elements
     * @throws MojoFailureException
     */
    private TestClassPath generateTestClasspath() throws MojoFailureException {
        Set<Artifact> classpathArtifacts = getProject().getArtifacts();

        if (getClasspathDependencyScopeExclude() != null
                && !getClasspathDependencyScopeExclude().isEmpty()) {
            ArtifactFilter dependencyFilter = new ScopeArtifactFilter(getClasspathDependencyScopeExclude());
            classpathArtifacts = filterArtifacts(classpathArtifacts, dependencyFilter);
        }

        if (getClasspathDependencyExcludes() != null) {
            List<String> excludedDependencies = asList(getClasspathDependencyExcludes());
            ArtifactFilter dependencyFilter = new PatternIncludesArtifactFilter(excludedDependencies);
            classpathArtifacts = filterArtifacts(classpathArtifacts, dependencyFilter);
        }

        Map<String, Artifact> dependencyConflictIdsProjectArtifacts = classpathArtifacts.stream()
                .collect(Collectors.toMap(Artifact::getDependencyConflictId, Function.identity()));
        Set<String> additionalClasspathElements = new HashSet<>();
        if (getAdditionalClasspathElements() != null) {
            Arrays.stream(getAdditionalClasspathElements()).forEach(additionalClasspathElements::add);
        }
        if (additionalClasspathDependencies != null && !additionalClasspathDependencies.isEmpty()) {
            Collection<Artifact> additionalArtifacts = resolveDependencies(additionalClasspathDependencies);
            // check for potential conflicts with project dependencies
            for (Artifact additionalArtifact : additionalArtifacts) {
                Artifact conflictingArtifact =
                        dependencyConflictIdsProjectArtifacts.get(additionalArtifact.getDependencyConflictId());
                if (conflictingArtifact != null
                        && !additionalArtifact.getVersion().equals(conflictingArtifact.getVersion())) {
                    getConsoleLogger()
                            .warning(
                                    "Potential classpath conflict between project dependency and resolved additionalClasspathDependency: Found multiple versions of "
                                            + additionalArtifact.getDependencyConflictId() + ": "
                                            + additionalArtifact.getVersion() + " and "
                                            + conflictingArtifact.getVersion());
                }
                additionalClasspathElements.add(additionalArtifact.getFile().getAbsolutePath());
            }
        }
        return new TestClassPath(
                classpathArtifacts, getMainBuildPath(), getTestClassesDirectory(), additionalClasspathElements);
    }

    protected Collection<Artifact> resolveDependencies(List<Dependency> dependencies) throws MojoFailureException {
        Map<String, Artifact> dependencyConflictIdsAndArtifacts = new HashMap<>();
        try {
            dependencies.stream()
                    .map(dependency -> {
                        try {
                            return surefireDependencyResolver.resolveDependencies(
                                    session.getRepositorySession(), project.getRemoteProjectRepositories(), dependency);
                        } catch (MojoExecutionException e) {
                            throw new IllegalStateException(e);
                        }
                    })
                    .forEach(artifacts -> {
                        for (Artifact a : artifacts) {
                            Artifact conflictingArtifact =
                                    dependencyConflictIdsAndArtifacts.get(a.getDependencyConflictId());
                            if (conflictingArtifact != null
                                    && !a.getVersion().equals(conflictingArtifact.getVersion())) {
                                getConsoleLogger()
                                        .warning(
                                                "Potential classpath conflict among resolved additionalClasspathDependencies: Found multiple versions of "
                                                        + a.getDependencyConflictId() + ": " + a.getVersion() + " and "
                                                        + conflictingArtifact.getVersion());
                            } else {
                                dependencyConflictIdsAndArtifacts.put(a.getDependencyConflictId(), a);
                            }
                        }
                    });
        } catch (IllegalStateException e) {
            throw new MojoFailureException(e.getMessage(), e.getCause());
        }
        return dependencyConflictIdsAndArtifacts.values();
    }

    /**
     * Return a new set containing only the artifacts not accepted by the given filter.
     *
     * @param artifacts The unfiltered artifacts
     * @param filter    The excludes filter to apply
     * @return The filtered result
     */
    private static Set<Artifact> filterArtifacts(Set<Artifact> artifacts, ArtifactFilter filter) {
        Set<Artifact> filteredArtifacts = new LinkedHashSet<>();

        for (Artifact artifact : artifacts) {
            if (!filter.include(artifact)) {
                filteredArtifacts.add(artifact);
            }
        }

        return filteredArtifacts;
    }

    private void showMap(Map<?, ?> map, String setting) {
        for (Object o : map.keySet()) {
            String key = (String) o;
            String value = (String) map.get(key);
            getConsoleLogger().debug("Setting " + setting + " [" + key + "]=[" + value + "]");
        }
    }

    private <T> void showArray(T[] array, String setting) {
        for (T e : array) {
            getConsoleLogger().debug("Setting " + setting + " [" + e + "]");
        }
    }

    private Classpath getArtifactClasspath(Artifact surefireArtifact) throws MojoExecutionException {
        Classpath existing = classpathCache.getCachedClassPath(surefireArtifact.getArtifactId());
        if (existing == null) {
            List<String> items = new ArrayList<>();
            Set<Artifact> booterArtifacts = surefireDependencyResolver.resolveArtifacts(
                    session.getRepositorySession(), project.getRemotePluginRepositories(), surefireArtifact);
            for (Artifact artifact : booterArtifacts) {
                items.add(artifact.getFile().getAbsolutePath());
            }
            existing = new Classpath(items);
            classpathCache.setCachedClasspath(surefireArtifact.getArtifactId(), existing);
        }
        return existing;
    }

    private Properties getUserProperties() {
        return getSession().getUserProperties();
    }

    private void ensureWorkingDirectoryExists() throws MojoFailureException {
        if (getWorkingDirectory() == null) {
            throw new MojoFailureException("workingDirectory cannot be null");
        }

        if (isForking()) {
            // Postpone directory creation till forked JVM creation
            // see ForkConfiguration.createCommandLine
            return;
        }

        if (!getWorkingDirectory().exists()) {
            if (!getWorkingDirectory().mkdirs()) {
                throw new MojoFailureException("Cannot create workingDirectory " + getWorkingDirectory());
            }
        }

        if (!getWorkingDirectory().isDirectory()) {
            throw new MojoFailureException(
                    "workingDirectory " + getWorkingDirectory() + " exists and is not a directory");
        }
    }

    private void ensureParallelRunningCompatibility() throws MojoFailureException {
        if (isMavenParallel() && isNotForking()) {
            throw new MojoFailureException("parallel maven execution is not compatible with surefire forkCount 0");
        }
    }

    private void warnIfUselessUseSystemClassLoaderParameter() {
        if (isUseSystemClassLoader() && isNotForking()) {
            getConsoleLogger().warning("useSystemClassLoader setting has no effect when not forking");
        }
    }

    private boolean isNotForking() {
        return !isForking();
    }

    private List<CommandLineOption> commandLineOptions() {
        return SurefireHelper.commandLineOptions(getSession(), getConsoleLogger());
    }

    private void warnIfDefunctGroupsCombinations() throws MojoFailureException, MojoExecutionException {
        if (isAnyGroupsSelected()) {
            if (getTestNgArtifact() == null) {
                Artifact junitArtifact = getJunitArtifact();
                boolean junit47Compatible = isJunit47Compatible(junitArtifact);
                boolean junit5PlatformCompatible = getJUnit5Artifact() != null;
                if (!junit47Compatible && !junit5PlatformCompatible) {
                    if (junitArtifact != null) {
                        throw new MojoFailureException("groups/excludedGroups are specified but JUnit version on "
                                + "classpath is too old to support groups. "
                                + "Check your dependency:tree to see if your project "
                                + "is picking up an old junit version");
                    }
                    throw new MojoFailureException("groups/excludedGroups require TestNG, JUnit48+ or JUnit 5 "
                            + "(a specific engine required on classpath) on project test classpath");
                }
            }
        }
    }

    private void warnIfRerunClashes() throws MojoFailureException {
        if (getRerunFailingTestsCount() < 0) {
            throw new MojoFailureException("Parameter \"rerunFailingTestsCount\" should not be negative.");
        }

        if (getSkipAfterFailureCount() < 0) {
            throw new MojoFailureException("Parameter \"skipAfterFailureCount\" should not be negative.");
        }
    }

    private void warnIfWrongShutdownValue() throws MojoFailureException {
        if (!Shutdown.isKnown(getShutdown())) {
            throw new MojoFailureException("Parameter \"shutdown\" should have values " + Shutdown.listParameters());
        }
    }

    private void warnIfNotApplicableSkipAfterFailureCount() throws MojoFailureException {
        int skipAfterFailureCount = getSkipAfterFailureCount();

        if (skipAfterFailureCount < 0) {
            throw new MojoFailureException("Parameter \"skipAfterFailureCount\" should not be negative.");
        } else if (skipAfterFailureCount > 0) {
            try {
                Artifact testng = getTestNgArtifact();
                if (testng != null) {
                    VersionRange range = VersionRange.createFromVersionSpec("[5.10,)");
                    if (!range.containsVersion(new DefaultArtifactVersion(testng.getVersion()))) {
                        throw new MojoFailureException(
                                "Parameter \"skipAfterFailureCount\" expects TestNG Version 5.10 or higher. "
                                        + "java.lang.NoClassDefFoundError: org/testng/IInvokedMethodListener");
                    }
                } else {
                    // TestNG is dependent on JUnit
                    Artifact junit = getJunitArtifact();
                    if (junit != null) {
                        VersionRange range = VersionRange.createFromVersionSpec("[4.0,)");
                        if (!range.containsVersion(new DefaultArtifactVersion(junit.getVersion()))) {
                            throw new MojoFailureException(
                                    "Parameter \"skipAfterFailureCount\" expects JUnit Version 4.0 or higher. "
                                            + "java.lang.NoSuchMethodError: "
                                            + "org.junit.runner.notification.RunNotifier.pleaseStop()V");
                        }
                    }
                }
            } catch (MojoExecutionException e) {
                throw new MojoFailureException(e.getLocalizedMessage());
            } catch (InvalidVersionSpecificationException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private void warnIfIllegalTempDir() throws MojoFailureException {
        if (isEmpty(getTempDir())) {
            throw new MojoFailureException("Parameter 'tempDir' should not be blank string.");
        }
    }

    protected void warnIfIllegalFailOnFlakeCount() throws MojoFailureException {}

    private void printDefaultSeedIfNecessary() {
        if (getRunOrder().equals(RunOrder.RANDOM.name())) {
            if (getRunOrderRandomSeed() == null) {
                setRunOrderRandomSeed(System.nanoTime());
            }
            getConsoleLogger()
                    .info("Tests will run in random order. To reproduce ordering use flag -D" + getPluginName()
                            + ".runOrder.random.seed=" + getRunOrderRandomSeed());
        }
    }

    final class TestNgProviderInfo implements ProviderInfo {
        private final Artifact testNgArtifact;

        TestNgProviderInfo(Artifact testNgArtifact) {
            this.testNgArtifact = testNgArtifact;
        }

        @Override
        @Nonnull
        public String getProviderName() {
            return "org.apache.maven.surefire.testng.TestNGProvider";
        }

        @Override
        public boolean isApplicable() {
            return testNgArtifact != null;
        }

        @Override
        public void addProviderProperties() throws MojoExecutionException {
            convertTestNGParameters();
        }

        @Nonnull
        @Override
        public List<String[]> getJpmsArguments(@Nonnull ProviderRequirements forkRequirements) {
            return emptyList();
        }

        @Override
        @Nonnull
        public Set<Artifact> getProviderClasspath() throws MojoExecutionException {
            Artifact surefireArtifact = getBooterArtifact();
            String version = surefireArtifact.getBaseVersion();
            return surefireDependencyResolver.getProviderClasspath(
                    session.getRepositorySession(), project.getRemotePluginRepositories(), "surefire-testng", version);
        }
    }

    final class JUnit3ProviderInfo implements ProviderInfo {
        @Override
        @Nonnull
        public String getProviderName() {
            return "org.apache.maven.surefire.junit.JUnit3Provider";
        }

        @Override
        public boolean isApplicable() {
            return true;
        }

        @Override
        public void addProviderProperties() {}

        @Nonnull
        @Override
        public List<String[]> getJpmsArguments(@Nonnull ProviderRequirements forkRequirements) {
            return emptyList();
        }

        @Override
        @Nonnull
        public Set<Artifact> getProviderClasspath() throws MojoExecutionException {
            // add the JUnit provider as default - it doesn't require JUnit to be present,
            // since it supports POJO tests.
            String version = getBooterArtifact().getBaseVersion();
            return surefireDependencyResolver.getProviderClasspath(
                    session.getRepositorySession(), project.getRemotePluginRepositories(), "surefire-junit3", version);
        }
    }

    final class JUnit4ProviderInfo implements ProviderInfo {
        private final Artifact junitArtifact;

        private final Artifact junitDepArtifact;

        JUnit4ProviderInfo(Artifact junitArtifact, Artifact junitDepArtifact) {
            this.junitArtifact = junitArtifact;
            this.junitDepArtifact = junitDepArtifact;
        }

        @Override
        @Nonnull
        public String getProviderName() {
            return "org.apache.maven.surefire.junit4.JUnit4Provider";
        }

        @Override
        public boolean isApplicable() {
            return junitDepArtifact != null || isAnyJunit4(junitArtifact);
        }

        @Override
        public void addProviderProperties() {}

        @Nonnull
        @Override
        public List<String[]> getJpmsArguments(@Nonnull ProviderRequirements forkRequirements) {
            return emptyList();
        }

        @Override
        @Nonnull
        public Set<Artifact> getProviderClasspath() throws MojoExecutionException {
            String version = getBooterArtifact().getBaseVersion();
            return surefireDependencyResolver.getProviderClasspath(
                    session.getRepositorySession(), project.getRemotePluginRepositories(), "surefire-junit4", version);
        }
    }

    final class JUnitPlatformProviderInfo implements ProviderInfo {
        private static final String PROVIDER_DEP_GID = "org.junit.platform";
        private static final String PROVIDER_DEP_AID = "junit-platform-launcher";

        private final Artifact junitPlatformRunnerArtifact;
        private final Artifact junitPlatformArtifact;
        private final TestClassPath testClasspath;

        JUnitPlatformProviderInfo(
                Artifact junitPlatformRunnerArtifact,
                Artifact junitPlatformArtifact,
                @Nonnull TestClassPath testClasspath) {
            this.junitPlatformRunnerArtifact = junitPlatformRunnerArtifact;
            this.junitPlatformArtifact = junitPlatformArtifact;
            this.testClasspath = testClasspath;
        }

        @Override
        @Nonnull
        public String getProviderName() {
            return "org.apache.maven.surefire.junitplatform.JUnitPlatformProvider";
        }

        @Override
        public boolean isApplicable() {
            return junitPlatformRunnerArtifact == null && junitPlatformArtifact != null;
        }

        @Override
        public void addProviderProperties() {
            convertGroupParameters();
            convertJunitEngineParameters();
        }

        @Nonnull
        @Override
        public List<String[]> getJpmsArguments(@Nonnull ProviderRequirements forkRequirements) {
            boolean hasTestDescriptor = forkRequirements.isModularPath() && forkRequirements.hasTestModuleDescriptor();
            return hasTestDescriptor ? getJpmsArgs() : Collections.<String[]>emptyList();
        }

        @Override
        @Nonnull
        public Set<Artifact> getProviderClasspath() throws MojoExecutionException {
            String surefireVersion = getBooterArtifact().getBaseVersion();
            Map<String, Artifact> providerArtifacts = surefireDependencyResolver.getProviderClasspathAsMap(
                    session.getRepositorySession(),
                    project.getRemotePluginRepositories(),
                    "surefire-junit-platform",
                    surefireVersion);
            Map<String, Artifact> testDeps = testClasspath.getTestDependencies();

            Plugin plugin = getPluginDescriptor().getPlugin();
            Map<String, Artifact> pluginDeps = surefireDependencyResolver.resolvePluginDependencies(
                    session.getRepositorySession(),
                    project.getRemotePluginRepositories(),
                    plugin,
                    getPluginArtifactMap());

            if (hasDependencyPlatformEngine(pluginDeps)) {
                providerArtifacts.putAll(pluginDeps);
            } else {
                String engineVersion = null;
                if (hasDependencyJupiterAPI(testDeps)
                        && !testDeps.containsKey("org.junit.jupiter:junit-jupiter-engine")) {
                    String engineGroupId = "org.junit.jupiter";
                    String engineArtifactId = "junit-jupiter-engine";
                    String engineCoordinates = engineGroupId + ":" + engineArtifactId;
                    String api = "org.junit.jupiter:junit-jupiter-api";
                    engineVersion = testDeps.get(api).getBaseVersion();
                    getConsoleLogger()
                            .debug("Test dependencies contain " + api + ". Resolving " + engineCoordinates + ":"
                                    + engineVersion);
                    addEngineByApi(engineGroupId, engineArtifactId, engineVersion, providerArtifacts);
                }

                if ((testDeps.containsKey("junit:junit") || testDeps.containsKey("junit:junit-dep"))
                        && !testDeps.containsKey("org.junit.vintage:junit-vintage-engine")) {
                    String engineGroupId = "org.junit.vintage";
                    String engineArtifactId = "junit-vintage-engine";
                    String engineCoordinates = engineGroupId + ":" + engineArtifactId;

                    if (engineVersion != null) {
                        getConsoleLogger()
                                .debug("Test dependencies contain JUnit4. Resolving " + engineCoordinates + ":"
                                        + engineVersion);
                        addEngineByApi(engineGroupId, engineArtifactId, engineVersion, providerArtifacts);
                    }
                }
            }

            narrowDependencies(providerArtifacts, testDeps);
            alignProviderVersions(providerArtifacts);

            return new LinkedHashSet<>(providerArtifacts.values());
        }

        private List<String[]> getJpmsArgs() {
            List<String[]> args = new ArrayList<>();

            args.add(new String[] {
                "--add-opens", "org.junit.platform.commons/org.junit.platform.commons.util=ALL-UNNAMED"
            });

            args.add(new String[] {
                "--add-opens", "org.junit.platform.commons/org.junit.platform.commons.logging=ALL-UNNAMED"
            });

            return args;
        }

        private void addEngineByApi(
                String engineGroupId,
                String engineArtifactId,
                String engineVersion,
                Map<String, Artifact> providerArtifacts)
                throws MojoExecutionException {
            for (Artifact dep : resolve(engineGroupId, engineArtifactId, engineVersion, null, "jar")) {
                String key = dep.getGroupId() + ":" + dep.getArtifactId();
                providerArtifacts.put(key, dep);
            }
        }

        private void narrowDependencies(
                Map<String, Artifact> providerArtifacts, Map<String, Artifact> testDependencies) {
            providerArtifacts.keySet().removeAll(testDependencies.keySet());
        }

        private void alignProviderVersions(Map<String, Artifact> providerArtifacts) throws MojoExecutionException {
            String version = junitPlatformArtifact.getBaseVersion();
            for (Artifact launcherArtifact : resolve(PROVIDER_DEP_GID, PROVIDER_DEP_AID, version, null, "jar")) {
                String key = launcherArtifact.getGroupId() + ":" + launcherArtifact.getArtifactId();
                if (providerArtifacts.containsKey(key)) {
                    providerArtifacts.put(key, launcherArtifact);
                }
            }
        }

        private Set<Artifact> resolve(String g, String a, String v, String c, String t) throws MojoExecutionException {
            ArtifactHandler handler = junitPlatformArtifact.getArtifactHandler();
            Artifact artifact = new DefaultArtifact(g, a, v, null, t, c, handler);
            getConsoleLogger().debug("Resolving artifact " + g + ":" + a + ":" + v);
            Set<Artifact> r = surefireDependencyResolver.resolveArtifacts(
                    session.getRepositorySession(), project.getRemoteProjectRepositories(), artifact);
            getConsoleLogger().debug("Resolved artifact " + g + ":" + a + ":" + v + " to " + r);
            return r;
        }

        private boolean hasDependencyJupiterAPI(Map<String, Artifact> dependencies) {
            return dependencies.containsKey("org.junit.jupiter:junit-jupiter-api");
        }

        private boolean hasDependencyPlatformEngine(Map<String, Artifact> dependencies) {
            for (Entry<String, Artifact> dependency : dependencies.entrySet()) {
                if (dependency.getKey().equals("org.junit.platform:junit-platform-engine")) {
                    return true;
                }
            }

            return false;
        }
    }

    final class JUnitCoreProviderInfo implements ProviderInfo {
        private final Artifact junitArtifact;

        private final Artifact junitDepArtifact;

        JUnitCoreProviderInfo(Artifact junitArtifact, Artifact junitDepArtifact) {
            this.junitArtifact = junitArtifact;
            this.junitDepArtifact = junitDepArtifact;
        }

        @Override
        @Nonnull
        public String getProviderName() {
            return "org.apache.maven.surefire.junitcore.JUnitCoreProvider";
        }

        private boolean is47CompatibleJunitDep() {
            return isJunit47Compatible(junitDepArtifact);
        }

        @Override
        public boolean isApplicable() {
            final boolean isJunitArtifact47 = isAnyJunit4(junitArtifact) && isJunit47Compatible(junitArtifact);
            final boolean isAny47ProvidersForces = isAnyConcurrencySelected() || isAnyGroupsSelected();
            return isAny47ProvidersForces && (isJunitArtifact47 || is47CompatibleJunitDep());
        }

        @Override
        public void addProviderProperties() throws MojoExecutionException {
            convertJunitCoreParameters();
            convertGroupParameters();
            convertJunitEngineParameters();
        }

        @Nonnull
        @Override
        public List<String[]> getJpmsArguments(@Nonnull ProviderRequirements forkRequirements) {
            return emptyList();
        }

        @Override
        @Nonnull
        public Set<Artifact> getProviderClasspath() throws MojoExecutionException {
            String version = getBooterArtifact().getBaseVersion();
            return surefireDependencyResolver.getProviderClasspath(
                    session.getRepositorySession(), project.getRemotePluginRepositories(), "surefire-junit47", version);
        }
    }

    /**
     * Provides the Provider information for manually configured providers.
     */
    final class DynamicProviderInfo implements ConfigurableProviderInfo {
        final String providerName;

        DynamicProviderInfo(String providerName) {
            this.providerName = providerName;
        }

        @Override
        public ProviderInfo instantiate(String providerName) {
            return new DynamicProviderInfo(providerName);
        }

        @Override
        @Nonnull
        public String getProviderName() {
            return providerName;
        }

        @Override
        public boolean isApplicable() {
            return true;
        }

        @Override
        public void addProviderProperties() throws MojoExecutionException {
            // Ok this is a bit lazy.
            convertJunitCoreParameters();
            convertTestNGParameters();
        }

        @Nonnull
        @Override
        public List<String[]> getJpmsArguments(@Nonnull ProviderRequirements forkRequirements) {
            return emptyList();
        }

        @Override
        @Nonnull
        public Set<Artifact> getProviderClasspath() throws MojoExecutionException {
            Plugin plugin = getPluginDescriptor().getPlugin();
            Map<String, Artifact> providerArtifacts = surefireDependencyResolver.resolvePluginDependencies(
                    session.getRepositorySession(),
                    project.getRemotePluginRepositories(),
                    plugin,
                    getPluginArtifactMap());
            return new LinkedHashSet<>(providerArtifacts.values());
        }
    }

    File createSurefireBootDirectoryInBuild() {
        File tmp = new File(getProjectBuildDirectory(), getTempDir());
        //noinspection ResultOfMethodCallIgnored
        tmp.mkdirs();
        return tmp;
    }

    File createSurefireBootDirectoryInTemp() {
        try {
            return Files.createTempDirectory(getTempDir()).toFile();
        } catch (IOException e) {
            return createSurefireBootDirectoryInBuild();
        }
    }

    @Override
    public String getLocalRepositoryPath() {
        return Optional.ofNullable(
                        session.getRepositorySession().getLocalRepository().getBasedir())
                .map(File::getAbsolutePath)
                .orElse(".");
    }

    public Properties getSystemProperties() {
        return systemProperties;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setSystemProperties(Properties systemProperties) {
        this.systemProperties = systemProperties;
    }

    public Map<String, String> getSystemPropertyVariables() {
        return systemPropertyVariables;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setSystemPropertyVariables(Map<String, String> systemPropertyVariables) {
        this.systemPropertyVariables = systemPropertyVariables;
    }

    /**
     * List of System properties, loaded from a file, to pass to the JUnit tests.
     *
     * @since 2.8.2
     */
    public abstract File getSystemPropertiesFile();

    @SuppressWarnings("UnusedDeclaration")
    public abstract void setSystemPropertiesFile(File systemPropertiesFile);

    private Properties getProperties() {
        return properties;
    }

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    public Map<String, Artifact> getPluginArtifactMap() {
        return pluginArtifactMap;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setPluginArtifactMap(Map<String, Artifact> pluginArtifactMap) {
        this.pluginArtifactMap = pluginArtifactMap;
    }

    public Map<String, Artifact> getProjectArtifactMap() {
        return projectArtifactMap;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setProjectArtifactMap(Map<String, Artifact> projectArtifactMap) {
        this.projectArtifactMap = projectArtifactMap;
    }

    public String getReportNameSuffix() {
        return reportNameSuffix;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setReportNameSuffix(String reportNameSuffix) {
        this.reportNameSuffix = reportNameSuffix;
    }

    public boolean isRedirectTestOutputToFile() {
        return redirectTestOutputToFile;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setRedirectTestOutputToFile(boolean redirectTestOutputToFile) {
        this.redirectTestOutputToFile = redirectTestOutputToFile;
    }

    public boolean getFailIfNoTests() {
        return failIfNoTests;
    }

    public void setFailIfNoTests(boolean failIfNoTests) {
        this.failIfNoTests = failIfNoTests;
    }

    public String getJvm() {
        return jvm;
    }

    public String getArgLine() {
        return argLine;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setArgLine(String argLine) {
        this.argLine = argLine;
    }

    public Map<String, String> getEnvironmentVariables() {
        return environmentVariables;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setEnvironmentVariables(Map<String, String> environmentVariables) {
        this.environmentVariables = environmentVariables;
    }

    public File getWorkingDirectory() {
        return workingDirectory;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setWorkingDirectory(File workingDirectory) {
        this.workingDirectory = workingDirectory;
    }

    public boolean isChildDelegation() {
        return childDelegation;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setChildDelegation(boolean childDelegation) {
        this.childDelegation = childDelegation;
    }

    public String getGroups() {
        return groups;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setGroups(String groups) {
        this.groups = groups;
    }

    public String getExcludedGroups() {
        return excludedGroups;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setExcludedGroups(String excludedGroups) {
        this.excludedGroups = excludedGroups;
    }

    public String getJunitArtifactName() {
        return junitArtifactName;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setJunitArtifactName(String junitArtifactName) {
        this.junitArtifactName = junitArtifactName;
    }

    public String getTestNGArtifactName() {
        return testNGArtifactName;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setTestNGArtifactName(String testNGArtifactName) {
        this.testNGArtifactName = testNGArtifactName;
    }

    public int getThreadCount() {
        return threadCount;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setThreadCount(int threadCount) {
        this.threadCount = threadCount;
    }

    public boolean getPerCoreThreadCount() {
        return perCoreThreadCount;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setPerCoreThreadCount(boolean perCoreThreadCount) {
        this.perCoreThreadCount = perCoreThreadCount;
    }

    public boolean getUseUnlimitedThreads() {
        return useUnlimitedThreads;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setUseUnlimitedThreads(boolean useUnlimitedThreads) {
        this.useUnlimitedThreads = useUnlimitedThreads;
    }

    public String getParallel() {
        return parallel;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setParallel(String parallel) {
        this.parallel = parallel;
    }

    public boolean isParallelOptimized() {
        return parallelOptimized;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setParallelOptimized(boolean parallelOptimized) {
        this.parallelOptimized = parallelOptimized;
    }

    public int getThreadCountSuites() {
        return threadCountSuites;
    }

    public void setThreadCountSuites(int threadCountSuites) {
        this.threadCountSuites = threadCountSuites;
    }

    public int getThreadCountClasses() {
        return threadCountClasses;
    }

    public void setThreadCountClasses(int threadCountClasses) {
        this.threadCountClasses = threadCountClasses;
    }

    public int getThreadCountMethods() {
        return threadCountMethods;
    }

    public void setThreadCountMethods(int threadCountMethods) {
        this.threadCountMethods = threadCountMethods;
    }

    public boolean isTrimStackTrace() {
        return trimStackTrace;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setTrimStackTrace(boolean trimStackTrace) {
        this.trimStackTrace = trimStackTrace;
    }

    public boolean isDisableXmlReport() {
        return disableXmlReport;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setDisableXmlReport(boolean disableXmlReport) {
        this.disableXmlReport = disableXmlReport;
    }

    public boolean isEnableAssertions() {
        return enableAssertions;
    }

    public boolean effectiveIsEnableAssertions() {
        if (getArgLine() != null) {
            List<String> args = asList(getArgLine().split(" "));
            if (args.contains("-da") || args.contains("-disableassertions")) {
                return false;
            }
        }
        return isEnableAssertions();
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setEnableAssertions(boolean enableAssertions) {
        this.enableAssertions = enableAssertions;
    }

    public MavenSession getSession() {
        return session;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setSession(MavenSession session) {
        this.session = session;
    }

    public String getObjectFactory() {
        return objectFactory;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setObjectFactory(String objectFactory) {
        this.objectFactory = objectFactory;
    }

    public ToolchainManager getToolchainManager() {
        return toolchainManager;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setToolchainManager(ToolchainManager toolchainManager) {
        this.toolchainManager = toolchainManager;
    }

    public boolean isMavenParallel() {
        return parallelMavenExecution != null && parallelMavenExecution;
    }

    public String[] getDependenciesToScan() {
        return dependenciesToScan;
    }

    public void setDependenciesToScan(String[] dependenciesToScan) {
        this.dependenciesToScan = dependenciesToScan;
    }

    @SuppressWarnings("UnusedDeclaration")
    void setPluginDescriptor(PluginDescriptor pluginDescriptor) {
        this.pluginDescriptor = pluginDescriptor;
    }

    public PluginDescriptor getPluginDescriptor() {
        return pluginDescriptor;
    }

    public MavenProject getProject() {
        return project;
    }

    @SuppressWarnings("UnusedDeclaration")
    public void setProject(MavenProject project) {
        this.project = project;
    }

    @Override
    public File getTestSourceDirectory() {
        return testSourceDirectory;
    }

    @Override
    public void setTestSourceDirectory(File testSourceDirectory) {
        this.testSourceDirectory = testSourceDirectory;
    }

    public String getForkCount() {
        return forkCount;
    }

    public boolean isReuseForks() {
        return reuseForks;
    }

    public String[] getAdditionalClasspathElements() {
        return additionalClasspathElements;
    }

    public void setAdditionalClasspathElements(String[] additionalClasspathElements) {
        this.additionalClasspathElements = additionalClasspathElements;
    }

    public String[] getClasspathDependencyExcludes() {
        return classpathDependencyExcludes;
    }

    public void setClasspathDependencyExcludes(String[] classpathDependencyExcludes) {
        this.classpathDependencyExcludes = classpathDependencyExcludes;
    }

    public String getClasspathDependencyScopeExclude() {
        return classpathDependencyScopeExclude;
    }

    public void setClasspathDependencyScopeExclude(String classpathDependencyScopeExclude) {
        this.classpathDependencyScopeExclude = classpathDependencyScopeExclude;
    }

    public File getProjectBuildDirectory() {
        return projectBuildDirectory;
    }

    public void setProjectBuildDirectory(File projectBuildDirectory) {
        this.projectBuildDirectory = projectBuildDirectory;
    }

    protected void logDebugOrCliShowErrors(String s) {
        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;
    }

    public void setTempDir(String tempDir) {
        this.tempDir = tempDir;
    }

    private static final class ClasspathCache {
        private final Map<String, Classpath> classpaths = new HashMap<>(4);

        private Classpath getCachedClassPath(@Nonnull String artifactId) {
            return classpaths.get(artifactId);
        }

        private void setCachedClasspath(@Nonnull String key, @Nonnull Classpath classpath) {
            classpaths.put(key, classpath);
        }

        private Classpath setCachedClasspath(@Nonnull String key, @Nonnull Set<Artifact> artifacts) {
            Collection<String> files = new ArrayList<>();
            for (Artifact artifact : artifacts) {
                files.add(artifact.getFile().getAbsolutePath());
            }
            Classpath classpath = new Classpath(files);
            setCachedClasspath(key, classpath);
            return classpath;
        }
    }

    /**
     * Determines whether the plugin should fail if no tests found to run.
     */
    enum PluginFailureReason {
        NONE,
        COULD_NOT_RUN_SPECIFIED_TESTS,
        COULD_NOT_RUN_DEFAULT_TESTS,
    }
}
