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}