/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.plugin.testing.testng.server;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import javax.inject.Inject;
import org.eclipse.che.api.project.server.ProjectManager;
import org.eclipse.che.api.testing.server.framework.TestRunner;
import org.eclipse.che.api.testing.server.listener.AbstractTestListener;
import org.eclipse.che.api.testing.server.listener.OutputTestListener;
import org.eclipse.che.api.testing.shared.TestCase;
import org.eclipse.che.api.testing.shared.TestResult;
import org.eclipse.che.dto.server.DtoFactory;
import org.eclipse.che.plugin.testing.classpath.server.TestClasspathProvider;
import org.eclipse.che.plugin.testing.classpath.server.TestClasspathRegistry;
import org.eclipse.core.resources.ResourcesPlugin;
import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
/**
* TestNG implementation for the test runner service.
*
* <pre>
* Available Parameters for {@link TestNGRunner#execute(Map, TestClasspathProvider)}
*
* <em>absoluteProjectPath</em> : Absolute path to the project directory
* <em>updateClasspath</em> : A boolean indicating whether rebuilding of class path is required.
* <em>runClass</em> : A boolean indicating whether the test runner should execute all, TestNG XML suite test cases or
* a test class indicated by <em>fqn</em> parameter.
* <em>fqn</em> : Fully qualified class name of the test class if the <em>runClass</em> is true.
* <em>testngXML</em> : Relative path to the testng.xml file. If this parameter is set, the TestNG test runner will
* execute given testng.xml test suite, otherwise all the test classes are get executed.
* (Note: If the <em>runClass</em> parameter is true then <em>testngXML</em> parameter gets ignored.)
* </pre>
*
* @author Mirage Abeysekara
*/
public class TestNGRunner implements TestRunner {
private ClassLoader projectClassLoader;
private ProjectManager projectManager;
private TestClasspathRegistry classpathRegistry;
@Inject
public TestNGRunner(ProjectManager projectManager, TestClasspathRegistry classpathRegistry) {
this.projectManager = projectManager;
this.classpathRegistry = classpathRegistry;
}
/**
* {@inheritDoc}
*/
@Override
public TestResult execute(Map<String, String> testParameters) throws Exception {
String projectAbsolutePath = testParameters.get("absoluteProjectPath");
String xmlPath = testParameters.get("testngXML");
boolean updateClasspath = Boolean.valueOf(testParameters.get("updateClasspath"));
boolean runClass = Boolean.valueOf(testParameters.get("runClass"));
String projectPath = testParameters.get("projectPath");
String projectType = "";
if (projectManager != null) {
projectType = projectManager.getProject(projectPath).getType();
}
ClassLoader currentClassLoader = this.getClass().getClassLoader();
TestClasspathProvider classpathProvider = classpathRegistry.getTestClasspathProvider(projectType);
URLClassLoader providedClassLoader = (URLClassLoader)classpathProvider.getClassLoader(projectAbsolutePath, projectPath,
updateClasspath);
projectClassLoader = new URLClassLoader(providedClassLoader.getURLs(), null) {
@Override
protected Class< ? > findClass(String name) throws ClassNotFoundException {
if (name.startsWith("javassist.")) {
return currentClassLoader.loadClass(name);
}
return super.findClass(name);
}
};
TestResult testResult;
if (runClass) {
String fqn = testParameters.get("fqn");
testResult = run(projectAbsolutePath, fqn);
} else {
if (xmlPath == null) {
testResult = runAll(projectAbsolutePath);
} else {
testResult = runTestXML(projectAbsolutePath, ResourcesPlugin.getPathToWorkspace() + xmlPath);
}
}
testResult.setProjectPath(projectPath);
return testResult;
}
/**
* {@inheritDoc}
*/
@Override
public String getName() {
return "testng";
}
private TestResult run(String projectAbsolutePath, String testClass) throws Exception {
ClassLoader classLoader = projectClassLoader;
Class< ? > clsTest = Class.forName(testClass, true, classLoader);
return runTestClasses(projectAbsolutePath, clsTest);
}
private TestResult runAll(String projectAbsolutePath) throws Exception {
List<String> testClassNames = new ArrayList<>();
Files.walk(Paths.get(projectAbsolutePath, "target", "test-classes")).forEach(filePath -> {
if (Files.isRegularFile(filePath) && filePath.toString().toLowerCase().endsWith(".class")) {
String path = Paths.get(projectAbsolutePath, "target", "test-classes").relativize(filePath).toString();
String className = path.replace(File.separatorChar, '.');
className = className.substring(0, className.length() - 6);
testClassNames.add(className);
}
});
@SuppressWarnings("rawtypes")
List<Class> testableClasses = new ArrayList<>();
for (String className : testClassNames) {
Class< ? > clazz = Class.forName(className, false, projectClassLoader);
if (isTestable(clazz)) {
testableClasses.add(clazz);
}
}
return runTestClasses(projectAbsolutePath, testableClasses.toArray(new Class[testableClasses.size()]));
}
private boolean isTestable(Class< ? > clazz) throws ClassNotFoundException {
for (Method method : clazz.getDeclaredMethods()) {
for (Annotation annotation : method.getAnnotations()) {
if (annotation.annotationType().getName().equals("org.testng.annotations.Test")) {
return true;
}
}
}
return false;
}
private Object createTestListener(ClassLoader loader, Class< ? > listenerClass, AbstractTestListener delegate) throws Exception {
ProxyFactory f = new ProxyFactory();
f.setSuperclass(listenerClass);
f.setFilter(new MethodFilter() {
@Override
public boolean isHandled(Method m) {
String methodName = m.getName();
switch (methodName) {
case "onTestStart":
case "onTestSuccess":
case "testFailure":
case "onTestFailure":
return true;
}
return false;
}
});
Class< ? > c = f.createClass();
MethodHandler mi = new MethodHandler() {
@Override
public Object invoke(Object self, Method m, Method method, Object[] args) throws Throwable {
String methodName = m.getName();
Object testResult = null;
Throwable throwable = null;
switch (methodName) {
case "onTestStart":
case "onTestSuccess":
testResult = args[0];
throwable = null;
break;
case "onTestFailure":
testResult = args[0];
throwable = (Throwable)testResult.getClass().getMethod("getThrowable", new Class< ? >[0]).invoke(args[0]);
break;
}
Object testClass = testResult.getClass().getMethod("getTestClass").invoke(testResult);
Object testMethod = testResult.getClass().getMethod("getMethod").invoke(testResult);
String testClassName = (String)testClass.getClass().getMethod("getName").invoke(testClass);
String testMethodName = (String)testMethod.getClass().getMethod("getMethodName").invoke(testMethod);
String testKey = new StringBuilder().append(testMethodName)
.append('(').append(testClassName).append(')').toString();
String testName = testKey;
switch (methodName) {
case "onTestStart":
delegate.startTest(testKey, testName);
break;
case "onTestSuccess":
delegate.endTest(testKey, testName);
break;
case "onTestFailure":
delegate.addFailure(testKey, throwable);
delegate.endTest(testKey, testName);
break;
default:
}
return method.invoke(self, args);
}
};
Object listener = c.getConstructor().newInstance();
((javassist.util.proxy.Proxy)listener).setHandler(mi);
return listener;
}
private TestResult runTest(String projectAbsolutePath, BiConsumer<Class< ? >, Object> configure) throws Exception {
ClassLoader classLoader = projectClassLoader;
Class< ? > clsTestNG = Class.forName("org.testng.TestNG", true, classLoader);
Class< ? > clsTestListner = Class.forName("org.testng.TestListenerAdapter", true, classLoader);
Class< ? > clsITestListner = Class.forName("org.testng.ITestListener", true, classLoader);
Class< ? > clsResult = Class.forName("org.testng.ITestResult", true, classLoader);
Class< ? > clsIClass = Class.forName("org.testng.IClass", true, classLoader);
Class< ? > clsITestNGMethod = Class.forName("org.testng.ITestNGMethod", true, classLoader);
Class< ? > clsThrowable = Class.forName("java.lang.Throwable", true, classLoader);
Class< ? > clsStackTraceElement = Class.forName("java.lang.StackTraceElement", true, classLoader);
Object testNG = clsTestNG.newInstance();
Object testListner;
try (OutputTestListener outputListener = new OutputTestListener(this.getClass().getName() + ".runTest")) {
testListner = createTestListener(classLoader, clsTestListner, outputListener);
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(projectClassLoader);
clsTestNG.getMethod("addListener", clsITestListner).invoke(testNG, testListner);
configure.accept(clsTestNG, testNG);
clsTestNG.getMethod("setOutputDirectory", String.class).invoke(testNG,
Paths.get(projectAbsolutePath, "target", "testng-out")
.toString());
clsTestNG.getMethod("run").invoke(testNG);
} finally {
Thread.currentThread().setContextClassLoader(tccl);
}
}
List<Object> allTests = new ArrayList<>();
for (Object failure : (List< ? >)clsTestListner.getMethod("getFailedTests").invoke(testListner)) {
allTests.add(failure);
}
int failureCount = allTests.size();
for (Object success : (List< ? >)clsTestListner.getMethod("getPassedTests").invoke(testListner)) {
allTests.add(success);
}
TestResult dtoResult = DtoFactory.getInstance().createDto(TestResult.class);
boolean isSuccess = (failureCount == 0);
List<TestCase> testCases = new ArrayList<>();
for (Object test : allTests) {
TestCase dtoFailure = DtoFactory.getInstance().createDto(TestCase.class);
Object testClass = clsResult.getMethod("getTestClass").invoke(test);
Object testMethod = clsResult.getMethod("getMethod").invoke(test);
String testClassName = (String)clsIClass.getMethod("getName").invoke(testClass);
String testMethodName = (String)clsITestNGMethod.getMethod("getMethodName").invoke(testMethod);
dtoFailure.setClassName(testClassName);
dtoFailure.setMethod(testMethodName);
Object throwable = clsResult.getMethod("getThrowable").invoke(test);
if (throwable != null) {
String message = (String)clsThrowable.getMethod("getMessage").invoke(throwable);
Object stackTrace = clsThrowable.getMethod("getStackTrace").invoke(throwable);
Integer failLine = null;
if (stackTrace.getClass().isArray()) {
int length = Array.getLength(stackTrace);
for (int i = 0; i < length; i++) {
Object arrayElement = Array.get(stackTrace, i);
String failClass = (String)clsStackTraceElement.getMethod("getClassName").invoke(arrayElement);
String failMethod = (String)clsStackTraceElement.getMethod("getMethodName").invoke(arrayElement);
if (failClass.equals(testClassName) && failMethod.equals(testMethodName)) {
failLine = (Integer)clsStackTraceElement.getMethod("getLineNumber").invoke(arrayElement);
break;
}
}
}
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
clsThrowable.getMethod("printStackTrace", PrintWriter.class).invoke(throwable, pw);
String trace = sw.toString();
dtoFailure.setFailingLine(failLine == null ? -1 : failLine);
dtoFailure.setMessage(message);
dtoFailure.setTrace(trace);
dtoFailure.setFailed(true);
} else {
dtoFailure.setFailingLine(-1);
dtoFailure.setFailed(false);
}
testCases.add(dtoFailure);
}
dtoResult.setTestFramework("TestNG");
dtoResult.setSuccess(isSuccess);
dtoResult.setFailureCount(failureCount);
dtoResult.setTestCaseCount(testCases.size());
dtoResult.setTestCases(testCases);
return dtoResult;
}
private TestResult runTestXML(String projectAbsolutePath, String xmlPath) throws Exception {
return runTest(projectAbsolutePath, (clsTestNG, testNG) -> {
try {
List<String> testSuites = new ArrayList<>();
testSuites.add(xmlPath);
clsTestNG.getMethod("setTestSuites", List.class).invoke(testNG, testSuites);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
private TestResult runTestClasses(String projectAbsolutePath, Class< ? >... classes) throws Exception {
return runTest(projectAbsolutePath, (clsTestNG, testNG) -> {
try {
clsTestNG.getMethod("setTestClasses", Class[].class).invoke(testNG, new Object[]{classes});
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}