View Javadoc

1   /**
2    * Copyright 2005-2013 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  package org.kuali.rice.test;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.apache.log4j.PropertyConfigurator;
21  import org.junit.After;
22  import org.junit.Before;
23  import org.kuali.rice.core.api.config.property.Config;
24  import org.kuali.rice.core.api.config.property.ConfigContext;
25  import org.kuali.rice.core.api.lifecycle.BaseLifecycle;
26  import org.kuali.rice.core.api.lifecycle.Lifecycle;
27  import org.kuali.rice.core.framework.resourceloader.SpringResourceLoader;
28  import org.kuali.rice.core.impl.config.property.JAXBConfigImpl;
29  import org.kuali.rice.test.data.PerSuiteUnitTestData;
30  import org.kuali.rice.test.lifecycles.PerSuiteDataLoaderLifecycle;
31  import org.springframework.beans.factory.BeanCreationNotAllowedException;
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      protected 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 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 }