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