001: // Copyright 2004, 2005 The Apache Software Foundation
002: //
003: // Licensed under the Apache License, Version 2.0 (the "License");
004: // you may not use this file except in compliance with the License.
005: // You may obtain a copy of the License at
006: //
007: // http://www.apache.org/licenses/LICENSE-2.0
008: //
009: // Unless required by applicable law or agreed to in writing, software
010: // distributed under the License is distributed on an "AS IS" BASIS,
011: // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: // See the License for the specific language governing permissions and
013: // limitations under the License.
014:
015: package org.apache.hivemind.test;
016:
017: import java.net.URL;
018: import java.util.ArrayList;
019: import java.util.Iterator;
020: import java.util.List;
021: import java.util.Locale;
022:
023: import junit.framework.AssertionFailedError;
024: import junit.framework.TestCase;
025:
026: import org.apache.hivemind.ApplicationRuntimeException;
027: import org.apache.hivemind.ClassResolver;
028: import org.apache.hivemind.Location;
029: import org.apache.hivemind.ModuleDescriptorProvider;
030: import org.apache.hivemind.Registry;
031: import org.apache.hivemind.Resource;
032: import org.apache.hivemind.impl.DefaultClassResolver;
033: import org.apache.hivemind.impl.LocationImpl;
034: import org.apache.hivemind.impl.RegistryBuilder;
035: import org.apache.hivemind.impl.XmlModuleDescriptorProvider;
036: import org.apache.hivemind.internal.ser.ServiceSerializationHelper;
037: import org.apache.hivemind.util.ClasspathResource;
038: import org.apache.hivemind.util.PropertyUtils;
039: import org.apache.hivemind.util.URLResource;
040: import org.apache.log4j.Level;
041: import org.apache.log4j.LogManager;
042: import org.apache.log4j.Logger;
043: import org.apache.log4j.spi.LoggingEvent;
044: import org.apache.oro.text.regex.Pattern;
045: import org.apache.oro.text.regex.Perl5Compiler;
046: import org.apache.oro.text.regex.Perl5Matcher;
047: import org.easymock.MockControl;
048: import org.easymock.classextension.MockClassControl;
049:
050: /**
051: * Contains some support for creating HiveMind tests; this is useful enough that has been moved into
052: * the main framework, to simplify creation of tests in the dependent libraries.
053: *
054: * @author Howard Lewis Ship
055: */
056: public abstract class HiveMindTestCase extends TestCase {
057: // /CLOVER:OFF
058:
059: /**
060: * An instance of {@link DefaultClassResolver} that can be used by tests.
061: */
062:
063: private ClassResolver _classResolver;
064:
065: protected String _interceptedLoggerName;
066:
067: protected StoreAppender _appender;
068:
069: private static Perl5Compiler _compiler;
070:
071: private static Perl5Matcher _matcher;
072:
073: /** List of {@link org.easymock.MockControl}. */
074:
075: private List _controls = new ArrayList();
076:
077: /** @since 1.1 */
078: interface MockControlFactory {
079: public MockControl newControl(Class mockClass);
080: }
081:
082: /** @since 1.1 */
083: private static class InterfaceMockControlFactory implements
084: MockControlFactory {
085: public MockControl newControl(Class mockClass) {
086: return MockControl.createStrictControl(mockClass);
087: }
088: }
089:
090: /** @since 1.1 */
091: private static class ClassMockControlFactory implements
092: MockControlFactory {
093: public MockControl newControl(Class mockClass) {
094: return MockClassControl.createStrictControl(mockClass);
095: }
096: }
097:
098: /** @since 1.1 */
099: static class PlaceholderClassMockControlFactory implements
100: MockControlFactory {
101: public MockControl newControl(Class mockClass) {
102: throw new RuntimeException(
103: "Unable to instantiate EasyMock control for "
104: + mockClass
105: + "; ensure that easymockclassextension-1.1.jar and cglib-full-2.0.1.jar are on the classpath.");
106: }
107: }
108:
109: /** @since 1.1 */
110: private static final MockControlFactory _interfaceMockControlFactory = new InterfaceMockControlFactory();
111:
112: /** @since 1.1 */
113: private static MockControlFactory _classMockControlFactory;
114:
115: static {
116: try {
117: _classMockControlFactory = new ClassMockControlFactory();
118: } catch (NoClassDefFoundError ex) {
119: _classMockControlFactory = new PlaceholderClassMockControlFactory();
120: }
121: }
122:
123: /**
124: * Returns the given file as a {@link Resource} from the classpath. Typically, this is to find
125: * files in the same folder as the invoking class.
126: */
127: protected Resource getResource(String file) {
128: URL url = getClass().getResource(file);
129:
130: if (url == null)
131: throw new NullPointerException("No resource named '" + file
132: + "'.");
133:
134: return new URLResource(url);
135: }
136:
137: /**
138: * Converts the actual list to an array and invokes
139: * {@link #assertListsEqual(Object[], Object[])}.
140: */
141: protected static void assertListsEqual(Object[] expected,
142: List actual) {
143: assertListsEqual(expected, actual.toArray());
144: }
145:
146: /**
147: * Asserts that the two arrays are equal; same length and all elements equal. Checks the
148: * elements first, then the length.
149: */
150: protected static void assertListsEqual(Object[] expected,
151: Object[] actual) {
152: assertNotNull(actual);
153:
154: int min = Math.min(expected.length, actual.length);
155:
156: for (int i = 0; i < min; i++)
157: assertEquals("list[" + i + "]", expected[i], actual[i]);
158:
159: assertEquals("list length", expected.length, actual.length);
160: }
161:
162: /**
163: * Called when code should not be reachable (because a test is expected to throw an exception);
164: * throws AssertionFailedError always.
165: */
166: protected static void unreachable() {
167: throw new AssertionFailedError(
168: "This code should be unreachable.");
169: }
170:
171: /**
172: * Sets up an appender to intercept logging for the specified logger. Captured log events can be
173: * recovered via {@link #getInterceptedLogEvents()}.
174: */
175: protected void interceptLogging(String loggerName) {
176: Logger logger = LogManager.getLogger(loggerName);
177:
178: logger.removeAllAppenders();
179:
180: _interceptedLoggerName = loggerName;
181: _appender = new StoreAppender();
182: _appender.activateOptions();
183:
184: logger.setLevel(Level.DEBUG);
185: logger.setAdditivity(false);
186: logger.addAppender(_appender);
187: }
188:
189: /**
190: * Gets the list of events most recently intercepted. This resets the appender, clearing the
191: * list of stored events.
192: *
193: * @see #interceptLogging(String)
194: */
195:
196: protected List getInterceptedLogEvents() {
197: return _appender.getEvents();
198: }
199:
200: /**
201: * Removes the appender that may have been setup by {@link #interceptLogging(String)}. Also,
202: * invokes {@link org.apache.hivemind.util.PropertyUtils#clearCache()}.
203: */
204: protected void tearDown() throws Exception {
205: super .tearDown();
206:
207: if (_appender != null) {
208: _appender = null;
209:
210: Logger logger = LogManager
211: .getLogger(_interceptedLoggerName);
212: logger.setLevel(null);
213: logger.setAdditivity(true);
214: logger.removeAllAppenders();
215: }
216:
217: PropertyUtils.clearCache();
218:
219: ServiceSerializationHelper.setServiceSerializationSupport(null);
220: }
221:
222: /**
223: * Checks that the provided substring exists in the exceptions message.
224: */
225: protected void assertExceptionSubstring(Throwable ex,
226: String substring) {
227: String message = ex.getMessage();
228: assertNotNull(message);
229:
230: int pos = message.indexOf(substring);
231:
232: if (pos < 0)
233: throw new AssertionFailedError("Exception message ("
234: + message + ") does not contain [" + substring
235: + "]");
236: }
237:
238: /**
239: * Checks that the message for an exception matches a regular expression.
240: */
241:
242: protected void assertExceptionRegexp(Throwable ex, String pattern)
243: throws Exception {
244: String message = ex.getMessage();
245: assertNotNull(message);
246:
247: setupMatcher();
248:
249: Pattern compiled = _compiler.compile(pattern);
250:
251: if (_matcher.contains(message, compiled))
252: return;
253:
254: throw new AssertionFailedError("Exception message (" + message
255: + ") does not contain regular expression [" + pattern
256: + "].");
257: }
258:
259: protected void assertRegexp(String pattern, String actual)
260: throws Exception {
261: setupMatcher();
262:
263: Pattern compiled = _compiler.compile(pattern);
264:
265: if (_matcher.contains(actual, compiled))
266: return;
267:
268: throw new AssertionFailedError("\"" + actual
269: + "\" does not contain regular expression[" + pattern
270: + "].");
271: }
272:
273: /**
274: * Digs down through (potentially) a stack of ApplicationRuntimeExceptions until it reaches the
275: * originating exception, which is returned.
276: */
277: protected Throwable findNestedException(
278: ApplicationRuntimeException ex) {
279: Throwable cause = ex.getRootCause();
280:
281: if (cause == null || cause == ex)
282: return ex;
283:
284: if (cause instanceof ApplicationRuntimeException)
285: return findNestedException((ApplicationRuntimeException) cause);
286:
287: return cause;
288: }
289:
290: /**
291: * Checks to see if a specific event matches the name and message.
292: *
293: * @param message
294: * exact message to search for
295: * @param events
296: * the list of events {@link #getInterceptedLogEvents()}
297: * @param index
298: * the index to check at
299: */
300: private void assertLoggedMessage(String message, List events,
301: int index) {
302: LoggingEvent e = (LoggingEvent) events.get(index);
303:
304: assertEquals("Message", message, e.getMessage());
305: }
306:
307: /**
308: * Checks the messages for all logged events for exact match against the supplied list.
309: */
310: protected void assertLoggedMessages(String[] messages) {
311: List events = getInterceptedLogEvents();
312:
313: for (int i = 0; i < messages.length; i++) {
314: assertLoggedMessage(messages[i], events, i);
315: }
316: }
317:
318: /**
319: * Asserts that some capture log event matches the given message exactly.
320: */
321: protected void assertLoggedMessage(String message) {
322: assertLoggedMessage(message, getInterceptedLogEvents());
323: }
324:
325: /**
326: * Asserts that some capture log event matches the given message exactly.
327: *
328: * @param message
329: * to search for; success is finding a logged message contain the parameter as a
330: * substring
331: * @param events
332: * from {@link #getInterceptedLogEvents()}
333: */
334: protected void assertLoggedMessage(String message, List events) {
335: int count = events.size();
336:
337: for (int i = 0; i < count; i++) {
338: LoggingEvent e = (LoggingEvent) events.get(i);
339:
340: String eventMessage = String.valueOf(e.getMessage());
341:
342: if (eventMessage.indexOf(message) >= 0)
343: return;
344: }
345:
346: throw new AssertionFailedError(
347: "Could not find logged message: " + message);
348: }
349:
350: protected void assertLoggedMessagePattern(String pattern)
351: throws Exception {
352: assertLoggedMessagePattern(pattern, getInterceptedLogEvents());
353: }
354:
355: protected void assertLoggedMessagePattern(String pattern,
356: List events) throws Exception {
357: setupMatcher();
358:
359: Pattern compiled = null;
360:
361: int count = events.size();
362:
363: for (int i = 0; i < count; i++) {
364: LoggingEvent e = (LoggingEvent) events.get(i);
365:
366: String eventMessage = e.getMessage().toString();
367:
368: if (compiled == null)
369: compiled = _compiler.compile(pattern);
370:
371: if (_matcher.contains(eventMessage, compiled))
372: return;
373:
374: }
375:
376: throw new AssertionFailedError(
377: "Could not find logged message with pattern: "
378: + pattern);
379: }
380:
381: private void setupMatcher() {
382: if (_compiler == null)
383: _compiler = new Perl5Compiler();
384:
385: if (_matcher == null)
386: _matcher = new Perl5Matcher();
387: }
388:
389: /**
390: * Convienience method for invoking {@link #buildFrameworkRegistry(String[])} with only a single
391: * file.
392: */
393: protected Registry buildFrameworkRegistry(String file)
394: throws Exception {
395: return buildFrameworkRegistry(new String[] { file });
396: }
397:
398: /**
399: * Builds a minimal registry, containing only the specified files, plus the master module
400: * descriptor (i.e., those visible on the classpath). Files are resolved using
401: * {@link HiveMindTestCase#getResource(String)}.
402: */
403: protected Registry buildFrameworkRegistry(String[] files)
404: throws Exception {
405: ClassResolver resolver = getClassResolver();
406:
407: List descriptorResources = new ArrayList();
408: for (int i = 0; i < files.length; i++) {
409: Resource resource = getResource(files[i]);
410:
411: descriptorResources.add(resource);
412: }
413:
414: ModuleDescriptorProvider provider = new XmlModuleDescriptorProvider(
415: resolver, descriptorResources);
416:
417: return buildFrameworkRegistry(provider);
418: }
419:
420: /**
421: * Builds a registry, containing only the modules delivered by the specified
422: * {@link org.apache.hivemind.ModuleDescriptorProvider}, plus the master module descriptor
423: * (i.e., those visible on the classpath).
424: */
425: protected Registry buildFrameworkRegistry(
426: ModuleDescriptorProvider customProvider) {
427: ClassResolver resolver = getClassResolver();
428:
429: RegistryBuilder builder = new RegistryBuilder();
430:
431: builder
432: .addModuleDescriptorProvider(new XmlModuleDescriptorProvider(
433: resolver));
434: builder.addModuleDescriptorProvider(customProvider);
435:
436: return builder.constructRegistry(Locale.getDefault());
437: }
438:
439: /**
440: * Builds a registry from exactly the provided resource; this registry will not include the
441: * <code>hivemind</code> module.
442: */
443: protected Registry buildMinimalRegistry(Resource l)
444: throws Exception {
445: RegistryBuilder builder = new RegistryBuilder();
446:
447: return builder.constructRegistry(Locale.getDefault());
448: }
449:
450: /**
451: * Creates a <em>managed</em> control via
452: * {@link MockControl#createStrictControl(java.lang.Class)}. The created control is remembered,
453: * and will be invoked by {@link #replayControls()}, {@link #verifyControls()}, etc.
454: * <p>
455: * The class to mock may be either an interface or a class. The EasyMock class extension
456: * (easymockclassextension-1.1.jar) and CGLIB (cglib-full-2.01.jar) must be present in the
457: * latter case (new since 1.1).
458: * <p>
459: * This method is not deprecated, but is rarely used; typically {@link #newMock(Class)} is used
460: * to create the control and the mock, and {@link #setReturnValue(Object, Object)} and
461: * {@link #setThrowable(Object, Throwable)} are used to while training it.
462: * {@link #getControl(Object)} is used for the rare cases where the MockControl itself is
463: * needed.
464: */
465: protected MockControl newControl(Class mockClass) {
466: MockControlFactory factory = mockClass.isInterface() ? _interfaceMockControlFactory
467: : _classMockControlFactory;
468:
469: MockControl result = factory.newControl(mockClass);
470:
471: addControl(result);
472:
473: return result;
474: }
475:
476: /**
477: * Accesses the control for a previously created mock object. Iterates over the list of managed
478: * controls until one is found whose mock object identity equals the mock object provided.
479: *
480: * @param Mock
481: * object whose control is needed
482: * @return the corresponding MockControl if found
483: * @throws IllegalArgumentException
484: * if not found
485: * @since 1.1
486: */
487:
488: protected MockControl getControl(Object mock) {
489: Iterator i = _controls.iterator();
490: while (i.hasNext()) {
491: MockControl control = (MockControl) i.next();
492:
493: if (control.getMock() == mock)
494: return control;
495: }
496:
497: throw new IllegalArgumentException(
498: mock
499: + " is not a mock object controlled by any registered MockControl instance.");
500: }
501:
502: /**
503: * Invoked when training a mock object to set the Throwable for the most recently invoked
504: * method.
505: *
506: * @param mock
507: * the mock object being trained
508: * @param t
509: * the exception the object should throw when it replays
510: * @since 1.1
511: */
512: protected void setThrowable(Object mock, Throwable t) {
513: getControl(mock).setThrowable(t);
514: }
515:
516: /**
517: * Invoked when training a mock object to set the return value for the most recently invoked
518: * method. Overrides of this method exist to support a number of primitive types.
519: *
520: * @param mock
521: * the mock object being trained
522: * @param returnValue
523: * the value to return from the most recently invoked methods
524: * @since 1.1
525: */
526: protected void setReturnValue(Object mock, Object returnValue) {
527: getControl(mock).setReturnValue(returnValue);
528: }
529:
530: /**
531: * Invoked when training a mock object to set the return value for the most recently invoked
532: * method. Overrides of this method exist to support a number of primitive types.
533: *
534: * @param mock
535: * the mock object being trained
536: * @param returnValue
537: * the value to return from the most recently invoked methods
538: * @since 1.1
539: */
540: protected void setReturnValue(Object mock, long returnValue) {
541: getControl(mock).setReturnValue(returnValue);
542: }
543:
544: /**
545: * Invoked when training a mock object to set the return value for the most recently invoked
546: * method. Overrides of this method exist to support a number of primitive types.
547: *
548: * @param mock
549: * the mock object being trained
550: * @param returnValue
551: * the value to return from the most recently invoked methods
552: * @since 1.1
553: */
554: protected void setReturnValue(Object mock, float returnValue) {
555: getControl(mock).setReturnValue(returnValue);
556: }
557:
558: /**
559: * Invoked when training a mock object to set the return value for the most recently invoked
560: * method. Overrides of this method exist to support a number of primitive types.
561: *
562: * @param mock
563: * the mock object being trained
564: * @param returnValue
565: * the value to return from the most recently invoked methods
566: * @since 1.1
567: */
568: protected void setReturnValue(Object mock, double returnValue) {
569: getControl(mock).setReturnValue(returnValue);
570: }
571:
572: /**
573: * Invoked when training a mock object to set the return value for the most recently invoked
574: * method. Overrides of this method exist to support a number of primitive types.
575: *
576: * @param mock
577: * the mock object being trained
578: * @param returnValue
579: * the value to return from the most recently invoked methods
580: * @since 1.1
581: */
582: protected void setReturnValue(Object mock, boolean returnValue) {
583: getControl(mock).setReturnValue(returnValue);
584: }
585:
586: /**
587: * Adds the control to the list of managed controls used by {@link #replayControls()} and
588: * {@link #verifyControls()}.
589: */
590: protected void addControl(MockControl control) {
591: _controls.add(control);
592: }
593:
594: /**
595: * Convienience for invoking {@link #newControl(Class)} and then invoking
596: * {@link MockControl#getMock()} on the result.
597: */
598: protected Object newMock(Class mockClass) {
599: return newControl(mockClass).getMock();
600: }
601:
602: /**
603: * Invokes {@link MockControl#replay()} on all controls created by {@link #newControl(Class)}.
604: */
605: protected void replayControls() {
606: Iterator i = _controls.iterator();
607: while (i.hasNext()) {
608: MockControl c = (MockControl) i.next();
609: c.replay();
610: }
611: }
612:
613: /**
614: * Invokes {@link org.easymock.MockControl#verify()} and {@link MockControl#reset()} on all
615: * controls created by {@link #newControl(Class)}.
616: */
617:
618: protected void verifyControls() {
619: Iterator i = _controls.iterator();
620: while (i.hasNext()) {
621: MockControl c = (MockControl) i.next();
622: c.verify();
623: c.reset();
624: }
625: }
626:
627: /**
628: * Invokes {@link org.easymock.MockControl#reset()} on all controls.
629: */
630:
631: protected void resetControls() {
632: Iterator i = _controls.iterator();
633: while (i.hasNext()) {
634: MockControl c = (MockControl) i.next();
635: c.reset();
636: }
637: }
638:
639: /**
640: * @deprecated To be removed in 1.2. Use {@link #newLocation()} instead.
641: */
642: protected Location fabricateLocation(int line) {
643: String path = "/" + getClass().getName().replace('.', '/');
644:
645: Resource r = new ClasspathResource(getClassResolver(), path);
646:
647: return new LocationImpl(r, line);
648: }
649:
650: private int _line = 1;
651:
652: /**
653: * Returns a new {@link Location} instance. The resource is the test class, and the line number
654: * increments by one from one for each invocation (thus each call will get a unique instance not
655: * equal to any previously obtained instance).
656: *
657: * @since 1.1
658: */
659: protected Location newLocation() {
660: return fabricateLocation(_line++);
661: }
662:
663: /**
664: * Returns a {@link DefaultClassResolver}. Repeated calls in the same test return the same
665: * value.
666: *
667: * @since 1.1
668: */
669:
670: protected ClassResolver getClassResolver() {
671: if (_classResolver == null)
672: _classResolver = new DefaultClassResolver();
673:
674: return _classResolver;
675: }
676:
677: protected boolean matches(String input, String pattern)
678: throws Exception {
679: setupMatcher();
680:
681: Pattern compiled = _compiler.compile(pattern);
682:
683: return _matcher.matches(input, compiled);
684: }
685:
686: }
|