/*
* Copyright 2010 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
*
* 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.gradle.api.internal.tasks.testing.testng;
import org.gradle.api.GradleException;
import org.gradle.api.internal.tasks.testing.TestClassProcessor;
import org.gradle.api.internal.tasks.testing.TestClassRunInfo;
import org.gradle.api.internal.tasks.testing.TestResultProcessor;
import org.gradle.api.internal.tasks.testing.filter.TestSelectionMatcher;
import org.gradle.api.tasks.testing.testng.TestNGOptions;
import org.gradle.internal.time.TimeProvider;
import org.gradle.internal.actor.Actor;
import org.gradle.internal.actor.ActorFactory;
import org.gradle.internal.id.IdGenerator;
import org.gradle.internal.reflect.JavaReflectionUtil;
import org.gradle.internal.reflect.NoSuchMethodException;
import org.gradle.util.CollectionUtils;
import org.gradle.util.GFileUtils;
import org.testng.IMethodInstance;
import org.testng.IMethodInterceptor;
import org.testng.ISuite;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.TestNG;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class TestNGTestClassProcessor implements TestClassProcessor {
private final List<Class<?>> testClasses = new ArrayList<Class<?>>();
private final File testReportDir;
private final TestNGSpec options;
private final List<File> suiteFiles;
private final IdGenerator<?> idGenerator;
private final TimeProvider timeProvider;
private final ActorFactory actorFactory;
private ClassLoader applicationClassLoader;
private Actor resultProcessorActor;
private TestResultProcessor resultProcessor;
public TestNGTestClassProcessor(File testReportDir, TestNGSpec options, List<File> suiteFiles, IdGenerator<?> idGenerator, TimeProvider timeProvider, ActorFactory actorFactory) {
this.testReportDir = testReportDir;
this.options = options;
this.suiteFiles = suiteFiles;
this.idGenerator = idGenerator;
this.timeProvider = timeProvider;
this.actorFactory = actorFactory;
}
@Override
public void startProcessing(TestResultProcessor resultProcessor) {
// Wrap the processor in an actor, to make it thread-safe
resultProcessorActor = actorFactory.createBlockingActor(resultProcessor);
this.resultProcessor = resultProcessorActor.getProxy(TestResultProcessor.class);
applicationClassLoader = Thread.currentThread().getContextClassLoader();
}
@Override
public void processTestClass(TestClassRunInfo testClass) {
// TODO - do this inside some 'testng' suite, so that failures and logging are attached to 'testng' rather than some 'test worker'
try {
testClasses.add(applicationClassLoader.loadClass(testClass.getTestClassName()));
} catch (Throwable e) {
throw new GradleException(String.format("Could not load test class '%s'.", testClass.getTestClassName()), e);
}
}
@Override
public void stop() {
try {
runTests();
} finally {
resultProcessorActor.stop();
}
}
private void runTests() {
TestNG testNg = new TestNG();
testNg.setOutputDirectory(testReportDir.getAbsolutePath());
testNg.setDefaultSuiteName(options.getDefaultSuiteName());
testNg.setDefaultTestName(options.getDefaultTestName());
testNg.setParallel(options.getParallel());
testNg.setThreadCount(options.getThreadCount());
invokeVerifiedMethod(testNg, "setConfigFailurePolicy", String.class, options.getConfigFailurePolicy(), TestNGOptions.DEFAULT_CONFIG_FAILURE_POLICY);
invokeVerifiedMethod(testNg, "setPreserveOrder", boolean.class, options.getPreserveOrder(), false);
invokeVerifiedMethod(testNg, "setGroupByInstances", boolean.class, options.getGroupByInstances(), false);
testNg.setUseDefaultListeners(options.getUseDefaultListeners());
testNg.setVerbose(0);
testNg.setGroups(CollectionUtils.join(",", options.getIncludeGroups()));
testNg.setExcludedGroups(CollectionUtils.join(",", options.getExcludeGroups()));
//adding custom test listeners before Gradle's listeners.
//this way, custom listeners are more powerful and, for example, they can change test status.
for (String listenerClass : options.getListeners()) {
try {
testNg.addListener(applicationClassLoader.loadClass(listenerClass).newInstance());
} catch (Throwable e) {
throw new GradleException(String.format("Could not add a test listener with class '%s'.", listenerClass), e);
}
}
if (!options.getIncludedTests().isEmpty()) {
testNg.addListener(new SelectedTestsFilter(options.getIncludedTests()));
}
if (!suiteFiles.isEmpty()) {
testNg.setTestSuites(GFileUtils.toPaths(suiteFiles));
} else {
testNg.setTestClasses(testClasses.toArray(new Class[0]));
}
testNg.addListener((Object) adaptListener(new TestNGTestResultProcessorAdapter(resultProcessor, idGenerator, timeProvider)));
testNg.run();
}
private void invokeVerifiedMethod(TestNG testNg, String methodName, Class<?> paramClass, Object value, Object defaultValue) {
try {
JavaReflectionUtil.method(TestNG.class, Object.class, methodName, paramClass).invoke(testNg, value);
} catch (NoSuchMethodException e) {
if (!value.equals(defaultValue)) {
// Should not reach this point as this is validated in the test framework implementation - just propagate the failure
throw e;
}
}
}
private ITestListener adaptListener(ITestListener listener) {
TestNGListenerAdapterFactory factory = new TestNGListenerAdapterFactory(applicationClassLoader);
return factory.createAdapter(listener);
}
private static class SelectedTestsFilter implements IMethodInterceptor {
private final TestSelectionMatcher matcher;
public SelectedTestsFilter(Set<String> includedTests) {
matcher = new TestSelectionMatcher(includedTests);
}
@Override
public List<IMethodInstance> intercept(List<IMethodInstance> methods, ITestContext context) {
ISuite suite = context.getSuite();
List<IMethodInstance> filtered = new LinkedList<IMethodInstance>();
for (IMethodInstance candidate : methods) {
if (matcher.matchesTest(candidate.getMethod().getTestClass().getName(), candidate.getMethod().getMethodName())
|| matcher.matchesTest(suite.getName(), null)) {
filtered.add(candidate);
}
}
return filtered;
}
}
}