diff --git a/pom.xml b/pom.xml index 5f1ab09..de4744a 100644 --- a/pom.xml +++ b/pom.xml @@ -47,6 +47,12 @@ 4.12 true + + org.junit.jupiter + junit-jupiter-api + 5.2.0 + true + org.assertj assertj-core diff --git a/src/main/kotlin/com/tyro/oss/logtesting/LogRule.kt b/src/main/kotlin/com/tyro/oss/logtesting/LogRule.kt new file mode 100644 index 0000000..2bd54b4 --- /dev/null +++ b/src/main/kotlin/com/tyro/oss/logtesting/LogRule.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2018 Tyro Payments Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tyro.oss.logtesting + +import org.junit.jupiter.api.extension.AfterEachCallback +import org.junit.jupiter.api.extension.BeforeEachCallback + +abstract class LogRule : BeforeEachCallback, AfterEachCallback { + + abstract val events: MutableList + + fun clear() { + events.clear() + } +} diff --git a/src/main/kotlin/com/tyro/oss/logtesting/log4j/Log4jAssert.kt b/src/main/kotlin/com/tyro/oss/logtesting/log4j/Log4jAssert.kt new file mode 100644 index 0000000..b7a257f --- /dev/null +++ b/src/main/kotlin/com/tyro/oss/logtesting/log4j/Log4jAssert.kt @@ -0,0 +1,296 @@ +/* + * Copyright 2018 Tyro Payments Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tyro.oss.logtesting.log4j + +import com.tyro.oss.logtesting.LogRuleAssert +import org.apache.log4j.Level +import org.apache.log4j.spi.LoggingEvent +import org.assertj.core.error.ShouldContainCharSequence.shouldContain +import org.assertj.core.error.ShouldNotContainCharSequence.shouldNotContain +import org.assertj.core.util.Objects.areEqual +import kotlin.reflect.KClass + +class Log4jAssert(actual: List) : LogRuleAssert(actual) { + + override fun hasInfo(): Log4jAssert = + hasEvent(Level.INFO) + + override fun hasInfo(predicate: (LoggingEvent) -> Boolean): Log4jAssert = + hasEvent(Level.INFO, predicate) + + override fun hasInfo(message: String): Log4jAssert = + hasEvent(Level.INFO, message) + + override fun hasInfo(message: String, throwable: Throwable): Log4jAssert = + hasEvent(Level.INFO, message, throwable) + + override fun hasInfo(message: String, throwableClass: Class): Log4jAssert = + hasEvent(Level.INFO, message, throwableClass) + + override fun hasInfo(message: String, throwableClass: KClass): Log4jAssert = + hasEvent(Level.INFO, message, throwableClass) + + override fun hasInfoContaining(vararg messages: String): Log4jAssert = + hasEventContaining(Level.INFO, *messages) + + override fun hasInfoMatching(regex: Regex): Log4jAssert = + hasEventMatching(Level.INFO, regex) + + override fun hasInfoMatching(regex: Regex, throwable: Throwable): Log4jAssert = + hasEventMatching(Level.INFO, regex, throwable) + + override fun hasInfoMatching(regex: Regex, throwableClass: Class): Log4jAssert = + hasEventMatching(Level.INFO, regex, throwableClass) + + override fun hasInfoMatching(regex: Regex, throwableClass: KClass): Log4jAssert = + hasEventMatching(Level.INFO, regex, throwableClass) + + override fun hasNoInfo(): Log4jAssert = + hasNoEvent(Level.INFO) + + override fun hasNoInfo(predicate: (LoggingEvent) -> Boolean): Log4jAssert = + hasNoEvent(Level.INFO, predicate) + + override fun hasNoInfo(message: String): Log4jAssert = + hasNoEvent(Level.INFO, message) + + override fun hasNoInfoContaining(vararg messages: String): Log4jAssert = + hasNoEventContaining(Level.INFO, *messages) + + override fun hasNoInfoMatching(regex: Regex): Log4jAssert = + hasNoEventMatching(Level.INFO, regex) + + override fun hasWarn(): Log4jAssert = + hasEvent(Level.WARN) + + override fun hasWarn(predicate: (LoggingEvent) -> Boolean): Log4jAssert = + hasEvent(Level.WARN, predicate) + + override fun hasWarn(message: String): Log4jAssert = + hasEvent(Level.WARN, message) + + override fun hasWarn(message: String, throwable: Throwable): Log4jAssert = + hasEvent(Level.WARN, message, throwable) + + override fun hasWarn(message: String, throwableClass: Class): Log4jAssert = + hasEvent(Level.WARN, message, throwableClass) + + override fun hasWarn(message: String, throwableClass: KClass): Log4jAssert = + hasEvent(Level.WARN, message, throwableClass) + + override fun hasWarnContaining(vararg messages: String): Log4jAssert = + hasEventContaining(Level.WARN, *messages) + + override fun hasWarnMatching(regex: Regex): Log4jAssert = + hasEventMatching(Level.WARN, regex) + + override fun hasWarnMatching(regex: Regex, throwable: Throwable): Log4jAssert = + hasEventMatching(Level.WARN, regex, throwable) + + override fun hasWarnMatching(regex: Regex, throwableClass: Class): Log4jAssert = + hasEventMatching(Level.WARN, regex, throwableClass) + + override fun hasWarnMatching(regex: Regex, throwableClass: KClass): Log4jAssert = + hasEventMatching(Level.WARN, regex, throwableClass) + + override fun hasNoWarn(): Log4jAssert = + hasNoEvent(Level.WARN) + + override fun hasNoWarn(predicate: (LoggingEvent) -> Boolean): Log4jAssert = + hasNoEvent(Level.WARN, predicate) + + override fun hasNoWarn(message: String): Log4jAssert = + hasNoEvent(Level.WARN, message) + + override fun hasNoWarnContaining(vararg messages: String): Log4jAssert = + hasNoEventContaining(Level.WARN, *messages) + + override fun hasNoWarnMatching(regex: Regex): Log4jAssert = + hasNoEventMatching(Level.WARN, regex) + + override fun hasError(): Log4jAssert = + hasEvent(Level.ERROR) + + override fun hasError(predicate: (LoggingEvent) -> Boolean): Log4jAssert = + hasEvent(Level.ERROR, predicate) + + override fun hasError(message: String): Log4jAssert = + hasEvent(Level.ERROR, message) + + override fun hasError(message: String, throwable: Throwable): Log4jAssert = + hasEvent(Level.ERROR, message, throwable) + + override fun hasError(message: String, throwableClass: Class): Log4jAssert = + hasEvent(Level.ERROR, message, throwableClass) + + override fun hasError(message: String, throwableClass: KClass): Log4jAssert = + hasEvent(Level.ERROR, message, throwableClass.java) + + override fun hasErrorContaining(vararg messages: String): Log4jAssert = + hasEventContaining(Level.ERROR, *messages) + + override fun hasErrorMatching(regex: Regex): Log4jAssert = + hasEventMatching(Level.ERROR, regex) + + override fun hasErrorMatching(regex: Regex, throwable: Throwable): Log4jAssert = + hasEventMatching(Level.ERROR, regex, throwable) + + override fun hasErrorMatching(regex: Regex, throwableClass: Class): Log4jAssert = + hasEventMatching(Level.ERROR, regex, throwableClass) + + override fun hasErrorMatching(regex: Regex, throwableClass: KClass): Log4jAssert = + hasEventMatching(Level.ERROR, regex, throwableClass.java) + + override fun hasNoError(): Log4jAssert = + hasNoEvent(Level.ERROR) + + override fun hasNoError(predicate: (LoggingEvent) -> Boolean): Log4jAssert = + hasNoEvent(Level.ERROR, predicate) + + override fun hasNoError(message: String): Log4jAssert = + hasNoEvent(Level.ERROR, message) + + override fun hasNoErrorContaining(vararg messages: String): Log4jAssert = + hasNoEventContaining(Level.ERROR, *messages) + + override fun hasNoErrorMatching(regex: Regex): Log4jAssert = + hasNoEventMatching(Level.ERROR, regex) + + override fun hasEvent(level: Level): Log4jAssert = + hasEvent("[$level]", + withLevel(level)) + + override fun hasEvent(level: Level, predicate: (LoggingEvent) -> Boolean): Log4jAssert = + hasEvent("$level event matching given predicate") { + withLevel(level)(it) + && predicate(it) } + + override fun hasEvent(level: Level, message: String): Log4jAssert = + hasEvent(formatLogMessage(level, message)) { + withLevel(level)(it) + && withMessage(message)(it) } + + override fun hasEvent(level: Level, message: String, throwable: Throwable): Log4jAssert = + hasEvent(formatLogMessage(level, message)) { + withLevel(level)(it) + && withMessage(message)(it) + && withThrowable(throwable)(it) } + + override fun hasEvent(level: Level, message: String, throwableClass: Class): Log4jAssert = + hasEvent(formatLogMessage(level, message)) { + withLevel(level)(it) + && withMessage(message)(it) + && withThrowableClass(throwableClass)(it) } + + override fun hasEvent(level: Level, message: String, throwableClass: KClass): Log4jAssert = + hasEvent(level, message, throwableClass.java) + + override fun hasEventContaining(level: Level, vararg messages: String): Log4jAssert = + hasEvent("$level message containing ${messages.contentToString()}") { + withLevel(level)(it) + && withMessageContaining(messages)(it) } + + override fun hasEventMatching(level: Level, regex: Regex): Log4jAssert = + hasEvent("$level message matching: $regex") { + withLevel(level)(it) + && withMessageMatching(regex)(it) } + + override fun hasEventMatching(level: Level, regex: Regex, throwable: Throwable): Log4jAssert = + hasEvent("$level message matching: $regex") { + withLevel(level)(it) + && withMessageMatching(regex)(it) + && withThrowable(throwable)(it) } + + override fun hasEventMatching(level: Level, regex: Regex, throwableClass: Class): Log4jAssert = + hasEvent("$level message matching: $regex") { + withLevel(level)(it) + && withMessageMatching(regex)(it) + && withThrowableClass(throwableClass)(it) } + + override fun hasEventMatching(level: Level, regex: Regex, throwableClass: KClass): Log4jAssert = + hasEventMatching(level, regex, throwableClass.java) + + override fun hasNoEvent(level: Level): Log4jAssert = + hasNoEvent("[$level]", + withLevel(level)) + + override fun hasNoEvent(level: Level, predicate: (LoggingEvent) -> Boolean): Log4jAssert = + hasNoEvent("$level event matching given predicate") { + withLevel(level)(it) + && predicate(it) } + + override fun hasNoEvent(level: Level, message: String): Log4jAssert = + hasNoEvent(formatLogMessage(level, message)) { + withLevel(level)(it) + && withMessage(message)(it) } + + override fun hasNoEventContaining(level: Level, vararg messages: String): Log4jAssert = + hasNoEvent("$level message containing ${messages.contentToString()}") { + withLevel(level)(it) + && withMessageContaining(messages)(it) } + + override fun hasNoEventMatching(level: Level, regex: Regex): Log4jAssert = + hasNoEvent("$level message matching: $regex") { + withLevel(level)(it) + && withMessageMatching(regex)(it) } + + private fun hasEvent(description: String, predicate: (LoggingEvent) -> Boolean): Log4jAssert { + if (!actual.any(predicate)) { + failWithMessage(shouldContain(formatLogEvents(actual).replace("%", "%%"), description.replace("%", "%%")).create()) + } + return this + } + + private fun hasNoEvent(description: String, predicate: (LoggingEvent) -> Boolean): Log4jAssert { + if (actual.any(predicate)) { + failWithMessage(shouldNotContain(formatLogEvents(actual).replace("%", "%%"), description.replace("%", "%%")).create()) + } + return this + } + + private fun withLevel(level: Level): (LoggingEvent) -> Boolean = + { event -> areEqual(event.getLevel(), level) } + + private fun withMessage(message: String): (LoggingEvent) -> Boolean = + { event -> areEqual(event.renderedMessage, message) } + + private fun withThrowable(throwable: Throwable): (LoggingEvent) -> Boolean = + { event -> event.throwableInformation != null && areEqual(event.throwableInformation.throwable, throwable) } + + private fun withThrowableClass(throwableClass: Class): (LoggingEvent) -> Boolean = + { event -> event.throwableInformation != null && throwableClass.isAssignableFrom(event.throwableInformation.throwable.javaClass) } + + private fun withMessageContaining(messages: Array): (LoggingEvent) -> Boolean = + { event: LoggingEvent -> messages.all { event.renderedMessage.contains(it) } } + + private fun withMessageMatching(regex: Regex): (LoggingEvent) -> Boolean = + { event: LoggingEvent -> event.renderedMessage.matches(regex) } + + companion object { + + @JvmStatic + fun assertThat(events: List): Log4jAssert = Log4jAssert(events).withRepresentation(Log4jRepresentation()) + + @JvmStatic + fun assertThat(rule: Log4jRule): Log4jAssert = assertThat(rule.events) + + fun formatLogEvent(event: LoggingEvent) = formatLogMessage(event.getLevel(), event.renderedMessage) + + fun formatLogEvents(events: List): String = events.joinToString("\n") { formatLogEvent(it) } + + private fun formatLogMessage(level: Level, message: String = "") = "[$level] $message" + } +} diff --git a/src/main/kotlin/com/tyro/oss/logtesting/log4j/Log4jRepresentation.kt b/src/main/kotlin/com/tyro/oss/logtesting/log4j/Log4jRepresentation.kt new file mode 100644 index 0000000..dc3fd2f --- /dev/null +++ b/src/main/kotlin/com/tyro/oss/logtesting/log4j/Log4jRepresentation.kt @@ -0,0 +1,14 @@ +package com.tyro.oss.logtesting.log4j + +import com.tyro.oss.logtesting.log4j.Log4jAssert.Companion.formatLogEvent +import com.tyro.oss.logtesting.log4j.Log4jAssert.Companion.formatLogEvents +import org.apache.log4j.spi.LoggingEvent +import org.assertj.core.presentation.StandardRepresentation + +class Log4jRepresentation : StandardRepresentation() { + override fun toStringOf(obj: Any?): String? { + if (obj is LoggingEvent) return formatLogEvent(obj) + if (obj is Collection<*>) return formatLogEvents(obj as List) + return super.toStringOf(obj) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/tyro/oss/logtesting/log4j/Log4jRule.kt b/src/main/kotlin/com/tyro/oss/logtesting/log4j/Log4jRule.kt new file mode 100644 index 0000000..7c67ce7 --- /dev/null +++ b/src/main/kotlin/com/tyro/oss/logtesting/log4j/Log4jRule.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Tyro Payments Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tyro.oss.logtesting.log4j + +import com.tyro.oss.logtesting.LogRule +import org.apache.log4j.AppenderSkeleton +import org.apache.log4j.LogManager +import org.apache.log4j.Logger +import org.apache.log4j.spi.LoggingEvent +import org.junit.jupiter.api.extension.ExtensionContext +import kotlin.reflect.KClass + +class Log4jRule : LogRule { + + private val logger: Logger + private lateinit var appender: CapturingAppender + + constructor(loggerClass: KClass<*>) { + logger = LogManager.getLogger(loggerClass.java) + } + + constructor(loggerClass: Class<*>) { + logger = LogManager.getLogger(loggerClass) + } + + constructor(loggerName: String) { + logger = LogManager.getLogger(loggerName) + } + + override fun beforeEach(context: ExtensionContext?) { + appender = CapturingAppender() + logger.addAppender(appender) + } + + override fun afterEach(context: ExtensionContext?) { + logger.removeAppender(appender) + } + + override val events: MutableList + get() = appender.events + + private class CapturingAppender : AppenderSkeleton() { + + val events = ArrayList() + + override fun append(event: LoggingEvent) { + events.add(event) + } + + override fun close() {} + + override fun requiresLayout(): Boolean { + return false + } + } +} diff --git a/src/main/kotlin/com/tyro/oss/logtesting/logback/LogbackAssert.kt b/src/main/kotlin/com/tyro/oss/logtesting/logback/LogbackAssert.kt new file mode 100644 index 0000000..88d08d0 --- /dev/null +++ b/src/main/kotlin/com/tyro/oss/logtesting/logback/LogbackAssert.kt @@ -0,0 +1,297 @@ +/* + * Copyright 2018 Tyro Payments Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tyro.oss.logtesting.logback + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.classic.spi.ThrowableProxy +import com.tyro.oss.logtesting.LogRuleAssert +import org.assertj.core.error.ShouldContainCharSequence.shouldContain +import org.assertj.core.error.ShouldNotContainCharSequence.shouldNotContain +import org.assertj.core.util.Objects.areEqual +import kotlin.reflect.KClass + +class LogbackAssert(actual: List) : LogRuleAssert(actual) { + + override fun hasInfo(): LogbackAssert = + hasEvent(Level.INFO) + + override fun hasInfo(predicate: (ILoggingEvent) -> Boolean): LogbackAssert = + hasEvent(Level.INFO, predicate) + + override fun hasInfo(message: String): LogbackAssert = + hasEvent(Level.INFO, message) + + override fun hasInfo(message: String, throwable: Throwable): LogbackAssert = + hasEvent(Level.INFO, message, throwable) + + override fun hasInfo(message: String, throwableClass: Class): LogbackAssert = + hasEvent(Level.INFO, message, throwableClass) + + override fun hasInfo(message: String, throwableClass: KClass): LogbackAssert = + hasEvent(Level.INFO, message, throwableClass) + + override fun hasInfoContaining(vararg messages: String): LogbackAssert = + hasEventContaining(Level.INFO, *messages) + + override fun hasInfoMatching(regex: Regex): LogbackAssert = + hasEventMatching(Level.INFO, regex) + + override fun hasInfoMatching(regex: Regex, throwable: Throwable): LogbackAssert = + hasEventMatching(Level.INFO, regex, throwable) + + override fun hasInfoMatching(regex: Regex, throwableClass: Class): LogbackAssert = + hasEventMatching(Level.INFO, regex, throwableClass) + + override fun hasInfoMatching(regex: Regex, throwableClass: KClass): LogbackAssert = + hasEventMatching(Level.INFO, regex, throwableClass) + + override fun hasNoInfo(): LogbackAssert = + hasNoEvent(Level.INFO) + + override fun hasNoInfo(predicate: (ILoggingEvent) -> Boolean): LogbackAssert = + hasNoEvent(Level.INFO, predicate) + + override fun hasNoInfo(message: String): LogbackAssert = + hasNoEvent(Level.INFO, message) + + override fun hasNoInfoContaining(vararg messages: String): LogbackAssert = + hasNoEventContaining(Level.INFO, *messages) + + override fun hasNoInfoMatching(regex: Regex): LogbackAssert = + hasNoEventMatching(Level.INFO, regex) + + override fun hasWarn(): LogbackAssert = + hasEvent(Level.WARN) + + override fun hasWarn(predicate: (ILoggingEvent) -> Boolean): LogbackAssert = + hasEvent(Level.WARN, predicate) + + override fun hasWarn(message: String): LogbackAssert = + hasEvent(Level.WARN, message) + + override fun hasWarn(message: String, throwable: Throwable): LogbackAssert = + hasEvent(Level.WARN, message, throwable) + + override fun hasWarn(message: String, throwableClass: Class): LogbackAssert = + hasEvent(Level.WARN, message, throwableClass) + + override fun hasWarn(message: String, throwableClass: KClass): LogbackAssert = + hasEvent(Level.WARN, message, throwableClass) + + override fun hasWarnContaining(vararg messages: String): LogbackAssert = + hasEventContaining(Level.WARN, *messages) + + override fun hasWarnMatching(regex: Regex): LogbackAssert = + hasEventMatching(Level.WARN, regex) + + override fun hasWarnMatching(regex: Regex, throwable: Throwable): LogbackAssert = + hasEventMatching(Level.WARN, regex, throwable) + + override fun hasWarnMatching(regex: Regex, throwableClass: Class): LogbackAssert = + hasEventMatching(Level.WARN, regex, throwableClass) + + override fun hasWarnMatching(regex: Regex, throwableClass: KClass): LogbackAssert = + hasEventMatching(Level.WARN, regex, throwableClass) + + override fun hasNoWarn(): LogbackAssert = + hasNoEvent(Level.WARN) + + override fun hasNoWarn(predicate: (ILoggingEvent) -> Boolean): LogbackAssert = + hasNoEvent(Level.WARN, predicate) + + override fun hasNoWarn(message: String): LogbackAssert = + hasNoEvent(Level.WARN, message) + + override fun hasNoWarnContaining(vararg messages: String): LogbackAssert = + hasNoEventContaining(Level.WARN, *messages) + + override fun hasNoWarnMatching(regex: Regex): LogbackAssert = + hasNoEventMatching(Level.WARN, regex) + + override fun hasError(): LogbackAssert = + hasEvent(Level.ERROR) + + override fun hasError(predicate: (ILoggingEvent) -> Boolean): LogbackAssert = + hasEvent(Level.ERROR, predicate) + + override fun hasError(message: String): LogbackAssert = + hasEvent(Level.ERROR, message) + + override fun hasError(message: String, throwable: Throwable): LogbackAssert = + hasEvent(Level.ERROR, message, throwable) + + override fun hasError(message: String, throwableClass: Class): LogbackAssert = + hasEvent(Level.ERROR, message, throwableClass) + + override fun hasError(message: String, throwableClass: KClass): LogbackAssert = + hasEvent(Level.ERROR, message, throwableClass) + + override fun hasErrorContaining(vararg messages: String): LogbackAssert = + hasEventContaining(Level.ERROR, *messages) + + override fun hasErrorMatching(regex: Regex): LogbackAssert = + hasEventMatching(Level.ERROR, regex) + + override fun hasErrorMatching(regex: Regex, throwable: Throwable): LogbackAssert = + hasEventMatching(Level.ERROR, regex, throwable) + + override fun hasErrorMatching(regex: Regex, throwableClass: Class): LogbackAssert = + hasEventMatching(Level.ERROR, regex, throwableClass) + + override fun hasErrorMatching(regex: Regex, throwableClass: KClass): LogbackAssert = + hasEventMatching(Level.ERROR, regex, throwableClass) + + override fun hasNoError(): LogbackAssert = + hasNoEvent(Level.ERROR) + + override fun hasNoError(predicate: (ILoggingEvent) -> Boolean): LogbackAssert = + hasNoEvent(Level.ERROR, predicate) + + override fun hasNoError(message: String): LogbackAssert = + hasNoEvent(Level.ERROR, message) + + override fun hasNoErrorContaining(vararg messages: String): LogbackAssert = + hasNoEventContaining(Level.ERROR, *messages) + + override fun hasNoErrorMatching(regex: Regex): LogbackAssert = + hasNoEventMatching(Level.ERROR, regex) + + override fun hasEvent(level: Level): LogbackAssert = + hasEvent("[$level]", + withLevel(level)) + + override fun hasEvent(level: Level, predicate: (ILoggingEvent) -> Boolean): LogbackAssert = + hasEvent("$level event matching given predicate") { + withLevel(level)(it) + && predicate(it) } + + override fun hasEvent(level: Level, message: String): LogbackAssert = + hasEvent(formatLogMessage(level, message)) { + withLevel(level)(it) + && withMessage(message)(it) } + + override fun hasEvent(level: Level, message: String, throwable: Throwable): LogbackAssert = + hasEvent(formatLogMessage(level, message)) { + withLevel(level)(it) + && withMessage(message)(it) + && withThrowable(throwable)(it) } + + override fun hasEvent(level: Level, message: String, throwableClass: Class): LogbackAssert = + hasEvent(formatLogMessage(level, message)) { + withLevel(level)(it) + && withMessage(message)(it) + && withThrowableClass(throwableClass)(it) } + + override fun hasEvent(level: Level, message: String, throwableClass: KClass): LogbackAssert = + hasEvent(level, message, throwableClass.java) + + override fun hasEventContaining(level: Level, vararg messages: String): LogbackAssert = + hasEvent("$level message containing ${messages.contentToString()}") { + withLevel(level)(it) + && withMessageContaining(messages)(it) } + + override fun hasEventMatching(level: Level, regex: Regex): LogbackAssert = + hasEvent("$level message matching: $regex") { + withLevel(level)(it) + && withMessageMatching(regex)(it) } + + override fun hasEventMatching(level: Level, regex: Regex, throwable: Throwable): LogbackAssert = + hasEvent("$level message matching: $regex") { + withLevel(level)(it) + && withMessageMatching(regex)(it) + && withThrowable(throwable)(it) } + + override fun hasEventMatching(level: Level, regex: Regex, throwableClass: Class): LogbackAssert = + hasEvent("$level message matching: $regex") { + withLevel(level)(it) + && withMessageMatching(regex)(it) + && withThrowableClass(throwableClass)(it) } + + override fun hasEventMatching(level: Level, regex: Regex, throwableClass: KClass): LogbackAssert = + hasEventMatching(level, regex, throwableClass.java) + + override fun hasNoEvent(level: Level): LogbackAssert = + hasNoEvent("[$level]", + withLevel(level)) + + override fun hasNoEvent(level: Level, predicate: (ILoggingEvent) -> Boolean): LogbackAssert = + hasNoEvent("$level event matching given predicate") { + withLevel(level)(it) + && predicate(it) } + + override fun hasNoEvent(level: Level, message: String): LogbackAssert = + hasNoEvent(formatLogMessage(level, message)) { + withLevel(level)(it) + && withMessage(message)(it) } + + override fun hasNoEventContaining(level: Level, vararg messages: String): LogbackAssert = + hasNoEvent("$level message containing ${messages.contentToString()}") { + withLevel(level)(it) + && withMessageContaining(messages)(it) } + + override fun hasNoEventMatching(level: Level, regex: Regex): LogbackAssert = + hasNoEvent("$level message matching: $regex") { + withLevel(level)(it) + && withMessageMatching(regex)(it) } + + private fun hasEvent(description: String, predicate: (ILoggingEvent) -> Boolean): LogbackAssert { + if (actual.none(predicate)) { + failWithMessage(shouldContain(formatLogEvents(actual).replace("%", "%%"), description.replace("%", "%%")).create()) + } + return this + } + + private fun hasNoEvent(description: String, predicate: (ILoggingEvent) -> Boolean): LogbackAssert { + if (actual.any(predicate)) { + failWithMessage(shouldNotContain(formatLogEvents(actual).replace("%", "%%"), description.replace("%", "%%")).create()) + } + return this + } + + private fun withLevel(level: Level): (ILoggingEvent) -> Boolean = + { event -> areEqual(event.level, level) } + + private fun withMessage(message: String): (ILoggingEvent) -> Boolean = + { event: ILoggingEvent -> areEqual(event.formattedMessage, message) } + + private fun withThrowable(throwable: Throwable): (ILoggingEvent) -> Boolean = + { event -> event.throwableProxy != null && areEqual((event.throwableProxy as ThrowableProxy).throwable, throwable) } + + private fun withThrowableClass(throwableClass: Class): (ILoggingEvent) -> Boolean = + { event -> event.throwableProxy != null && throwableClass.isAssignableFrom((event.throwableProxy as ThrowableProxy).throwable.javaClass) } + + private fun withMessageContaining(messages: Array): (ILoggingEvent) -> Boolean = + { event: ILoggingEvent -> messages.all { event.formattedMessage.contains(it) } } + + private fun withMessageMatching(regex: Regex): (ILoggingEvent) -> Boolean = + { event: ILoggingEvent -> event.formattedMessage.matches(regex) } + + companion object { + + @JvmStatic + fun assertThat(events: List): LogbackAssert = LogbackAssert(events).withRepresentation(LogbackRepresentation()) + + @JvmStatic + fun assertThat(rule: LogbackRule): LogbackAssert = assertThat(rule.events) + + fun formatLogEvent(event: ILoggingEvent) = formatLogMessage(event.level, event.formattedMessage) + + fun formatLogEvents(events: List): String = events.joinToString("\n") { formatLogEvent(it) } + + private fun formatLogMessage(level: Level, message: String) = "[$level] $message" + } +} diff --git a/src/main/kotlin/com/tyro/oss/logtesting/logback/LogbackRepresentation.kt b/src/main/kotlin/com/tyro/oss/logtesting/logback/LogbackRepresentation.kt new file mode 100644 index 0000000..a691706 --- /dev/null +++ b/src/main/kotlin/com/tyro/oss/logtesting/logback/LogbackRepresentation.kt @@ -0,0 +1,14 @@ +package com.tyro.oss.logtesting.logback + +import ch.qos.logback.classic.spi.ILoggingEvent +import com.tyro.oss.logtesting.logback.LogbackAssert.Companion.formatLogEvent +import com.tyro.oss.logtesting.logback.LogbackAssert.Companion.formatLogEvents +import org.assertj.core.presentation.StandardRepresentation + +class LogbackRepresentation : StandardRepresentation() { + override fun toStringOf(obj: Any?): String? { + if (obj is ILoggingEvent) return formatLogEvent(obj) + if (obj is List<*>) return formatLogEvents(obj as List) + return super.toStringOf(obj) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/tyro/oss/logtesting/logback/LogbackRule.kt b/src/main/kotlin/com/tyro/oss/logtesting/logback/LogbackRule.kt new file mode 100644 index 0000000..bed5a8e --- /dev/null +++ b/src/main/kotlin/com/tyro/oss/logtesting/logback/LogbackRule.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2018 Tyro Payments Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tyro.oss.logtesting.logback + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.AppenderBase +import com.tyro.oss.logtesting.LogRule +import org.junit.jupiter.api.extension.ExtensionContext +import org.slf4j.LoggerFactory +import kotlin.reflect.KClass + +class LogbackRule : LogRule { + + private val logger: Logger + private lateinit var appender: CapturingAppender + + constructor(loggerClass: KClass<*>) { + logger = LoggerFactory.getLogger(loggerClass.java) as Logger + } + + constructor(loggerClass: Class<*>) { + logger = LoggerFactory.getLogger(loggerClass) as Logger + } + + constructor(loggerName: String) { + logger = LoggerFactory.getLogger(loggerName) as Logger + } + + override fun beforeEach(context: ExtensionContext?) { + appender = CapturingAppender() + appender.start() + logger.addAppender(appender) + } + + override fun afterEach(context: ExtensionContext?) { + logger.detachAppender(appender) + } + + override val events: MutableList + get() = appender.events + + private class CapturingAppender : AppenderBase() { + + val events = ArrayList() + + override fun append(event: ILoggingEvent) { + events.add(event) + } + } +} diff --git a/src/test/kotlin/com/tyro/oss/logtesting/log4j/Log4jRuleTest.kt b/src/test/kotlin/com/tyro/oss/logtesting/log4j/Log4jRuleTest.kt new file mode 100644 index 0000000..3a060ff --- /dev/null +++ b/src/test/kotlin/com/tyro/oss/logtesting/log4j/Log4jRuleTest.kt @@ -0,0 +1,483 @@ +/* + * Copyright 2018 Tyro Payments Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tyro.oss.logtesting.log4j + +import com.tyro.oss.logtesting.log4j.Log4jAssert.Companion.assertThat +import org.apache.log4j.Level +import org.apache.log4j.LogManager +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.extension.ExtendWith + +class Log4jRuleTest { + + private val LOG = LogManager.getLogger(Log4jRuleTest::class.java) + + @get:ExtendWith + var log = Log4jRule(Log4jRuleTest::class) + + @Test + fun shouldClearLog() { + LOG.info("test message") + + assertThat(log).isNotEmpty() + + log.clear() + + assertThat(log).isEmpty() + } + + @Test + fun shouldAssertLogSize() { + assertThat(log).hasSize(0) + + LOG.info("test message 1") + LOG.info("test message 2") + LOG.info("test message 3") + + assertThat(log).hasSize(3) + + assertThatThrownBy { assertThat(log).hasSize(4) } + .isInstanceOf(AssertionError::class.java) + .hasMessageContaining("\nExpected size:<4> but was:<3> in:\n<[INFO] test message 1\n" + + "[INFO] test message 2\n" + + "[INFO] test message 3>") + } + + @Test + fun shouldAssertLogIsEmpty() { + assertThat(log).isEmpty() + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).isEmpty() } + .isInstanceOf(AssertionError::class.java) + .hasMessageContaining("\nExpecting empty but was:<[INFO] test message>") + } + + @Test + fun shouldAssertNoInfoEvents() { + assertThat(log).hasNoInfo() + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasNoInfo() } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "not to contain:\n" + + " <\"[INFO]\"> ") + } + + @Test + fun shouldAssertNoInfoEventsMatchingPredicate() { + assertThat(log).hasNoInfo { it.renderedMessage.startsWith("test") } + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasNoInfo { it.renderedMessage.startsWith("test") } } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "not to contain:\n" + + " <\"INFO event matching given predicate\"> ") + } + + @Test + fun shouldAssertNoInfoEventWithMessage() { + assertThat(log).hasNoInfo("test message") + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasNoInfo("test message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "not to contain:\n" + + " <\"[INFO] test message\"> ") + } + + @Test + fun shouldAssertNoInfoEventContainingMessages() { + assertThat(log).hasNoInfoContaining("test", "message") + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasNoInfoContaining("test", "message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "not to contain:\n" + + " <\"INFO message containing [test, message]\"> ") + } + + @Test + fun shouldAssertNoInfoEventMatchingMessage() { + assertThat(log).hasNoInfoMatching(Regex("test \\w+")) + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasNoInfoMatching(Regex("test \\w+")) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "not to contain:\n" + + " <\"INFO message matching: test \\w+\"> ") + } + + @Test + fun shouldAssertNoWarnEvents() { + assertThat(log).hasNoWarn() + + LOG.warn("test message") + + assertThatThrownBy { assertThat(log).hasNoWarn() } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[WARN] test message\">\n" + + "not to contain:\n" + + " <\"[WARN]\"> ") + } + + @Test + fun shouldAssertNoWarnEventsMatchingPredicate() { + assertThat(log).hasNoWarn { it.renderedMessage.startsWith("test") } + + LOG.warn("test message") + + assertThatThrownBy { assertThat(log).hasNoWarn { it.renderedMessage.startsWith("test") } } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[WARN] test message\">\n" + + "not to contain:\n" + + " <\"WARN event matching given predicate\"> ") + } + + @Test + fun shouldAssertNoWarnEventWithMessage() { + assertThat(log).hasNoWarn("test message") + + LOG.warn("test message") + + assertThatThrownBy { assertThat(log).hasNoWarn("test message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[WARN] test message\">\n" + + "not to contain:\n" + + " <\"[WARN] test message\"> ") + } + + @Test + fun shouldAssertNoWarnEventContainingMessages() { + assertThat(log).hasNoWarnContaining("test", "message") + + LOG.warn("test message") + + assertThatThrownBy { assertThat(log).hasNoWarnContaining("test", "message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[WARN] test message\">\n" + + "not to contain:\n" + + " <\"WARN message containing [test, message]\"> ") + } + + @Test + fun shouldAssertNoWarnEventMatchingMessage() { + assertThat(log).hasNoWarnMatching(Regex("test \\w+")) + + LOG.warn("test message") + + assertThatThrownBy { assertThat(log).hasNoWarnMatching(Regex("test \\w+")) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[WARN] test message\">\n" + + "not to contain:\n" + + " <\"WARN message matching: test \\w+\"> ") + } + + @Test + fun shouldAssertNoErrorEvents() { + assertThat(log).hasNoError() + + LOG.error("test message") + + assertThatThrownBy { assertThat(log).hasNoError() } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[ERROR] test message\">\n" + + "not to contain:\n" + + " <\"[ERROR]\"> ") + } + + @Test + fun shouldAssertNoErrorEventsMatchingPredicate() { + assertThat(log).hasNoError { it.renderedMessage.startsWith("test") } + + LOG.error("test message") + + assertThatThrownBy { assertThat(log).hasNoError { it.renderedMessage.startsWith("test") } } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[ERROR] test message\">\n" + + "not to contain:\n" + + " <\"ERROR event matching given predicate\"> ") + } + + @Test + fun shouldAssertNoErrorEventWithMessage() { + assertThat(log).hasNoError("test message") + + LOG.error("test message") + + assertThatThrownBy { assertThat(log).hasNoError("test message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[ERROR] test message\">\n" + + "not to contain:\n" + + " <\"[ERROR] test message\"> ") + } + + @Test + fun shouldAssertNoErrorEventContainingMessages() { + assertThat(log).hasNoErrorContaining("test", "message") + + LOG.error("test message") + + assertThatThrownBy { assertThat(log).hasNoErrorContaining("test", "message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[ERROR] test message\">\n" + + "not to contain:\n" + + " <\"ERROR message containing [test, message]\"> ") + } + + @Test + fun shouldAssertNoErrorEventMatchingMessage() { + assertThat(log).hasNoErrorMatching(Regex("test \\w+")) + + LOG.error("test message") + + assertThatThrownBy { assertThat(log).hasNoErrorMatching(Regex("test \\w+")) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[ERROR] test message\">\n" + + "not to contain:\n" + + " <\"ERROR message matching: test \\w+\"> ") + } + + @Test + fun shouldAssertLogIsNotEmpty() { + assertThatThrownBy { assertThat(log).isNotEmpty() } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting actual not to be empty") + + LOG.info("test message") + + assertThat(log).isNotEmpty() + } + + @Test + fun shouldCaptureLogEvents() { + val expectedException = RuntimeException("expected") + val expectedMessage = "test message" + + LOG.debug(expectedMessage, expectedException) + + assertThat(log) + .hasEvent(Level.DEBUG) + .hasEvent(Level.DEBUG) { it.renderedMessage.isNotEmpty() } + .hasEvent(Level.DEBUG, expectedMessage) + .hasEvent(Level.DEBUG, expectedMessage, expectedException) + .hasEvent(Level.DEBUG, expectedMessage, RuntimeException::class.java) + .hasEvent(Level.DEBUG, expectedMessage, RuntimeException::class) + .hasEventContaining(Level.DEBUG, "test", "message") + .hasEventMatching(Level.DEBUG, Regex("[a-z]+ message")) + .hasEventMatching(Level.DEBUG, Regex("[a-z]+ message"), expectedException) + .hasEventMatching(Level.DEBUG, Regex("[a-z]+ message"), RuntimeException::class.java) + .hasEventMatching(Level.DEBUG, Regex("[a-z]+ message"), RuntimeException::class) + } + + @Test + fun shouldCaptureInfoLogEvents() { + val expectedException = RuntimeException("expected") + val expectedMessage = "test message" + + LOG.info(expectedMessage, expectedException) + + assertThat(log) + .hasInfo() + .hasInfo { it.renderedMessage.isNotEmpty() } + .hasInfo(expectedMessage) + .hasInfo(expectedMessage, expectedException) + .hasInfo(expectedMessage, RuntimeException::class) + .hasInfoContaining("test", "message") + .hasInfoMatching(Regex("[a-z]+ message")) + .hasInfoMatching(Regex("[a-z]+ message"), expectedException) + .hasInfoMatching(Regex("[a-z]+ message"), RuntimeException::class) + } + + @Test + fun shouldCaptureWarnLogEvents() { + val expectedException = RuntimeException("expected") + val expectedMessage = "test message" + + LOG.warn(expectedMessage, expectedException) + + assertThat(log) + .hasWarn() + .hasWarn { it.renderedMessage.isNotEmpty() } + .hasWarn(expectedMessage) + .hasWarn(expectedMessage, expectedException) + .hasWarn(expectedMessage, RuntimeException::class) + .hasWarnContaining("test", "message") + .hasWarnMatching(Regex("[a-z]+ message")) + .hasWarnMatching(Regex("[a-z]+ message")) + .hasWarnMatching(Regex("[a-z]+ message"), expectedException) + .hasWarnMatching(Regex("[a-z]+ message"), RuntimeException::class) + } + + @Test + fun shouldCaptureErrorLogEvents() { + val expectedException = RuntimeException("expected") + val expectedMessage = "test message" + + LOG.error(expectedMessage, expectedException) + + assertThat(log) + .hasError() + .hasError { it.renderedMessage.isNotEmpty() } + .hasError(expectedMessage) + .hasError(expectedMessage, expectedException) + .hasError(expectedMessage, RuntimeException::class) + .hasErrorContaining("test", "message") + .hasErrorMatching(Regex("[a-z]+ message")) + .hasErrorMatching(Regex("[a-z]+ message")) + .hasErrorMatching(Regex("[a-z]+ message"), expectedException) + .hasErrorMatching(Regex("[a-z]+ message"), RuntimeException::class) + } + + @Test + fun shouldFailAssertionWhenLevelIsNotFound() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasWarn() } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"[WARN]\"> ") + } + + @Test + fun shouldFailAssertionWhenPredicateDoesNotMatch() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfo { it.renderedMessage.isEmpty() } } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"INFO event matching given predicate\"> ") + } + + @Test + fun shouldFailAssertionWhenMessageIsNotFound() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfo("other message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"[INFO] other message\"> ") + } + + @Test + fun shouldCorrectlyConstructFailureMessageWithPercentage() { + LOG.info("test message %") + + assertThatThrownBy { assertThat(log).hasInfo("other message %") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message %\">\n" + + "to contain:\n" + + " <\"[INFO] other message %\"> ") + } + + @Test + fun shouldFailAssertionWhenMessageWithLevelIsNotFound() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasError("other message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\n" + + "Expecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"[ERROR] other message\"> ") + } + + @Test + fun shouldFailAssertionWhenThrowableIsNotFound() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfo("test message", RuntimeException()) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\n" + + "Expecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"[INFO] test message\"> ") + } + + @Test + fun shouldFailAssertionWhenThrowableClassIsNotFound() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfo("test message", RuntimeException::class) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\n" + + "Expecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"[INFO] test message\"> ") + } + + @Test + fun shouldFailAssertionWhenMessageDoesNotContain() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfoContaining("other", "message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"INFO message containing [other, message]\"> ") + } + + @Test + fun shouldFailAssertionWhenMessageDoesNotMatch() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfoMatching(Regex("[0-9]+ message")) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"INFO message matching: [0-9]+ message\"> ") + } +} diff --git a/src/test/kotlin/com/tyro/oss/logtesting/logback/LogbackRuleTest.kt b/src/test/kotlin/com/tyro/oss/logtesting/logback/LogbackRuleTest.kt new file mode 100644 index 0000000..23bc70f --- /dev/null +++ b/src/test/kotlin/com/tyro/oss/logtesting/logback/LogbackRuleTest.kt @@ -0,0 +1,482 @@ +/* + * Copyright 2018 Tyro Payments Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.tyro.oss.logtesting.logback + +import ch.qos.logback.classic.Level +import com.tyro.oss.logtesting.logback.LogbackAssert.Companion.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +import org.slf4j.LoggerFactory + +class LogbackRuleTest { + + private val LOG = LoggerFactory.getLogger(LogbackRuleTest::class.java) + + @get:ExtendWith + var log = LogbackRule(LogbackRuleTest::class) + + @Test + fun shouldClearLog() { + LOG.info("test message") + + assertThat(log).isNotEmpty() + + log.clear() + + assertThat(log).isEmpty() + } + + @Test + fun shouldAssertLogSize() { + assertThat(log).hasSize(0) + + LOG.info("test message 1") + LOG.info("test message 2") + LOG.info("test message 3") + + assertThat(log).hasSize(3) + + assertThatThrownBy { assertThat(log).hasSize(4) } + .isInstanceOf(AssertionError::class.java) + .hasMessageContaining("\nExpected size:<4> but was:<3> in:\n<[INFO] test message 1\n[INFO] test message 2\n[INFO] test message 3>") + } + + @Test + fun shouldAssertLogIsEmpty() { + assertThat(log).isEmpty() + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).isEmpty() } + .isInstanceOf(AssertionError::class.java) + .hasMessageContaining("\nExpecting empty but was:<[INFO] test message>") + } + + @Test + fun shouldAssertNoInfoEvents() { + assertThat(log).hasNoInfo() + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasNoInfo() } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "not to contain:\n" + + " <\"[INFO]\"> ") + } + + @Test + fun shouldAssertNoInfoEventsMatchingPredicate() { + assertThat(log).hasNoInfo { it.formattedMessage.startsWith("test") } + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasNoInfo { it.formattedMessage.startsWith("test") } } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "not to contain:\n" + + " <\"INFO event matching given predicate\"> ") + } + + @Test + fun shouldAssertNoInfoEventWithMessage() { + assertThat(log).hasNoInfo("test message") + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasNoInfo("test message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "not to contain:\n" + + " <\"[INFO] test message\"> ") + } + + @Test + fun shouldAssertNoInfoEventContainingMessages() { + assertThat(log).hasNoInfoContaining("test", "message") + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasNoInfoContaining("test", "message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "not to contain:\n" + + " <\"INFO message containing [test, message]\"> ") + } + + @Test + fun shouldAssertNoInfoEventMatchingMessage() { + assertThat(log).hasNoInfoMatching(Regex("test \\w+")) + + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasNoInfoMatching(Regex("test \\w+")) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "not to contain:\n" + + " <\"INFO message matching: test \\w+\"> ") + } + + @Test + fun shouldAssertNoWarnEvents() { + assertThat(log).hasNoWarn() + + LOG.warn("test message") + + assertThatThrownBy { assertThat(log).hasNoWarn() } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[WARN] test message\">\n" + + "not to contain:\n" + + " <\"[WARN]\"> ") + } + + @Test + fun shouldAssertNoWarnEventsMatchingPredicate() { + assertThat(log).hasNoWarn { it.formattedMessage.startsWith("test") } + + LOG.warn("test message") + + assertThatThrownBy { assertThat(log).hasNoWarn { it.formattedMessage.startsWith("test") } } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[WARN] test message\">\n" + + "not to contain:\n" + + " <\"WARN event matching given predicate\"> ") + } + + @Test + fun shouldAssertNoWarnEventWithMessage() { + assertThat(log).hasNoWarn("test message") + + LOG.warn("test message") + + assertThatThrownBy { assertThat(log).hasNoWarn("test message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[WARN] test message\">\n" + + "not to contain:\n" + + " <\"[WARN] test message\"> ") + } + + @Test + fun shouldAssertNoWarnEventContainingMessages() { + assertThat(log).hasNoWarnContaining("test", "message") + + LOG.warn("test message") + + assertThatThrownBy { assertThat(log).hasNoWarnContaining("test", "message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[WARN] test message\">\n" + + "not to contain:\n" + + " <\"WARN message containing [test, message]\"> ") + } + + @Test + fun shouldAssertNoWarnEventMatchingMessage() { + assertThat(log).hasNoWarnMatching(Regex("test \\w+")) + + LOG.warn("test message") + + assertThatThrownBy { assertThat(log).hasNoWarnMatching(Regex("test \\w+")) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[WARN] test message\">\n" + + "not to contain:\n" + + " <\"WARN message matching: test \\w+\"> ") + } + + @Test + fun shouldAssertNoErrorEvents() { + assertThat(log).hasNoError() + + LOG.error("test message") + + assertThatThrownBy { assertThat(log).hasNoError() } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[ERROR] test message\">\n" + + "not to contain:\n" + + " <\"[ERROR]\"> ") + } + + @Test + fun shouldAssertNoErrorEventsMatchingPredicate() { + assertThat(log).hasNoError { it.formattedMessage.startsWith("test") } + + LOG.error("test message") + + assertThatThrownBy { assertThat(log).hasNoError { it.formattedMessage.startsWith("test") } } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[ERROR] test message\">\n" + + "not to contain:\n" + + " <\"ERROR event matching given predicate\"> ") + } + + @Test + fun shouldAssertNoErrorEventWithMessage() { + assertThat(log).hasNoError("test message") + + LOG.error("test message") + + assertThatThrownBy { assertThat(log).hasNoError("test message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[ERROR] test message\">\n" + + "not to contain:\n" + + " <\"[ERROR] test message\"> ") + } + + @Test + fun shouldAssertNoErrorEventContainingMessages() { + assertThat(log).hasNoErrorContaining("test", "message") + + LOG.error("test message") + + assertThatThrownBy { assertThat(log).hasNoErrorContaining("test", "message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[ERROR] test message\">\n" + + "not to contain:\n" + + " <\"ERROR message containing [test, message]\"> ") + } + + @Test + fun shouldAssertNoErrorEventMatchingMessage() { + assertThat(log).hasNoErrorMatching(Regex("test \\w+")) + + LOG.error("test message") + + assertThatThrownBy { assertThat(log).hasNoErrorMatching(Regex("test \\w+")) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[ERROR] test message\">\n" + + "not to contain:\n" + + " <\"ERROR message matching: test \\w+\"> ") + } + + @Test + fun shouldAssertLogIsNotEmpty() { + assertThatThrownBy { assertThat(log).isNotEmpty() } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting actual not to be empty") + + LOG.info("test message") + + assertThat(log).isNotEmpty() + } + + @Test + fun shouldCaptureLogEvents() { + val expectedException = RuntimeException("expected") + val expectedMessage = "test message" + + LOG.debug(expectedMessage, expectedException) + + assertThat(log) + .hasEvent(Level.DEBUG) + .hasEvent(Level.DEBUG) { it.message.isNotEmpty() } + .hasEvent(Level.DEBUG, expectedMessage) + .hasEvent(Level.DEBUG, expectedMessage, expectedException) + .hasEvent(Level.DEBUG, expectedMessage, RuntimeException::class.java) + .hasEvent(Level.DEBUG, expectedMessage, RuntimeException::class) + .hasEventContaining(Level.DEBUG, "test", "message") + .hasEventMatching(Level.DEBUG, Regex("[a-z]+ message")) + .hasEventMatching(Level.DEBUG, Regex("[a-z]+ message"), expectedException) + .hasEventMatching(Level.DEBUG, Regex("[a-z]+ message"), RuntimeException::class.java) + .hasEventMatching(Level.DEBUG, Regex("[a-z]+ message"), RuntimeException::class) + } + + @Test + fun shouldCaptureInfoLogEvents() { + val expectedException = RuntimeException("test exception") + val expectedMessage = "test message" + + LOG.info(expectedMessage, expectedException) + + assertThat(log) + .hasInfo() + .hasInfo { it.message.isNotEmpty() } + .hasInfo(expectedMessage) + .hasInfo(expectedMessage, expectedException) + .hasInfo(expectedMessage, RuntimeException::class) + .hasInfoContaining("test", "message") + .hasInfoMatching(Regex("[a-z]+ message")) + .hasInfoMatching(Regex("[a-z]+ message")) + .hasInfoMatching(Regex("[a-z]+ message"), expectedException) + .hasInfoMatching(Regex("[a-z]+ message"), RuntimeException::class) + } + + @Test + fun shouldCaptureWarnLogEvents() { + val expectedException = RuntimeException("test exception") + val expectedMessage = "test message" + + LOG.warn(expectedMessage, expectedException) + + assertThat(log) + .hasWarn() + .hasWarn { it.message.isNotEmpty() } + .hasWarn(expectedMessage) + .hasWarn(expectedMessage, expectedException) + .hasWarn(expectedMessage, RuntimeException::class) + .hasWarnContaining("test", "message") + .hasWarnMatching(Regex("[a-z]+ message")) + .hasWarnMatching(Regex("[a-z]+ message")) + .hasWarnMatching(Regex("[a-z]+ message"), expectedException) + .hasWarnMatching(Regex("[a-z]+ message"), RuntimeException::class) + } + + @Test + fun shouldCaptureWarnErrorEvents() { + val expectedException = RuntimeException("test exception") + val expectedMessage = "test message" + + LOG.error(expectedMessage, expectedException) + + assertThat(log) + .hasError() + .hasError { it.message.isNotEmpty() } + .hasError(expectedMessage) + .hasError(expectedMessage, expectedException) + .hasError(expectedMessage, RuntimeException::class) + .hasErrorContaining("test", "message") + .hasErrorMatching(Regex("[a-z]+ message")) + .hasErrorMatching(Regex("[a-z]+ message")) + .hasErrorMatching(Regex("[a-z]+ message"), expectedException) + .hasErrorMatching(Regex("[a-z]+ message"), RuntimeException::class) + } + + @Test + fun shouldFailAssertionWhenLevelIsNotFound() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasWarn() } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"[WARN]\"> ") + } + + @Test + fun shouldFailAssertionWhenPredicateDoesNotMatch() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfo { it.message.isEmpty() } } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"INFO event matching given predicate\"> ") + } + + @Test + fun shouldFailAssertionWhenMessageIsNotFound() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfo("other message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"[INFO] other message\"> ") + } + + @Test + fun shouldCorrectlyConstructFauilureMessageWithPercentage() { + LOG.info("test message %") + + assertThatThrownBy { assertThat(log).hasInfo("other message %") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message %\">\n" + + "to contain:\n" + + " <\"[INFO] other message %\"> ") + } + + @Test + fun shouldFailAssertionWhenMessageWithLevelIsNotFound() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasError("other message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\n" + + "Expecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"[ERROR] other message\"> ") + } + + @Test + fun shouldFailAssertionWhenThrowableIsNotFound() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfo("test message", RuntimeException()) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\n" + + "Expecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"[INFO] test message\"> ") + } + + @Test + fun shouldFailAssertionWhenThrowableClassIsNotFound() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfo("test message", RuntimeException::class) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\n" + + "Expecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"[INFO] test message\"> ") + } + + @Test + fun shouldFailAssertionWhenMessageDoesNotContain() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfoContaining("other", "message") } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"INFO message containing [other, message]\"> ") + } + + @Test + fun shouldFailAssertionWhenMessageDoesNotMatch() { + LOG.info("test message") + + assertThatThrownBy { assertThat(log).hasInfoMatching(Regex("[0-9]+ message")) } + .isInstanceOf(AssertionError::class.java) + .hasMessage("\nExpecting:\n" + + " <\"[INFO] test message\">\n" + + "to contain:\n" + + " <\"INFO message matching: [0-9]+ message\"> ") + } +}