View Javadoc

1   /*
2    * Copyright 2007 The Kuali Foundation
3    * 
4    * Licensed under the Educational Community License, Version 2.0 (the "License"); you may not use this file except in
5    * compliance with the License. You may obtain a copy of the License at
6    * 
7    * http://www.opensource.org/licenses/ecl2.php
8    * 
9    * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS
10   * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
11   * language governing permissions and limitations under the License.
12   */
13  package org.kuali.rice.test;
14  
15  import java.io.IOException;
16  import java.util.ArrayList;
17  import java.util.HashSet;
18  import java.util.LinkedList;
19  import java.util.List;
20  import java.util.ListIterator;
21  import java.util.Properties;
22  import java.util.Set;
23  
24  import javax.xml.namespace.QName;
25  
26  import org.apache.commons.lang.StringUtils;
27  import org.apache.log4j.Logger;
28  import org.apache.log4j.PropertyConfigurator;
29  import org.junit.After;
30  import org.junit.Before;
31  import org.kuali.rice.core.api.config.property.Config;
32  import org.kuali.rice.core.api.config.property.ConfigContext;
33  import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
34  import org.kuali.rice.core.api.lifecycle.Lifecycle;
35  import org.kuali.rice.core.impl.config.property.JAXBConfigImpl;
36  import org.kuali.rice.core.framework.resourceloader.SpringResourceLoader;
37  import org.kuali.rice.test.data.PerSuiteUnitTestData;
38  import org.kuali.rice.test.lifecycles.PerSuiteDataLoaderLifecycle;
39  import org.springframework.core.io.FileSystemResourceLoader;
40  import org.springframework.core.io.Resource;
41  import org.springframework.core.io.ResourceLoader;
42  
43  /**
44   * Useful superclass for all Rice test cases. Handles setup of test utilities and a test environment. Configures the
45   * Spring test environment providing a template method for custom context files in test mode. Also provides a template method
46   * for running custom transactional setUp. Tear down handles automatic tear down of objects created inside the test
47   * environment.
48   * 
49   * @author Kuali Rice Team (rice.collab@kuali.org)
50   * @since 0.9
51   */
52  public abstract class RiceTestCase extends BaseRiceTestCase {
53  
54      private static final Logger LOG = Logger.getLogger(RiceTestCase.class);
55  
56      private static final String ALT_LOG4J_CONFIG_LOCATION_PROP = "alt.log4j.config.location";
57      private static final String DEFAULT_LOG4J_CONFIG = "classpath:rice-testharness-default-log4j.properties";
58      protected static final String DEFAULT_TEST_HARNESS_SPRING_BEANS = "classpath:TestHarnessSpringBeans.xml";
59      protected static boolean SUITE_LIFE_CYCLES_RAN = false;
60      protected static boolean SUITE_LIFE_CYCLES_FAILED = false;
61      protected static String failedSuiteTestName;
62  
63      protected List<Lifecycle> perTestLifeCycles = new LinkedList<Lifecycle>();
64  
65      protected List<Lifecycle> suiteLifeCycles = new LinkedList<Lifecycle>();
66  
67      private static Set<String> perSuiteDataLoaderLifecycleNamesRun = new HashSet<String>();
68  
69      private List<String> reports = new ArrayList<String>();
70  
71      private SpringResourceLoader testHarnessSpringResourceLoader;
72  
73      @Before
74      public void setUp() throws Exception {
75          try {
76              configureLogging();
77              logBeforeRun();
78  
79              final long initTime = System.currentTimeMillis();
80  
81              setUpInternal();
82  
83              report("Time to start all Lifecycles: " + (System.currentTimeMillis() - initTime));
84          } catch (Throwable e) {
85              e.printStackTrace();
86              tearDown();
87              throw new RuntimeException(e);
88          }
89      }
90  
91      /**
92       * Internal setUp() implementation which is invoked by the main setUp() and wrapped
93       * with exception handling.  Subclasses should override this method if they want to
94       * add set up steps that should occur in the standard set up process, wrapped by
95       * exception handling.
96       */
97      protected void setUpInternal() throws Exception {
98          assertNotNull(getModuleName());
99          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 }