001 /* 002 * Copyright 2007 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); you may not use this file except in 005 * compliance with the License. You may obtain a copy of the License at 006 * 007 * http://www.opensource.org/licenses/ecl2.php 008 * 009 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS 010 * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific 011 * language governing permissions and limitations under the License. 012 */ 013 package org.kuali.rice.test; 014 015 import java.io.IOException; 016 import java.util.ArrayList; 017 import java.util.HashSet; 018 import java.util.LinkedList; 019 import java.util.List; 020 import java.util.ListIterator; 021 import java.util.Properties; 022 import java.util.Set; 023 024 import javax.xml.namespace.QName; 025 026 import org.apache.commons.lang.StringUtils; 027 import org.apache.log4j.Logger; 028 import org.apache.log4j.PropertyConfigurator; 029 import org.junit.After; 030 import org.junit.Before; 031 import org.kuali.rice.core.api.config.property.Config; 032 import org.kuali.rice.core.api.config.property.ConfigContext; 033 import org.kuali.rice.core.api.lifecycle.BaseLifecycle; 034 import org.kuali.rice.core.api.lifecycle.Lifecycle; 035 import org.kuali.rice.core.impl.config.property.JAXBConfigImpl; 036 import org.kuali.rice.core.framework.resourceloader.SpringResourceLoader; 037 import org.kuali.rice.test.data.PerSuiteUnitTestData; 038 import org.kuali.rice.test.lifecycles.PerSuiteDataLoaderLifecycle; 039 import org.springframework.core.io.FileSystemResourceLoader; 040 import org.springframework.core.io.Resource; 041 import org.springframework.core.io.ResourceLoader; 042 043 /** 044 * Useful superclass for all Rice test cases. Handles setup of test utilities and a test environment. Configures the 045 * Spring test environment providing a template method for custom context files in test mode. Also provides a template method 046 * for running custom transactional setUp. Tear down handles automatic tear down of objects created inside the test 047 * environment. 048 * 049 * @author Kuali Rice Team (rice.collab@kuali.org) 050 * @since 0.9 051 */ 052 public abstract class RiceTestCase extends BaseRiceTestCase { 053 054 private static final Logger LOG = Logger.getLogger(RiceTestCase.class); 055 056 private static final String ALT_LOG4J_CONFIG_LOCATION_PROP = "alt.log4j.config.location"; 057 private static final String DEFAULT_LOG4J_CONFIG = "classpath:rice-testharness-default-log4j.properties"; 058 protected static final String DEFAULT_TEST_HARNESS_SPRING_BEANS = "classpath:TestHarnessSpringBeans.xml"; 059 protected static boolean SUITE_LIFE_CYCLES_RAN = false; 060 protected static boolean SUITE_LIFE_CYCLES_FAILED = false; 061 protected static String failedSuiteTestName; 062 063 protected List<Lifecycle> perTestLifeCycles = new LinkedList<Lifecycle>(); 064 065 protected List<Lifecycle> suiteLifeCycles = new LinkedList<Lifecycle>(); 066 067 private static Set<String> perSuiteDataLoaderLifecycleNamesRun = new HashSet<String>(); 068 069 private List<String> reports = new ArrayList<String>(); 070 071 private SpringResourceLoader testHarnessSpringResourceLoader; 072 073 @Before 074 public void setUp() throws Exception { 075 try { 076 configureLogging(); 077 logBeforeRun(); 078 079 final long initTime = System.currentTimeMillis(); 080 081 setUpInternal(); 082 083 report("Time to start all Lifecycles: " + (System.currentTimeMillis() - initTime)); 084 } catch (Throwable e) { 085 e.printStackTrace(); 086 tearDown(); 087 throw new RuntimeException(e); 088 } 089 } 090 091 /** 092 * Internal setUp() implementation which is invoked by the main setUp() and wrapped 093 * with exception handling. Subclasses should override this method if they want to 094 * add set up steps that should occur in the standard set up process, wrapped by 095 * exception handling. 096 */ 097 protected void setUpInternal() throws Exception { 098 assertNotNull(getModuleName()); 099 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 }