001/* 002 * Copyright 2012-2016 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.cli.command.test; 018 019import java.lang.annotation.Annotation; 020import java.lang.reflect.Method; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.List; 024 025import org.springframework.boot.cli.compiler.GroovyCompiler; 026import org.springframework.boot.groovy.DelegateTestRunner; 027import org.springframework.util.ReflectionUtils; 028 029/** 030 * Compile and run groovy based tests. 031 * 032 * @author Phillip Webb 033 * @author Graeme Rocher 034 */ 035public class TestRunner { 036 037 private static final String JUNIT_TEST_ANNOTATION = "org.junit.Test"; 038 039 private final String[] sources; 040 041 private final GroovyCompiler compiler; 042 043 private volatile Throwable threadException; 044 045 /** 046 * Create a new {@link TestRunner} instance. 047 * @param configuration the configuration 048 * @param sources the sources 049 * @param args the args 050 */ 051 TestRunner(TestRunnerConfiguration configuration, String[] sources, String[] args) { 052 this.sources = sources.clone(); 053 this.compiler = new GroovyCompiler(configuration); 054 } 055 056 public void compileAndRunTests() throws Exception { 057 Object[] sources = this.compiler.compile(this.sources); 058 if (sources.length == 0) { 059 throw new RuntimeException( 060 "No classes found in '" + Arrays.toString(this.sources) + "'"); 061 } 062 063 // Run in new thread to ensure that the context classloader is setup 064 RunThread runThread = new RunThread(sources); 065 runThread.start(); 066 runThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { 067 @Override 068 public void uncaughtException(Thread t, Throwable ex) { 069 TestRunner.this.threadException = ex; 070 } 071 }); 072 073 runThread.join(); 074 if (this.threadException != null) { 075 TestFailedException ex = new TestFailedException(this.threadException); 076 this.threadException = null; 077 throw ex; 078 } 079 } 080 081 /** 082 * Thread used to launch the Spring Application with the correct context classloader. 083 */ 084 private class RunThread extends Thread { 085 086 private final Class<?>[] testClasses; 087 088 private final Class<?> spockSpecificationClass; 089 090 /** 091 * Create a new {@link RunThread} instance. 092 * @param sources the sources to launch 093 */ 094 RunThread(Object... sources) { 095 super("testrunner"); 096 setDaemon(true); 097 if (sources.length != 0 && sources[0] instanceof Class) { 098 setContextClassLoader(((Class<?>) sources[0]).getClassLoader()); 099 } 100 this.spockSpecificationClass = loadSpockSpecificationClass( 101 getContextClassLoader()); 102 this.testClasses = getTestClasses(sources); 103 } 104 105 private Class<?> loadSpockSpecificationClass(ClassLoader contextClassLoader) { 106 try { 107 return getContextClassLoader().loadClass("spock.lang.Specification"); 108 } 109 catch (Exception ex) { 110 return null; 111 } 112 } 113 114 private Class<?>[] getTestClasses(Object[] sources) { 115 List<Class<?>> testClasses = new ArrayList<Class<?>>(); 116 for (Object source : sources) { 117 if ((source instanceof Class) && isTestable((Class<?>) source)) { 118 testClasses.add((Class<?>) source); 119 } 120 } 121 return testClasses.toArray(new Class<?>[testClasses.size()]); 122 } 123 124 private boolean isTestable(Class<?> sourceClass) { 125 return (isJunitTest(sourceClass) || isSpockTest(sourceClass)); 126 } 127 128 private boolean isJunitTest(Class<?> sourceClass) { 129 for (Method method : sourceClass.getMethods()) { 130 for (Annotation annotation : method.getAnnotations()) { 131 if (annotation.annotationType().getName() 132 .equals(JUNIT_TEST_ANNOTATION)) { 133 return true; 134 } 135 } 136 } 137 return false; 138 } 139 140 private boolean isSpockTest(Class<?> sourceClass) { 141 return (this.spockSpecificationClass != null 142 && this.spockSpecificationClass.isAssignableFrom(sourceClass)); 143 } 144 145 @Override 146 public void run() { 147 try { 148 if (this.testClasses.length == 0) { 149 System.out.println("No tests found"); 150 } 151 else { 152 ClassLoader contextClassLoader = Thread.currentThread() 153 .getContextClassLoader(); 154 Class<?> delegateClass = contextClassLoader 155 .loadClass(DelegateTestRunner.class.getName()); 156 Class<?> resultClass = contextClassLoader 157 .loadClass("org.junit.runner.Result"); 158 Method runMethod = delegateClass.getMethod("run", Class[].class, 159 resultClass); 160 Object result = resultClass.newInstance(); 161 runMethod.invoke(null, this.testClasses, result); 162 boolean wasSuccessful = (Boolean) resultClass 163 .getMethod("wasSuccessful").invoke(result); 164 if (!wasSuccessful) { 165 throw new RuntimeException("Tests Failed."); 166 } 167 } 168 } 169 catch (Exception ex) { 170 ReflectionUtils.rethrowRuntimeException(ex); 171 } 172 } 173 174 } 175 176}