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