/* * Copyright 2023 the original author or authors. * * Licensed 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 * * https://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 rife.bld.extension; import rife.bld.BaseProject; import rife.bld.operations.AbstractProcessOperation; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; /** * Run tests with TestNG. * * @author Erik C. Thauvin * @since 1.0 */ @SuppressWarnings("PMD.TestClassWithoutTestCases") public class TestNgOperation extends AbstractProcessOperation { public static final String TEST_CLASS_ARG = "-testclass"; private static final Logger LOGGER = Logger.getLogger(TestNgOperation.class.getName()); /** * The run options. */ protected final Map options = new ConcurrentHashMap<>(); /** * The suite packages to run. */ protected final List packages = new ArrayList<>(); /** * THe suites to run. */ protected final List suites = new ArrayList<>(); private final List args = new ArrayList<>(); private BaseProject project; /** * Should MethodInvocation Listeners be run even for skipped methods. Default is {@code true}. */ public TestNgOperation alwaysRunListeners(Boolean isAlwaysRunListeners) { options.put("-alwaysrunlisteners", String.valueOf(isAlwaysRunListeners)); return this; } /** * This sets the default maximum number of threads to use for data providers when running tests in parallel. * It will only take effect if the parallel mode has been selected (for example, with the parallel option). * This can be overridden in the suite definition. */ public TestNgOperation dataProviderThreadCount(int count) { options.put("-dataproviderthreadcount", String.valueOf(count)); return this; } /** * The dependency injector factory implementation that TestNG should use. */ public TestNgOperation dependencyInjectorFactory(String injectorFactory) { options.put("-dependencyinjectorfactory", injectorFactory); return this; } /** * The directory where the reports will be generated (defaults to {@code build/test-output}). */ public TestNgOperation directory(String directoryPath) { options.put("-d", directoryPath); return this; } /** * The list of groups you want to be excluded from this run. */ public TestNgOperation excludeGroups(String... group) { options.put("-excludegroups", String.join(",", group)); return this; } /** * Part of the {@link #execute} operation, constructs the command list to use for building the process. */ @Override protected List executeConstructProcessCommandList() { if (project == null) { LOGGER.severe("A project must be specified."); } else if (packages.isEmpty() && suites.isEmpty()) { LOGGER.severe("At least one package or XML suite is required."); } if (!options.containsKey("-d")) { options.put("d", Path.of(project.buildDirectory().getPath(), "test-output").toString()); } args.clear(); args.add(javaTool()); args.add("-cp"); args.add(String.format("%s:%s:%s", Path.of(project.libTestDirectory().getPath(), "*"), project.buildMainDirectory(), project.buildTestDirectory())); args.add("org.testng.TestNG"); options.forEach((k, v) -> { args.add(k); args.add(v); }); if (!suites.isEmpty()) { args.addAll(suites); } else if (!options.containsKey(TEST_CLASS_ARG)) { try { var temp = tempFile(); try (var bufWriter = Files.newBufferedWriter(Paths.get(temp.getPath()))) { bufWriter.write("" + "" + ""); for (var p : packages) { bufWriter.write(String.format("", p)); } bufWriter.write(""); args.add(temp.getPath()); } } catch (IOException ioe) { LOGGER.log(Level.SEVERE, "An IO error occurred while accessing the default testng.xml file", ioe); } } if (LOGGER.isLoggable(Level.INFO)) { LOGGER.info(String.join(" ", args)); LOGGER.info(String.format("Report will be saved in file://%s", new File(options.get("-d")))); } return args; } /** * Configures a PMD operation from a {@link BaseProject}. */ @Override public TestNgOperation fromProject(BaseProject project) { this.project = project; directory(Path.of(project.buildDirectory().getPath(), "test-output").toString()); return this; } /** * Whether TestNG should continue to execute the remaining tests in the suite or skip them if in a {@code @Before*} * method. */ public TestNgOperation failurePolicy(FailurePolicy policy) { options.put("-configfailurepolicy", policy.name().toLowerCase(Locale.getDefault())); return this; } /** * Should TestNG consider failures in Data Providers as test failures. Default is {@code false}. */ public TestNgOperation generateResultsPerSuite(Boolean resultsPerSuite) { options.put("-generateResultsPerSuite", String.valueOf(resultsPerSuite)); return this; } /** * The list of groups you want to run (e.g. "{@code "windows", "linux", "regression}"). */ public TestNgOperation groups(String... group) { options.put("-groups", String.join(",", group)); return this; } /** * Ignore missed test names given by '-testnames' and continue to run existing tests, if any. * Default is {@code false}. */ public TestNgOperation ignoreMissedTestName(Boolean isIgnoreMissedTestNames) { options.put("-ignoreMissedTestNames", String.valueOf(isIgnoreMissedTestNames)); return this; } /** * Should TestNG report all iterations of a data driven test as individual skips, in-case of upstream failures. * Default is {@code false}. */ public TestNgOperation includeAllDataDrivenTestsWhenSkipping(Boolean isIncludeDrivenTestsWhenSkipping) { options.put("-includeAllDataDrivenTestsWhenSkipping", String.valueOf(isIncludeDrivenTestsWhenSkipping)); return this; } /** * Enables or disables the JUnit mode. Default is {@code false}. */ public TestNgOperation jUnit(Boolean isJunit) { options.put("-junit", String.valueOf(isJunit)); return this; } /** * The list of {@code .class} files or list of class names implementing {@code ITestListener} or * {@code ISuiteListener} */ public TestNgOperation listener(String... listener) { options.put("-listener", String.join(",", listener)); return this; } /** * Specifies list of {@code .class} files or list of class names implementing {@code IMethodSelector{} * For example: {@code "com.example.Selector1:3", "com.example.Selector2:2"} */ public TestNgOperation methodSelectors(String... selector) { options.put("-methodselectors", String.join(",", selector)); return this; } /** * Lets you specify individual methods to run. * For example: {@code "com.example.Foo.f1", "com.example.Bar.f2"} */ public TestNgOperation methods(String... method) { options.put("-methods", String.join(",", method)); return this; } /** * Mixed mode autodetects the type of current test and run it with appropriate runner. * Default is {@code false} */ public TestNgOperation mixed(Boolean isMixed) { options.put("-mixed", String.valueOf(isMixed)); return this; } /** * The list of {@code .class} files or list of class names implementing {@code ITestRunnerFactory}. */ public TestNgOperation objectFactory(String... factory) { options.put("-objectfactory", String.join(",", factory)); return this; } /** * The list of fully qualified class names of listeners that should be skipped from being wired in via * Service Loaders. */ public TestNgOperation overrideIncludedMethods(String... method) { options.put("-overrideincludedmethods", String.join(",", method)); return this; } /** * The list of packages to include in this test. For example: {@code "com.example", "test.sample.*"} * If the package name ends with .* then subpackages are included too. * Required if no {@link #suites(String...)} * specified. */ public TestNgOperation packages(String... name) { packages.addAll(Arrays.stream(name).toList()); return this; } /** * If specified, sets the default mechanism used to determine how to use parallel threads when running tests. * If not set, default mechanism is not to use parallel threads at all. * This can be overridden in the suite definition. */ public TestNgOperation parallel(Parallel mechanism) { options.put("-parallel", mechanism.name().toLowerCase(Locale.getDefault())); return this; } /** * Specifies the port number. */ public TestNgOperation port(int port) { options.put("-port", String.valueOf(port)); return this; } /** * Should TestNG consider failures in Data Providers as test failures. Default is {@code false}. */ public TestNgOperation propagateDataProviderFailureAsTestFailure(Boolean isPropagateDataProviderFailure) { options.put("-propagateDataProviderFailureAsTestFailure", String.valueOf(isPropagateDataProviderFailure)); return this; } /** * Specifies the extended configuration for custom report listener. */ public TestNgOperation reporter(String reporter) { options.put("-reporter", reporter); return this; } /** * The directories where your javadoc annotated test sources are. This option is only necessary * if you are using javadoc type annotations. (e.g. {@code "src/test"} or * {@code "src/test/org/testng/eclipse-plugin", "src/test/org/testng/testng"}). */ public TestNgOperation sourceDir(String... directory) { options.put("-sourcedir", String.join(";", directory)); return this; } /** * List fully qualified class names of listeners that should be skipped from being wired in via Service Loaders. */ public TestNgOperation spiListenersToSkip(String... listenerToSkip) { options.put("-spilistenerstoskip", String.join(",", listenerToSkip)); return this; } /** * This specifies the default name of test suite, if not specified in suite definition file or source code. * This option is ignored if the {@code suite.xml} file or the source code specifies a different suite name. */ public TestNgOperation suiteName(String name) { options.put("-suitename", '"' + name + '"'); return this; } /** * Specifies the size of the thread pool to use to run suites. * Required if no {@link #packages(String...)} specified. */ public TestNgOperation suiteThreadPoolSize(int poolSize) { options.put("-suitethreadpoolsize", String.valueOf(poolSize)); return this; } /** * Specifies the suites to run. For example: {@code "testng.xml", "testng2.xml"} */ public TestNgOperation suites(String... suite) { suites.addAll(Arrays.stream(suite).toList()); return this; } private File tempFile() throws IOException { var temp = File.createTempFile("testng", ".xml"); temp.deleteOnExit(); return temp; } /** * A list of class files separated by commas (e.g. {@code "org.foo.Test1","org.foo.test2"}). */ public TestNgOperation testClass(String... aClass) { options.put("-testclass", String.join(",", aClass)); return this; } /** * Specifies a jar file that contains test classes. If a testng.xml file is found at the root of that jar file, * it will be used, otherwise, all the test classes found in this jar file will be considered test classes. */ public TestNgOperation testJar(String jar) { options.put("-testjar", jar); return this; } /** * This specifies the default name of test, if not specified in suite definition file or source code. * This option is ignored if the suite.xml file or the source code specifies a different test name. */ public TestNgOperation testName(String name) { options.put("-testname", '"' + name + '"'); return this; } /** * Only tests defined in a {@code } tag matching one of these names will be run. */ public TestNgOperation testNames(String... name) { options.put("-testnames", Arrays.stream(name).map(s -> '"' + s + '"').collect(Collectors.joining(","))); return this; } /** * Specifies the factory used to create tests. */ public TestNgOperation testRunFactory(String factory) { options.put("-testrunfactory", factory); return this; } /** * This sets the default maximum number of threads to use for running tests in parallel. It will only take effect * if the parallel mode has been selected (for example, with the -parallel option). This can be overridden in the * suite definition. */ public TestNgOperation threadCount(int count) { options.put("-threadcount", String.valueOf(count)); return this; } /** * Specifies the thread pool executor factory implementation that TestNG should use. */ public TestNgOperation threadPoolFactoryClass(String factoryClass) { options.put("-threadpoolfactoryclass", factoryClass); return this; } /** * Whether to use the default listeners. Default is {@code true}. */ public TestNgOperation useDefaultListeners(Boolean isDefaultListener) { options.put("-usedefaultlisteners", String.valueOf(isDefaultListener)); return this; } /** * This attribute should contain the path to a valid XML file inside the test jar * (e.g. {@code "resources/testng.xml"}). The default is {@code testng.xml}, which means a file called * {@code testng.xml} at the root of the jar file. This option will be ignored unless a test jar is specified. */ public TestNgOperation xmlPathInJar(String path) { options.put("-xmlpathinjar", path); return this; } /** * Parallel Mechanisms */ public enum Parallel { METHODS, TESTS, CLASSES } /** * Failure Policies */ public enum FailurePolicy { SKIP, CONTINUE } }