/*
* ModeShape (http://www.modeshape.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.modeshape.common.logging;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.SimpleLayout;
import org.apache.log4j.WriterAppender;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.ThrowableInformation;
import org.modeshape.common.i18n.I18n;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* 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
* logging framework, which can sit on top of multiple logging frameworks, including Log4J. Therefore, this test assumes that
* 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
* resulting Log4J output.
* <p>
* To ensure that the Log4J configuration used in the remaining tests (in <code>src/test/resources/log4j.properties</code>)
* does not interfere with this test, the underlying Log4J logger is obtained before each test and programmatically reconfigured
* and, after each test, is then restored to it's previous state. This reconfiguration involves identifying and removing all of
* the {@link Appender Log4J Appender} on the tree of Log4J loggers, and substituting a special {@link LogRecorder Appender} that
* records the log messages in memory. During the test, this in-memory list of log messages is checked by the test case using
* standard assertions to verify the proper order and content of the log messages. After each of the tests, all of the original
* Adapters are restored to the appropriate Log4J loggers.
* </p>
*/
public class LoggerTest {
public static I18n errorMessageWithNoParameters;
public static I18n warningMessageWithNoParameters;
public static I18n infoMessageWithNoParameters;
public static I18n errorMessageWithTwoParameters;
public static I18n warningMessageWithTwoParameters;
public static I18n infoMessageWithTwoParameters;
public static I18n errorMessageWithException;
public static I18n warningMessageWithException;
public static I18n infoMessageWithException;
public static I18n errorMessageWithNullException;
public static I18n warningMessageWithNullException;
public static I18n infoMessageWithNullException;
public static I18n someMessage;
private LogRecorder log;
private Logger logger;
private org.apache.log4j.Logger log4jLogger;
private Map<String, List<Appender>> existingAppendersByLoggerName = new HashMap<String, List<Appender>>();
@BeforeClass
public static void beforeAll() {
// Initialize the I18n static fields ...
I18n.initialize(LoggerTest.class);
}
@Before
public void beforeEach() {
logger = Logger.getLogger(LoggerTest.class);
// Find all of the existing appenders on all of the loggers, and
// remove them all (keeping track of which appender they're on)
log4jLogger = org.apache.log4j.Logger.getLogger(logger.getName());
org.apache.log4j.Logger theLogger = log4jLogger;
while (theLogger != null) {
List<Appender> appenders = new ArrayList<Appender>();
Enumeration<?> previousAppenders = theLogger.getAllAppenders();
while (previousAppenders.hasMoreElements()) {
appenders.add((Appender)previousAppenders.nextElement());
}
existingAppendersByLoggerName.put(theLogger.getName(), appenders);
theLogger.removeAllAppenders();
theLogger = (org.apache.log4j.Logger)theLogger.getParent();
}
// Set up the appender from which we can easily grab the content of the log during the tests.
// This assumes we're using Log4J. Also, the Log4J properties should specify that the
// logger for this particular class.
log = new LogRecorder();
log4jLogger = org.apache.log4j.Logger.getLogger(logger.getName());
log4jLogger.addAppender(this.log);
log4jLogger.setLevel(Level.ALL);
}
@After
public void afterEach() {
// Put all of the existing appenders onto the correct logger, and remove the testing appender ...
for (Map.Entry<String, List<Appender>> entry : this.existingAppendersByLoggerName.entrySet()) {
String loggerName = entry.getKey();
List<Appender> appenders = entry.getValue();
org.apache.log4j.Logger theLogger = org.apache.log4j.Logger.getLogger(loggerName);
theLogger.removeAllAppenders(); // removes the testing appender, if on this logger
for (Appender appender : appenders) {
theLogger.addAppender(appender);
}
}
}
@Test
public void shouldLogAppropriateMessagesIfSetToAllLevel() {
log4jLogger.setLevel(Level.ALL);
logger.error(errorMessageWithNoParameters);
logger.warn(warningMessageWithNoParameters);
logger.info(infoMessageWithNoParameters);
logger.debug("This is a debug message with no parameters");
logger.trace("This is a trace message with no parameters");
log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
log.removeFirst(Logger.Level.INFO, "This is an info message with no parameters");
log.removeFirst(Logger.Level.DEBUG, "This is a debug message with no parameters");
log.removeFirst(Logger.Level.TRACE, "This is a trace message with no parameters");
assertEquals(false, log.hasEvents());
}
@Test
public void shouldLogAppropriateMessagesIfLog4jSetToTraceLevel() {
log4jLogger.setLevel(Level.TRACE);
logger.error(errorMessageWithNoParameters);
logger.warn(warningMessageWithNoParameters);
logger.info(infoMessageWithNoParameters);
logger.debug("This is a debug message with no parameters");
logger.trace("This is a trace message with no parameters");
log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
log.removeFirst(Logger.Level.INFO, "This is an info message with no parameters");
log.removeFirst(Logger.Level.DEBUG, "This is a debug message with no parameters");
log.removeFirst(Logger.Level.TRACE, "This is a trace message with no parameters");
assertEquals(false, log.hasEvents());
}
@Test
public void shouldLogAppropriateMessagesIfLog4jSetToDebugLevel() {
log4jLogger.setLevel(Level.DEBUG);
logger.error(errorMessageWithNoParameters);
logger.warn(warningMessageWithNoParameters);
logger.info(infoMessageWithNoParameters);
logger.debug("This is a debug message with no parameters");
logger.trace("This is a trace message with no parameters");
log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
log.removeFirst(Logger.Level.INFO, "This is an info message with no parameters");
log.removeFirst(Logger.Level.DEBUG, "This is a debug message with no parameters");
assertEquals(false, log.hasEvents());
}
@Test
public void shouldLogAppropriateMessagesIfLog4jSetToInfoLevel() {
log4jLogger.setLevel(Level.INFO);
logger.error(errorMessageWithNoParameters);
logger.warn(warningMessageWithNoParameters);
logger.info(infoMessageWithNoParameters);
logger.debug("This is a debug message with no parameters");
logger.trace("This is a trace message with no parameters");
log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
log.removeFirst(Logger.Level.INFO, "This is an info message with no parameters");
assertEquals(false, log.hasEvents());
}
@Test
public void shouldLogAppropriateMessagesIfLog4jSetToWarningLevel() {
log4jLogger.setLevel(Level.WARN);
logger.error(errorMessageWithNoParameters);
logger.warn(warningMessageWithNoParameters);
logger.info(infoMessageWithNoParameters);
logger.debug("This is a debug message with no parameters");
logger.trace("This is a trace message with no parameters");
log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
assertEquals(false, log.hasEvents());
}
@Test
public void shouldLogAppropriateMessagesIfLog4jSetToErrorLevel() {
log4jLogger.setLevel(Level.ERROR);
logger.error(errorMessageWithNoParameters);
logger.warn(warningMessageWithNoParameters);
logger.info(infoMessageWithNoParameters);
logger.debug("This is a debug message with no parameters");
logger.trace("This is a trace message with no parameters");
log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
assertEquals(false, log.hasEvents());
}
@Test
public void shouldLogNoMessagesIfLog4jSetToOffLevel() {
log4jLogger.setLevel(Level.OFF);
logger.error(errorMessageWithNoParameters);
logger.warn(warningMessageWithNoParameters);
logger.info(infoMessageWithNoParameters);
logger.debug("This is a debug message with no parameters");
logger.trace("This is a trace message with no parameters");
assertEquals(false, log.hasEvents());
}
@Test
public void shouldNotAcceptMessageWithNonNullAndNullParameters() {
logger.error(errorMessageWithTwoParameters, "first", null);
logger.warn(warningMessageWithTwoParameters, "first", null);
logger.info(infoMessageWithTwoParameters, "first", null);
logger.debug("This is a debug message with a {0} parameter and the {1} parameter", "first", null);
logger.trace("This is a trace message with a {0} parameter and the {1} parameter", "first", null);
log.removeFirst(Logger.Level.ERROR, "This is an error message with a first parameter and the null parameter");
log.removeFirst(Logger.Level.WARNING, "This is a warning message with a first parameter and the null parameter");
log.removeFirst(Logger.Level.INFO, "This is an info message with a first parameter and the null parameter");
log.removeFirst(Logger.Level.DEBUG, "This is a debug message with a first parameter and the null parameter");
log.removeFirst(Logger.Level.TRACE, "This is a trace message with a first parameter and the null parameter");
assertEquals(false, log.hasEvents());
}
@Test( expected = IllegalArgumentException.class )
public void shouldNotAcceptErrorMessageWithTooFewParameters() {
logger.error(errorMessageWithTwoParameters, (Object[])null);
}
@Test( expected = IllegalArgumentException.class )
public void shouldNotAcceptWarningMessageWithTooFewParameters() {
logger.warn(warningMessageWithTwoParameters, (Object[])null);
}
@Test( expected = IllegalArgumentException.class )
public void shouldNotAcceptInfoMessageWithTooFewParameters() {
logger.info(infoMessageWithTwoParameters, (Object[])null);
}
@Test( expected = IllegalArgumentException.class )
public void shouldNotAcceptDebugMessageWithTooFewParameters() {
logger.debug("This is a debug message with a {0} parameter and the {1} parameter", (Object[])null);
}
@Test( expected = IllegalArgumentException.class )
public void shouldNotAcceptTraceMessageWithTooFewParameters() {
logger.trace("This is a trace message with a {0} parameter and the {1} parameter", (Object[])null);
}
@Test
public void shouldAcceptMessageWithNoParameters() {
logger.error(errorMessageWithNoParameters);
logger.warn(warningMessageWithNoParameters);
logger.info(infoMessageWithNoParameters);
logger.debug("This is a debug message with no parameters");
logger.trace("This is a trace message with no parameters");
log.removeFirst(Logger.Level.ERROR, "This is an error message with no parameters");
log.removeFirst(Logger.Level.WARNING, "This is a warning message with no parameters");
log.removeFirst(Logger.Level.INFO, "This is an info message with no parameters");
log.removeFirst(Logger.Level.DEBUG, "This is a debug message with no parameters");
log.removeFirst(Logger.Level.TRACE, "This is a trace message with no parameters");
assertEquals(false, log.hasEvents());
}
@Test
public void shouldAcceptMessageWithObjectAndPrimitiveParameters() {
logger.error(errorMessageWithTwoParameters, "first", 2);
logger.warn(warningMessageWithTwoParameters, "first", 2);
logger.info(infoMessageWithTwoParameters, "first", 2);
logger.debug("This is a debug message with a {0} parameter and the {1} parameter", "first", 2);
logger.trace("This is a trace message with a {0} parameter and the {1} parameter", "first", 2);
log.removeFirst(Logger.Level.ERROR, "This is an error message with a first parameter and the 2 parameter");
log.removeFirst(Logger.Level.WARNING, "This is a warning message with a first parameter and the 2 parameter");
log.removeFirst(Logger.Level.INFO, "This is an info message with a first parameter and the 2 parameter");
log.removeFirst(Logger.Level.DEBUG, "This is a debug message with a first parameter and the 2 parameter");
log.removeFirst(Logger.Level.TRACE, "This is a trace message with a first parameter and the 2 parameter");
assertEquals(false, log.hasEvents());
}
@Test
public void shouldAcceptMessageAndThrowable() {
Throwable t = new RuntimeException("This is the runtime exception message");
logger.error(t, errorMessageWithException);
logger.warn(t, warningMessageWithException);
logger.info(t, infoMessageWithException);
logger.debug(t, "This is a debug message with an exception");
logger.trace(t, "This is a trace message with an exception");
log.removeFirst(Logger.Level.ERROR, "This is an error message with an exception", RuntimeException.class);
log.removeFirst(Logger.Level.WARNING, "This is a warning message with an exception", RuntimeException.class);
log.removeFirst(Logger.Level.INFO, "This is an info message with an exception", RuntimeException.class);
log.removeFirst(Logger.Level.DEBUG, "This is a debug message with an exception", RuntimeException.class);
log.removeFirst(Logger.Level.TRACE, "This is a trace message with an exception", RuntimeException.class);
assertEquals(false, log.hasEvents());
}
@Test
public void shouldAcceptMessageAndNullThrowable() {
Throwable t = null;
logger.error(t, errorMessageWithNullException);
logger.warn(t, warningMessageWithNullException);
logger.info(t, infoMessageWithNullException);
logger.debug(t, "This is a debug message with a null exception");
logger.trace(t, "This is a trace message with a null exception");
log.removeFirst(Logger.Level.ERROR, "This is an error message with a null exception");
log.removeFirst(Logger.Level.WARNING, "This is a warning message with a null exception");
log.removeFirst(Logger.Level.INFO, "This is an info message with a null exception");
log.removeFirst(Logger.Level.DEBUG, "This is a debug message with a null exception");
log.removeFirst(Logger.Level.TRACE, "This is a trace message with a null exception");
assertEquals(false, log.hasEvents());
}
public void shouldQuietlyAcceptNullMessage() {
logger.error(null);
logger.warn(null);
logger.info(null);
logger.debug(null);
logger.trace(null);
assertEquals(false, log.hasEvents());
}
@Test
public void shouldAcceptNullMessageAndThrowable() {
Throwable t = new RuntimeException("This is the runtime exception message in LoggerTest");
logger.error(t, null);
logger.warn(t, null);
logger.info(t, null);
logger.debug(t, null);
logger.trace(t, null);
log.removeFirst(Logger.Level.ERROR, null, RuntimeException.class);
log.removeFirst(Logger.Level.WARNING, null, RuntimeException.class);
log.removeFirst(Logger.Level.INFO, null, RuntimeException.class);
log.removeFirst(Logger.Level.DEBUG, null, RuntimeException.class);
log.removeFirst(Logger.Level.TRACE, null, RuntimeException.class);
assertEquals(false, log.hasEvents());
}
@Test
public void shouldAcceptNullThrowableInError() {
logger.error((Throwable)null, someMessage);
logger.warn((Throwable)null, someMessage);
logger.info((Throwable)null, someMessage);
logger.debug((Throwable)null, "some message");
logger.trace((Throwable)null, "some message");
log.removeFirst(Logger.Level.ERROR, "some message");
log.removeFirst(Logger.Level.WARNING, "some message");
log.removeFirst(Logger.Level.INFO, "some message");
log.removeFirst(Logger.Level.DEBUG, "some message");
log.removeFirst(Logger.Level.TRACE, "some message");
}
@Test
public void shouldSupportAskingWhetherLoggingLevelsAreEnabled() {
logger.isErrorEnabled();
logger.isWarnEnabled();
logger.isInfoEnabled();
logger.isDebugEnabled();
logger.isTraceEnabled();
}
/**
* A special Log4J Appender that records log messages and whose content can be
* {@link #removeFirst(org.modeshape.common.logging.Logger.Level, String, Class) validated} to ensure that the log contains
* messages in the proper order and with the proper content.
*/
public class LogRecorder extends WriterAppender {
private final LinkedList<LoggingEvent> events = new LinkedList<LoggingEvent>();
private int lineNumber;
public LogRecorder( StringWriter writer ) {
super(new SimpleLayout(), writer);
}
public LogRecorder() {
this(new StringWriter());
}
@Override
protected void subAppend( LoggingEvent event ) {
super.subAppend(event);
this.events.add(event);
}
public LoggingEvent removeFirst() {
if (hasEvents()) {
++lineNumber;
return this.events.removeFirst();
}
return null;
}
public boolean hasEvents() {
return this.events.size() != 0;
}
/**
* Remove the message that is currently at the front of the log, and verify that it contains the supplied information.
*
* @param expectedLevel the level that the next log message should have
* @param expectedMessageExpression the message that the next log message should have, or a regular expression that would
* match the log message
* @param expectedExceptionClass the exception class that was expected, or null if there should not be an exception
*/
public void removeFirst( Logger.Level expectedLevel,
String expectedMessageExpression,
Class<? extends Throwable> expectedExceptionClass ) {
if (!hasEvents()) {
fail("Expected log message but found none: " + expectedLevel + " - " + expectedMessageExpression);
}
LoggingEvent event = removeFirst();
// Check the log message ...
if (expectedMessageExpression != null && event.getMessage() == null) {
fail("Log line " + lineNumber + " was missing expected message: " + expectedMessageExpression);
} else if (expectedMessageExpression == null && event.getMessage() != null) {
fail("Log line " + lineNumber + " had unexpected message: " + event.getMessage());
} else if (expectedMessageExpression != null) {
String actual = event.getMessage().toString();
// Treat as a regular expression, which works for both regular expressions and strings ...
if (!actual.matches(expectedMessageExpression)) {
fail("Log line " + lineNumber + " differed: \nwas :\t" + actual + "\nexpected:\t"
+ expectedMessageExpression);
}
} // else they are both null
// Check the exception ...
ThrowableInformation throwableInfo = event.getThrowableInformation();
if (expectedExceptionClass == null && throwableInfo != null) {
fail("Log line " + lineNumber + " had unexpected exception: "
+ event.getThrowableInformation().getThrowableStrRep());
} else if (expectedExceptionClass != null && throwableInfo == null) {
fail("Log line " + lineNumber + " was missing expected exception of type "
+ expectedExceptionClass.getCanonicalName());
} else if (expectedExceptionClass != null && throwableInfo != null) {
Throwable actualException = throwableInfo.getThrowable();
assertSame(expectedExceptionClass, actualException.getClass());
} // else they are both null
}
public void removeFirst( Logger.Level expectedLevel,
String expectedMessageExpression ) {
removeFirst(expectedLevel, expectedMessageExpression, null);
}
}
}