diff options
| author | Chick Markley | 2017-05-18 12:23:40 -0700 |
|---|---|---|
| committer | GitHub | 2017-05-18 12:23:40 -0700 |
| commit | 9c50af20027801d8623edd1db2c63c4eb449b3ae (patch) | |
| tree | bee5ddcab07ca0e498c52a53f0b8d3e1c3b22293 /src | |
| parent | d824c60c9643973e0ae9cddc5007b3d9592f8a52 (diff) | |
Upgrade Logging facility (#488)
* Upgrade Logging facility
Make thread-safe
Make logging by package name work
Use caching of class names to level for performance
Make some tests to show this working
* quick fix for dynamic logging variable
* A number of changes based on Adam's suggestions
Default LoggerState
But there is an invoke method now to handle threading issues. This should be propagated to other
projects Driver.execute methods
* Add built-in support for string capture of Logging
* Usability fixes for logging stuff. Settings made to the logger prior to execute/invoke will be passed along if possible.
* A couple style fixes
Comment and privatize Logger state
* Name and save string buffers used for logging
* Fix default logging state setting
Fix logging test, did not have change to command argument
* comment out logging in InlineInstanceTests
* Changed invoke to makeScope
Nested makeScopes share same state object
Removed earlier named string buffer implementation
* Better name for captor get data
* Add trace tests to make sure it works too
* Fix call into logger settings
Diffstat (limited to 'src')
| -rw-r--r-- | src/main/scala/firrtl/Driver.scala | 72 | ||||
| -rw-r--r-- | src/main/scala/firrtl/ExecutionOptionsManager.scala | 10 | ||||
| -rw-r--r-- | src/main/scala/firrtl/Utils.scala | 1 | ||||
| -rw-r--r-- | src/main/scala/logger/Logger.scala | 327 | ||||
| -rw-r--r-- | src/test/scala/firrtlTests/InlineInstancesTests.scala | 19 | ||||
| -rw-r--r-- | src/test/scala/loggertests/LoggerSpec.scala | 327 |
6 files changed, 682 insertions, 74 deletions
diff --git a/src/main/scala/firrtl/Driver.scala b/src/main/scala/firrtl/Driver.scala index 5db572fe..5e3d046a 100644 --- a/src/main/scala/firrtl/Driver.scala +++ b/src/main/scala/firrtl/Driver.scala @@ -39,6 +39,7 @@ import Utils.throwInternalError */ object Driver { + //noinspection ScalaDeprecation // Compiles circuit. First parses a circuit from an input file, // executes all compiler passes, and writes result to an output // file. @@ -118,51 +119,51 @@ object Driver { * Run the firrtl compiler using the provided option * * @param optionsManager the desired flags to the compiler - * @return a FirrtlExectionResult indicating success or failure, provide access to emitted data on success + * @return a FirrtlExecutionResult indicating success or failure, provide access to emitted data on success * for downstream tools as desired */ + //scalastyle:off cyclomatic.complexity method.length def execute(optionsManager: ExecutionOptionsManager with HasFirrtlOptions): FirrtlExecutionResult = { def firrtlConfig = optionsManager.firrtlOptions - Logger.setOptions(optionsManager) - - val firrtlSource = firrtlConfig.firrtlSource match { - case Some(text) => text.split("\n").toIterator - case None => - if(optionsManager.topName.isEmpty && firrtlConfig.inputFileNameOverride.isEmpty) { - val message = "either top-name or input-file-override must be set" - dramaticError(message) - return FirrtlExecutionFailure(message) - } - if( - optionsManager.topName.isEmpty && - firrtlConfig.inputFileNameOverride.nonEmpty && - firrtlConfig.outputFileNameOverride.isEmpty) { - val message = "inputFileName set but neither top-name or output-file-override is set" - dramaticError(message) - return FirrtlExecutionFailure(message) - } - val inputFileName = firrtlConfig.getInputFileName(optionsManager) - try { - io.Source.fromFile(inputFileName).getLines() - } - catch { - case _: FileNotFoundException => - val message = s"Input file $inputFileName not found" + Logger.makeScope(optionsManager) { + val firrtlSource = firrtlConfig.firrtlSource match { + case Some(text) => text.split("\n").toIterator + case None => + if (optionsManager.topName.isEmpty && firrtlConfig.inputFileNameOverride.isEmpty) { + val message = "either top-name or input-file-override must be set" dramaticError(message) return FirrtlExecutionFailure(message) } - } + if ( + optionsManager.topName.isEmpty && + firrtlConfig.inputFileNameOverride.nonEmpty && + firrtlConfig.outputFileNameOverride.isEmpty) { + val message = "inputFileName set but neither top-name or output-file-override is set" + dramaticError(message) + return FirrtlExecutionFailure(message) + } + val inputFileName = firrtlConfig.getInputFileName(optionsManager) + try { + io.Source.fromFile(inputFileName).getLines() + } + catch { + case _: FileNotFoundException => + val message = s"Input file $inputFileName not found" + dramaticError(message) + return FirrtlExecutionFailure(message) + } + } - loadAnnotations(optionsManager) + loadAnnotations(optionsManager) - val parsedInput = Parser.parse(firrtlSource, firrtlConfig.infoMode) + val parsedInput = Parser.parse(firrtlSource, firrtlConfig.infoMode) - // Does this need to be before calling compiler? - optionsManager.makeTargetDir() + // Does this need to be before calling compiler? + optionsManager.makeTargetDir() - // Output Annotations - val outputAnnos = firrtlConfig.getEmitterAnnos(optionsManager) + // Output Annotations + val outputAnnos = firrtlConfig.getEmitterAnnos(optionsManager) // Should these and outputAnnos be moved to loadAnnotations? val globalAnnos = Seq(TargetDirAnnotation(optionsManager.targetDirName)) @@ -196,7 +197,8 @@ object Driver { "" // Should we return something different here? } - FirrtlExecutionSuccess(firrtlConfig.compilerName, emittedRes) + FirrtlExecutionSuccess(firrtlConfig.compilerName, emittedRes) + } } /** @@ -295,7 +297,7 @@ object FileUtils { def isCommandAvailable(cmd: String): Boolean = { // Eat any output. val sb = new StringBuffer - val ioToDevNull = BasicIO(false, sb, None) + val ioToDevNull = BasicIO(withIn = false, sb, None) Seq("bash", "-c", "which %s".format(cmd)).run(ioToDevNull).exitValue == 0 } diff --git a/src/main/scala/firrtl/ExecutionOptionsManager.scala b/src/main/scala/firrtl/ExecutionOptionsManager.scala index 2e95ee0c..76b497ec 100644 --- a/src/main/scala/firrtl/ExecutionOptionsManager.scala +++ b/src/main/scala/firrtl/ExecutionOptionsManager.scala @@ -29,11 +29,11 @@ abstract class HasParser(applicationName: String) { * circuit and then set the topName from that if it has not already been set. */ case class CommonOptions( - topName: String = "", - targetDirName: String = ".", - globalLogLevel: LogLevel.Value = LogLevel.Error, - logToFile: Boolean = false, - logClassNames: Boolean = false, + topName: String = "", + targetDirName: String = ".", + globalLogLevel: LogLevel.Value = LogLevel.None, + logToFile: Boolean = false, + logClassNames: Boolean = false, classLogLevels: Map[String, LogLevel.Value] = Map.empty) extends ComposableOptions { def getLogFileName(optionsManager: ExecutionOptionsManager): String = { diff --git a/src/main/scala/firrtl/Utils.scala b/src/main/scala/firrtl/Utils.scala index 3bde6f11..32869dd3 100644 --- a/src/main/scala/firrtl/Utils.scala +++ b/src/main/scala/firrtl/Utils.scala @@ -400,7 +400,6 @@ object Utils extends LazyLogging { ilen + get_size(t1x.tpe), jlen + get_size(t2x.tpe)) }._1 case (ClockType, ClockType) => if (flip1 == flip2) Seq((0, 0)) else Nil - case (AnalogType(w1), AnalogType(w2)) => Nil case _ => error("shouldn't be here") } } diff --git a/src/main/scala/logger/Logger.scala b/src/main/scala/logger/Logger.scala index dcacbc63..806083d1 100644 --- a/src/main/scala/logger/Logger.scala +++ b/src/main/scala/logger/Logger.scala @@ -2,10 +2,12 @@ package logger -import java.io.{PrintStream, File, FileOutputStream} +import java.io.{ByteArrayOutputStream, File, FileOutputStream, PrintStream} import firrtl.ExecutionOptionsManager +import scala.util.DynamicVariable + /** * This provides a facility for a log4scala* type logging system. Why did we write our own? Because * the canned ones are just to darned hard to turn on, particularly when embedded in a distribution. @@ -26,7 +28,7 @@ import firrtl.ExecutionOptionsManager * The supported log levels, what do they mean? Whatever you want them to. */ object LogLevel extends Enumeration { - val Error, Warn, Info, Debug, Trace = Value + val Error, Warn, Info, Debug, Trace, None = Value } /** @@ -37,44 +39,313 @@ trait LazyLogging { } /** - * Singleton in control of what is supposed to get logged, how it's to be logged and where it is to be logged + * Mutable state of the logging system. Multiple LoggerStates may be present + * when used in multi-threaded environments */ -object Logger { - var globalLevel = LogLevel.Error +private class LoggerState { + var globalLevel = LogLevel.None val classLevels = new scala.collection.mutable.HashMap[String, LogLevel.Value] + val classToLevelCache = new scala.collection.mutable.HashMap[String, LogLevel.Value] var logClassNames = false + var stream: PrintStream = System.out + var fromInvoke: Boolean = false // this is used to not have invokes re-create run-state + var stringBufferOption: Option[Logger.OutputCaptor] = None + + override def toString: String = { + s"gl $globalLevel classLevels ${classLevels.mkString("\n")}" + } + + /** + * create a new state object copying the basic values of this state + * @return new state object + */ + def copy: LoggerState = { + val newState = new LoggerState + newState.globalLevel = this.globalLevel + newState.classLevels ++= this.classLevels + newState.stream = this.stream + newState.logClassNames = this.logClassNames + newState + } +} + +/** + * Singleton in control of what is supposed to get logged, how it's to be logged and where it is to be logged + * We uses a dynamic variable in case multiple threads are used as can be in scalatests + */ +object Logger { + private val updatableLoggerState = new DynamicVariable[Option[LoggerState]](Some(new LoggerState)) + private def state: LoggerState = { + updatableLoggerState.value.get + } + + /** + * a class for managing capturing logging output in a string buffer + */ + class OutputCaptor { + val byteArrayOutputStream = new ByteArrayOutputStream() + val printStream = new PrintStream(byteArrayOutputStream) + + /** + * Get logged messages to this captor as a string + * @return + */ + def getOutputAsString: String = { + byteArrayOutputStream.toString + } - def showMessage(level: LogLevel.Value, className: String, message: => String): Unit = { - if(globalLevel == level || (classLevels.nonEmpty && classLevels.getOrElse(className, LogLevel.Error) >= level)) { - if(logClassNames) { - stream.println(s"[$level:$className] $message") + /** + * Clear the string buffer + */ + def clear(): Unit = { + byteArrayOutputStream.reset() + } + } + + /** + * This creates a block of code that will have access to the + * thread specific logger. The state will be set according to the + * logging options set in the common options of the manager + * @param manager source of logger settings + * @param codeBlock code to be run with these logger settings + * @tparam A The return type of codeBlock + * @return Whatever block returns + */ + def makeScope[A](manager: ExecutionOptionsManager)(codeBlock: => A): A = { + val runState: LoggerState = { + val newRunState = updatableLoggerState.value.getOrElse(new LoggerState) + if(newRunState.fromInvoke) { + newRunState } else { - stream.println(message) + val forcedNewRunState = new LoggerState + forcedNewRunState.fromInvoke = true + forcedNewRunState } } + + updatableLoggerState.withValue(Some(runState)) { + setOptions(manager) + codeBlock + } } - var stream: PrintStream = System.out + /** + * See makeScope using manager. This creates a manager from a command line arguments style + * list of strings + * @param args List of strings + * @param codeBlock the block to call + * @tparam A return type of codeBlock + * @return + */ + def makeScope[A](args: Array[String] = Array.empty)(codeBlock: => A): A = { + val executionOptionsManager = new ExecutionOptionsManager("logger") + if(executionOptionsManager.parse(args)) { + makeScope(executionOptionsManager)(codeBlock) + } + else { + throw new Exception(s"logger invoke failed to parse args ${args.mkString(", ")}") + } + } + + + /** + * Used to test whether a given log statement should generate some logging output. + * It breaks up a class name into a list of packages. From this list generate progressively + * broader names (lopping off from right side) checking for a match + * @param className class name that the logging statement came from + * @param level the level of the log statement being evaluated + * @return + */ + private def testPackageNameMatch(className: String, level: LogLevel.Value): Option[Boolean] = { + val classLevels = state.classLevels + if(classLevels.isEmpty) return None + + // If this class name in cache just use that value + val levelForThisClassName = state.classToLevelCache.getOrElse(className, { + // otherwise break up the class name in to full package path as list and find most specific entry you can + val packageNameList = className.split("""\.""").toList + /* + * start with full class path, lopping off from the tail until nothing left + */ + def matchPathToFindLevel(packageList: List[String]): LogLevel.Value = { + if(packageList.isEmpty) { + LogLevel.None + } + else { + val partialName = packageList.mkString(".") + val level = classLevels.getOrElse(partialName, { + matchPathToFindLevel(packageList.reverse.tail.reverse) + }) + level + } + } + + val levelSpecified = matchPathToFindLevel(packageNameList) + if(levelSpecified != LogLevel.None) { + state.classToLevelCache(className) = levelSpecified + } + levelSpecified + }) + if(levelForThisClassName != LogLevel.None) { + Some(levelForThisClassName >= level) + } + else { + None + } + } + + /** + * Used as the common log routine, for warn, debug etc. Only calls message if log should be generated + * Allows lazy evaluation of any string interpolation or function that generates the message itself + * @note package level supercedes global, which allows one to turn on debug everywhere except for specific classes + * @param level level of the called statement + * @param className class name of statement + * @param message a function returning a string with the message + */ + //scalastyle:off regex + private def showMessage(level: LogLevel.Value, className: String, message: => String): Unit = { + def logIt(): Unit = { + if(state.logClassNames) { + state.stream.println(s"[$level:$className] $message") + } + else { + state.stream.println(message) + } + } + testPackageNameMatch(className, level) match { + case Some(true) => logIt() + case Some(false) => + case None => + if((state.globalLevel == LogLevel.None && level == LogLevel.Error) || + (state.globalLevel != LogLevel.None && state.globalLevel >= level)) { + logIt() + } + } + } + + def getGlobalLevel: LogLevel.Value = { + state.globalLevel + } + /** + * This resets everything in the current Logger environment, including the destination + * use this with caution. Unexpected things can happen + */ + def reset(): Unit = { + state.classLevels.clear() + clearCache() + state.logClassNames = false + state.globalLevel = LogLevel.Error + state.stream = System.out + } + + /** + * clears the cache of class names top class specific log levels + */ + private def clearCache(): Unit = { + state.classToLevelCache.clear() + } + + /** + * This sets the global logging level + * @param level The desired global logging level + */ + def setLevel(level: LogLevel.Value): Unit = { + state.globalLevel = level + } + + /** + * This sets the logging level for a particular class or package + * The package name must be general to specific. I.e. + * package1.package2.class + * package1.package2 + * package1 + * Will work. + * package2.class will not work if package2 is within package1 + * @param classOrPackageName The string based class name or + * @param level The desired global logging level + */ + def setLevel(classOrPackageName: String, level: LogLevel.Value): Unit = { + clearCache() + state.classLevels(classOrPackageName) = level + } + + /** + * Set the log level based on a class type + * @example {{{ setLevel(classOf[SomeClass], LogLevel.Debug) }}} + * @param classType Kind of class + * @param level log level to set + */ + def setLevel(classType: Class[_ <: LazyLogging], level: LogLevel.Value): Unit = { + clearCache() + val name = classType.getCanonicalName + state.classLevels(name) = level + } + + /** + * Clears the logging data in the string capture buffer if it exists + * @return The logging data if it exists + */ + def clearStringBuffer(): Unit = { + state.stringBufferOption match { + case Some(x) => x.byteArrayOutputStream.reset() + case None => + } + } + + /** + * Set the logging destination to a file name + * @param fileName destination name + */ def setOutput(fileName: String): Unit = { - stream = new PrintStream(new FileOutputStream(new File(fileName))) + state.stream = new PrintStream(new FileOutputStream(new File(fileName))) + } + + /** + * Set the logging destination to a print stream + * @param stream destination stream + */ + def setOutput(stream: PrintStream): Unit = { + state.stream = stream } + + /** + * Sets the logging destination to Console.out + */ def setConsole(): Unit = { - stream = Console.out + state.stream = Console.out } + + /** + * Adds a list of of className, loglevel tuples to the global (dynamicVar) + * See [[testPackageNameMatch]] for a description of how class name matching works + * @param namesToLevel a list of tuples (class name, log level) + */ def setClassLogLevels(namesToLevel: Map[String, LogLevel.Value]): Unit = { - classLevels ++= namesToLevel + clearCache() + state.classLevels ++= namesToLevel } + /** + * This is used to set the options that have been set in a optionsManager or are coming + * from the command line via an options manager + * @param optionsManager manager + */ def setOptions(optionsManager: ExecutionOptionsManager): Unit = { val commonOptions = optionsManager.commonOptions - globalLevel = commonOptions.globalLogLevel + state.globalLevel = (state.globalLevel, commonOptions.globalLogLevel) match { + case (LogLevel.None, LogLevel.None) => LogLevel.None + case (x, LogLevel.None) => x + case (LogLevel.None, x) => x + case (_, x) => x + case _ => LogLevel.Error + } setClassLogLevels(commonOptions.classLogLevels) if(commonOptions.logToFile) { setOutput(commonOptions.getLogFileName(optionsManager)) } - logClassNames = commonOptions.logClassNames + state.logClassNames = commonOptions.logClassNames } } @@ -83,23 +354,39 @@ object Logger { * @param containerClass passed in from the LazyLogging trait in order to provide class level logging granularity */ class Logger(containerClass: String) { + /** + * Log message at Error level + * @param message message generator to be invoked if level is right + */ def error(message: => String): Unit = { Logger.showMessage(LogLevel.Error, containerClass, message) - Logger.showMessage(LogLevel.Debug, containerClass, message) } + /** + * Log message at Warn level + * @param message message generator to be invoked if level is right + */ def warn(message: => String): Unit = { Logger.showMessage(LogLevel.Warn, containerClass, message) - Logger.showMessage(LogLevel.Debug, containerClass, message) } + /** + * Log message at Inof level + * @param message message generator to be invoked if level is right + */ def info(message: => String): Unit = { Logger.showMessage(LogLevel.Info, containerClass, message) - Logger.showMessage(LogLevel.Debug, containerClass, message) } + /** + * Log message at Debug level + * @param message message generator to be invoked if level is right + */ def debug(message: => String): Unit = { Logger.showMessage(LogLevel.Debug, containerClass, message) } + /** + * Log message at Trace level + * @param message message generator to be invoked if level is right + */ def trace(message: => String): Unit = { Logger.showMessage(LogLevel.Trace, containerClass, message) - Logger.showMessage(LogLevel.Debug, containerClass, message) } } diff --git a/src/test/scala/firrtlTests/InlineInstancesTests.scala b/src/test/scala/firrtlTests/InlineInstancesTests.scala index a3b7386d..9e8f8054 100644 --- a/src/test/scala/firrtlTests/InlineInstancesTests.scala +++ b/src/test/scala/firrtlTests/InlineInstancesTests.scala @@ -5,19 +5,12 @@ package firrtlTests import org.scalatest.FlatSpec import org.scalatest.Matchers import org.scalatest.junit.JUnitRunner - import firrtl.ir.Circuit -import firrtl.{Parser, AnnotationMap} +import firrtl.{AnnotationMap, Parser} import firrtl.passes.PassExceptions -import firrtl.annotations.{ - Named, - CircuitName, - ModuleName, - ComponentName, - Annotation -} -import firrtl.passes.{InlineInstances, InlineAnnotation} -import logger.Logger +import firrtl.annotations.{Annotation, CircuitName, ComponentName, ModuleName, Named} +import firrtl.passes.{InlineAnnotation, InlineInstances} +import logger.{LogLevel, Logger} import logger.LogLevel.Debug @@ -26,8 +19,8 @@ import logger.LogLevel.Debug */ class InlineInstancesTests extends LowTransformSpec { def transform = new InlineInstances - // Set this to debug - // Logger.setClassLogLevels(Map(this.getClass.getName -> Debug)) + // Set this to debug, this will apply to all tests + // Logger.setLevel(this.getClass, Debug) "The module Inline" should "be inlined" in { val input = """circuit Top : diff --git a/src/test/scala/loggertests/LoggerSpec.scala b/src/test/scala/loggertests/LoggerSpec.scala new file mode 100644 index 00000000..0a9aaa51 --- /dev/null +++ b/src/test/scala/loggertests/LoggerSpec.scala @@ -0,0 +1,327 @@ +// See LICENSE for license details. + +package loggertests + +import logger.Logger.OutputCaptor +import logger.{LazyLogging, LogLevel, Logger} +import org.scalatest.{FreeSpec, Matchers, OneInstancePerTest} + +object LoggerSpec { + val ErrorMsg = "message error" + val WarnMsg = "message warn" + val InfoMsg = "message info" + val DebugMsg = "message debug" + val TraceMsg = "message trace" +} + +class Logger1 extends LazyLogging { + def run(): Unit = { + logger.error(LoggerSpec.ErrorMsg) + logger.warn(LoggerSpec.WarnMsg) + logger.info(LoggerSpec.InfoMsg) + logger.debug(LoggerSpec.DebugMsg) + logger.trace(LoggerSpec.TraceMsg) + } +} + +class LogsInfo2 extends LazyLogging { + def run(): Unit = { + logger.info("logger2") + } +} +class LogsInfo3 extends LazyLogging { + def run(): Unit = { + logger.info("logger3") + } +} +class LoggerSpec extends FreeSpec with Matchers with OneInstancePerTest with LazyLogging { + "Logger is a simple but powerful logging system" - { + "Following tests show how global level can control logging" - { + "only error shows up by default" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + + val r1 = new Logger1 + r1.run() + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains(LoggerSpec.ErrorMsg) should be(true) + messagesLogged.contains(LoggerSpec.WarnMsg) should be(false) + messagesLogged.contains(LoggerSpec.InfoMsg) should be(false) + messagesLogged.contains(LoggerSpec.DebugMsg) should be(false) + messagesLogged.contains(LoggerSpec.TraceMsg) should be(false) + } + } + + "setting level to warn will result in error and warn messages" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + Logger.setLevel(LogLevel.Warn) + + val r1 = new Logger1 + r1.run() + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains(LoggerSpec.ErrorMsg) should be(true) + messagesLogged.contains(LoggerSpec.WarnMsg) should be(true) + messagesLogged.contains(LoggerSpec.InfoMsg) should be(false) + messagesLogged.contains(LoggerSpec.DebugMsg) should be(false) + } + } + "setting level to info will result in error, info, and warn messages" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + Logger.setLevel(LogLevel.Info) + + val r1 = new Logger1 + r1.run() + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains(LoggerSpec.ErrorMsg) should be(true) + messagesLogged.contains(LoggerSpec.WarnMsg) should be(true) + messagesLogged.contains(LoggerSpec.InfoMsg) should be(true) + messagesLogged.contains(LoggerSpec.DebugMsg) should be(false) + } + } + "setting level to debug will result in error, info, debug, and warn messages" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + + Logger.setLevel(LogLevel.Error) + Logger.setOutput(captor.printStream) + Logger.setLevel(LogLevel.Debug) + + val r1 = new Logger1 + r1.run() + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains(LoggerSpec.ErrorMsg) should be(true) + messagesLogged.contains(LoggerSpec.WarnMsg) should be(true) + messagesLogged.contains(LoggerSpec.InfoMsg) should be(true) + messagesLogged.contains(LoggerSpec.DebugMsg) should be(true) + messagesLogged.contains(LoggerSpec.TraceMsg) should be(false) + } + } + "setting level to trace will result in error, info, debug, trace, and warn messages" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + + Logger.setLevel(LogLevel.Error) + Logger.setOutput(captor.printStream) + Logger.setLevel(LogLevel.Trace) + + val r1 = new Logger1 + r1.run() + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains(LoggerSpec.ErrorMsg) should be(true) + messagesLogged.contains(LoggerSpec.WarnMsg) should be(true) + messagesLogged.contains(LoggerSpec.InfoMsg) should be(true) + messagesLogged.contains(LoggerSpec.DebugMsg) should be(true) + messagesLogged.contains(LoggerSpec.TraceMsg) should be(true) + } + } + } + "the following tests show how logging can be controlled by package and class name" - { + "only capture output by class name" - { + "capture logging from LogsInfo2" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + + Logger.setLevel("loggertests.LogsInfo2", LogLevel.Info) + + val r2 = new LogsInfo2 + val r3 = new LogsInfo3 + r3.run() + r2.run() + + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains("logger3") should be(false) + messagesLogged.contains("logger2") should be(true) + } + } + "capture logging from LogsInfo2 using class" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + + Logger.setLevel(classOf[LogsInfo2], LogLevel.Info) + + val r2 = new LogsInfo2 + val r3 = new LogsInfo3 + r3.run() + r2.run() + + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains("logger3") should be(false) + messagesLogged.contains("logger2") should be(true) + } + } + "capture logging from LogsInfo3" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + + Logger.setLevel("loggertests.LogsInfo3", LogLevel.Info) + + val r2 = new LogsInfo2 + val r3 = new LogsInfo3 + r2.run() + r3.run() + + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains("logger2") should be(false) + messagesLogged.contains("logger3") should be(true) + } + } + } + "log based on package name" - { + "both log because of package, also showing re-run after change works" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + + Logger.setLevel(LogLevel.Error) + Logger.setLevel("loggertests", LogLevel.Error) + + val r2 = new LogsInfo2 + val r3 = new LogsInfo3 + r2.run() + r3.run() + + var messagesLogged = captor.getOutputAsString + + messagesLogged.contains("logger2") should be(false) + messagesLogged.contains("logger3") should be(false) + + Logger.setLevel("loggertests", LogLevel.Debug) + + r2.run() + r3.run() + + messagesLogged = captor.getOutputAsString + + messagesLogged.contains("logger2") should be(true) + messagesLogged.contains("logger3") should be(true) + } + } + } + "check for false positives" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + + Logger.setLevel("bad-loggertests", LogLevel.Info) + + val r2 = new LogsInfo2 + val r3 = new LogsInfo3 + r2.run() + r3.run() + + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains("logger2") should be(false) + messagesLogged.contains("logger3") should be(false) + } + } + "show that class specific level supercedes global level" in { + Logger.makeScope() { + val captor = new OutputCaptor + Logger.setOutput(captor.printStream) + + + Logger.setLevel(LogLevel.Info) + Logger.setLevel("loggertests.LogsInfo2", LogLevel.Error) + + val r2 = new LogsInfo2 + val r3 = new LogsInfo3 + r2.run() + r3.run() + + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains("logger2") should be(false) + messagesLogged.contains("logger3") should be(true) + } + } + "Show logging can be set with command options" in { + val captor = new Logger.OutputCaptor + + Logger.makeScope(Array("--class-log-level", "loggertests.LogsInfo3:info")) { + Logger.setOutput(captor.printStream) + val r2 = new LogsInfo2 + val r3 = new LogsInfo3 + r2.run() + r3.run() + + val messagesLogged = captor.getOutputAsString + + messagesLogged.contains("logger2") should be(false) + messagesLogged.contains("logger3") should be(true) + } + } + "Show that printstream remains across makeScopes" in { + Logger.makeScope() { + val captor = new Logger.OutputCaptor + Logger.setOutput(captor.printStream) + + logger.error("message 1") + Logger.makeScope() { + logger.error("message 2") + } + + val logText = captor.getOutputAsString + logText should include ("message 1") + logText should include ("message 2") + } + } + "Show that nested makeScopes share same state" in { + Logger.getGlobalLevel should be (LogLevel.None) + + Logger.makeScope() { + Logger.setLevel(LogLevel.Info) + + Logger.getGlobalLevel should be (LogLevel.Info) + + Logger.makeScope() { + Logger.getGlobalLevel should be (LogLevel.Info) + } + + Logger.makeScope() { + Logger.setLevel(LogLevel.Debug) + Logger.getGlobalLevel should be (LogLevel.Debug) + } + + Logger.getGlobalLevel should be (LogLevel.Debug) + } + + Logger.getGlobalLevel should be (LogLevel.None) + } + + "Show that first makeScope starts with fresh state" in { + Logger.getGlobalLevel should be (LogLevel.None) + + Logger.setLevel(LogLevel.Warn) + Logger.getGlobalLevel should be (LogLevel.Warn) + + Logger.makeScope() { + Logger.getGlobalLevel should be (LogLevel.None) + + Logger.setLevel(LogLevel.Trace) + Logger.getGlobalLevel should be (LogLevel.Trace) + } + + Logger.getGlobalLevel should be (LogLevel.Warn) + } + } + } +} |
