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 }