aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorChick Markley2017-05-18 12:23:40 -0700
committerGitHub2017-05-18 12:23:40 -0700
commit9c50af20027801d8623edd1db2c63c4eb449b3ae (patch)
treebee5ddcab07ca0e498c52a53f0b8d3e1c3b22293 /src
parentd824c60c9643973e0ae9cddc5007b3d9592f8a52 (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.scala72
-rw-r--r--src/main/scala/firrtl/ExecutionOptionsManager.scala10
-rw-r--r--src/main/scala/firrtl/Utils.scala1
-rw-r--r--src/main/scala/logger/Logger.scala327
-rw-r--r--src/test/scala/firrtlTests/InlineInstancesTests.scala19
-rw-r--r--src/test/scala/loggertests/LoggerSpec.scala327
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)
+ }
+ }
+ }
+}