/******************************************************************************* * 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.junit.server; import java.io.File; 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.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; 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 javassist.util.proxy.MethodFilter; import javassist.util.proxy.MethodHandler; import javassist.util.proxy.ProxyFactory; /** * JUnit implementation for the test runner service. * * <pre> * Available Parameters for {@link JUnitTestRunner#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 the 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. * </pre> * * @author Mirage Abeysekara * @author David Festal */ public class JUnitTestRunner implements TestRunner { private static final String JUNIT4X_RUNNER_CLASS = "org.junit.runner.JUnitCore"; private static final String JUNIT3X_RUNNER_CLASS = "junit.textui.TestRunner"; private ClassLoader projectClassLoader; private ProjectManager projectManager; private TestClasspathRegistry classpathRegistry; @Inject public JUnitTestRunner(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"); 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); } }; boolean isJUnit4Compatible = false; boolean isJUnit3Compatible = false; try { Class.forName(JUNIT4X_RUNNER_CLASS, true, projectClassLoader); isJUnit4Compatible = true; } catch (Exception ignored) { } try { Class.forName(JUNIT3X_RUNNER_CLASS, true, projectClassLoader); isJUnit3Compatible = true; } catch (Exception ignored) { } boolean useJUnitV3API = false; if (!isJUnit4Compatible) { if (!isJUnit3Compatible) { throw new ClassNotFoundException("JUnit classes not found in the following project classpath: " + Arrays.asList(providedClassLoader.getURLs())); } else { useJUnitV3API = true; } } String currentWorkingDir = System.getProperty("user.dir"); try { System.setProperty("user.dir", projectAbsolutePath); TestResult testResult; if (runClass) { String fqn = testParameters.get("fqn"); testResult = useJUnitV3API ? run3x(fqn) : run4x(fqn); } else { testResult = useJUnitV3API ? runAll3x(projectAbsolutePath) : runAll4x(projectAbsolutePath); } testResult.setProjectPath(projectPath); return testResult; } finally { System.setProperty("user.dir", currentWorkingDir); } } /** * {@inheritDoc} */ @Override public String getName() { return "junit"; } private TestResult run4x(String testClass) throws Exception { ClassLoader classLoader = projectClassLoader; Class< ? > clsTest = Class.forName(testClass, true, classLoader); return run4xTestClasses(clsTest); } private TestResult runAll4x(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 (isTestable4x(clazz)) { testableClasses.add(clazz); } } return run4xTestClasses(testableClasses.toArray(new Class[testableClasses.size()])); } private boolean isTestable4x(Class< ? > clazz) throws ClassNotFoundException { for (Method method : clazz.getDeclaredMethods()) { for (Annotation annotation : method.getAnnotations()) { if (annotation.annotationType().getName().equals("org.junit.Test")) { return true; } } } return false; } private Object create4xTestListener(ClassLoader loader, Class< ? > listenerClass, List<Object> allTests, 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 "testStarted": case "testFinished": case "testFailure": case "testAssumptionFailure": 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 description = null; Throwable throwable = null; switch (methodName) { case "testStarted": case "testFinished": description = args[0]; throwable = null; break; case "testFailure": case "testAssumptionFailure": description = args[0].getClass().getMethod("getDescription", new Class< ? >[0]).invoke(args[0]); throwable = (Throwable)args[0].getClass().getMethod("getException", new Class< ? >[0]).invoke(args[0]); break; default: return null; } String testKey = (String)description.getClass().getMethod("getDisplayName", new Class< ? >[0]).invoke(description); String testName = testKey; switch (methodName) { case "testStarted": delegate.startTest(testKey, testName); allTests.add(description); break; case "testFinished": delegate.endTest(testKey, testName); break; case "testFailure": delegate.addFailure(testKey, throwable); break; case "testAssumptionFailure": delegate.addError(testKey, throwable); break; } return null; } }; Object listener = c.getConstructor().newInstance(); ((javassist.util.proxy.Proxy)listener).setHandler(mi); return listener; } private TestResult run4xTestClasses(Class< ? >... classes) throws Exception { ClassLoader classLoader = projectClassLoader; Class< ? > clsJUnitCore = Class.forName("org.junit.runner.JUnitCore", true, classLoader); Class< ? > clsRequest = Class.forName("org.junit.runner.Request", true, classLoader); Class< ? > clsRunner = Class.forName("org.junit.runner.Runner", true, classLoader); Class< ? > clsResult = Class.forName("org.junit.runner.Result", true, classLoader); Class< ? > clsFailure = Class.forName("org.junit.runner.notification.Failure", true, classLoader); Class< ? > clsDescription = Class.forName("org.junit.runner.Description", true, classLoader); Class< ? > clsThrowable = Class.forName("java.lang.Throwable", true, classLoader); Class< ? > clsStackTraceElement = Class.forName("java.lang.StackTraceElement", true, classLoader); Class< ? > clsTestRunner = Class.forName("org.junit.runner.notification.RunListener", true, classLoader); Object jUnitCore = clsJUnitCore.getConstructor().newInstance(); Object result; List<Object> allRunTests = new ArrayList<Object>(); try (OutputTestListener outputListener = new OutputTestListener(this.getClass().getName() + ".run4xTestClasses")) { Object testListener = create4xTestListener(classLoader, clsTestRunner, allRunTests, outputListener); ClassLoader tccl = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(projectClassLoader); clsJUnitCore.getMethod("addListener", clsTestRunner).invoke(jUnitCore, testListener); Object request = clsRequest.getMethod("classes", Class[].class).invoke(null, new Object[]{classes}); Object runner = clsRequest.getMethod("getRunner").invoke(request); Object suiteDescription = clsRunner.getMethod("getDescription").invoke(runner); result = clsJUnitCore.getMethod("run", clsRequest).invoke(jUnitCore, request); } finally { Thread.currentThread().setContextClassLoader(tccl); clsJUnitCore.getMethod("removeListener", clsTestRunner).invoke(jUnitCore, testListener); } } TestResult dtoResult = DtoFactory.getInstance().createDto(TestResult.class); boolean isSuccess = (Boolean)clsResult.getMethod("wasSuccessful").invoke(result); Map<String, TestCase> testCases = new HashMap<>(); for (Object testDescription : allRunTests) { String testKey = (String)clsDescription.getMethod("getDisplayName").invoke(testDescription); TestCase dtoTestCase = DtoFactory.getInstance().createDto(TestCase.class); String className = (String)clsDescription.getMethod("getClassName").invoke(testDescription); String methodName = (String)clsDescription.getMethod("getMethodName").invoke(testDescription); dtoTestCase.setClassName(className); dtoTestCase.setMethod(methodName); dtoTestCase.setFailingLine(-1); dtoTestCase.setMessage(""); dtoTestCase.setTrace(""); testCases.put(testKey, dtoTestCase); } List< ? > failures = (List< ? >)clsResult.getMethod("getFailures").invoke(result); for (Object failure : failures) { TestCase dtoFailure = DtoFactory.getInstance().createDto(TestCase.class); Object description = clsFailure.getMethod("getDescription").invoke(failure); String testKey = (String)clsDescription.getMethod("getDisplayName").invoke(description); String className = (String)clsDescription.getMethod("getClassName").invoke(description); String methodName = (String)clsDescription.getMethod("getMethodName").invoke(description); String message = (String)clsFailure.getMethod("getMessage").invoke(failure); Object exception = clsFailure.getMethod("getException").invoke(failure); Object stackTrace = clsThrowable.getMethod("getStackTrace").invoke(exception); Integer failLine = null; if (stackTrace.getClass().isArray()) { int length = Array.getLength(stackTrace); for (int i = 0; i < length; i++) { Object stackElement = Array.get(stackTrace, i); String failClass = (String)clsStackTraceElement.getMethod("getClassName").invoke(stackElement); String failMethod = (String)clsStackTraceElement.getMethod("getMethodName").invoke(stackElement); if (failClass.equals(className) && failMethod.equals(methodName)) { failLine = (Integer)clsStackTraceElement.getMethod("getLineNumber").invoke(stackElement); break; } } } String trace = (String)clsFailure.getMethod("getTrace").invoke(failure); dtoFailure.setClassName(className); dtoFailure.setMethod(methodName); dtoFailure.setFailed(true); dtoFailure.setFailingLine(failLine == null ? -1 : failLine); dtoFailure.setMessage(message); dtoFailure.setTrace(trace); testCases.put(testKey, dtoFailure); } dtoResult.setTestFramework("JUnit4x"); dtoResult.setSuccess(isSuccess); dtoResult.setFailureCount(failures.size()); dtoResult.setTestCaseCount(testCases.size()); List<TestCase> testList = new ArrayList<>(testCases.size()); testList.addAll(testCases.values()); dtoResult.setTestCases(testList); return dtoResult; } private TestResult run3x(String testClass) throws Exception { ClassLoader classLoader = projectClassLoader; Class< ? > clsTest = Class.forName(testClass, true, classLoader); return run3xTestClasses(clsTest); } private TestResult runAll3x(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 (isTestable3x(clazz)) { testableClasses.add(clazz); } } return run3xTestClasses(testableClasses.toArray(new Class[testableClasses.size()])); } private boolean isTestable3x(Class< ? > clazz) throws ClassNotFoundException { Class< ? > superClass = Class.forName("junit.framework.TestCase", true, projectClassLoader); return superClass.isAssignableFrom(clazz); } private Object create3xTestListener(ClassLoader loader, Class< ? > listenerClass, List<Object> allRunTests, AbstractTestListener delegate) throws Exception { ProxyFactory f = new ProxyFactory(); f.setSuperclass(Object.class); f.setInterfaces(new Class< ? >[]{listenerClass}); f.setFilter(new MethodFilter() { @Override public boolean isHandled(Method m) { String methodName = m.getName(); switch (methodName) { case "startTest": case "endTest": case "addError": case "addFailure": return true; } return false; } }); Class< ? > c = f.createClass(); MethodHandler mi = new MethodHandler() { @Override public Object invoke(Object self, Method method, Method proceed, Object[] args) throws Throwable { Object testCaseObject = args[0]; allRunTests.add(testCaseObject); Class< ? > testCaseClass = testCaseObject.getClass(); String testClassName = testCaseClass.getName(); String testMethodName = (String)testCaseClass.getMethod("getName").invoke(testCaseObject); String testKey = testMethodName + "(" + testClassName + ")"; String testName = testKey; String methodName = method.getName(); switch (methodName) { case "startTest": delegate.startTest(testKey, testName); break; case "endTest": delegate.endTest(testKey, testName); break; case "addError": delegate.addError(testKey, (Throwable)args[1]); break; case "addFailure": delegate.addFailure(testKey, (Throwable)args[1]); break; } return null; } }; Object listener = c.getConstructor().newInstance(); ((javassist.util.proxy.Proxy)listener).setHandler(mi); return listener; } private TestResult run3xTestClasses(Class< ? >... classes) throws Exception { ClassLoader classLoader = projectClassLoader; Class< ? > clsTestSuite = Class.forName("junit.framework.TestSuite", true, classLoader); Class< ? > clsTestCase = Class.forName("junit.framework.TestCase", true, classLoader); Class< ? > clsTestResult = Class.forName("junit.framework.TestResult", true, classLoader); Class< ? > clsThrowable = Class.forName("java.lang.Throwable", true, classLoader); Class< ? > clsStackTraceElement = Class.forName("java.lang.StackTraceElement", true, classLoader); Class< ? > clsFailure = Class.forName("junit.framework.TestFailure", true, classLoader); Object testSuite = clsTestSuite.getConstructor().newInstance(); Object testResult = clsTestResult.getConstructor().newInstance(); Class< ? > clsTestListener = Class.forName("junit.framework.TestListener", true, classLoader); List<Object> allRunTests = new ArrayList<>(); try (OutputTestListener outputListener = new OutputTestListener(this.getClass().getName() + ".run3xTestClasses")) { Object testListener = create3xTestListener(classLoader, clsTestListener, allRunTests, outputListener); ClassLoader tccl = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(projectClassLoader); clsTestResult.getMethod("addListener", clsTestListener).invoke( testResult, testListener); for (Class< ? > testClass : classes) { clsTestSuite.getMethod("addTestSuite", Class.class).invoke(testSuite, testClass); } clsTestSuite.getMethod("run", clsTestResult).invoke(testSuite, testResult); } finally { Thread.currentThread().setContextClassLoader(tccl); clsTestResult.getMethod("removeListener", clsTestListener).invoke( testResult, testListener); } } boolean isSuccess = (Boolean)clsTestResult.getMethod("wasSuccessful").invoke(testResult); Map<String, TestCase> testCases = new HashMap<>(); TestResult dtoResult = DtoFactory.getInstance().createDto(TestResult.class); Enumeration< ? > failures = (Enumeration< ? >)clsTestResult.getMethod("failures").invoke(testResult); for (Object testDescription : allRunTests) { String className = testDescription.getClass().getName(); String methodName = (String)clsTestCase.getMethod("getName").invoke(testDescription); String testKey = methodName + "(" + className + ")"; TestCase dtoTestCase = DtoFactory.getInstance().createDto(TestCase.class); dtoTestCase.setClassName(className); dtoTestCase.setMethod(methodName); dtoTestCase.setFailingLine(-1); dtoTestCase.setMessage(""); dtoTestCase.setTrace(""); testCases.put(testKey, dtoTestCase); } int failureCount = 0; while (failures.hasMoreElements()) { failureCount++; TestCase dtoTestCase = DtoFactory.getInstance().createDto(TestCase.class); Object failure = failures.nextElement(); Object failClassObject = clsFailure.getMethod("failedTest").invoke(failure); String className = failClassObject.getClass().getName(); String methodName = (String)clsTestCase.getMethod("getName").invoke(failClassObject); String testFailureKey = methodName + "(" + className + ")"; String message = (String)clsFailure.getMethod("exceptionMessage").invoke(failure); String trace = (String)clsFailure.getMethod("trace").invoke(failure); Object exception = clsFailure.getMethod("thrownException").invoke(failure); Object stackTrace = clsThrowable.getMethod("getStackTrace").invoke(exception); 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(className) && failMethod.equals(methodName)) { failLine = (Integer)clsStackTraceElement.getMethod("getLineNumber").invoke(arrayElement); break; } } } dtoTestCase.setClassName(className); dtoTestCase.setMethod(methodName); dtoTestCase.setFailed(true); dtoTestCase.setFailingLine(failLine == null ? -1 : failLine); dtoTestCase.setMessage(message); dtoTestCase.setTrace(trace); testCases.put(testFailureKey, dtoTestCase); } dtoResult.setTestFramework("JUnit3x"); dtoResult.setSuccess(isSuccess); dtoResult.setFailureCount(failureCount); dtoResult.setTestCaseCount(testCases.size()); List<TestCase> testList = new ArrayList<>(testCases.size()); testList.addAll(testCases.values()); dtoResult.setTestCases(testList); return dtoResult; } }