blob: 8fa55f1b8e3f6df956a8e2b5ea00d348da13b106 [file] [log] [blame]
package org.testng.log4testng;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import org.testng.Assert;
/**
* TestNG support logging via a custom logging framework similar to
* <a href="http://logging.apache.org/log4j"> Log4j</a>. To control logging, add a
* resource named "log4testng.properties" to your classpath. The logging levels are
* TRACE, DEBUG, INFO, WARN, ERROR and FATAL.
* The Logging framework has the following characteristics:
*
* <ul>
* <li>All logging is done using System.out (for levels &lt; ERROR) or System.err. There
* is no way to specify Appenders.</li>
* <li>There is no way to control logging programmatically.</li>
* <li>The log4testng.properties resource is searched in the classpath on the first
* call to the logging API. If it is not present, logging defaults to the WARN
* level.</li>
* </ul>
*
* The property file contains lines in the following format:
*
* <pre><code>
* # log4testng will log its own behavior (generally used for debugging this package only).
* log4testng.debug=true
*
* # Specifies the root Logger’s logging level. Will log DEBUG level and above
* log4testng.rootLogger=DEBUG
*
* # The org.testng.reporters.EmailableReporter Logger will log TRACE level and above
* log4testng.logger.org.testng.reporters.EmailableReporter=TRACE
*
* # All Logger in packages below org.testng will log WARN level and above
* log4testng.logger.org.testng=WARN
* </code></pre>
*
* In your source files you will typically instantiate and use loggers this ways:
* <pre><code>
* import org.testng.log4testng.Logger;
*
* class ThisClass {
* private static final Logger LOGGER = Logger.getLogger(ThisClass.class);
*
* ...
* LOGGER.debug("entering myMethod()");
* ...
* LOGGER.warn("unknown file: " + filename);
* ...
* LOGGER.error("Unexpected error", exception);
* </code></pre>
*/
public class Logger {
// Attribute an hierarchical integer value to all levels.
private static int i= 0;
private static final int TRACE= i++;
private static final int DEBUG= i++;
private static final int INFO= i++;
private static final int WARN= i++;
private static final int ERROR= i++;
private static final int FATAL= i++;
private static final int LEVEL_COUNT= i;
/** Standard prefix of all property names in log4testng.properties. */
private static final String PREFIX= "log4testng.";
/** Standard prefix of all logger names in log4testng.properties. */
private static final String LOGGER_PREFIX= PREFIX + "logger.";
/** Root logger name in log4testng.properties. */
private static final String ROOT_LOGGER= PREFIX + "rootLogger";
/** Debug property name in log4testng.properties. */
private static final String DEBUG_PROPERTY= PREFIX + "debug";
/** The standard error stream (this is allways System.err except for unit tests) */
private static PrintStream err= System.err;
/** The standard output stream (this is allways System.out except for unit tests) */
private static PrintStream out= System.out;
/** An ordered list of level names. */
private static final String[] levelNames= new String[LEVEL_COUNT];
static {
levelNames[TRACE]= "TRACE";
levelNames[DEBUG]= "DEBUG";
levelNames[INFO]= "INFO";
levelNames[WARN]= "WARN";
levelNames[ERROR]= "ERROR";
levelNames[FATAL]= "FATAL";
}
/** A map from level name to level integer index (TRACE->0, DEBUG->1 ...) */
private static final Map<String, Integer> levelMap= new HashMap<String, Integer>();
static {
for(i= 0; i < LEVEL_COUNT; ++i) {
levelMap.put(levelNames[i], new Integer(i));
}
}
/** true if the Logging system has been initialized. */
private static boolean initialized;
/** Map from Logger names to level index (as specified in log4testng.properties) */
private static final Map<String, Integer> loggerLevels= new HashMap<String, Integer>();
/** Map of all known loggers. */
private static final Map<Class, Logger> loggers= new HashMap<Class, Logger>();
/** The logging level of the root logger (defaults to warn). */
private static int rootLoggerLevel= WARN;
/** Should log4testng log what it is doing (defaults to false). */
private static boolean debug= false;
/** The logger's level */
private final int level;
/** The logger's name. */
private final Class klass;
private final String m_className;
/**
* Retrieve a logger named according to the value of the pClass.getName()
* parameter. If the named logger already exists, then the existing instance
* will be returned. Otherwise, a new instance is created. By default, loggers
* do not have a set level but inherit it from their nearest ancestor with
* a set level.
*
* @param pClass The class' logger to retrieve.
* @return a logger named according to the value of the pClass.getName().
*/
public static synchronized Logger getLogger(Class pClass) {
initialize();
Logger logger= loggers.get(pClass);
if(logger != null) {
return logger;
}
int level= getLevel(pClass);
logger= new Logger(pClass, level);
loggers.put(pClass, logger);
return logger;
}
/**
* Check whether this logger is enabled for the TRACE Level.
* @return true if this logger is enabled for level TRACE, false otherwise.
*/
public boolean isTraceEnabled() {
return isLevelEnabled(TRACE);
}
/**
* Log a message object with the TRACE level. This method first checks if this
* logger is TRACE enabled. If this logger is TRACE enabled, then it converts
* the message object (passed as parameter) to a string by invoking toString().
* WARNING Note that passing a Throwable to this method will print the name of
* the Throwable but no stack trace. To print a stack trace use the
* trace(Object, Throwable) form instead.
* @param message the message object to log.
*/
public void trace(Object message) {
log(TRACE, message, null);
}
/**
* Log a message object with the TRACE level including the stack trace of the
* Throwable t passed as parameter.
* See Logger.trace(Object) form for more detailed information.
* @param message the message object to log.
* @param t the exception to log, including its stack trace.
*/
public void trace(Object message, Throwable t) {
log(TRACE, message, t);
}
/**
* Check whether this logger is enabled for the DEBUG Level.
* @return true if this logger is enabled for level DEBUG, false otherwise.
*/
public boolean isDebugEnabled() {
return isLevelEnabled(DEBUG);
}
/**
* Log a message object with the DEBUG level.
* See Logger.trace(Object) form for more detailed information.
* @param message the message object to log.
*/
public void debug(Object message) {
log(DEBUG, message, null);
}
/**
* Log a message object with the DEBUG level including the stack trace of the
* Throwable t passed as parameter.
* See Logger.trace(Object, Throwable) form for more detailed information.
* @param message the message object to log.
* @param t the exception to log, including its stack trace.
*/
public void debug(Object message, Throwable t) {
log(DEBUG, message, t);
}
/**
* Check whether this logger is enabled for the INFO Level.
* @return true if this logger is enabled for level INFO, false otherwise.
*/
public boolean isInfoEnabled() {
return isLevelEnabled(INFO);
}
/**
* Log a message object with the INFO level.
* See Logger.trace(Object) form for more detailed information.
* @param message the message object to log.
*/
public void info(Object message) {
log(INFO, message, null);
}
/**
* Log a message object with the WARN level including the stack trace of the
* Throwable t passed as parameter.
* See Logger.trace(Object, Throwable) form for more detailed information.
* @param message the message object to log.
* @param t the exception to log, including its stack trace.
*/
public void info(Object message, Throwable t) {
log(INFO, message, t);
}
/**
* Log a message object with the WARN level.
* See Logger.trace(Object) form for more detailed information.
* @param message the message object to log.
*/
public void warn(Object message) {
log(WARN, message, null);
}
/**
* Log a message object with the ERROR level including the stack trace of the
* Throwable t passed as parameter.
* See Logger.trace(Object, Throwable) form for more detailed information.
* @param message the message object to log.
* @param t the exception to log, including its stack trace.
*/
public void warn(Object message, Throwable t) {
log(WARN, message, t);
}
/**
* Log a message object with the ERROR level.
* See Logger.trace(Object) form for more detailed information.
* @param message the message object to log.
*/
public void error(Object message) {
log(ERROR, message, null);
}
/**
* Log a message object with the DEBUG level including the stack trace of the
* Throwable t passed as parameter.
* See Logger.trace(Object, Throwable) form for more detailed information.
* @param message the message object to log.
* @param t the exception to log, including its stack trace.
*/
public void error(Object message, Throwable t) {
log(ERROR, message, t);
}
/**
* Log a message object with the FATAL level.
* See Logger.trace(Object) form for more detailed information.
* @param message the message object to log.
*/
public void fatal(Object message) {
log(FATAL, message, null);
}
/**
* Log a message object with the FATAL level including the stack trace of the
* Throwable t passed as parameter.
* See Logger.trace(Object, Throwable) form for more detailed information.
* @param message the message object to log.
* @param t the exception to log, including its stack trace.
*/
public void fatal(Object message, Throwable t) {
log(FATAL, message, t);
}
private Logger(Class pClass, int pLevel) {
level= pLevel;
klass= pClass;
m_className= pClass.getName().substring(pClass.getName().lastIndexOf('.') + 1);
}
private static synchronized void initialize() {
if(initialized) {
return;
}
// We flag as initialized right away because if anything goes wrong
// We still consider it initialized. TODO Is this OK?
initialized= true;
InputStream is= Thread.currentThread().getContextClassLoader().getResourceAsStream("log4testng.properties");
if(is == null) {
return;
}
Properties properties= new Properties();
try {
properties.load(is);
}
catch(IOException e) {
throw new RuntimeException(e);
}
checkProperties(properties);
}
private static void checkProperties(Properties pProperties) {
{
// See if we want to debug log4testng
String debugStr= pProperties.getProperty(DEBUG_PROPERTY);
if(debugStr != null) {
if(debugStr.equalsIgnoreCase("true")) {
debug= true;
}
else if(debugStr.equalsIgnoreCase("false")) {
debug= false;
}
else {
throw new IllegalArgumentException("Unknown " + DEBUG_PROPERTY
+ " value " + debugStr);
}
}
loglog4testng("log4testng.debug set to " + debug);
}
{
// Set the value of the root logger (if any).
String rootLevelStr= pProperties.getProperty(ROOT_LOGGER);
if(rootLevelStr != null) {
Integer ilevel= levelMap.get(rootLevelStr.toUpperCase());
if(ilevel == null) {
throw new IllegalArgumentException("Unknown level for log4testng.rootLogger "
+ rootLevelStr + " in log4testng.properties");
}
rootLoggerLevel= ilevel.intValue();
loglog4testng("Root level logger set to " + rootLevelStr + " level.");
}
}
Iterator it= pProperties.entrySet().iterator();
while(it.hasNext()) {
Map.Entry entry= (Entry) it.next();
String logger= (String) entry.getKey();
String level= (String) entry.getValue();
if(!logger.startsWith(PREFIX)) {
throw new IllegalArgumentException("Illegal property value: " + logger);
}
if(logger.equals(DEBUG_PROPERTY)) {
// Already handled
}
else if(logger.equals(ROOT_LOGGER)) {
// Already handled
}
else {
if(!logger.startsWith(LOGGER_PREFIX)) {
throw new IllegalArgumentException("Illegal property value: " + logger);
}
Integer ilevel= levelMap.get(level.toUpperCase());
if(ilevel == null) {
throw new IllegalArgumentException("Unknown level " + level + " for logger " + logger
+ " in log4testng.properties");
}
loggerLevels.put(logger.substring(LOGGER_PREFIX.length()), ilevel);
loglog4testng("logger " + logger + " set to " + ilevel + " level.");
}
}
}
/**
* Returns the level associated to the current class. The level is obtain by searching
* for a logger in the "testng-logging.properties" resource. For example, if class is
* "org.testng.TestNG" the the following loggers are searched in this order:
* <ol>
* <li>"org.testng.TestNG"</li>
* <li>"org.testng"</li>
* <li>"org"</li>
* <li>The root level</li>
* </ol>
*
* @param pClass the class name used for logger name.
* @return the level associated to the current class.
*/
private static int getLevel(Class pClass) {
String name= pClass.getName();
loglog4testng("Getting level for logger " + name);
while(true) {
Integer level= loggerLevels.get(name);
if(level != null) {
loglog4testng("Found level " + level + " for logger " + name);
return level.intValue();
}
int dot= name.lastIndexOf('.');
if(dot != -1) {
loglog4testng("Found level " + rootLoggerLevel + " for root logger");
// Logger name not found. Defaults to root logger level.
return rootLoggerLevel;
}
name= name.substring(0, dot);
}
}
private boolean isLevelEnabled(int pLevel) {
return level <= pLevel;
}
private void log(int pLevel, Object pMessage, Throwable pT) {
if(isLevelEnabled(pLevel)) {
PrintStream ps= (pLevel >= ERROR) ? err : out;
if(null != pT) {
synchronized(ps) {
ps.println("[" + m_className + "] [" + levelNames[pLevel] + "] " + pMessage);
pT.printStackTrace(ps);
}
}
else {
ps.println("[" + m_className + "] [" + levelNames[pLevel] + "] " + pMessage);
}
}
}
/**
* Logs the message to System.out of debug is on.
* @param pmessage the message to log to the console
*/
private static void loglog4testng(String pmessage) {
if(debug) {
out.println("[log4testng] [debug] " + pmessage);
}
}
/**
* This method is for debugging purpose only.
*
* @param pProperties a properties bundle initialised as log4testng
* property file would be.
* @param pOut the standard output stream to be used for logging.
* @param pErr the standard error stream to be used for logging.
*/
private static synchronized void testInitialize(Properties pProperties,
PrintStream pOut,
PrintStream pErr) {
initialized= true;
loggers.clear();
rootLoggerLevel= WARN;
debug= false;
out= pOut;
err= pErr;
checkProperties(pProperties);
}
/**
* Makes sure the default debug value is false.
*/
private static void testDebugDefault() {
Properties props= new Properties();
ByteArrayOutputStream out1= new ByteArrayOutputStream();
ByteArrayOutputStream err1= new ByteArrayOutputStream();
PrintStream out2= new PrintStream(out1);
PrintStream err2= new PrintStream(err1);
props.put("log4testng.rootLogger", "WARN");
testInitialize(props, out2, err2);
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
}
/**
* Makes sure the debug value can be turned on and actualls logs something.
*/
private static void testDebugOn() {
Properties props= new Properties();
ByteArrayOutputStream out1= new ByteArrayOutputStream();
ByteArrayOutputStream err1= new ByteArrayOutputStream();
PrintStream out2= new PrintStream(out1);
PrintStream err2= new PrintStream(err1);
props.put("log4testng.debug", "true");
props.put("log4testng.rootLogger", "WARN");
testInitialize(props, out2, err2);
Assert.assertTrue(out1.toString().startsWith("[log4testng][debug]"));
Assert.assertEquals(err1.toString(), "");
}
/**
* Makes sure the debug value can be turned off and logs nothing.
*/
private static void testDebugOff() {
Properties props= new Properties();
ByteArrayOutputStream out1= new ByteArrayOutputStream();
ByteArrayOutputStream err1= new ByteArrayOutputStream();
PrintStream out2= new PrintStream(out1);
PrintStream err2= new PrintStream(err1);
props.put("log4testng.debug", "false");
props.put("log4testng.rootLogger", "WARN");
testInitialize(props, out2, err2);
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
}
/**
* Makes sure an illegal debug value throws an exception.
*/
private static void testDebugError() {
Properties props= new Properties();
ByteArrayOutputStream out1= new ByteArrayOutputStream();
ByteArrayOutputStream err1= new ByteArrayOutputStream();
PrintStream out2= new PrintStream(out1);
PrintStream err2= new PrintStream(err1);
props.put("log4testng.debug", "unknown");
props.put("log4testng.rootLogger", "WARN");
try {
testInitialize(props, out2, err2);
throw new RuntimeException("failure");
}
catch(IllegalArgumentException pEx) {
// Normal case
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
}
}
/**
* Tests that the root logger's default level is WARN and that loggers do not
* log bellow this level and do log in the correct stream for levels equal to
* and above WARN.
*/
private static void testRootLoggerDefault() {
Properties props= new Properties();
ByteArrayOutputStream out1= new ByteArrayOutputStream();
ByteArrayOutputStream err1= new ByteArrayOutputStream();
PrintStream out2= new PrintStream(out1);
PrintStream err2= new PrintStream(err1);
testInitialize(props, out2, err2);
Logger strLogger= Logger.getLogger(String.class);
strLogger.trace("trace should not appear");
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
strLogger.debug("debug should not appear");
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
strLogger.info("info should not appear");
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
strLogger.warn("warn should appear");
int outlength= out1.toString().length();
Assert.assertTrue(out1.toString().startsWith("[java.lang.String] [WARN] warn should appear"));
Assert.assertEquals(err1.toString(), "");
strLogger.error("error should appear");
Assert.assertEquals(out1.toString().length(), outlength);
Assert.assertTrue(err1.toString().startsWith("[java.lang.String] [ERROR] error should appear"));
strLogger.fatal("fatal should appear");
Assert.assertEquals(out1.toString().length(), outlength);
Assert.assertTrue(err1.toString().contains("[java.lang.String] [FATAL] fatal should appear"));
}
/**
* Test setting the root logger level
*/
private static void testRootLoggerSet() {
Properties props= new Properties();
ByteArrayOutputStream out1= new ByteArrayOutputStream();
ByteArrayOutputStream err1= new ByteArrayOutputStream();
PrintStream out2= new PrintStream(out1);
PrintStream err2= new PrintStream(err1);
props.put("log4testng.rootLogger", "DEBUG");
testInitialize(props, out2, err2);
Logger strLogger= Logger.getLogger(String.class);
strLogger.trace("trace should appear");
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
strLogger.debug("debug should appear");
Assert.assertTrue(out1.toString().startsWith("[java.lang.String] [DEBUG] debug should appear"));
Assert.assertEquals(err1.toString(), "");
}
/**
* Test setting the root logger to an illegal level value throws an exception.
*/
private static void testRootLoggerSetError() {
Properties props= new Properties();
ByteArrayOutputStream out1= new ByteArrayOutputStream();
ByteArrayOutputStream err1= new ByteArrayOutputStream();
PrintStream out2= new PrintStream(out1);
PrintStream err2= new PrintStream(err1);
props.put("log4testng.rootLogger", "unknown");
try {
testInitialize(props, out2, err2);
throw new RuntimeException("failure");
}
catch(IllegalArgumentException pEx) {
// Normal case
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
}
}
/**
* Test setting a user logger level
*/
private static void testUserLoggerSet() {
Properties props= new Properties();
ByteArrayOutputStream out1= new ByteArrayOutputStream();
ByteArrayOutputStream err1= new ByteArrayOutputStream();
PrintStream out2= new PrintStream(out1);
PrintStream err2= new PrintStream(err1);
props.put("log4testng.logger.java.lang.String", "DEBUG");
testInitialize(props, out2, err2);
Logger strLogger= Logger.getLogger(String.class);
strLogger.trace("trace should not appear");
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
strLogger.debug("debug should appear");
int outLength= out1.toString().length();
Assert.assertTrue(out1.toString().startsWith("[java.lang.String] [DEBUG] debug should appear"));
Assert.assertEquals(err1.toString(), "");
Logger classLogger= Logger.getLogger(Class.class);
classLogger.debug("debug should not appear");
Assert.assertEquals(out1.toString().length(), outLength);
Assert.assertEquals(err1.toString(), "");
classLogger.warn("warn should appear");
Assert.assertTrue(out1.toString().contains("[java.lang.Class] [WARN] warn should appear"));
Assert.assertEquals(err1.toString(), "");
}
/**
* Test setting a user logger to an illegal level value throws an exception
*/
private static void testUserLoggerSetError() {
Properties props= new Properties();
ByteArrayOutputStream out1= new ByteArrayOutputStream();
ByteArrayOutputStream err1= new ByteArrayOutputStream();
PrintStream out2= new PrintStream(out1);
PrintStream err2= new PrintStream(err1);
props.put("log4testng.logger.java.lang.String", "unknown");
try {
testInitialize(props, out2, err2);
throw new RuntimeException("failure");
}
catch(IllegalArgumentException pEx) {
// Normal case
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
}
}
/**
* Tests setting a partial logger name (a hierarchy scope)
*/
private static void testUserLoggerSetHierarchy() {
Properties props= new Properties();
ByteArrayOutputStream out1= new ByteArrayOutputStream();
ByteArrayOutputStream err1= new ByteArrayOutputStream();
PrintStream out2= new PrintStream(out1);
PrintStream err2= new PrintStream(err1);
props.put("log4testng.logger.java.lang", "DEBUG");
testInitialize(props, out2, err2);
Logger strLogger= Logger.getLogger(String.class);
strLogger.trace("trace should not appear");
Assert.assertEquals(out1.toString(), "");
Assert.assertEquals(err1.toString(), "");
strLogger.debug("debug should appear");
Assert.assertTrue(out1.toString().startsWith("[java.lang.String] [DEBUG] debug should appear"));
Assert.assertEquals(err1.toString(), "");
}
/**
* Run all tests. (very crusty ...)
* @param pArgs not used
*/
public static void main(String[] pArgs) {
testDebugDefault();
testDebugOn();
testDebugOff();
testDebugError();
testRootLoggerDefault();
testRootLoggerSet();
testRootLoggerSetError();
testUserLoggerSet();
testUserLoggerSetError();
testUserLoggerSetHierarchy();
}
}