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\"> ")
+ }
+}