001/**
002 * Copyright 2005-2014 The Kuali Foundation
003 *
004 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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 */
016package org.kuali.rice.test;
017
018import org.apache.logging.log4j.Logger;
019import org.apache.logging.log4j.LogManager;
020import org.junit.After;
021import org.junit.Before;
022import org.kuali.rice.core.api.config.property.Config;
023import org.kuali.rice.core.api.config.property.ConfigContext;
024import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
025import org.kuali.rice.core.api.lifecycle.Lifecycle;
026import org.kuali.rice.core.framework.resourceloader.SpringResourceLoader;
027import org.kuali.rice.core.impl.config.property.JAXBConfigImpl;
028import org.kuali.rice.test.data.PerSuiteUnitTestData;
029import org.kuali.rice.test.lifecycles.PerSuiteDataLoaderLifecycle;
030import org.springframework.beans.factory.BeanCreationNotAllowedException;
031
032import javax.xml.namespace.QName;
033import java.io.File;
034import java.io.IOException;
035import java.util.ArrayList;
036import java.util.Collections;
037import java.util.HashSet;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.Set;
041
042import static org.junit.Assert.assertNotNull;
043import static org.junit.Assert.fail;
044
045
046/**
047 * Useful superclass for all Rice test cases. Handles setup of test utilities and a test environment. Configures the
048 * Spring test environment providing a template method for custom context files in test mode. Also provides a template method
049 * for running custom transactional setUp. Tear down handles automatic tear down of objects created inside the test
050 * environment.
051 * 
052 * @author Kuali Rice Team (rice.collab@kuali.org)
053 * @since 0.9
054 */
055public abstract class RiceTestCase extends BaseRiceTestCase {
056
057    protected static final Logger LOG = LogManager.getLogger(RiceTestCase.class);
058
059    protected static final String DEFAULT_TEST_HARNESS_SPRING_BEANS = "classpath:TestHarnessSpringBeans.xml";
060    protected static boolean SUITE_LIFE_CYCLES_RAN = false;
061    protected static boolean SUITE_LIFE_CYCLES_FAILED = false;
062    protected static String failedSuiteTestName;
063
064    protected List<Lifecycle> perTestLifeCycles = new LinkedList<Lifecycle>();
065
066    protected List<Lifecycle> suiteLifeCycles = new LinkedList<Lifecycle>();
067
068    private static Set<String> perSuiteDataLoaderLifecycleNamesRun = new HashSet<String>();
069
070    private List<String> reports = new ArrayList<String>();
071
072    private SpringResourceLoader testHarnessSpringResourceLoader;
073    private boolean clearTables = true;
074
075    private long testStart;
076    private long testEnd;
077
078    @Override
079        @Before
080    public void setUp() throws Exception {
081        testStart = System.currentTimeMillis();
082        try {
083            configureLogging();
084            logBeforeRun();
085
086            final long initTime = System.currentTimeMillis();
087
088            setUpInternal();
089
090            report("Time to start all Lifecycles: " + (System.currentTimeMillis() - initTime));
091        } catch (Throwable e) {
092            e.printStackTrace();
093            tearDown();
094            throw new RuntimeException(e);
095        }
096    }
097
098    /**
099     * Internal setUp() implementation which is invoked by the main setUp() and wrapped with exception handling
100     *
101     * <p>Subclasses should override this method if they want to
102     * add set up steps that should occur in the standard set up process, wrapped by
103     * exception handling.</p>
104     */
105    protected void setUpInternal() throws Exception {
106        assertNotNull(getModuleName());
107        setModuleName(getModuleName());
108        setBaseDirSystemProperty(getModuleName());
109
110        this.perTestLifeCycles = getPerTestLifecycles();
111        this.suiteLifeCycles = getSuiteLifecycles();
112
113        if (SUITE_LIFE_CYCLES_FAILED) {
114                fail("Suite Lifecycles startup failed on test " + failedSuiteTestName + "!!!  Please see logs for details.");
115        }
116        if (!SUITE_LIFE_CYCLES_RAN) {
117                try {
118                startLifecycles(this.suiteLifeCycles);
119                    SUITE_LIFE_CYCLES_RAN = true;
120                } catch (Throwable e) {
121                        e.printStackTrace();
122                SUITE_LIFE_CYCLES_RAN = false;
123                SUITE_LIFE_CYCLES_FAILED = true;
124                failedSuiteTestName = getFullTestName();
125                tearDown();
126                stopLifecycles(this.suiteLifeCycles);
127                throw new RuntimeException(e);
128            }
129        }
130
131        startSuiteDataLoaderLifecycles();
132
133        startLifecycles(this.perTestLifeCycles);
134
135    }
136
137    /**
138     * This block is walking up the class hierarchy of the current unit test looking for PerSuiteUnitTestData annotations. If it finds one,
139     * it will run it once, then add it to a set so that it does not get run again. This is needed so that multiple 
140     * tests can extend from the same suite and so that there can be multiple suites throughout the test source branch.
141     * 
142     * @throws Exception if a PerSuiteDataLoaderLifecycle is unable to be started
143     */
144    protected void startSuiteDataLoaderLifecycles() throws Exception {
145        List<Class> classes = TestUtilities.getHierarchyClassesToHandle(getClass(), new Class[] { PerSuiteUnitTestData.class }, perSuiteDataLoaderLifecycleNamesRun);
146        for (Class c: classes) {
147            new PerSuiteDataLoaderLifecycle(c).start();
148            perSuiteDataLoaderLifecycleNamesRun.add(c.getName());
149        }
150    }
151
152    /**
153     * maven will set this property and find resources from the config based on it. This makes eclipse testing work because
154     * we have to put the basedir in our config files in order to find things when testing from maven
155     */
156    protected void setBaseDirSystemProperty(String moduleBaseDir) {
157        if (System.getProperty("basedir") == null) {
158                final String userDir = System.getProperty("user.dir");
159                
160            System.setProperty("basedir", userDir + ((userDir.endsWith(File.separator + "it" + File.separator + moduleBaseDir)) ? "" : File.separator + "it" + File.separator + moduleBaseDir));
161        }
162    }
163
164    /**
165     * the absolute path on the file system to the root folder of the maven module containing a child of this class
166     * e.g. for krad: [rice-project-dir]/it/krad
167     *
168     * <p>
169     * the user.dir property can be set on the CLI or IDE run configuration e.g. -Duser.dir=/some/dir
170     * </p>
171     * @return the value of a system property 'user.dir' if it exists, null if not
172     */
173    protected String getUserDir() {
174        return System.getProperty("user.dir");
175    }
176
177    protected void setModuleName(String moduleName) {
178        if (System.getProperty("module.name") == null) {
179            System.setProperty("module.name", moduleName);
180        }
181    }
182
183    @Override
184        @After
185    public void tearDown() throws Exception {
186        // wait for outstanding threads to complete for 1 minute
187        ThreadMonitor.tearDown(60000);
188        try {
189            stopLifecycles(this.perTestLifeCycles);
190        // Avoid failing test for creation of bean in destroy.
191        } catch (BeanCreationNotAllowedException bcnae) {
192            LOG.warn("BeanCreationNotAllowedException during stopLifecycles during tearDown " + bcnae.getMessage());
193        }
194        testEnd = System.currentTimeMillis();
195        report("Total time to run test: " + (testEnd - testStart));
196        logAfterRun();
197    }
198
199    protected void logBeforeRun() {
200        LOG.info("##############################################################");
201        LOG.info("# Starting test " + getFullTestName() + "...");
202        LOG.info("# " + dumpMemory());
203        LOG.info("##############################################################");
204    }
205
206    protected void logAfterRun() {
207        LOG.info("##############################################################");
208        LOG.info("# ...finished test " + getFullTestName());
209        LOG.info("# " + dumpMemory());
210        for (final String report : this.reports) {
211            LOG.info("# " + report);
212        }
213        LOG.info("##############################################################\n\n\n");
214    }
215    
216    protected String getFullTestName() {
217        return getClass().getSimpleName() + "." + getName();
218    }
219
220    /**
221     * configures logging using custom properties file if specified, or the default one.
222     * Log4j also uses any file called log4.properties in the classpath
223     *
224     * <p>To configure a custom logging file, set a JVM system property on using -D. For example
225     * -Dalt.log4j.config.location=file:/home/me/kuali/test/dev/log4j.properties
226     * </p>
227     *
228     * <p>The above option can also be set in the run configuration for the unit test in the IDE.
229     * To avoid log4j using files called log4j.properties that are defined in the classpath, add the following system property:
230     * -Dlog4j.defaultInitOverride=true
231     * </p>
232     * @throws IOException
233     */
234        protected void configureLogging() throws IOException {
235
236    }
237
238        /**
239         * Executes the start() method of each of the lifecycles in the given list.
240         */
241    protected void startLifecycles(List<Lifecycle> lifecycles) throws Exception {
242        for (Lifecycle lifecycle : lifecycles) {
243                lifecycle.start();
244        }
245    }
246
247    /**
248     * Executes the stop() method of each of the lifecyles in the given list.  The
249     * List of lifecycles is processed in reverse order.
250     */
251    protected void stopLifecycles(List<Lifecycle> lifecycles) throws Exception {
252        int lifecyclesSize = lifecycles.size() - 1;
253        for (int i = lifecyclesSize; i >= 0; i--) {
254            try {
255                if (lifecycles.get(i) == null) {
256                        LOG.warn("Attempted to stop a null lifecycle");
257                } else {
258                        if (lifecycles.get(i).isStarted()) {
259                        LOG.warn("Attempting to stop a lifecycle " + lifecycles.get(i).getClass());
260                                lifecycles.get(i).stop();
261                        }
262                }
263            } catch (Exception e) {
264                LOG.error("Failed to shutdown one of the lifecycles!", e);
265            }
266        }
267    }
268
269    /**
270     * Returns the List of Lifecycles to start when the unit test suite is started
271     */
272    protected List<Lifecycle> getSuiteLifecycles() {
273        List<Lifecycle> lifecycles = new LinkedList<Lifecycle>();
274        
275        /**
276         * Initializes Rice configuration from the test harness configuration file.
277         */
278        lifecycles.add(new BaseLifecycle() {
279            @Override
280                        public void start() throws Exception {
281                Config config = getTestHarnessConfig();
282                ConfigContext.init(config);
283                super.start();
284            }
285        });
286        
287        /**
288         * Loads the TestHarnessSpringBeans.xml file which obtains connections to the DB for us
289         */
290        lifecycles.add(getTestHarnessSpringResourceLoader());
291        
292        /**
293         * Establishes the TestHarnessServiceLocator so that it has a reference to the Spring context
294         * created from TestHarnessSpringBeans.xml
295         */
296        lifecycles.add(new BaseLifecycle() {
297            @Override
298                        public void start() throws Exception {
299                TestHarnessServiceLocator.setContext(getTestHarnessSpringResourceLoader().getContext());
300                super.start();
301            }
302        });
303        
304        /**
305         * Clears the tables in the database.
306         */
307        if (clearTables) {
308                lifecycles.add(new ClearDatabaseLifecycle());
309        }
310        
311        /**
312         * Loads Suite Test Data
313         */
314        lifecycles.add(new BaseLifecycle() {
315                @Override
316                        public void start() throws Exception {
317                        loadSuiteTestData();
318                        super.start();
319                }
320        });
321        
322        Lifecycle loadApplicationLifecycle = getLoadApplicationLifecycle();
323        if (loadApplicationLifecycle != null) {
324                lifecycles.add(loadApplicationLifecycle);
325        }
326        return lifecycles;
327    }
328    
329    /**
330     * This should return a Lifecycle that can be used to load the application
331     * being tested.  For example, this could start a Jetty Server which loads
332     * the application, or load a Spring context to establish a set of services,
333     * or any other application startup activities that the test depends upon.
334     */
335    protected Lifecycle getLoadApplicationLifecycle() {
336        // by default return null, do nothing
337        return null;
338    }
339
340    /**
341     * @return Lifecycles run every test run
342     */
343    protected List<Lifecycle> getPerTestLifecycles() {
344        List<Lifecycle> lifecycles = new LinkedList<Lifecycle>();
345        lifecycles.add(getPerTestDataLoaderLifecycle());
346        lifecycles.add(new BaseLifecycle() {
347            @Override
348                        public void start() throws Exception {
349                loadPerTestData();
350                super.start();
351            }
352        });
353        return lifecycles;
354    }
355    
356    /**
357     * A method that can be overridden to load test data for the unit test Suite.
358     */
359    protected void loadSuiteTestData() throws Exception {
360        // do nothing by default, subclass can override
361    }
362    
363    /**
364     * A method that can be overridden to load test data on a test-by-test basis
365     */
366    protected void loadPerTestData() throws Exception {
367        // do nothing by default, subclass can override
368    }
369
370    protected void report(final String report) {
371        this.reports.add(report);
372    }
373
374    protected String dumpMemory() {
375        final long total = Runtime.getRuntime().totalMemory();
376        final long free = Runtime.getRuntime().freeMemory();
377        final long max = Runtime.getRuntime().maxMemory();
378        return "[Memory] max: " + max + ", total: " + total + ", free: " + free;
379    }
380
381    public SpringResourceLoader getTestHarnessSpringResourceLoader() {
382        if (testHarnessSpringResourceLoader == null) {
383            testHarnessSpringResourceLoader = new SpringResourceLoader(new QName("TestHarnessSpringContext"), getTestHarnessSpringBeansLocation(), null);
384        }
385        return testHarnessSpringResourceLoader;
386    }
387
388    /**
389     * Returns the location of the test harness spring beans context file.
390     * Subclasses may override to specify a different location.
391     * @return the location of the test harness spring beans context file.
392     */
393    protected List<String> getTestHarnessSpringBeansLocation() {
394        return Collections.singletonList( DEFAULT_TEST_HARNESS_SPRING_BEANS );
395    }
396
397    protected Config getTestHarnessConfig() throws Exception {
398        Config config = new JAXBConfigImpl(getConfigLocations(), System.getProperties());
399        config.parseConfig();
400        return config;
401    }
402
403    /**
404     * Subclasses may override this method to customize the location(s) of the Rice configuration.
405     * By default it is: classpath:META-INF/" + getModuleName().toLowerCase() + "-test-config.xml"
406     * @return List of config locations to add to this tests config location.
407     */
408    protected List<String> getConfigLocations() {
409        List<String> configLocations = new ArrayList<String>();
410        configLocations.add(getRiceMasterDefaultConfigFile());
411        configLocations.add(getModuleTestConfigLocation());
412        return configLocations;
413    }
414    
415    protected String getModuleTestConfigLocation() {
416        return "classpath:META-INF/" + getModuleName().toLowerCase() + "-test-config.xml";
417    }
418
419    protected String getRiceMasterDefaultConfigFile() {
420        return "classpath:META-INF/test-config-defaults.xml";
421    }
422
423    /**
424     * same as the module directory in the project.
425     * 
426     * @return name of module that the tests located
427     */
428    protected abstract String getModuleName();
429
430    protected void setClearTables(boolean clearTables) {
431        this.clearTables = clearTables;
432    }
433    
434}