View Javadoc

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