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