001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
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.modeshape.common.logging;
017
018import static org.junit.Assert.assertEquals;
019import static org.junit.Assert.assertSame;
020import static org.junit.Assert.fail;
021import java.io.StringWriter;
022import java.util.ArrayList;
023import java.util.Enumeration;
024import java.util.HashMap;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import org.apache.log4j.Appender;
029import org.apache.log4j.Level;
030import org.apache.log4j.SimpleLayout;
031import org.apache.log4j.WriterAppender;
032import org.apache.log4j.spi.LoggingEvent;
033import org.apache.log4j.spi.ThrowableInformation;
034import org.modeshape.common.i18n.I18n;
035import org.junit.After;
036import org.junit.Before;
037import org.junit.BeforeClass;
038import org.junit.Test;
039
040/**
041 * Test the {@link org.modeshape.common.logging.Logger} class, ensuring that it uses Log4J appropriately. The {@link org.modeshape.common.logging.Logger} class uses the SLF4J generalized
042 * logging framework, which can sit on top of multiple logging frameworks, including Log4J. Therefore, this test assumes that
043 * SLF4J works correctly for all logging frameworks, then the {@link org.modeshape.common.logging.Logger} class can be tested by using it and checking the
044 * resulting Log4J output.
045 * <p>
046 * To ensure that the Log4J configuration used in the remaining tests (in <code>src/test/resources/log4j.properties</code>)
047 * does not interfere with this test, the underlying Log4J logger is obtained before each test and programmatically reconfigured
048 * and, after each test, is then restored to it's previous state. This reconfiguration involves identifying and removing all of
049 * the {@link Appender Log4J Appender} on the tree of Log4J loggers, and substituting a special {@link LogRecorder Appender} that
050 * records the log messages in memory. During the test, this in-memory list of log messages is checked by the test case using
051 * standard assertions to verify the proper order and content of the log messages. After each of the tests, all of the original
052 * Adapters are restored to the appropriate Log4J loggers.
053 * </p>
054 */
055public class LoggerTest {
056
057    public static I18n errorMessageWithNoParameters;
058    public static I18n warningMessageWithNoParameters;
059    public static I18n infoMessageWithNoParameters;
060    public static I18n errorMessageWithTwoParameters;
061    public static I18n warningMessageWithTwoParameters;
062    public static I18n infoMessageWithTwoParameters;
063    public static I18n errorMessageWithException;
064    public static I18n warningMessageWithException;
065    public static I18n infoMessageWithException;
066    public static I18n errorMessageWithNullException;
067    public static I18n warningMessageWithNullException;
068    public static I18n infoMessageWithNullException;
069    public static I18n someMessage;
070
071    private LogRecorder log;
072    private Logger logger;
073    private org.apache.log4j.Logger log4jLogger;
074    private Map<String, List<Appender>> existingAppendersByLoggerName = new HashMap<String, List<Appender>>();
075
076    @BeforeClass
077    public static void beforeAll() {
078        // Initialize the I18n static fields ...
079        I18n.initialize(LoggerTest.class);
080    }
081
082    @Before
083    public void beforeEach() {
084        logger = Logger.getLogger(LoggerTest.class);
085
086        // Find all of the existing appenders on all of the loggers, and
087        // remove them all (keeping track of which appender they're on)
088        log4jLogger = org.apache.log4j.Logger.getLogger(logger.getName());
089        org.apache.log4j.Logger theLogger = log4jLogger;
090        while (theLogger != null) {
091            List<Appender> appenders = new ArrayList<Appender>();
092            Enumeration<?> previousAppenders = theLogger.getAllAppenders();
093            while (previousAppenders.hasMoreElements()) {
094                appenders.add((Appender)previousAppenders.nextElement());
095            }
096            existingAppendersByLoggerName.put(theLogger.getName(), appenders);
097            theLogger.removeAllAppenders();
098            theLogger = (org.apache.log4j.Logger)theLogger.getParent();
099        }
100
101        // Set up the appender from which we can easily grab the content of the log during the tests.
102        // This assumes we're using Log4J. Also, the Log4J properties should specify that the
103        // logger for this particular class.
104        log = new LogRecorder();
105        log4jLogger = org.apache.log4j.Logger.getLogger(logger.getName());
106        log4jLogger.addAppender(this.log);
107        log4jLogger.setLevel(Level.ALL);
108    }
109
110    @After
111    public void afterEach() {
112        // Put all of the existing appenders onto the correct logger, and remove the testing appender ...
113        for (Map.Entry<String, List<Appender>> entry : this.existingAppendersByLoggerName.entrySet()) {
114            String loggerName = entry.getKey();
115            List<Appender> appenders = entry.getValue();
116            org.apache.log4j.Logger theLogger = org.apache.log4j.Logger.getLogger(loggerName);
117            theLogger.removeAllAppenders(); // removes the testing appender, if on this logger
118            for (Appender appender : appenders) {
119                theLogger.addAppender(appender);
120            }
121        }
122    }
123
124    @Test
125    public void shouldLogAppropriateMessagesIfSetToAllLevel() {
126        log4jLogger.setLevel(Level.ALL);
127        logger.error(errorMessageWithNoParameters);
128        logger.warn(warningMessageWithNoParameters);
129        logger.info(infoMessageWithNoParameters);
130        logger.debug("This is a debug message with no parameters");
131        logger.trace("This is a trace message with no parameters");
132
133        log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
134        log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
135        log.removeFirst(Logger.Level.INFO, "This is an info message with no parameters");
136        log.removeFirst(Logger.Level.DEBUG, "This is a debug message with no parameters");
137        log.removeFirst(Logger.Level.TRACE, "This is a trace message with no parameters");
138        assertEquals(false, log.hasEvents());
139    }
140
141    @Test
142    public void shouldLogAppropriateMessagesIfLog4jSetToTraceLevel() {
143        log4jLogger.setLevel(Level.TRACE);
144        logger.error(errorMessageWithNoParameters);
145        logger.warn(warningMessageWithNoParameters);
146        logger.info(infoMessageWithNoParameters);
147        logger.debug("This is a debug message with no parameters");
148        logger.trace("This is a trace message with no parameters");
149
150        log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
151        log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
152        log.removeFirst(Logger.Level.INFO, "This is an info message with no parameters");
153        log.removeFirst(Logger.Level.DEBUG, "This is a debug message with no parameters");
154        log.removeFirst(Logger.Level.TRACE, "This is a trace message with no parameters");
155        assertEquals(false, log.hasEvents());
156    }
157
158    @Test
159    public void shouldLogAppropriateMessagesIfLog4jSetToDebugLevel() {
160        log4jLogger.setLevel(Level.DEBUG);
161        logger.error(errorMessageWithNoParameters);
162        logger.warn(warningMessageWithNoParameters);
163        logger.info(infoMessageWithNoParameters);
164        logger.debug("This is a debug message with no parameters");
165        logger.trace("This is a trace message with no parameters");
166
167        log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
168        log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
169        log.removeFirst(Logger.Level.INFO, "This is an info message with no parameters");
170        log.removeFirst(Logger.Level.DEBUG, "This is a debug message with no parameters");
171        assertEquals(false, log.hasEvents());
172    }
173
174    @Test
175    public void shouldLogAppropriateMessagesIfLog4jSetToInfoLevel() {
176        log4jLogger.setLevel(Level.INFO);
177        logger.error(errorMessageWithNoParameters);
178        logger.warn(warningMessageWithNoParameters);
179        logger.info(infoMessageWithNoParameters);
180        logger.debug("This is a debug message with no parameters");
181        logger.trace("This is a trace message with no parameters");
182
183        log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
184        log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
185        log.removeFirst(Logger.Level.INFO, "This is an info message with no parameters");
186        assertEquals(false, log.hasEvents());
187    }
188
189    @Test
190    public void shouldLogAppropriateMessagesIfLog4jSetToWarningLevel() {
191        log4jLogger.setLevel(Level.WARN);
192        logger.error(errorMessageWithNoParameters);
193        logger.warn(warningMessageWithNoParameters);
194        logger.info(infoMessageWithNoParameters);
195        logger.debug("This is a debug message with no parameters");
196        logger.trace("This is a trace message with no parameters");
197
198        log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
199        log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
200        assertEquals(false, log.hasEvents());
201    }
202
203    @Test
204    public void shouldLogAppropriateMessagesIfLog4jSetToErrorLevel() {
205        log4jLogger.setLevel(Level.ERROR);
206        logger.error(errorMessageWithNoParameters);
207        logger.warn(warningMessageWithNoParameters);
208        logger.info(infoMessageWithNoParameters);
209        logger.debug("This is a debug message with no parameters");
210        logger.trace("This is a trace message with no parameters");
211
212        log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
213        assertEquals(false, log.hasEvents());
214    }
215
216    @Test
217    public void shouldLogNoMessagesIfLog4jSetToOffLevel() {
218        log4jLogger.setLevel(Level.OFF);
219        logger.error(errorMessageWithNoParameters);
220        logger.warn(warningMessageWithNoParameters);
221        logger.info(infoMessageWithNoParameters);
222        logger.debug("This is a debug message with no parameters");
223        logger.trace("This is a trace message with no parameters");
224
225        assertEquals(false, log.hasEvents());
226    }
227
228    @Test
229    public void shouldNotAcceptMessageWithNonNullAndNullParameters() {
230        logger.error(errorMessageWithTwoParameters, "first", null);
231        logger.warn(warningMessageWithTwoParameters, "first", null);
232        logger.info(infoMessageWithTwoParameters, "first", null);
233        logger.debug("This is a debug message with a {0} parameter and the {1} parameter", "first", null);
234        logger.trace("This is a trace message with a {0} parameter and the {1} parameter", "first", null);
235
236        log.removeFirst(Logger.Level.ERROR, "This is an error message with a first parameter and the null parameter");
237        log.removeFirst(Logger.Level.WARNING, "This is a warning message with a first parameter and the null parameter");
238        log.removeFirst(Logger.Level.INFO, "This is an info message with a first parameter and the null parameter");
239        log.removeFirst(Logger.Level.DEBUG, "This is a debug message with a first parameter and the null parameter");
240        log.removeFirst(Logger.Level.TRACE, "This is a trace message with a first parameter and the null parameter");
241        assertEquals(false, log.hasEvents());
242    }
243
244    @Test( expected = IllegalArgumentException.class )
245    public void shouldNotAcceptErrorMessageWithTooFewParameters() {
246        logger.error(errorMessageWithTwoParameters, (Object[])null);
247    }
248
249    @Test( expected = IllegalArgumentException.class )
250    public void shouldNotAcceptWarningMessageWithTooFewParameters() {
251        logger.warn(warningMessageWithTwoParameters, (Object[])null);
252    }
253
254    @Test( expected = IllegalArgumentException.class )
255    public void shouldNotAcceptInfoMessageWithTooFewParameters() {
256        logger.info(infoMessageWithTwoParameters, (Object[])null);
257    }
258
259    @Test( expected = IllegalArgumentException.class )
260    public void shouldNotAcceptDebugMessageWithTooFewParameters() {
261        logger.debug("This is a debug message with a {0} parameter and the {1} parameter", (Object[])null);
262    }
263
264    @Test( expected = IllegalArgumentException.class )
265    public void shouldNotAcceptTraceMessageWithTooFewParameters() {
266        logger.trace("This is a trace message with a {0} parameter and the {1} parameter", (Object[])null);
267    }
268
269    @Test
270    public void shouldAcceptMessageWithNoParameters() {
271        logger.error(errorMessageWithNoParameters);
272        logger.warn(warningMessageWithNoParameters);
273        logger.info(infoMessageWithNoParameters);
274        logger.debug("This is a debug message with no parameters");
275        logger.trace("This is a trace message with no parameters");
276
277        log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
278        log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
279        log.removeFirst(Logger.Level.INFO, "This is an info message with no parameters");
280        log.removeFirst(Logger.Level.DEBUG, "This is a debug message with no parameters");
281        log.removeFirst(Logger.Level.TRACE, "This is a trace message with no parameters");
282        assertEquals(false, log.hasEvents());
283    }
284
285    @Test
286    public void shouldAcceptMessageWithObjectAndPrimitiveParameters() {
287        logger.error(errorMessageWithTwoParameters, "first", 2);
288        logger.warn(warningMessageWithTwoParameters, "first", 2);
289        logger.info(infoMessageWithTwoParameters, "first", 2);
290        logger.debug("This is a debug message with a {0} parameter and the {1} parameter", "first", 2);
291        logger.trace("This is a trace message with a {0} parameter and the {1} parameter", "first", 2);
292
293        log.removeFirst(Logger.Level.ERROR, "This is an error message with a first parameter and the 2 parameter");
294        log.removeFirst(Logger.Level.WARNING, "This is a warning message with a first parameter and the 2 parameter");
295        log.removeFirst(Logger.Level.INFO, "This is an info message with a first parameter and the 2 parameter");
296        log.removeFirst(Logger.Level.DEBUG, "This is a debug message with a first parameter and the 2 parameter");
297        log.removeFirst(Logger.Level.TRACE, "This is a trace message with a first parameter and the 2 parameter");
298        assertEquals(false, log.hasEvents());
299    }
300
301    @Test
302    public void shouldAcceptMessageAndThrowable() {
303        Throwable t = new RuntimeException("This is the runtime exception message");
304        logger.error(t, errorMessageWithException);
305        logger.warn(t, warningMessageWithException);
306        logger.info(t, infoMessageWithException);
307        logger.debug(t, "This is a debug message with an exception");
308        logger.trace(t, "This is a trace message with an exception");
309
310        log.removeFirst(Logger.Level.ERROR, "This is an error message with an exception", RuntimeException.class);
311        log.removeFirst(Logger.Level.WARNING, "This is a warning message with an exception", RuntimeException.class);
312        log.removeFirst(Logger.Level.INFO, "This is an info message with an exception", RuntimeException.class);
313        log.removeFirst(Logger.Level.DEBUG, "This is a debug message with an exception", RuntimeException.class);
314        log.removeFirst(Logger.Level.TRACE, "This is a trace message with an exception", RuntimeException.class);
315        assertEquals(false, log.hasEvents());
316    }
317
318    @Test
319    public void shouldAcceptMessageAndNullThrowable() {
320        Throwable t = null;
321        logger.error(t, errorMessageWithNullException);
322        logger.warn(t, warningMessageWithNullException);
323        logger.info(t, infoMessageWithNullException);
324        logger.debug(t, "This is a debug message with a null exception");
325        logger.trace(t, "This is a trace message with a null exception");
326
327        log.removeFirst(Logger.Level.ERROR, "This is an error message with a null exception");
328        log.removeFirst(Logger.Level.WARNING, "This is a warning message with a null exception");
329        log.removeFirst(Logger.Level.INFO, "This is an info message with a null exception");
330        log.removeFirst(Logger.Level.DEBUG, "This is a debug message with a null exception");
331        log.removeFirst(Logger.Level.TRACE, "This is a trace message with a null exception");
332        assertEquals(false, log.hasEvents());
333    }
334
335    public void shouldQuietlyAcceptNullMessage() {
336        logger.error(null);
337        logger.warn(null);
338        logger.info(null);
339        logger.debug(null);
340        logger.trace(null);
341
342        assertEquals(false, log.hasEvents());
343    }
344
345    @Test
346    public void shouldAcceptNullMessageAndThrowable() {
347        Throwable t = new RuntimeException("This is the runtime exception message in LoggerTest");
348        logger.error(t, null);
349        logger.warn(t, null);
350        logger.info(t, null);
351        logger.debug(t, null);
352        logger.trace(t, null);
353
354        log.removeFirst(Logger.Level.ERROR, null, RuntimeException.class);
355        log.removeFirst(Logger.Level.WARNING, null, RuntimeException.class);
356        log.removeFirst(Logger.Level.INFO, null, RuntimeException.class);
357        log.removeFirst(Logger.Level.DEBUG, null, RuntimeException.class);
358        log.removeFirst(Logger.Level.TRACE, null, RuntimeException.class);
359        assertEquals(false, log.hasEvents());
360    }
361
362    @Test
363    public void shouldAcceptNullThrowableInError() {
364        logger.error((Throwable)null, someMessage);
365        logger.warn((Throwable)null, someMessage);
366        logger.info((Throwable)null, someMessage);
367        logger.debug((Throwable)null, "some message");
368        logger.trace((Throwable)null, "some message");
369
370        log.removeFirst(Logger.Level.ERROR, "some message");
371        log.removeFirst(Logger.Level.WARNING, "some message");
372        log.removeFirst(Logger.Level.INFO, "some message");
373        log.removeFirst(Logger.Level.DEBUG, "some message");
374        log.removeFirst(Logger.Level.TRACE, "some message");
375    }
376
377    @Test
378    public void shouldSupportAskingWhetherLoggingLevelsAreEnabled() {
379        logger.isErrorEnabled();
380        logger.isWarnEnabled();
381        logger.isInfoEnabled();
382        logger.isDebugEnabled();
383        logger.isTraceEnabled();
384    }
385
386    /**
387     * A special Log4J Appender that records log messages and whose content can be
388     * {@link #removeFirst(org.modeshape.common.logging.Logger.Level, String, Class) validated} to ensure that the log contains
389     * messages in the proper order and with the proper content.
390     */
391    public class LogRecorder extends WriterAppender {
392
393        private final LinkedList<LoggingEvent> events = new LinkedList<LoggingEvent>();
394        private int lineNumber;
395
396        public LogRecorder( StringWriter writer ) {
397            super(new SimpleLayout(), writer);
398        }
399
400        public LogRecorder() {
401            this(new StringWriter());
402        }
403
404        @Override
405        protected void subAppend( LoggingEvent event ) {
406            super.subAppend(event);
407            this.events.add(event);
408        }
409
410        public LoggingEvent removeFirst() {
411            if (hasEvents()) {
412                ++lineNumber;
413                return this.events.removeFirst();
414            }
415            return null;
416        }
417
418        public boolean hasEvents() {
419            return this.events.size() != 0;
420        }
421
422        /**
423         * Remove the message that is currently at the front of the log, and verify that it contains the supplied information.
424         * 
425         * @param expectedLevel the level that the next log message should have
426         * @param expectedMessageExpression the message that the next log message should have, or a regular expression that would
427         *        match the log message
428         * @param expectedExceptionClass the exception class that was expected, or null if there should not be an exception
429         */
430        public void removeFirst( Logger.Level expectedLevel,
431                                 String expectedMessageExpression,
432                                 Class<? extends Throwable> expectedExceptionClass ) {
433            if (!hasEvents()) {
434                fail("Expected log message but found none: " + expectedLevel + " - " + expectedMessageExpression);
435            }
436            LoggingEvent event = removeFirst();
437
438            // Check the log message ...
439            if (expectedMessageExpression != null && event.getMessage() == null) {
440                fail("Log line " + lineNumber + " was missing expected message: " + expectedMessageExpression);
441            } else if (expectedMessageExpression == null && event.getMessage() != null) {
442                fail("Log line " + lineNumber + " had unexpected message: " + event.getMessage());
443            } else if (expectedMessageExpression != null) {
444                String actual = event.getMessage().toString();
445                // Treat as a regular expression, which works for both regular expressions and strings ...
446                if (!actual.matches(expectedMessageExpression)) {
447                    fail("Log line " + lineNumber + " differed: \nwas     :\t" + actual + "\nexpected:\t"
448                         + expectedMessageExpression);
449                }
450            } // else they are both null
451
452            // Check the exception ...
453            ThrowableInformation throwableInfo = event.getThrowableInformation();
454            if (expectedExceptionClass == null && throwableInfo != null) {
455                fail("Log line " + lineNumber + " had unexpected exception: "
456                     + event.getThrowableInformation().getThrowableStrRep());
457            } else if (expectedExceptionClass != null && throwableInfo == null) {
458                fail("Log line " + lineNumber + " was missing expected exception of type "
459                     + expectedExceptionClass.getCanonicalName());
460            } else if (expectedExceptionClass != null && throwableInfo != null) {
461                Throwable actualException = throwableInfo.getThrowable();
462                assertSame(expectedExceptionClass, actualException.getClass());
463            } // else they are both null
464        }
465
466        public void removeFirst( Logger.Level expectedLevel,
467                                 String expectedMessageExpression ) {
468            removeFirst(expectedLevel, expectedMessageExpression, null);
469        }
470    }
471
472}