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