View Javadoc

1   /*
2    * Copyright 2006-2011 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.kuali.rice.test;
18  
19  import org.apache.commons.lang.StringUtils;
20  import org.apache.log4j.Logger;
21  import org.apache.log4j.PropertyConfigurator;
22  import org.junit.After;
23  import org.junit.Before;
24  import org.kuali.rice.core.api.config.property.Config;
25  import org.kuali.rice.core.api.config.property.ConfigContext;
26  import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
27  import org.kuali.rice.core.api.lifecycle.Lifecycle;
28  import org.kuali.rice.core.impl.config.property.JAXBConfigImpl;
29  import org.kuali.rice.core.impl.resourceloader.SpringResourceLoader;
30  import org.kuali.rice.test.data.PerSuiteUnitTestData;
31  import org.kuali.rice.test.lifecycles.PerSuiteDataLoaderLifecycle;
32  import org.springframework.core.io.FileSystemResourceLoader;
33  import org.springframework.core.io.Resource;
34  import org.springframework.core.io.ResourceLoader;
35  
36  import javax.xml.namespace.QName;
37  import java.io.File;
38  import java.io.IOException;
39  import java.util.ArrayList;
40  import java.util.Collections;
41  import java.util.HashSet;
42  import java.util.LinkedList;
43  import java.util.List;
44  import java.util.Properties;
45  import java.util.Set;
46  
47  import static org.junit.Assert.assertNotNull;
48  import static org.junit.Assert.fail;
49  
50  
51  /**
52   * Useful superclass for all Rice test cases. Handles setup of test utilities and a test environment. Configures the
53   * Spring test environment providing a template method for custom context files in test mode. Also provides a template method
54   * for running custom transactional setUp. Tear down handles automatic tear down of objects created inside the test
55   * environment.
56   * 
57   * @author Kuali Rice Team (rice.collab@kuali.org)
58   * @since 0.9
59   */
60  public abstract class RiceTestCase extends BaseRiceTestCase {
61  
62      private static final Logger LOG = Logger.getLogger(RiceTestCase.class);
63  
64      private static final String ALT_LOG4J_CONFIG_LOCATION_PROP = "alt.log4j.config.location";
65      private static final String DEFAULT_LOG4J_CONFIG = "classpath:rice-testharness-default-log4j.properties";
66      protected static final String DEFAULT_TEST_HARNESS_SPRING_BEANS = "classpath:TestHarnessSpringBeans.xml";
67      protected static boolean SUITE_LIFE_CYCLES_RAN = false;
68      protected static boolean SUITE_LIFE_CYCLES_FAILED = false;
69      protected static String failedSuiteTestName;
70  
71      protected List<Lifecycle> perTestLifeCycles = new LinkedList<Lifecycle>();
72  
73      protected List<Lifecycle> suiteLifeCycles = new LinkedList<Lifecycle>();
74  
75      private static Set<String> perSuiteDataLoaderLifecycleNamesRun = new HashSet<String>();
76  
77      private List<String> reports = new ArrayList<String>();
78  
79      private SpringResourceLoader testHarnessSpringResourceLoader;
80      private boolean clearTables = true;
81  
82      @Override
83  	@Before
84      public void setUp() throws Exception {
85          try {
86              configureLogging();
87              logBeforeRun();
88  
89              final long initTime = System.currentTimeMillis();
90  
91              setUpInternal();
92  
93              report("Time to start all Lifecycles: " + (System.currentTimeMillis() - initTime));
94          } catch (Throwable e) {
95              e.printStackTrace();
96              tearDown();
97              throw new RuntimeException(e);
98          }
99      }
100 
101     /**
102      * Internal setUp() implementation which is invoked by the main setUp() and wrapped
103      * with exception handling.  Subclasses should override this method if they want to
104      * add set up steps that should occur in the standard set up process, wrapped by
105      * exception handling.
106      */
107     protected void setUpInternal() throws Exception {
108         assertNotNull(getModuleName());
109         setModuleName(getModuleName());
110         setBaseDirSystemProperty(getModuleName());
111 
112         this.perTestLifeCycles = getPerTestLifecycles();
113         this.suiteLifeCycles = getSuiteLifecycles();
114 
115         if (SUITE_LIFE_CYCLES_FAILED) {
116         	fail("Suite Lifecycles startup failed on test " + failedSuiteTestName + "!!!  Please see logs for details.");
117         }
118         if (!SUITE_LIFE_CYCLES_RAN) {
119 	        try {
120     	        startLifecycles(this.suiteLifeCycles);
121         	    SUITE_LIFE_CYCLES_RAN = true;
122         	} catch (Throwable e) {
123         		e.printStackTrace();
124                 SUITE_LIFE_CYCLES_RAN = false;
125                 SUITE_LIFE_CYCLES_FAILED = true;
126                 failedSuiteTestName = getFullTestName();
127                 tearDown();
128                 stopLifecycles(this.suiteLifeCycles);
129                 throw new RuntimeException(e);
130             }
131         }
132 
133         startSuiteDataLoaderLifecycles();
134 
135         startLifecycles(this.perTestLifeCycles);
136 
137     }
138 
139     /**
140      * This block is walking up the class hierarchy of the current unit test looking for PerSuiteUnitTestData annotations. If it finds one,
141      * 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 
142      * tests can extend from the same suite and so that there can be multiple suites throughout the test source branch.
143      * 
144      * @throws Exception if a PerSuiteDataLoaderLifecycle is unable to be started
145      */
146     protected void startSuiteDataLoaderLifecycles() throws Exception {
147         List<Class> classes = TestUtilities.getHierarchyClassesToHandle(getClass(), new Class[] { PerSuiteUnitTestData.class }, perSuiteDataLoaderLifecycleNamesRun);
148         for (Class c: classes) {
149             new PerSuiteDataLoaderLifecycle(c).start();
150             perSuiteDataLoaderLifecycleNamesRun.add(c.getName());
151         }
152     }
153 
154     /**
155      * maven will set this property and find resources from the config based on it. This makes eclipse testing work because
156      * we have to put the basedir in our config files in order to find things when testing from maven
157      */
158     protected void setBaseDirSystemProperty(String moduleBaseDir) {
159         if (System.getProperty("basedir") == null) {
160         	final String userDir = System.getProperty("user.dir");
161         	
162             System.setProperty("basedir", userDir + ((userDir.endsWith(File.separator + "it" + File.separator + moduleBaseDir)) ? "" : File.separator + "it" + File.separator + moduleBaseDir));
163         }
164     }
165 
166     protected String getUserDir() {
167         return System.getProperty("user.dir");
168     }
169 
170     /**
171      * Returns the basedir for the module under which the tests are currently executing.
172      */
173     protected String getBaseDir() {
174         return System.getProperty("basedir");
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         stopLifecycles(this.perTestLifeCycles);
189         logAfterRun();
190     }
191 
192     protected void logBeforeRun() {
193         LOG.info("##############################################################");
194         LOG.info("# Starting test " + getFullTestName() + "...");
195         LOG.info("# " + dumpMemory());
196         LOG.info("##############################################################");
197     }
198 
199     protected void logAfterRun() {
200         LOG.info("##############################################################");
201         LOG.info("# ...finished test " + getFullTestName());
202         LOG.info("# " + dumpMemory());
203         for (final String report : this.reports) {
204             LOG.info("# " + report);
205         }
206         LOG.info("##############################################################\n\n\n");
207     }
208     
209     protected String getFullTestName() {
210     	return getClass().getSimpleName() + "." + getName();
211     }
212 
213 	protected void configureLogging() throws IOException {
214         ResourceLoader resourceLoader = new FileSystemResourceLoader();
215         String altLog4jConfigLocation = System.getProperty(ALT_LOG4J_CONFIG_LOCATION_PROP);
216         Resource log4jConfigResource = null;
217         if (!StringUtils.isEmpty(altLog4jConfigLocation)) { 
218             log4jConfigResource = resourceLoader.getResource(altLog4jConfigLocation);
219         }
220         if (log4jConfigResource == null || !log4jConfigResource.exists()) {
221             System.out.println("Alternate Log4j config resource does not exist! " + altLog4jConfigLocation);
222             System.out.println("Using default log4j configuration: " + DEFAULT_LOG4J_CONFIG);
223             log4jConfigResource = resourceLoader.getResource(DEFAULT_LOG4J_CONFIG);
224         } else {
225             System.out.println("Using alternate log4j configuration at: " + altLog4jConfigLocation);
226         }
227         Properties p = new Properties();
228         p.load(log4jConfigResource.getInputStream());
229         PropertyConfigurator.configure(p);
230     }
231 
232 	/**
233 	 * Executes the start() method of each of the lifecycles in the given list.
234 	 */
235     protected void startLifecycles(List<Lifecycle> lifecycles) throws Exception {
236         for (Lifecycle lifecycle : lifecycles) {
237                 lifecycle.start();
238         }
239     }
240 
241     /**
242      * Executes the stop() method of each of the lifecyles in the given list.  The
243      * List of lifecycles is processed in reverse order.
244      */
245     protected void stopLifecycles(List<Lifecycle> lifecycles) throws Exception {
246         int lifecyclesSize = lifecycles.size() - 1;
247         for (int i = lifecyclesSize; i >= 0; i--) {
248             try {
249             	if (lifecycles.get(i) == null) {
250             		LOG.warn("Attempted to stop a null lifecycle");
251             	} else {
252             		if (lifecycles.get(i).isStarted()) {
253                         LOG.warn("Attempting to stop a lifecycle " + lifecycles.get(i).getClass());
254             			lifecycles.get(i).stop();
255             		}
256             	}
257             } catch (Exception e) {
258                 LOG.error("Failed to shutdown one of the lifecycles!", e);
259             }
260         }
261     }
262 
263     /**
264      * Returns the List of Lifecycles to start when the unit test suite is started
265      */
266     protected List<Lifecycle> getSuiteLifecycles() {
267         List<Lifecycle> lifecycles = new LinkedList<Lifecycle>();
268         
269         /**
270          * Initializes Rice configuration from the test harness configuration file.
271          */
272         lifecycles.add(new BaseLifecycle() {
273             @Override
274 			public void start() throws Exception {
275                 Config config = getTestHarnessConfig();
276                 ConfigContext.init(config);
277                 super.start();
278             }
279         });
280         
281         /**
282          * Loads the TestHarnessSpringBeans.xml file which obtains connections to the DB for us
283          */
284         lifecycles.add(getTestHarnessSpringResourceLoader());
285         
286         /**
287          * Establishes the TestHarnessServiceLocator so that it has a reference to the Spring context
288          * created from TestHarnessSpringBeans.xml
289          */
290         lifecycles.add(new BaseLifecycle() {
291             @Override
292 			public void start() throws Exception {
293                 TestHarnessServiceLocator.setContext(getTestHarnessSpringResourceLoader().getContext());
294                 super.start();
295             }
296         });
297         
298         /**
299          * Clears the tables in the database.
300          */
301         if (clearTables) {
302         	lifecycles.add(new ClearDatabaseLifecycle());
303         }
304         
305         /**
306          * Loads Suite Test Data
307          */
308         lifecycles.add(new BaseLifecycle() {
309         	@Override
310 			public void start() throws Exception {
311         		loadSuiteTestData();
312         		super.start();
313         	}
314         });
315         
316         Lifecycle loadApplicationLifecycle = getLoadApplicationLifecycle();
317         if (loadApplicationLifecycle != null) {
318         	lifecycles.add(loadApplicationLifecycle);
319         }
320         return lifecycles;
321     }
322     
323     /**
324      * This should return a Lifecycle that can be used to load the application
325      * being tested.  For example, this could start a Jetty Server which loads
326      * the application, or load a Spring context to establish a set of services,
327      * or any other application startup activities that the test depends upon.
328      */
329     protected Lifecycle getLoadApplicationLifecycle() {
330     	// by default return null, do nothing
331     	return null;
332     }
333 
334     /**
335      * @return Lifecycles run every test run
336      */
337     protected List<Lifecycle> getPerTestLifecycles() {
338     	List<Lifecycle> lifecycles = new LinkedList<Lifecycle>();
339         lifecycles.add(getPerTestDataLoaderLifecycle());
340         lifecycles.add(new BaseLifecycle() {
341             @Override
342 			public void start() throws Exception {
343                 loadPerTestData();
344                 super.start();
345             }
346         });
347         return lifecycles;
348     }
349     
350     /**
351      * A method that can be overridden to load test data for the unit test Suite.
352      */
353     protected void loadSuiteTestData() throws Exception {
354     	// do nothing by default, subclass can override
355     }
356     
357     /**
358      * A method that can be overridden to load test data on a test-by-test basis
359      */
360     protected void loadPerTestData() throws Exception {
361     	// do nothing by default, subclass can override
362     }
363 
364     protected void report(final String report) {
365         this.reports.add(report);
366     }
367 
368     protected String dumpMemory() {
369         final long total = Runtime.getRuntime().totalMemory();
370         final long free = Runtime.getRuntime().freeMemory();
371         final long max = Runtime.getRuntime().maxMemory();
372         return "[Memory] max: " + max + ", total: " + total + ", free: " + free;
373     }
374 
375     public SpringResourceLoader getTestHarnessSpringResourceLoader() {
376         if (testHarnessSpringResourceLoader == null) {
377             testHarnessSpringResourceLoader = new SpringResourceLoader(new QName("TestHarnessSpringContext"), getTestHarnessSpringBeansLocation(), null);
378         }
379         return testHarnessSpringResourceLoader;
380     }
381 
382     /**
383      * Returns the location of the test harness spring beans context file.
384      * Subclasses may override to specify a different location.
385      * @return the location of the test harness spring beans context file.
386      */
387     protected List<String> getTestHarnessSpringBeansLocation() {
388         return Collections.singletonList( DEFAULT_TEST_HARNESS_SPRING_BEANS );
389     }
390 
391     protected Config getTestHarnessConfig() throws Exception {
392         Config config = new JAXBConfigImpl(getConfigLocations(), System.getProperties());
393         config.parseConfig();
394         return config;
395     }
396 
397     /**
398      * Subclasses may override this method to customize the location(s) of the Rice configuration.
399      * By default it is: classpath:META-INF/" + getModuleName().toLowerCase() + "-test-config.xml"
400      * @return List of config locations to add to this tests config location.
401      */
402     protected List<String> getConfigLocations() {
403         List<String> configLocations = new ArrayList<String>();
404         configLocations.add(getRiceMasterDefaultConfigFile());
405         configLocations.add(getModuleTestConfigLocation());
406         return configLocations;
407     }
408     
409     protected String getModuleTestConfigLocation() {
410         return "classpath:META-INF/" + getModuleName().toLowerCase() + "-test-config.xml";
411     }
412 
413     protected String getRiceMasterDefaultConfigFile() {
414         return "classpath:META-INF/test-config-defaults.xml";
415     }
416 
417     /**
418      * same as the module directory in the project.
419      * 
420      * @return name of module that the tests located
421      */
422     protected abstract String getModuleName();
423 
424     protected void setClearTables(boolean clearTables) {
425     	this.clearTables = clearTables;
426     }
427     
428 }