From de8a89757e6318dfc223565061285db0a79a9aed Mon Sep 17 00:00:00 2001 From: Alyssa Date: Wed, 22 Nov 2023 08:51:18 +0000 Subject: [PATCH] add more tests, and test in CI --- .github/workflows/ci.yml | 6 +- .../logging/LoggerUncaughtExceptionHandler.kt | 3 - core/src/main/kotlin/logging/LoggingCodes.kt | 81 -------- .../logging/exceptions/ApplicationFault.kt | 8 - core/src/test/kotlin/helper/TestConstants.kt | 7 + core/src/test/kotlin/helper/TestUtils.kt | 50 +++++ .../logging/LogDestinationSystemOutTest.kt | 68 +++++++ core/src/test/kotlin/logging/LoggerTest.kt | 177 ++++++++++++++++++ .../LoggerUncaughtExceptionHandlerTest.kt | 50 +++++ .../test/kotlin/logging/TestLoggingConsole.kt | 132 +++++++++++++ 10 files changed, 488 insertions(+), 94 deletions(-) delete mode 100644 core/src/main/kotlin/logging/exceptions/ApplicationFault.kt create mode 100644 core/src/test/kotlin/helper/TestUtils.kt create mode 100644 core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt create mode 100644 core/src/test/kotlin/logging/LoggerTest.kt create mode 100644 core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt create mode 100644 core/src/test/kotlin/logging/TestLoggingConsole.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad156c9..9acebd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,5 +15,7 @@ jobs: java-version: 11.0.18 - name: Ktlint format run: ./gradlew :${{ matrix.project }}:ktlintFormat - - name: Build - run: xvfb-run --auto-servernum ./gradlew :${{ matrix.project }}:build + - name: Compile + run: ./gradlew :${{ matrix.project }}:compileKotlin + - name: Test + run: xvfb-run --auto-servernum ./gradlew :${{ matrix.project }}:test diff --git a/core/src/main/kotlin/logging/LoggerUncaughtExceptionHandler.kt b/core/src/main/kotlin/logging/LoggerUncaughtExceptionHandler.kt index 9d8232f..291e57f 100644 --- a/core/src/main/kotlin/logging/LoggerUncaughtExceptionHandler.kt +++ b/core/src/main/kotlin/logging/LoggerUncaughtExceptionHandler.kt @@ -1,6 +1,5 @@ package logging -import logging.exceptions.ApplicationFault import utils.InjectedThings.logger import java.lang.Thread.UncaughtExceptionHandler @@ -9,8 +8,6 @@ class LoggerUncaughtExceptionHandler : UncaughtExceptionHandler { if (isSuppressed(arg1)) { // Still stack trace, but don't show an error message or email a log logger.warn(CODE_UNCAUGHT_EXCEPTION, "Suppressing uncaught exception: $arg1", KEY_THREAD to arg0.toString(), KEY_EXCEPTION_MESSAGE to arg1.message) - } else if (arg1 is ApplicationFault) { - logger.error(arg1.loggingCode, "Uncaught exception: ${arg1.message}", arg1, KEY_THREAD to arg0.toString()) } else { logger.error(CODE_UNCAUGHT_EXCEPTION, "Uncaught exception: $arg1 in thread $arg0", arg1, KEY_THREAD to arg0.toString()) } diff --git a/core/src/main/kotlin/logging/LoggingCodes.kt b/core/src/main/kotlin/logging/LoggingCodes.kt index 450c6a6..2360eba 100644 --- a/core/src/main/kotlin/logging/LoggingCodes.kt +++ b/core/src/main/kotlin/logging/LoggingCodes.kt @@ -1,89 +1,8 @@ package logging // Info -val CODE_SQL = LoggingCode("sql") -val CODE_BULK_SQL = LoggingCode("bulkSql") -val CODE_JUST_UPDATED = LoggingCode("justUpdated") -val CODE_MEMORY_SETTINGS = LoggingCode("memorySettings") -val CODE_TABLE_CREATED = LoggingCode("tableCreated") -val CODE_TABLE_EXISTS = LoggingCode("tableExists") -val CODE_LOOK_AND_FEEL_SET = LoggingCode("lafSet") -val CODE_DATABASE_UP_TO_DATE = LoggingCode("databaseCurrent") -val CODE_DATABASE_NEEDS_UPDATE = LoggingCode("databaseNeedsUpdate") -val CODE_DATABASE_CREATING = LoggingCode("databaseCreating") -val CODE_DATABASE_CREATED = LoggingCode("databaseCreated") val CODE_THREAD_STACKS = LoggingCode("threadStacks") val CODE_THREAD_STACK = LoggingCode("threadStack") -val CODE_NEW_CONNECTION = LoggingCode("newConnection") -val CODE_SANITY_CHECK_STARTED = LoggingCode("sanityCheckStarted") -val CODE_SANITY_CHECK_COMPLETED = LoggingCode("sanityCheckCompleted") -val CODE_SANITY_CHECK_RESULT = LoggingCode("sanityCheckResult") -val CODE_SIMULATION_STARTED = LoggingCode("simulationStarted") -val CODE_SIMULATION_PROGRESS = LoggingCode("simulationProgress") -val CODE_SIMULATION_CANCELLED = LoggingCode("simulationCancelled") -val CODE_SIMULATION_FINISHED = LoggingCode("simulationFinished") -val CODE_DIALOG_SHOWN = LoggingCode("dialogShown") -val CODE_DIALOG_CLOSED = LoggingCode("dialogClosed") -val CODE_COMMAND_ENTERED = LoggingCode("commandEntered") -val CODE_UPDATE_CHECK = LoggingCode("updateCheck") -val CODE_UPDATE_CHECK_RESULT = LoggingCode("updateCheckResult") -val CODE_LOADED_RESOURCES = LoggingCode("loadedResources") -val CODE_STARTING_BACKUP = LoggingCode("startingBackup") -val CODE_STARTING_RESTORE = LoggingCode("startingRestore") -val CODE_SLOW_DARTBOARD_RENDER = LoggingCode("slowDartboardRender") -val CODE_PLAYER_PAUSED = LoggingCode("playerPaused") -val CODE_PLAYER_UNPAUSED = LoggingCode("playerUnpaused") -val CODE_FETCHING_DATABASE = LoggingCode("fetchingDatabase") -val CODE_FETCHED_DATABASE = LoggingCode("fetchedDatabase") -val CODE_UNZIPPED_DATABASE = LoggingCode("unzippedDatabase") -val CODE_PUSHING_DATABASE = LoggingCode("pushingDatabase") -val CODE_ZIPPED_DATABASE = LoggingCode("zippedDatabase") -val CODE_PUSHED_DATABASE = LoggingCode("pushedDatabase") -val CODE_PUSHED_DATABASE_BACKUP = LoggingCode("pushedDatabaseBackup") -val CODE_MERGE_STARTED = LoggingCode("mergeStarted") -val CODE_MERGING_ENTITY = LoggingCode("mergingEntity") -val CODE_ACHIEVEMENT_CONVERSION_STARTED = LoggingCode("achievementConversionStarted") -val CODE_ACHIEVEMENT_CONVERSION_FINISHED = LoggingCode("achievementConversionFinished") -val CODE_SWITCHING_FILES = LoggingCode("switchingFiles") -val CODE_REVERT_TO_PULL = LoggingCode("revertToPull") -val CODE_SWITCHED_SCREEN = LoggingCode("switchedScreen") -val CODE_GAME_LAUNCHED = LoggingCode("gameLaunched") -val CODE_MATCH_LAUNCHED = LoggingCode("matchLaunched") -val CODE_MATCH_FINISHED = LoggingCode("matchFinished") - -// Warn -val CODE_UNEXPECTED_ARGUMENT = LoggingCode("unexpectedArgument") -val CODE_DATABASE_TOO_OLD = LoggingCode("databaseTooOld") -val CODE_RESOURCE_CACHE_NOT_INITIALISED = LoggingCode("resourceCacheNotInitialised") -val CODE_DATABASE_IN_USE = LoggingCode("databaseInUse") -val CODE_NO_STREAMS = LoggingCode("noStreams") // Error -val CODE_LOOK_AND_FEEL_ERROR = LoggingCode("lafError") -val CODE_SQL_EXCEPTION = LoggingCode("sqlException") val CODE_UNCAUGHT_EXCEPTION = LoggingCode("uncaughtException") -val CODE_SIMULATION_ERROR = LoggingCode("simulationError") -val CODE_LOAD_ERROR = LoggingCode("loadError") -val CODE_INSTANTIATION_ERROR = LoggingCode("instantiationError") -val CODE_AUDIO_ERROR = LoggingCode("audioError") -val CODE_SCREEN_LOAD_ERROR = LoggingCode("screenLoadError") -val CODE_UPDATE_ERROR = LoggingCode("updateError") -val CODE_PARSE_ERROR = LoggingCode("parseError") -val CODE_BATCH_ERROR = LoggingCode("batchFileError") -val CODE_TEST_CONNECTION_ERROR = LoggingCode("testConnectionError") -val CODE_RESOURCE_LOAD_ERROR = LoggingCode("resourceLoadError") -val CODE_COMMAND_ERROR = LoggingCode("commandError") -val CODE_RENDER_ERROR = LoggingCode("renderError") -val CODE_FILE_ERROR = LoggingCode("fileError") -val CODE_SWING_ERROR = LoggingCode("swingError") -val CODE_AI_ERROR = LoggingCode("aiError") -val CODE_ELASTICSEARCH_ERROR = LoggingCode("elasticsearchError") -val CODE_MERGE_ERROR = LoggingCode("mergeError") -val CODE_SYNC_ERROR = LoggingCode("syncError") -val CODE_PUSH_ERROR = LoggingCode("pushError") -val CODE_PULL_ERROR = LoggingCode("pullError") -val CODE_BACKUP_ERROR = LoggingCode("backupError") -val CODE_RESTORE_ERROR = LoggingCode("restoreError") -val CODE_DELETE_ERROR = LoggingCode("deleteError") -val CODE_HYPERLINK_ERROR = LoggingCode("hyperlinkError") -val CODE_EDT_FROZEN = LoggingCode("edtFrozen") diff --git a/core/src/main/kotlin/logging/exceptions/ApplicationFault.kt b/core/src/main/kotlin/logging/exceptions/ApplicationFault.kt deleted file mode 100644 index 9365daf..0000000 --- a/core/src/main/kotlin/logging/exceptions/ApplicationFault.kt +++ /dev/null @@ -1,8 +0,0 @@ -package logging.exceptions - -import logging.LoggingCode - -data class ApplicationFault( - val loggingCode: LoggingCode, - override val message: String, -) : Exception() diff --git a/core/src/test/kotlin/helper/TestConstants.kt b/core/src/test/kotlin/helper/TestConstants.kt index c06ecae..b88627e 100644 --- a/core/src/test/kotlin/helper/TestConstants.kt +++ b/core/src/test/kotlin/helper/TestConstants.kt @@ -1,3 +1,10 @@ import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.* val CURRENT_TIME: Instant = Instant.parse("2020-04-13T11:04:00.00Z") +val CURRENT_TIME_STRING: String = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withLocale(Locale.UK) + .withZone(ZoneId.systemDefault()) + .format(CURRENT_TIME) diff --git a/core/src/test/kotlin/helper/TestUtils.kt b/core/src/test/kotlin/helper/TestUtils.kt new file mode 100644 index 0000000..65351b9 --- /dev/null +++ b/core/src/test/kotlin/helper/TestUtils.kt @@ -0,0 +1,50 @@ +package helper + +import CURRENT_TIME +import io.kotest.matchers.maps.shouldContainExactly +import logging.LogRecord +import logging.LoggingCode +import logging.Severity +import java.awt.Component +import java.awt.Container +import java.time.Instant + +fun LogRecord.shouldContainKeyValues(vararg values: Pair) { + keyValuePairs.shouldContainExactly(mapOf(*values)) +} + +fun makeLogRecord( + timestamp: Instant = CURRENT_TIME, + severity: Severity = Severity.INFO, + loggingCode: LoggingCode = LoggingCode("log"), + message: String = "A thing happened", + errorObject: Throwable? = null, + keyValuePairs: Map = mapOf(), +) = + LogRecord(timestamp, severity, loggingCode, message, errorObject, keyValuePairs) + +/** + * Recurses through all child components, returning an ArrayList of all children of the appropriate type + */ +inline fun Container.getAllChildComponentsForType(): List { + val ret = mutableListOf() + + val components = components + addComponents(ret, components, T::class.java) + + return ret +} + +@Suppress("UNCHECKED_CAST") +fun addComponents(ret: MutableList, components: Array, desiredClazz: Class) { + for (comp in components) { + if (desiredClazz.isInstance(comp)) { + ret.add(comp as T) + } + + if (comp is Container) { + val subComponents = comp.components + addComponents(ret, subComponents, desiredClazz) + } + } +} diff --git a/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt b/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt new file mode 100644 index 0000000..93f8120 --- /dev/null +++ b/core/src/test/kotlin/logging/LogDestinationSystemOutTest.kt @@ -0,0 +1,68 @@ +package logging + +import CURRENT_TIME_STRING +import helper.AbstractTest +import helper.makeLogRecord +import io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +class LogDestinationSystemOutTest : AbstractTest() { + private val originalOut = System.out + + private val newOut = ByteArrayOutputStream() + + @BeforeEach + fun beforeEach() { + System.setOut(PrintStream(newOut)) + } + + @AfterEach + fun afterEach() { + System.setOut(originalOut) + } + + @Test + fun `Should log the record to system out`() { + val dest = LogDestinationSystemOut() + + val record = makeLogRecord(severity = Severity.INFO, loggingCode = LoggingCode("some.event"), message = "blah") + dest.log(record) + + val output = newOut.toString() + output shouldContain "$CURRENT_TIME_STRING [some.event] blah" + } + + @Test + fun `Should print the stack trace for errors`() { + val dest = LogDestinationSystemOut() + + val error = Throwable("oh no") + val record = makeLogRecord(severity = Severity.ERROR, loggingCode = LoggingCode("some.event"), message = "blah", errorObject = error) + dest.log(record) + + val output = newOut.toString() + output shouldContain "$CURRENT_TIME_STRING [some.event] blah" + output shouldContain "$CURRENT_TIME_STRING java.lang.Throwable: oh no" + } + + @Test + fun `Should print the stack for a thread dump`() { + val dest = LogDestinationSystemOut() + + val record = makeLogRecord( + severity = Severity.INFO, + loggingCode = LoggingCode("some.event"), + message = "blah", + keyValuePairs = mapOf(KEY_STACK to "at Something.blah"), + ) + dest.log(record) + + val output = newOut.toString() + output shouldContain "$CURRENT_TIME_STRING [some.event] blah" + output shouldContain "at Something.blah" + } +} diff --git a/core/src/test/kotlin/logging/LoggerTest.kt b/core/src/test/kotlin/logging/LoggerTest.kt new file mode 100644 index 0000000..e87dd35 --- /dev/null +++ b/core/src/test/kotlin/logging/LoggerTest.kt @@ -0,0 +1,177 @@ +package logging + +import CURRENT_TIME +import helper.AbstractTest +import helper.FakeLogDestination +import helper.shouldContainKeyValues +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Test + +class LoggerTest : AbstractTest() { + @Test + fun `Should log INFO`() { + val destination = FakeLogDestination() + val logger = Logger(listOf(destination)) + + val loggingCode = LoggingCode("some.event") + logger.info(loggingCode, "A thing happened") + logger.waitUntilLoggingFinished() + + val record = destination.logRecords.first() + record.severity shouldBe Severity.INFO + record.loggingCode shouldBe loggingCode + record.message shouldBe "A thing happened" + record.errorObject shouldBe null + record.timestamp shouldBe CURRENT_TIME + record.keyValuePairs.size shouldBe 0 + } + + @Test + fun `Should support extra key values when logging INFO`() { + val destination = FakeLogDestination() + val logger = Logger(listOf(destination)) + + val loggingCode = LoggingCode("some.event") + logger.info(loggingCode, "A thing happened", "Key" to "Value") + logger.waitUntilLoggingFinished() + + val record = destination.logRecords.first() + record.severity shouldBe Severity.INFO + record.loggingCode shouldBe loggingCode + record.message shouldBe "A thing happened" + record.errorObject shouldBe null + record.timestamp shouldBe CURRENT_TIME + record.shouldContainKeyValues("Key" to "Value") + } + + @Test + fun `Should log WARN`() { + val destination = FakeLogDestination() + val logger = Logger(listOf(destination)) + + val loggingCode = LoggingCode("some.event") + logger.warn(loggingCode, "A slightly bad thing happened") + logger.waitUntilLoggingFinished() + + val record = destination.logRecords.first() + record.severity shouldBe Severity.WARN + record.loggingCode shouldBe loggingCode + record.message shouldBe "A slightly bad thing happened" + record.errorObject shouldBe null + record.timestamp shouldBe CURRENT_TIME + record.keyValuePairs.size shouldBe 0 + } + + @Test + fun `Should log ERROR`() { + val destination = FakeLogDestination() + val logger = Logger(listOf(destination)) + + val loggingCode = LoggingCode("bad.thing") + val throwable = Throwable("Boo") + logger.error(LoggingCode("bad.thing"), "An exception happened!", throwable, "other.info" to 60) + logger.waitUntilLoggingFinished() + + val record = destination.logRecords.first() + record.severity shouldBe Severity.ERROR + record.errorObject shouldBe throwable + record.loggingCode shouldBe loggingCode + record.timestamp shouldBe CURRENT_TIME + record.shouldContainKeyValues("other.info" to 60, KEY_EXCEPTION_MESSAGE to "Boo") + } + + @Test + fun `Should log progress correctly`() { + val destination = FakeLogDestination() + val logger = Logger(listOf(destination)) + logger.logProgress(LoggingCode("progress"), 9, 100) + logger.logProgress(LoggingCode("progress"), 11, 100) + logger.waitUntilLoggingFinished() + destination.logRecords.shouldBeEmpty() + + logger.logProgress(LoggingCode("progress"), 10, 100) + logger.waitUntilLoggingFinished() + val log = destination.logRecords.last() + log.message shouldBe "Done 10/100 (10.0%)" + } + + @Test + fun `Should log to all destinations`() { + val destinationOne = FakeLogDestination() + val destinationTwo = FakeLogDestination() + val logger = Logger(listOf(destinationOne, destinationTwo)) + logger.info(LoggingCode("foo"), "bar") + logger.waitUntilLoggingFinished() + + destinationOne.logRecords.shouldHaveSize(1) + destinationTwo.logRecords.shouldHaveSize(1) + } + + @Test + fun `Should not log on the current thread, but should be possible to await all logging having finished`() { + val destination = SleepyLogDestination() + val logger = Logger(listOf(destination)) + + logger.info(LoggingCode("foo"), "bar") + + destination.logRecords.shouldBeEmpty() + logger.waitUntilLoggingFinished() + destination.logRecords.shouldHaveSize(1) + } + + @Test + fun `Should be possible to continue logging after awaiting logging to finish`() { + val destination = SleepyLogDestination() + val logger = Logger(listOf(destination)) + + logger.info(LoggingCode("foo"), "bar") + logger.waitUntilLoggingFinished() + + logger.info(LoggingCode("foo"), "baz") + logger.waitUntilLoggingFinished() + + destination.logRecords.shouldHaveSize(2) + } + + @Test + fun `Should automatically include logging context fields`() { + val destination = FakeLogDestination() + val logger = Logger(listOf(destination)) + + logger.addToContext("appVersion", "4.1.1") + logger.info(LoggingCode("foo"), "a thing happened", "otherKey" to "otherValue") + logger.waitUntilLoggingFinished() + + val record = destination.logRecords.last() + record.shouldContainKeyValues("appVersion" to "4.1.1", "otherKey" to "otherValue") + } + + @Test + fun `Should notify destinations when context is updated`() { + val destination = mockk(relaxed = true) + val logger = Logger(listOf(destination)) + + logger.addToContext("appVersion", "4.1.1") + + verify { destination.contextUpdated(mapOf("appVersion" to "4.1.1")) } + } +} + +class SleepyLogDestination : ILogDestination { + val logRecords: MutableList = mutableListOf() + + override fun log(record: LogRecord) { + Thread.sleep(500) + logRecords.add(record) + } + + override fun contextUpdated(context: Map) {} + + fun clear() { + logRecords.clear() + } +} diff --git a/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt b/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt new file mode 100644 index 0000000..8b2aebd --- /dev/null +++ b/core/src/test/kotlin/logging/LoggerUncaughtExceptionHandlerTest.kt @@ -0,0 +1,50 @@ +package logging + +import helper.AbstractTest +import helper.shouldContainKeyValues +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.Test + +class LoggerUncaughtExceptionHandlerTest : AbstractTest() { + @Test + fun `Should log a single WARN line for suppressed errors`() { + val handler = LoggerUncaughtExceptionHandler() + + val message = "javax.swing.plaf.FontUIResource cannot be cast to class javax.swing.Painter" + val ex = Exception(message) + handler.uncaughtException(Thread.currentThread(), ex) + + val log = verifyLog(CODE_UNCAUGHT_EXCEPTION, Severity.WARN) + log.errorObject shouldBe null + log.message shouldBe "Suppressing uncaught exception: $ex" + log.shouldContainKeyValues(KEY_THREAD to Thread.currentThread().toString(), KEY_EXCEPTION_MESSAGE to message) + } + + @Test + fun `Should not suppress errors without a message`() { + val handler = LoggerUncaughtExceptionHandler() + + val ex = Exception() + handler.uncaughtException(Thread.currentThread(), ex) + + val log = verifyLog(CODE_UNCAUGHT_EXCEPTION, Severity.ERROR) + log.errorObject shouldBe ex + log.shouldContainKeyValues(KEY_THREAD to Thread.currentThread().toString(), KEY_EXCEPTION_MESSAGE to null) + log.message shouldContain "Uncaught exception: $ex" + } + + @Test + fun `Should not suppress errors with an unrecognised message`() { + val t = Thread("Foo") + val handler = LoggerUncaughtExceptionHandler() + + val ex = Exception("Argh") + handler.uncaughtException(t, ex) + + val log = verifyLog(CODE_UNCAUGHT_EXCEPTION, Severity.ERROR) + log.errorObject shouldBe ex + log.shouldContainKeyValues(KEY_THREAD to t.toString(), KEY_EXCEPTION_MESSAGE to "Argh") + log.message shouldContain "Uncaught exception: $ex" + } +} diff --git a/core/src/test/kotlin/logging/TestLoggingConsole.kt b/core/src/test/kotlin/logging/TestLoggingConsole.kt new file mode 100644 index 0000000..14fbb12 --- /dev/null +++ b/core/src/test/kotlin/logging/TestLoggingConsole.kt @@ -0,0 +1,132 @@ +package logging + +import com.github.alyssaburlton.swingtest.flushEdt +import helper.AbstractTest +import helper.getAllChildComponentsForType +import helper.makeLogRecord +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.comparables.shouldBeGreaterThan +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.Test +import java.awt.Color +import javax.swing.JLabel +import javax.swing.text.StyleConstants + +class TestLoggingConsole : AbstractTest() { + @Test + fun `Should separate log records with a new line`() { + val recordOne = makeLogRecord(loggingCode = LoggingCode("foo"), message = "log one") + val recordTwo = makeLogRecord(loggingCode = LoggingCode("bar"), message = "log two") + + val console = LoggingConsole() + console.log(recordOne) + console.log(recordTwo) + + val text = console.getText() + text shouldBe "\n$recordOne\n$recordTwo" + } + + @Test + fun `Should log a regular INFO log in green`() { + val console = LoggingConsole() + val infoLog = makeLogRecord(severity = Severity.INFO) + + console.log(infoLog) + console.getTextColour() shouldBe Color.GREEN + } + + @Test + fun `Should log an ERROR log in red`() { + val console = LoggingConsole() + val errorLog = makeLogRecord(severity = Severity.ERROR) + + console.log(errorLog) + console.getTextColour() shouldBe Color.RED + } + + @Test + fun `Should log the error message and stack trace`() { + val console = LoggingConsole() + val t = Throwable("Boom") + + val errorLog = makeLogRecord(severity = Severity.ERROR, message = "Failed to load screen", errorObject = t) + console.log(errorLog) + + console.getText() shouldContain "Failed to load screen" + console.getText() shouldContain "java.lang.Throwable: Boom" + + val endColour = console.getTextColour(console.doc.length - 1) + endColour shouldBe Color.RED + } + + @Test + fun `Should log thread stacks`() { + val console = LoggingConsole() + + val threadStackLock = makeLogRecord(severity = Severity.INFO, message = "AWT Thread", keyValuePairs = mapOf(KEY_STACK to "at Foo.bar(58)")) + console.log(threadStackLock) + + console.getText() shouldContain "AWT Thread" + console.getText() shouldContain "at Foo.bar(58)" + } + + @Test + fun `Should scroll to the bottom when a new log is added`() { + val console = LoggingConsole() + console.pack() + console.scrollPane.verticalScrollBar.value shouldBe 0 + + repeat(50) { + console.log(makeLogRecord()) + } + + flushEdt() + console.scrollPane.verticalScrollBar.value shouldBeGreaterThan 0 + } + + @Test + fun `Should support clearing the logs`() { + val console = LoggingConsole() + console.log(makeLogRecord()) + + console.clear() + + console.getText() shouldBe "" + } + + @Test + fun `Should update when logging context changes`() { + val console = LoggingConsole() + console.contextUpdated(mapOf()) + console.getAllChildComponentsForType().shouldBeEmpty() + + console.contextUpdated(mapOf("appVersion" to "4.1.1")) + flushEdt() + val labels = console.getAllChildComponentsForType() + labels.size shouldBe 1 + labels.first().text shouldBe "appVersion: 4.1.1" + + console.contextUpdated(mapOf("appVersion" to "4.1.1", "devMode" to false)) + flushEdt() + val newLabels = console.getAllChildComponentsForType() + newLabels.map { it.text }.shouldContainExactly("appVersion: 4.1.1", "devMode: false") + } + + private fun LoggingConsole.getText(): String = + try { + doc.getText(0, doc.length) + } catch (t: Throwable) { + "" + } + + private fun LoggingConsole.getTextColour(position: Int = 0): Color { + val style = doc.getCharacterElement(position) + return StyleConstants.getForeground(style.attributes) + } + private fun LoggingConsole.getBackgroundColour(): Color { + val style = doc.getCharacterElement(0) + return StyleConstants.getBackground(style.attributes) + } +}