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