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