From 356001546ec5a545865f9f3c16a1ad656e81887d Mon Sep 17 00:00:00 2001 From: Daniel Flassak Date: Fri, 24 Nov 2023 14:11:05 +0100 Subject: [PATCH 1/8] update dependencies and use Java 17 --- .github/workflows/run-with-maven.yml | 4 +-- pom.xml | 43 +++++++++++----------------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/.github/workflows/run-with-maven.yml b/.github/workflows/run-with-maven.yml index d3acbad..df8ed57 100644 --- a/.github/workflows/run-with-maven.yml +++ b/.github/workflows/run-with-maven.yml @@ -39,10 +39,10 @@ jobs: restore-keys: | ${{ runner.os }}-maven- - - name: Set up JDK 8 + - name: Set up JDK 17 uses: actions/setup-java@v2 with: - java-version: '8' + java-version: '17' distribution: 'adopt' cache: maven diff --git a/pom.xml b/pom.xml index e42f49a..7583536 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ - 4.0.0 @@ -31,25 +31,24 @@ 3.5.0-SNAPSHOT - 1.8 - 1.18.22 - 1.2.11 - 5.8.2 + 17 + 1.18.30 + 1.4.11 + 5.10.1 UTF-8 - 0.23.1 - 3.22.0 - 8.45.1 - 0.8.7 - 2.22.2 - 3.1.1 - 3.2.1 - 3.3.1 - 3.9.0 - 2.5.3 - 3.0.1 - 4.4.0 - 1.6.8 + 1.2.0 + 3.24.2 + 10.12.5 + 0.8.11 + 3.2.2 + 3.3.1 + 3.3.0 + 3.6.2 + 3.11.0 + 3.1.0 + 5.7.0 + 1.6.13 @@ -106,14 +105,6 @@ true - - org.apache.maven.plugins - maven-release-plugin - ${maven-release-plugin.version} - - v@{project.version}-${project.artifactId} - - org.sonatype.plugins nexus-staging-maven-plugin From 22017abb4b1be67ecae73cd59160d24c15ef89bb Mon Sep 17 00:00:00 2001 From: Daniel Flassak Date: Fri, 24 Nov 2023 14:58:22 +0100 Subject: [PATCH 2/8] remove deprecated API --- pom.xml | 6 + .../logcapture/CapturingAppender.java | 9 +- .../logcapture/ExpectedMdcEntry.java | 38 +-- .../logcapture/FluentLogAssertion.java | 146 ------------ .../infrastructure/logcapture/LogCapture.java | 210 +---------------- .../logcapture/ExpectedMdcEntryUnitTest.java | 6 +- .../logcapture/FluentApiTest.java | 221 ------------------ .../LogAsserterIntegrationTest.java | 6 +- .../logcapture/LogCaptureTest.java | 174 +------------- .../logcapture/LogCaptureWrongUsageTest.java | 11 +- 10 files changed, 35 insertions(+), 792 deletions(-) delete mode 100644 src/main/java/de/dm/infrastructure/logcapture/FluentLogAssertion.java delete mode 100644 src/test/java/de/dm/infrastructure/logcapture/FluentApiTest.java diff --git a/pom.xml b/pom.xml index 7583536..2ae7333 100644 --- a/pom.xml +++ b/pom.xml @@ -89,6 +89,12 @@ ${archunit.version} test + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + diff --git a/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java b/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java index 2c99ea4..0ac3adb 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java +++ b/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java @@ -16,14 +16,15 @@ import java.util.Optional; import java.util.Set; -@Getter -@Setter class CapturingAppender extends ContextAwareBase implements Appender { - + @Getter List loggedEvents = new ArrayList<>(); private final Set capturedPackages; + @Getter + @Setter private String name; + @Getter private boolean started; CapturingAppender(LoggerContext loggerContext, Set capturedPackages) { @@ -42,7 +43,7 @@ public synchronized void doAppend(ILoggingEvent loggingEvent) { .formattedMessage(loggingEvent.getFormattedMessage()) .mdcData(loggingEvent.getMDCPropertyMap()) .loggedException(getLoggedException(loggingEvent.getThrowableProxy())) - .marker(loggingEvent.getMarker()) + .marker(loggingEvent.getMarker()) // TODO: this has been deprecated .argumentArray(loggingEvent.getArgumentArray()) .build()); } diff --git a/src/main/java/de/dm/infrastructure/logcapture/ExpectedMdcEntry.java b/src/main/java/de/dm/infrastructure/logcapture/ExpectedMdcEntry.java index e295300..f5f161a 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/ExpectedMdcEntry.java +++ b/src/main/java/de/dm/infrastructure/logcapture/ExpectedMdcEntry.java @@ -27,7 +27,7 @@ public final class ExpectedMdcEntry implements LogEventMatcher { * @return expected Mdc entry to use in log expectation */ public static ExpectedMdcEntry mdc(String key, String valueRegex) { - return ExpectedMdcEntry.withMdc(key, valueRegex); + return new ExpectedMdcEntry(key, new PatternMatcher(valueRegex)); } /** @@ -40,7 +40,7 @@ public static ExpectedMdcEntry mdc(String key, String valueRegex) { * @return expected Mdc entry to use in log expectation */ public static ExpectedMdcEntry mdc(String key, MdcMatcher mdcMatcher) { - return ExpectedMdcEntry.withMdc(key, mdcMatcher); + return new ExpectedMdcEntry(key, mdcMatcher); } @Override @@ -56,8 +56,7 @@ public String getNonMatchingErrorMessage(LoggedEvent loggedEvent) { StringBuilder assertionMessage = new StringBuilder(format(" captured message: \"%s\"", loggedEvent.getFormattedMessage())); assertionMessage.append(lineSeparator()); assertionMessage.append(format(" expected MDC key: %s", key)); - if (matcher instanceof PatternMatcher) { - PatternMatcher patternMatcher = (PatternMatcher) matcher; + if (matcher instanceof PatternMatcher patternMatcher) { assertionMessage.append(lineSeparator()); assertionMessage.append(format(" expected MDC value: \"%s\"", patternMatcher.pattern)); } @@ -80,37 +79,6 @@ public String getMatcherDetailDescription() { return format("MDCValue with key: \"%s\"", key); } - /** - * use this in LogCapture.assertLogged(...) to verify that something has been logged with an MDC value - * - * @param key MDC key - * @param valueRegex regex that must match the expected value. Will be wrapped with .* - * - * @return expected entry - * - * @deprecated in favor of mdc(key, valueRegex) that matches the new assertion style - */ - @Deprecated - public static ExpectedMdcEntry withMdc(String key, String valueRegex) { - return new ExpectedMdcEntry(key, new PatternMatcher(valueRegex)); - } - - /** - * use this in LogCapture.assertLogged(...) to verify that something has been logged with an MDC value using - * your own {@link MdcMatcher} if MDC contents are structured and the Matcher needs to understand the structure - * - * @param key MDC key - * @param matcher implementation of {@link MdcMatcher} that checks if the actual MDC content matches the expectations - * - * @return expected entry - * - * @deprecated in favor of mdc(key, matcher) that matches the new assertion style - */ - @Deprecated - public static ExpectedMdcEntry withMdc(String key, MdcMatcher matcher) { - return new ExpectedMdcEntry(key, matcher); - } - private static class PatternMatcher implements MdcMatcher { private final Pattern pattern; diff --git a/src/main/java/de/dm/infrastructure/logcapture/FluentLogAssertion.java b/src/main/java/de/dm/infrastructure/logcapture/FluentLogAssertion.java deleted file mode 100644 index 67f929f..0000000 --- a/src/main/java/de/dm/infrastructure/logcapture/FluentLogAssertion.java +++ /dev/null @@ -1,146 +0,0 @@ -package de.dm.infrastructure.logcapture; - -import ch.qos.logback.classic.Level; -import de.dm.infrastructure.logcapture.LogCapture.LastCapturedLogEvent; -import lombok.RequiredArgsConstructor; - -import java.util.LinkedList; -import java.util.List; -import java.util.Optional; - -import static ch.qos.logback.classic.Level.DEBUG; -import static ch.qos.logback.classic.Level.ERROR; -import static ch.qos.logback.classic.Level.INFO; -import static ch.qos.logback.classic.Level.TRACE; -import static ch.qos.logback.classic.Level.WARN; -import static lombok.AccessLevel.PACKAGE; -import static lombok.AccessLevel.PRIVATE; - -/** - * Helper class for fluent log assertions/expectations. Use this via {@link LogCapture#info()} et al. or {@link LogCapture#withMdcForAll(String, String)} - * - * @deprecated in favor or the new assertion style - */ -@RequiredArgsConstructor(access = PACKAGE) -@Deprecated -public final class FluentLogAssertion { - private final LogCapture logCapture; - private final Optional lastCapturedLogEvent; - private final List expectedMdcEntries = new LinkedList<>(); - - /** - * prepare the assertion of log messages with MDC contents - * - * @param key MDC key - * @param regex regular expression describing the MDC value - * - * @return FluentLogAssertion to assert the messages with MDC - */ - public FluentLogAssertion withMdcForAll(String key, String regex) { - FluentLogAssertion newAssertion = new FluentLogAssertion(logCapture, lastCapturedLogEvent); - newAssertion.expectedMdcEntries.addAll(expectedMdcEntries); - newAssertion.expectedMdcEntries.add(ExpectedMdcEntry.withMdc(key, regex)); - - return newAssertion; - } - - /** - * prepare the assertion of a logged warn message - * - * @return FluentLogAssertion to assert an warn message - */ - public ConfiguredLogAssertion warn() { - return new ConfiguredLogAssertion(WARN); - } - - /** - * prepare the assertion of a logged error message - * - * @return FluentLogAssertion to assert an error message - */ - public ConfiguredLogAssertion error() { - return new ConfiguredLogAssertion(ERROR); - } - - /** - * prepare the assertion of a logged info message - * - * @return FluentLogAssertion to assert an info message - */ - public ConfiguredLogAssertion info() { - return new ConfiguredLogAssertion(INFO); - } - - /** - * prepare the assertion of a logged debug message - * - * @return FluentLogAssertion to assert an debug message - */ - public ConfiguredLogAssertion debug() { - return new ConfiguredLogAssertion(DEBUG); - } - - /** - * prepare the assertion of a logged trace message - * - * @return FluentLogAssertion to assert an trace message - */ - public ConfiguredLogAssertion trace() { - return new ConfiguredLogAssertion(TRACE); - } - - /** - * assert that nothing else has been logged - */ - public void assertNothingElseLogged() { - LastCapturedLogEvent presentEvent = lastCapturedLogEvent.orElseThrow(() -> - new IllegalStateException("assertNothingElseLogged() must be called with a previous log expectation")); - presentEvent.assertNothingElseLogged(); - } - - /** - * Helper class for fluent log assertions. Use this via {@link LogCapture#info()} et al. or {@link LogCapture#withMdcForAll(String, String)} - * - * @deprecated in favor of the new api - */ - @Deprecated - @RequiredArgsConstructor(access = PRIVATE) - public class ConfiguredLogAssertion { - private final Level level; - private final List expectedMdcEntries = new LinkedList<>(FluentLogAssertion.this.expectedMdcEntries); - - /** - * configure the next message assertion to also assert MDC contents - * - * @param key MDC key - * @param regex regular expression describing the MDC value - * - * @return pre-configured assertion - */ - public ConfiguredLogAssertion withMdc(String key, String regex) { - ConfiguredLogAssertion newAssertion = new ConfiguredLogAssertion(level); - newAssertion.expectedMdcEntries.add(ExpectedMdcEntry.withMdc(key, regex)); - - return newAssertion; - } - - /** - * assert that a message has been logged, depending on previous configuration - * - * @param regex regex to match formatted log message (with Pattern.DOTALL and Pattern.MULTILINE) - * - * @return configuration to allow further assertions - */ - public FluentLogAssertion assertLogged(String regex) { - final LastCapturedLogEvent capturedLogEvent; - if (lastCapturedLogEvent.isPresent()) { - capturedLogEvent = lastCapturedLogEvent.get().thenLogged(level, regex, expectedMdcEntries.toArray(new ExpectedMdcEntry[0])); - } else { - capturedLogEvent = logCapture.assertLogged(level, regex, expectedMdcEntries.toArray(new ExpectedMdcEntry[0])); - } - FluentLogAssertion newFluentLogAssertion = new FluentLogAssertion(logCapture, Optional.of(capturedLogEvent)); - newFluentLogAssertion.expectedMdcEntries.addAll(FluentLogAssertion.this.expectedMdcEntries); - return newFluentLogAssertion; - } - } -} diff --git a/src/main/java/de/dm/infrastructure/logcapture/LogCapture.java b/src/main/java/de/dm/infrastructure/logcapture/LogCapture.java index b1f2d6c..9169fc0 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/LogCapture.java +++ b/src/main/java/de/dm/infrastructure/logcapture/LogCapture.java @@ -2,19 +2,15 @@ import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; -import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.slf4j.LoggerFactory; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; -import java.util.List; -import java.util.Optional; import java.util.Set; import static org.slf4j.Logger.ROOT_LOGGER_NAME; @@ -86,16 +82,6 @@ public void addAppenderAndSetLogLevelToTrace() { setLogLevelToTrace(); } - /** - * delegates to {@link LogCapture#addAppenderAndSetLogLevelToTrace()} for compatibility - * - * @deprecated (because log level actually needs to be set to TRACE, not DEBUG) - */ - @Deprecated - public void addAppenderAndSetLogLevelToDebug() { - addAppenderAndSetLogLevelToTrace(); - } - private void setLogLevelToTrace() { if (originalLogLevels != null) { throw new IllegalStateException("LogCapture.addAppenderAndSetLogLevelToTrace() should not be called only once or after calling removeAppenderAndResetLogLevel() again."); @@ -128,41 +114,7 @@ public void removeAppenderAndResetLogLevel() { rootLogger.detachAppender(capturingAppender); resetLogLevel(); } - - /** - * assert that some message has been logged - * - * @param level expected log level - * @param regex regex to match formatted log message (with Pattern.DOTALL and Pattern.MULTILINE) - * @param expectedMdcEntries expected MDC entries, see @{@link ExpectedMdcEntry} - * - * @return a LastCapturedLogEvent from which .thenLogged(...) can be called to assert if things have been logged in a specific order - * - * @throws AssertionError if the expected log message has not been logged - * @deprecated use the new assertion methods from ({@link #assertLogged(LogExpectation)}) instead. Use {@link ExpectedMdcEntry#mdc(String, String)} for MDC assertions. - */ - @Deprecated - public LastCapturedLogEvent assertLogged(Level level, String regex, ExpectedMdcEntry... expectedMdcEntries) { - return assertLogged(level, regex, null, expectedMdcEntries); - } - - @Deprecated - private LastCapturedLogEvent assertLogged(Level level, String regex, LastCapturedLogEvent lastCapturedLogEvent, ExpectedMdcEntry... expectedMdcEntries) { - if (capturingAppender == null) { - throw new IllegalStateException("capturingAppender is null. " + - "Please make sure that either LogCapture is used with a @Rule annotation or that addAppenderAndSetLogLevelToTrace is called manually."); - } - - int startIndex = lastCapturedLogEvent == null ? 0 : lastCapturedLogEvent.lastAssertedLogMessageIndex + 1; - int numberOfAssertedLogMessages = lastCapturedLogEvent == null ? 1 : lastCapturedLogEvent.numberOfAssertedLogMessages + 1; - - List expectedMdcEntriesList = expectedMdcEntries != null ? Arrays.asList(expectedMdcEntries) : Collections.emptyList(); - - Integer foundAtIndex = new LogAsserter(capturingAppender, new LinkedList<>()).assertCapturedNext(Optional.of(level), Optional.of(regex), startIndex, expectedMdcEntriesList); - - return new LastCapturedLogEvent(foundAtIndex, numberOfAssertedLogMessages); - } - + /** * assert that a certain expected message has been logged. * @@ -276,164 +228,4 @@ public LogAsserter with(LogEventMatcher... logEventMatchers) { return new LogAsserter(capturingAppender, Arrays.asList(logEventMatchers)); } - /** - * Helper to allow for comfortable assertions to check the order in which things are logged - * - * @deprecated in favor of the new API - */ - @RequiredArgsConstructor - @Deprecated - public class LastCapturedLogEvent { - private final int lastAssertedLogMessageIndex; - private final int numberOfAssertedLogMessages; - - /** - * assert that something has been logged after this event - * - * @param level expected log level - * @param regex regex to match formatted log message - * @param expectedMdcEntries expected MDC entries, see @{@link ExpectedMdcEntry} - * - * @return another LastCapturedLogEvent - for obvious reasons - * - * @throws AssertionError if the expected log message has not been logged - * @deprecated use the new assertion methods from ({@link #assertLoggedInOrder(LogExpectation...)} (LogExpectation)}) instead. Use {@link ExpectedMdcEntry#mdc(String, String)} for MDC assertions. - */ - @Deprecated - public LastCapturedLogEvent thenLogged(Level level, String regex, ExpectedMdcEntry... expectedMdcEntries) { - return assertLogged(level, regex, this, expectedMdcEntries); - } - - /** - * assert that nothing else has been logged except for the asserted log messages - * - * @throws AssertionError if something else has been logged - * @deprecated use the new log assertions with {@link LogAsserter.NothingElseLoggedAsserter#assertNothingElseLogged()} instead - */ - @Deprecated - public void assertNothingElseLogged() { - if (capturingAppender.loggedEvents.size() > numberOfAssertedLogMessages) { - throw new AssertionError("There have been other log messages than the asserted ones."); - } - } - } - - /** - * prepare the assertion of log messages with MDC contents - * - *

Example: - *

{@code
-     * logCapture
-     *     .withMdcForAll("key", "value")
-     *     .info().assertLogged("hello world")
-     *     .warn().assertLogged("bye world"));
-     * }
- * - * @param key MDC key - * @param regex regular expression describing the MDC value - * - * @return FluentLogAssertion to assert the messages with MDC - * - * @deprecated use {@link #with(LogEventMatcher...)} before ({@link #assertLoggedInOrder(LogExpectation...)} or {@link #assertLoggedInAnyOrder(LogExpectation...)} instead - */ - @Deprecated - public FluentLogAssertion withMdcForAll(String key, String regex) { - return new FluentLogAssertion(this, Optional.empty()) - .withMdcForAll(key, regex); - } - - /** - * prepare the assertion of a logged error message - * - *

Example: - *

{@code
-     * logCapture
-     *     .error().assertLogged("hello world")
-     * }
- * - * @return FluentLogAssertion to assert an error message - * - * @deprecated use {@link LogExpectation#error(LogEventMatcher...)} instead - */ - @Deprecated - public FluentLogAssertion.ConfiguredLogAssertion error() { - return new FluentLogAssertion(this, Optional.empty()) - .error(); - } - - /** - * prepare the assertion of a logged warn message - * - *

Example: - *

{@code
-     * logCapture
-     *     .warn().assertLogged("hello world")
-     * }
- * - * @return FluentLogAssertion to assert an warn message - * - * @deprecated use {@link LogExpectation#warn(LogEventMatcher...)} instead - */ - @Deprecated - public FluentLogAssertion.ConfiguredLogAssertion warn() { - return new FluentLogAssertion(this, Optional.empty()) - .warn(); - } - - /** - * prepare the assertion of a logged info message - * - *

Example: - *

{@code
-     * logCapture
-     *     .info().assertLogged("hello world")
-     * }
- * - * @return FluentLogAssertion to assert an info message - * - * @deprecated use {@link LogExpectation#info(LogEventMatcher...)} instead - */ - @Deprecated - public FluentLogAssertion.ConfiguredLogAssertion info() { - return new FluentLogAssertion(this, Optional.empty()) - .info(); - } - - /** - * prepare the assertion of a logged debug message - * - *

Example: - *

{@code
-     * logCapture
-     *     .debug().assertLogged("hello world")
-     * }
- * - * @return FluentLogAssertion to assert an debug message - * - * @deprecated use {@link LogExpectation#debug(LogEventMatcher...)} instead - */ - @Deprecated - public FluentLogAssertion.ConfiguredLogAssertion debug() { - return new FluentLogAssertion(this, Optional.empty()) - .debug(); - } - - /** - * prepare the assertion of a logged trace message - * - *

Example: - *

{@code
-     * logCapture
-     *     .trace().assertLogged("hello world")
-     * }
- * - * @return FluentLogAssertion to assert an trace message - * - * @deprecated use {@link LogExpectation#trace(LogEventMatcher...)} instead - */ - @Deprecated - public FluentLogAssertion.ConfiguredLogAssertion trace() { - return new FluentLogAssertion(this, Optional.empty()) - .trace(); - } } diff --git a/src/test/java/de/dm/infrastructure/logcapture/ExpectedMdcEntryUnitTest.java b/src/test/java/de/dm/infrastructure/logcapture/ExpectedMdcEntryUnitTest.java index ae6ebac..0719fd3 100644 --- a/src/test/java/de/dm/infrastructure/logcapture/ExpectedMdcEntryUnitTest.java +++ b/src/test/java/de/dm/infrastructure/logcapture/ExpectedMdcEntryUnitTest.java @@ -6,13 +6,15 @@ import java.util.HashMap; import java.util.Map; +import static de.dm.infrastructure.logcapture.ExpectedMdcEntry.mdc; + class ExpectedMdcEntryUnitTest { private static final String TEST_KEY = "test_key"; @Test void isContainedIn() { - ExpectedMdcEntry expectedMdcEntry = ExpectedMdcEntry.withMdc(TEST_KEY, "test value"); + ExpectedMdcEntry expectedMdcEntry = mdc(TEST_KEY, "test value"); Map mdcContents = new HashMap<>(); mdcContents.put(TEST_KEY, "this is a test value, cool!"); @@ -25,7 +27,7 @@ void isContainedIn() { @Test void isNotContainedIn() { - ExpectedMdcEntry expectedMdcEntry = ExpectedMdcEntry.withMdc(TEST_KEY, "test value"); + ExpectedMdcEntry expectedMdcEntry = mdc(TEST_KEY, "test value"); Map mdcContents = new HashMap<>(); mdcContents.put(TEST_KEY, "this is a value, cool!"); mdcContents.put("some_other_key", "this is a test value, cool!"); diff --git a/src/test/java/de/dm/infrastructure/logcapture/FluentApiTest.java b/src/test/java/de/dm/infrastructure/logcapture/FluentApiTest.java deleted file mode 100644 index dc0a107..0000000 --- a/src/test/java/de/dm/infrastructure/logcapture/FluentApiTest.java +++ /dev/null @@ -1,221 +0,0 @@ -package de.dm.infrastructure.logcapture; - -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.slf4j.MDC; - -import java.util.Optional; - -import static java.lang.System.lineSeparator; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@Slf4j -class FluentApiTest { - @RegisterExtension - LogCapture logCapture = LogCapture.forCurrentPackage(); - - - @Test - void basicFluentApi() { - log.info("hello world"); - - logCapture.info().assertLogged("hello world"); - } - - @Test - void inOrderSucceeds() { - log.info("hello world"); - log.warn("bye world"); - - logCapture - .info().assertLogged("hello world") - .warn().assertLogged("bye world"); - } - - @Test - void inOrderFails() { - log.warn("bye world"); - log.info("hello world"); - - AssertionError assertionError = assertThrows(AssertionError.class, () -> - logCapture - .info().assertLogged("hello world") - .warn().assertLogged("bye world")); - - assertThat(assertionError).hasMessage("Expected log message has not occurred: Level: WARN, Regex: \"bye world\""); - } - - @Test - void nothingElseLoggedSucceeds() { - log.info("hello world"); - - logCapture - .info().assertLogged("hello world") - .assertNothingElseLogged(); - } - - @Test - void nothingElseLoggedFails() { - log.info("hello world"); - log.info("hello universe"); - - - AssertionError assertionError = assertThrows(AssertionError.class, () -> - logCapture - .info().assertLogged("hello world") - .assertNothingElseLogged()); - - assertThat(assertionError).hasMessage("There have been other log messages than the asserted ones."); - } - - @Test - void wrongUsageThrowsIllegalState() { - // this constructor is package private, so it should be effectively impossible to actually cause this IllegalStateException - FluentLogAssertion fluentLogAssertion = new FluentLogAssertion(null, Optional.empty()); - - IllegalStateException illegalStateException = assertThrows(IllegalStateException.class, () -> fluentLogAssertion.assertNothingElseLogged()); - - assertThat(illegalStateException).hasMessage("assertNothingElseLogged() must be called with a previous log expectation"); - } - - @Test - void singleMdcSucceeds() { - log.info("hello world"); - MDC.put("key", "value"); - log.warn("bye world"); - MDC.clear(); - - logCapture - .info().assertLogged("hello world") - .warn().withMdc("key", "value").assertLogged("bye world"); - } - - @Test - void singleMdcFails() { - MDC.put("key", "value"); - log.info("hello world"); - MDC.clear(); - log.warn("bye world"); - - AssertionError assertionError = assertThrows(AssertionError.class, () -> - logCapture - .info().assertLogged("hello world") - .warn().withMdc("key", "value").assertLogged("bye world")); - - assertThat(assertionError).hasMessage("Expected log message has occurred, but never with the expected MDC value: Level: WARN, Regex: \"bye world\"" + - lineSeparator() + " captured message: \"bye world\"" + - lineSeparator() + " expected MDC key: key" + - lineSeparator() + " expected MDC value: \".*value.*\"" + - lineSeparator() + " captured MDC values:" + - lineSeparator()); - } - - @Test - void mdcForAllFails() { - MDC.put("key", "value"); - log.info("hello world"); - MDC.clear(); - log.warn("bye world"); - - AssertionError assertionError = assertThrows(AssertionError.class, () -> - logCapture - .withMdcForAll("key", "value") - .info().assertLogged("hello world") - .warn().assertLogged("bye world")); - - assertThat(assertionError).hasMessage("Expected log message has occurred, but never with the expected MDC value: Level: WARN, Regex: \"bye world\"" + - lineSeparator() + " captured message: \"bye world\"" + - lineSeparator() + " expected MDC key: key" + - lineSeparator() + " expected MDC value: \".*value.*\"" + - lineSeparator() + " captured MDC values:" + - lineSeparator()); - } - - @Test - void mdcForAllSucceeds() { - MDC.put("key", "value"); - log.info("hello world"); - log.warn("bye world"); - MDC.clear(); - - logCapture - .withMdcForAll("key", "value") - .info().assertLogged("hello world") - .warn().assertLogged("bye world"); - } - - @Test - void mdcForAllAndSingleMdcFails() { - MDC.put("key", "value"); - MDC.put("another_key", "another_value"); - log.info("hello world"); - MDC.remove("another_key"); - log.warn("bye world"); - MDC.clear(); - - AssertionError assertionError = assertThrows(AssertionError.class, () -> - logCapture - .withMdcForAll("key", "value") - .info().assertLogged("hello world") - .warn().withMdc("another_key", "another_value").assertLogged("bye world") - ); - - assertThat(assertionError).hasMessage( - "Expected log message has occurred, but never with the expected MDC value: Level: WARN, Regex: \"bye world\"" + - lineSeparator() + " captured message: \"bye world\"" + - lineSeparator() + " expected MDC key: another_key" + - lineSeparator() + " expected MDC value: \".*another_value.*\"" + - lineSeparator() + " captured MDC values:" + - lineSeparator() + " key: \"value\"" + - lineSeparator()); - } - - @Test - void mdcForAllAndSingleMdcSucceeds() { - MDC.put("key", "value"); - log.info("hello world"); - MDC.put("another_key", "another_value"); - log.warn("bye world"); - MDC.remove("another_key"); - log.error("hello again"); - MDC.clear(); - - logCapture - .withMdcForAll("key", "value") - .info().assertLogged("hello world") - .warn().withMdc("another_key", "another_value").assertLogged("bye world") - .error().assertLogged("hello again"); - } - - @Test - void allLevelsWork() { - log.error("first error"); - log.error("second error"); - log.warn("first warn"); - log.warn("second warn"); - log.info("first info"); - log.info("second info"); - log.debug("first debug"); - log.debug("second debug"); - log.trace("first trace"); - log.trace("second trace"); - - logCapture - .error().assertLogged("first error") - .error().assertLogged("second error"); - logCapture - .warn().assertLogged("first warn") - .warn().assertLogged("second warn"); - logCapture - .info().assertLogged("first info") - .info().assertLogged("second info"); - logCapture - .debug().assertLogged("first debug") - .debug().assertLogged("second debug"); - logCapture - .trace().assertLogged("first trace") - .trace().assertLogged("second trace"); - } -} diff --git a/src/test/java/de/dm/infrastructure/logcapture/LogAsserterIntegrationTest.java b/src/test/java/de/dm/infrastructure/logcapture/LogAsserterIntegrationTest.java index dab34da..6968751 100644 --- a/src/test/java/de/dm/infrastructure/logcapture/LogAsserterIntegrationTest.java +++ b/src/test/java/de/dm/infrastructure/logcapture/LogAsserterIntegrationTest.java @@ -8,6 +8,8 @@ import java.util.List; import java.util.Map; +import static de.dm.infrastructure.logcapture.ExpectedMdcEntry.mdc; + class LogAsserterIntegrationTest { @Test @@ -15,8 +17,8 @@ void containsMdcEntries() { String testKey = "test_key"; String otherKey = "test_key_2"; - ExpectedMdcEntry expectedMdcEntry1 = ExpectedMdcEntry.withMdc(testKey, "test value"); - ExpectedMdcEntry expectedMdcEntry2 = ExpectedMdcEntry.withMdc(otherKey, "good value"); + ExpectedMdcEntry expectedMdcEntry1 = mdc(testKey, "test value"); + ExpectedMdcEntry expectedMdcEntry2 = mdc(otherKey, "good value"); List expectedMdcEntries = new LinkedList<>(); expectedMdcEntries.add(expectedMdcEntry1); diff --git a/src/test/java/de/dm/infrastructure/logcapture/LogCaptureTest.java b/src/test/java/de/dm/infrastructure/logcapture/LogCaptureTest.java index 2685502..8493ed4 100644 --- a/src/test/java/de/dm/infrastructure/logcapture/LogCaptureTest.java +++ b/src/test/java/de/dm/infrastructure/logcapture/LogCaptureTest.java @@ -2,19 +2,13 @@ import ch.qos.logback.classic.Level; import com.example.app.LogCaptureCreatorInOtherPackage; -import de.dm.infrastructure.logcapture.LogCapture.LastCapturedLogEvent; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.MDC; -import static ch.qos.logback.classic.Level.DEBUG; import static ch.qos.logback.classic.Level.INFO; import static ch.qos.logback.classic.Level.TRACE; -import static de.dm.infrastructure.logcapture.ExpectedMdcEntry.withMdc; -import static java.lang.System.lineSeparator; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -29,145 +23,17 @@ class LogCaptureTest { @RegisterExtension public LogCapture logCaptureForCurrentPackage = LogCapture.forCurrentPackage(); - @Test - void twoLogMessagesInOrderAndNothingElse() { - log.info("something interesting"); - log.error("something terrible"); - - logCapture - .assertLogged(Level.INFO, "^something interesting") - .thenLogged(Level.ERROR, "terrible") - .assertNothingElseLogged(); - } - - @Test - void twoLogMessagesInOrderAndSomethingElseFails() { - log.info("something interesting"); - log.info("something unexpected"); - log.error("something terrible"); - - LastCapturedLogEvent lastCapturedLogEvent = logCapture - .assertLogged(INFO, "^something interesting") - .thenLogged(Level.ERROR, "terrible"); - - AssertionError assertionError = assertThrows(AssertionError.class, () -> { - lastCapturedLogEvent.assertNothingElseLogged(); - }); - - assertThat(assertionError).hasMessage("There have been other log messages than the asserted ones."); - } - - @Test - void captureMultilineMessages() { - log.info("something interesting\nwith something else in other lines, for example exception details"); - - logCapture.assertLogged(Level.INFO, "something interesting"); - } - - @Test - void captureLogsForCurrentPackage() { - log.info("Hello from logcapture"); - - final Logger acmeLogger = LoggerFactory.getLogger("com.acme"); - acmeLogger.info("Hello from com.acme"); - - logCaptureForCurrentPackage - .assertLogged(INFO, "^Hello from logcapture$"); - - AssertionError assertionError = assertThrows(AssertionError.class, () -> { - logCaptureForCurrentPackage.assertLogged(INFO, "Hello from com.acme"); - }); - - assertThat(assertionError).hasMessage("Expected log message has not occurred: Level: INFO, Regex: \"Hello from com.acme\""); - } - - @Test - void captureLogsForMultiplePackages() { - log.info("something interesting"); - log.error("something terrible"); - - final Logger comCaptureLogger = LoggerFactory.getLogger("com.capture.foo.bar"); - comCaptureLogger.info("some info"); - - logCapture - .assertLogged(Level.INFO, "^something interesting") - .thenLogged(Level.ERROR, "terrible") - .thenLogged(Level.INFO, "some info"); - } - - @Test - void twoLogMessagesOutOfOrder() { - log.error("something terrible"); - log.info("something interesting"); - LastCapturedLogEvent lastCapturedLogEvent = logCapture - .assertLogged(INFO, "^something interesting"); - - AssertionError assertionError = assertThrows(AssertionError.class, () -> { - lastCapturedLogEvent.thenLogged(Level.ERROR, "terrible"); - }); - - assertThat(assertionError).hasMessage("Expected log message has not occurred: Level: ERROR, Regex: \"terrible\""); - } - - @Test - void catchMissingLogMessage() { - AssertionError assertionError = assertThrows(AssertionError.class, () -> logCapture.assertLogged(INFO, "something that has not been logged")); - - assertThat(assertionError).hasMessage("Expected log message has not occurred: Level: INFO, Regex: \"something that has not been logged\""); - } - - @Test - void filterOutIrrelevantLogMessagesInIntegrationTest() { - ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME); - rootLogger.getLoggerContext().getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME).setLevel(DEBUG); - Logger logger = LoggerFactory.getLogger("com.acme.whatever"); - logger.info("something from another package"); - - AssertionError assertionError = assertThrows(AssertionError.class, () -> { - logCapture.assertLogged(INFO, "something from another package"); - }); - - assertThat(assertionError).hasMessage("Expected log message has not occurred: Level: INFO, Regex: \"something from another package\""); - } - - @Test - void logMessagesWithMdc() { - final String MDC_KEY = "mdc_key"; - - MDC.put(MDC_KEY, "an mdc value here"); - log.info("this should have an mdc value"); - MDC.clear(); - - logCapture - .assertLogged(Level.INFO, "mdc value", withMdc(MDC_KEY, "mdc value")); - } - - @Test - void correctLogMessagesWithMissingMdc() { - final String MDC_KEY = "mdc_key"; - final String actualMdcValue = "a wrong value here"; - - MDC.put(MDC_KEY, actualMdcValue); - log.info("some message"); - MDC.clear(); - - AssertionError assertionError = assertThrows(AssertionError.class, () -> - logCapture.assertLogged(INFO, "some message", withMdc(MDC_KEY, "mdc value"))); - - assertThat(assertionError).hasMessage("Expected log message has occurred, but never with the expected MDC value: Level: INFO, Regex: \"some message\"" + - lineSeparator() + " captured message: \"some message\"" + - lineSeparator() + " expected MDC key: mdc_key" + - lineSeparator() + " expected MDC value: \".*mdc value.*\"" + - lineSeparator() + " captured MDC values:" + - lineSeparator() + " " + MDC_KEY + ": \"" + actualMdcValue + "\"" + - lineSeparator()); - } - - + @SuppressWarnings("java:S5778") //this rule does not increase the clarity of this test @Test void fromCurrentPackageWorks() { - LogCapture logCapture = LogCaptureCreatorInOtherPackage.getLogCaptureFromCurrentPackage(); - assertThat(logCapture.capturedPackages).containsExactly(LogCaptureCreatorInOtherPackage.class.getPackage().getName()); + LogCapture logCaptureFromOtherPackage = LogCaptureCreatorInOtherPackage.getLogCaptureFromCurrentPackage(); + logCaptureFromOtherPackage.addAppenderAndSetLogLevelToTrace(); + log.info("hello from de.dm.infrastructure.logcapture"); + logCaptureFromOtherPackage.removeAppenderAndResetLogLevel(); + + assertThat(logCaptureFromOtherPackage.capturedPackages).containsExactly(LogCaptureCreatorInOtherPackage.class.getPackage().getName()); + var thrown = assertThrows(AssertionError.class, () -> logCaptureFromOtherPackage.assertLogged(LogExpectation.info("hello from"))); + assertThat(thrown).hasMessage("Expected log message has not occurred: Level: INFO, Regex: \"hello from\""); } @Test @@ -180,13 +46,6 @@ void logLevelIsResetToNull() { logLevelIsResetTo(null); } - @Test - void doesNotFailForNullArrayInMdcEntried() { - log.info("something interesting"); - - logCapture.assertLogged(Level.INFO, "^something interesting", null); - } - private void logLevelIsResetTo(Level originalLevel) { final ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME); final ch.qos.logback.classic.Logger comExampleLogger = rootLogger.getLoggerContext().getLogger("com.example"); @@ -203,19 +62,4 @@ private void logLevelIsResetTo(Level originalLevel) { assertThat(comExampleLogger.getLevel()).isEqualTo(originalLevel); } - private void logLevelIsResetWithDeprecatedMedhod() { - final ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME); - final ch.qos.logback.classic.Logger comExampleLogger = rootLogger.getLoggerContext().getLogger("com.example"); - - comExampleLogger.setLevel(INFO); - - LogCapture logCapture = LogCapture.forPackages("com.example"); - logCapture.addAppenderAndSetLogLevelToDebug(); - - assertThat(comExampleLogger.getLevel()).isEqualTo(TRACE); - - logCapture.removeAppenderAndResetLogLevel(); - - assertThat(comExampleLogger.getLevel()).isEqualTo(INFO); - } } diff --git a/src/test/java/de/dm/infrastructure/logcapture/LogCaptureWrongUsageTest.java b/src/test/java/de/dm/infrastructure/logcapture/LogCaptureWrongUsageTest.java index 79bc728..7472a03 100644 --- a/src/test/java/de/dm/infrastructure/logcapture/LogCaptureWrongUsageTest.java +++ b/src/test/java/de/dm/infrastructure/logcapture/LogCaptureWrongUsageTest.java @@ -2,7 +2,7 @@ import org.junit.jupiter.api.Test; -import static ch.qos.logback.classic.Level.INFO; +import static de.dm.infrastructure.logcapture.LogExpectation.info; import static org.junit.jupiter.api.Assertions.assertThrows; class LogCaptureWrongUsageTest { @@ -20,15 +20,10 @@ void resetWithoutInitializationFails() { assertThrows(IllegalStateException.class, () -> logCapture.removeAppenderAndResetLogLevel()); } + @SuppressWarnings("java:S5778") //this rule does not increase the clarity of this test @Test void assertionWithoutInitializationFails() { - assertThrows(IllegalStateException.class, () -> logCapture.assertLogged(INFO, "something")); + assertThrows(IllegalStateException.class, () -> logCapture.assertLogged(info("something"))); } - @Test - void deprecatedAddAppenderDelegates() { - logCapture.addAppenderAndSetLogLevelToDebug(); - - assertThrows(IllegalStateException.class, () -> logCapture.addAppenderAndSetLogLevelToDebug()); - } } From b1957f63011c67650ac5e7d97eb9a2fd3b229402 Mon Sep 17 00:00:00 2001 From: Daniel Flassak Date: Fri, 24 Nov 2023 14:58:28 +0100 Subject: [PATCH 3/8] add CapturingAppenderUnitTest --- .../logcapture/CapturingAppenderUnitTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/test/java/de/dm/infrastructure/logcapture/CapturingAppenderUnitTest.java diff --git a/src/test/java/de/dm/infrastructure/logcapture/CapturingAppenderUnitTest.java b/src/test/java/de/dm/infrastructure/logcapture/CapturingAppenderUnitTest.java new file mode 100644 index 0000000..761d24c --- /dev/null +++ b/src/test/java/de/dm/infrastructure/logcapture/CapturingAppenderUnitTest.java @@ -0,0 +1,49 @@ +package de.dm.infrastructure.logcapture; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.util.Set; + +import static ch.qos.logback.core.spi.FilterReply.ACCEPT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class CapturingAppenderUnitTest { + + @Mock + LoggerContext loggerContext; + + CapturingAppender sut = new CapturingAppender(loggerContext, Set.of( + "a.package", + "another.package") + ); + + @SuppressWarnings("unchecked") // filter types are irrelevant + @Test + void neverFiltersAndNeverHasAnyFilters() { + assertThat(sut.getFilterChainDecision(mock(ILoggingEvent.class))).isEqualTo(ACCEPT); + + sut.addFilter(mock(Filter.class)); + + assertThat(sut.getFilterChainDecision(mock(ILoggingEvent.class))).isEqualTo(ACCEPT); + assertThat(sut.getCopyOfAttachedFiltersList()).isEmpty(); + + sut.clearAllFilters(); + + assertThat(sut.getFilterChainDecision(mock(ILoggingEvent.class))).isEqualTo(ACCEPT); + } + + @Test + void startStop() { + assertThat(sut.isStarted()).isFalse(); + sut.start(); + assertThat(sut.isStarted()).isTrue(); + sut.stop(); + assertThat(sut.isStarted()).isFalse(); + } + +} From d1e757dcdd6c3629a5d5fb4205c3ee6a0ff9a859 Mon Sep 17 00:00:00 2001 From: Daniel Flassak Date: Fri, 24 Nov 2023 15:08:36 +0100 Subject: [PATCH 4/8] use some Java 17 niceness --- .../logcapture/ExpectedException.java | 14 +++++++------- .../dm/infrastructure/logcapture/LogAsserter.java | 6 +++--- .../dm/infrastructure/logcapture/LoggedEvent.java | 3 ++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/dm/infrastructure/logcapture/ExpectedException.java b/src/main/java/de/dm/infrastructure/logcapture/ExpectedException.java index 03f1f69..30f1989 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/ExpectedException.java +++ b/src/main/java/de/dm/infrastructure/logcapture/ExpectedException.java @@ -56,7 +56,7 @@ public String getMatcherDetailDescription() { } private static String loggedExceptionToString(Optional optionalException) { - if (!optionalException.isPresent()) { + if (optionalException.isEmpty()) { return "(null)"; } @@ -75,10 +75,10 @@ public String getMatcherTypeDescription() { } private static boolean exceptionMatches(Optional optionalActualException, Optional optionalExpectedException) { - if (!optionalActualException.isPresent() && optionalExpectedException.isPresent()) { + if (optionalActualException.isEmpty() && optionalExpectedException.isPresent()) { return false; } - if (!optionalExpectedException.isPresent()) { + if (optionalExpectedException.isEmpty()) { return true; } @@ -91,12 +91,12 @@ private static boolean exceptionMatches(Optional op } private static boolean expectedMessageMatches(LoggedEvent.LoggedException loggedException, ExpectedException expectedException) { - return !expectedException.expectedMessage.isPresent() || + return expectedException.expectedMessage.isEmpty() || expectedException.expectedMessage.get().matcher(loggedException.getMessage()).matches(); } private static boolean expectedTypeMatches(LoggedEvent.LoggedException loggedException, ExpectedException expectedException) { - if (!expectedException.expectedType.isPresent()) { + if (expectedException.expectedType.isEmpty()) { return true; } try { @@ -117,7 +117,7 @@ public String toString() { } /** - * helper for building ExcpectedExceptions + * helper for building ExpectedExceptions */ public static final class ExpectedExceptionBuilder { private String expectedMessageRegex; @@ -130,7 +130,7 @@ private ExpectedExceptionBuilder() { /** * set an expected message that should be matched for an expected Exception * - * @param expectedMessageRegex regular expression mathing an exception's message. Will be padded with .* + * @param expectedMessageRegex regular expression matching an exception's message. Will be padded with .* * * @return the builder with the expected message set */ diff --git a/src/main/java/de/dm/infrastructure/logcapture/LogAsserter.java b/src/main/java/de/dm/infrastructure/logcapture/LogAsserter.java index 5a4cf47..25c95c7 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/LogAsserter.java +++ b/src/main/java/de/dm/infrastructure/logcapture/LogAsserter.java @@ -233,7 +233,7 @@ static boolean isMatchedByAll(LoggedEvent loggedEvent, List level, Optional regex) { - if (!level.isPresent() && !regex.isPresent()) { + if (level.isEmpty() && regex.isEmpty()) { return ""; } if (level.isPresent() && regex.isPresent()) { @@ -245,11 +245,11 @@ private static String getDescriptionForExpectedMessage(Optional level, Op private static String getDescriptionForUnwantedLogMessage(Optional level, Optional regex, List matchers) { String matchersText = ""; - if (!matchers.isEmpty()) { + if (matchers != null && !matchers.isEmpty()) { matchersText = ", with matchers:" + lineSeparator() + " " + matchers.stream().map(LogEventMatcher::getMatcherDetailDescription) .collect(Collectors.joining(lineSeparator() + " ")); } - if (!level.isPresent() && !regex.isPresent()) { + if (level.isEmpty() && regex.isEmpty()) { return "" + matchersText; } if (level.isPresent() && regex.isPresent()) { diff --git a/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java b/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java index 662688b..98b6156 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java +++ b/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java @@ -14,7 +14,7 @@ @AllArgsConstructor(access = PRIVATE) @Builder @Getter -class LoggedEvent { +public class LoggedEvent { private final Level level; private final String formattedMessage; private final Map mdcData; @@ -23,6 +23,7 @@ class LoggedEvent { private final Marker marker; private final Object[] argumentArray; + @SuppressWarnings("squid:S2166") // LoggedException is not an Exception, but the name is still appropriate @AllArgsConstructor(access = PRIVATE) @Builder @Getter From 8033ffbad35ff546dde6fa10752347cd932929e1 Mon Sep 17 00:00:00 2001 From: Daniel Flassak Date: Fri, 24 Nov 2023 15:26:00 +0100 Subject: [PATCH 5/8] remove usage of deprecated getMarker() from CapturingAppender --- .../logcapture/CapturingAppender.java | 2 +- .../logcapture/ExpectedMarker.java | 7 +++-- .../logcapture/LoggedEvent.java | 3 ++- .../java/com/example/app/ReadableApiTest.java | 26 ++++++++++++++++--- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java b/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java index 0ac3adb..ad16c0d 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java +++ b/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java @@ -43,7 +43,7 @@ public synchronized void doAppend(ILoggingEvent loggingEvent) { .formattedMessage(loggingEvent.getFormattedMessage()) .mdcData(loggingEvent.getMDCPropertyMap()) .loggedException(getLoggedException(loggingEvent.getThrowableProxy())) - .marker(loggingEvent.getMarker()) // TODO: this has been deprecated + .markers(loggingEvent.getMarkerList()) .argumentArray(loggingEvent.getArgumentArray()) .build()); } diff --git a/src/main/java/de/dm/infrastructure/logcapture/ExpectedMarker.java b/src/main/java/de/dm/infrastructure/logcapture/ExpectedMarker.java index cb2a8bd..d04f3a4 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/ExpectedMarker.java +++ b/src/main/java/de/dm/infrastructure/logcapture/ExpectedMarker.java @@ -15,17 +15,16 @@ private ExpectedMarker(String expectedName) { @Override public boolean matches(LoggedEvent loggedEvent) { - return loggedEvent.getMarker() != null && - (loggedEvent.getMarker().getName().equals(expectedName) || loggedEvent.getMarker().contains(expectedName)); + return loggedEvent.getMarkers() != null && loggedEvent.getMarkers().stream().anyMatch(marker -> marker.contains(expectedName)); } @Override public String getNonMatchingErrorMessage(LoggedEvent loggedEvent) { String expected = format(" expected marker name: \"%s\"", expectedName) + lineSeparator(); - if (loggedEvent.getMarker() == null) { + if (loggedEvent.getMarkers() == null) { return expected + " but no marker was found"; } - return expected + format(" actual marker names: \"%s\"", loggedEvent.getMarker()); + return expected + format(" actual marker names: \"%s\"", loggedEvent.getMarkers()); } @Override diff --git a/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java b/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java index 98b6156..c609d45 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java +++ b/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java @@ -6,6 +6,7 @@ import lombok.Getter; import org.slf4j.Marker; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -20,7 +21,7 @@ public class LoggedEvent { private final Map mdcData; private final Optional loggedException; private final String loggerName; - private final Marker marker; + private final List markers; private final Object[] argumentArray; @SuppressWarnings("squid:S2166") // LoggedException is not an Exception, but the name is still appropriate diff --git a/src/test/java/com/example/app/ReadableApiTest.java b/src/test/java/com/example/app/ReadableApiTest.java index 0ad175e..48fc951 100644 --- a/src/test/java/com/example/app/ReadableApiTest.java +++ b/src/test/java/com/example/app/ReadableApiTest.java @@ -208,7 +208,7 @@ void markerFailsBecauseOtherMarkerIsPresent() { assertThat(assertionError).hasMessage("Expected log message has occurred, but never with the expected marker name: Level: INFO, Regex: \"hello with marker\"" + lineSeparator() + " expected marker name: \"expected\"" + - lineSeparator() + " actual marker names: \"unexpected\"" + + lineSeparator() + " actual marker names: \"[unexpected]\"" + lineSeparator()); } @@ -229,7 +229,7 @@ void markerFailsBecauseNoMarkerIsPresent() { } @Test - void markerFailsBecauseMultipleOtherMarkersArePresent() { + void markerFailsWhileNestedMarkersArePresent() { Marker marker = MarkerFactory.getMarker("unexpected_top"); marker.add(MarkerFactory.getMarker("unexpected_nested")); log.info(marker, "hello with marker"); @@ -242,7 +242,27 @@ void markerFailsBecauseMultipleOtherMarkersArePresent() { assertThat(assertionError).hasMessage("Expected log message has occurred, but never with the expected marker name: Level: INFO, Regex: \"hello with marker\"" + lineSeparator() + " expected marker name: \"expected\"" + - lineSeparator() + " actual marker names: \"unexpected_top [ unexpected_nested ]\"" + + lineSeparator() + " actual marker names: \"[unexpected_top [ unexpected_nested ]]\"" + + lineSeparator()); + } + + @Test + void markerFailsWhileMultipleMarkersArePresent() { + log.atInfo() + .setMessage("hello with markers") + .addMarker(MarkerFactory.getMarker("unexpected1")) + .addMarker(MarkerFactory.getMarker("unexpected2")) + .log(); + + AssertionError assertionError = assertThrows(AssertionError.class, + () -> logCapture.assertLogged( + info("hello with markers", + marker("expected")) + )); + + assertThat(assertionError).hasMessage("Expected log message has occurred, but never with the expected marker name: Level: INFO, Regex: \"hello with markers\"" + + lineSeparator() + " expected marker name: \"expected\"" + + lineSeparator() + " actual marker names: \"[unexpected1, unexpected2]\"" + lineSeparator()); } From 0d7cfe73bb1690dbf5cdd17985521ffd77a5c094 Mon Sep 17 00:00:00 2001 From: Daniel Flassak Date: Fri, 24 Nov 2023 15:38:37 +0100 Subject: [PATCH 6/8] remove unused argumentArray and add Javadoc comment --- .../de/dm/infrastructure/logcapture/CapturingAppender.java | 1 - .../java/de/dm/infrastructure/logcapture/LoggedEvent.java | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java b/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java index ad16c0d..46ef844 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java +++ b/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java @@ -44,7 +44,6 @@ public synchronized void doAppend(ILoggingEvent loggingEvent) { .mdcData(loggingEvent.getMDCPropertyMap()) .loggedException(getLoggedException(loggingEvent.getThrowableProxy())) .markers(loggingEvent.getMarkerList()) - .argumentArray(loggingEvent.getArgumentArray()) .build()); } } diff --git a/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java b/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java index c609d45..6934b19 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java +++ b/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java @@ -12,6 +12,9 @@ import static lombok.AccessLevel.PRIVATE; +/** + * represents a captured logged event + */ @AllArgsConstructor(access = PRIVATE) @Builder @Getter @@ -22,7 +25,6 @@ public class LoggedEvent { private final Optional loggedException; private final String loggerName; private final List markers; - private final Object[] argumentArray; @SuppressWarnings("squid:S2166") // LoggedException is not an Exception, but the name is still appropriate @AllArgsConstructor(access = PRIVATE) From 62c838e9f32fbeb72ae2941364d8a037e9513454 Mon Sep 17 00:00:00 2001 From: Daniel Flassak Date: Fri, 24 Nov 2023 16:35:09 +0100 Subject: [PATCH 7/8] add ExpectedKeyValue assertions --- pom.xml | 2 +- .../logcapture/CapturingAppender.java | 1 + .../logcapture/ExpectedKeyValue.java | 74 ++++++++++ .../logcapture/LoggedEvent.java | 2 + .../java/com/example/app/ReadableApiTest.java | 138 ++++++++++++++++++ 5 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/dm/infrastructure/logcapture/ExpectedKeyValue.java diff --git a/pom.xml b/pom.xml index 2ae7333..f81e35c 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT 17 1.18.30 diff --git a/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java b/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java index 46ef844..88d71f1 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java +++ b/src/main/java/de/dm/infrastructure/logcapture/CapturingAppender.java @@ -44,6 +44,7 @@ public synchronized void doAppend(ILoggingEvent loggingEvent) { .mdcData(loggingEvent.getMDCPropertyMap()) .loggedException(getLoggedException(loggingEvent.getThrowableProxy())) .markers(loggingEvent.getMarkerList()) + .keyValuePairs(loggingEvent.getKeyValuePairs()) .build()); } } diff --git a/src/main/java/de/dm/infrastructure/logcapture/ExpectedKeyValue.java b/src/main/java/de/dm/infrastructure/logcapture/ExpectedKeyValue.java new file mode 100644 index 0000000..7bbd1ac --- /dev/null +++ b/src/main/java/de/dm/infrastructure/logcapture/ExpectedKeyValue.java @@ -0,0 +1,74 @@ +package de.dm.infrastructure.logcapture; + +import java.util.Objects; +import java.util.stream.Collectors; + +import static java.lang.String.format; +import static java.lang.System.lineSeparator; + +/** + * define expected key-value pair to be attached to a log message + */ +public final class ExpectedKeyValue implements LogEventMatcher { + private final String key; + private final Object value; + + private ExpectedKeyValue(String key, Object value) { + if (key == null || value == null) { + throw new IllegalArgumentException("key and value are required for key-value log assertion"); + } + this.key = key; + this.value = value; + } + + @Override + public boolean matches(LoggedEvent loggedEvent) { + return loggedEvent.getKeyValuePairs() != null && + loggedEvent.getKeyValuePairs().stream() + .anyMatch(pair -> key.equals(pair.key) && ( + Objects.equals(value, pair.value) || + areEqualAsNumbers(value, pair.value) + )); + } + + /* + * this is only done for Numbers because + * 1. toString() should not be expensive for these + * 2. when Logging 2L can be considered equal to 2, for example, but maybe not to 2.0 + */ + private boolean areEqualAsNumbers(Object expectedValue, Object actualValue) { + return expectedValue instanceof Number && actualValue instanceof Number && + expectedValue.toString().equals(actualValue.toString()); + } + + @Override + public String getNonMatchingErrorMessage(LoggedEvent loggedEvent) { + String expected = format(" expected key-value pair (%s, %s)", key, value) + lineSeparator(); + return expected + format(" actual pairs: [%s]", loggedEvent.getKeyValuePairs() == null ? "" : + loggedEvent.getKeyValuePairs().stream() + .map(pair -> "(%s, %s)".formatted(pair.key, pair.value)) + .collect(Collectors.joining(", "))); + } + + @Override + public String getMatcherTypeDescription() { + return "key-value pair"; + } + + @Override + public String getMatcherDetailDescription() { + return format("key-value pair (%s, %s)", key, value); + } + + /** + * use this in a log expectation to verify that something has been logged with a certain key-value pair + * + * @param key expected key + * @param value expected value + * + * @return expected key-value to use in log expectation + */ + public static ExpectedKeyValue keyValue(String key, Object value) { + return new ExpectedKeyValue(key, value); + } +} diff --git a/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java b/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java index 6934b19..be3947c 100644 --- a/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java +++ b/src/main/java/de/dm/infrastructure/logcapture/LoggedEvent.java @@ -5,6 +5,7 @@ import lombok.Builder; import lombok.Getter; import org.slf4j.Marker; +import org.slf4j.event.KeyValuePair; import java.util.List; import java.util.Map; @@ -25,6 +26,7 @@ public class LoggedEvent { private final Optional loggedException; private final String loggerName; private final List markers; + private final List keyValuePairs; @SuppressWarnings("squid:S2166") // LoggedException is not an Exception, but the name is still appropriate @AllArgsConstructor(access = PRIVATE) diff --git a/src/test/java/com/example/app/ReadableApiTest.java b/src/test/java/com/example/app/ReadableApiTest.java index 48fc951..be9a6b5 100644 --- a/src/test/java/com/example/app/ReadableApiTest.java +++ b/src/test/java/com/example/app/ReadableApiTest.java @@ -9,7 +9,11 @@ import org.slf4j.Marker; import org.slf4j.MarkerFactory; +import java.math.BigDecimal; +import java.util.concurrent.atomic.AtomicInteger; + import static de.dm.infrastructure.logcapture.ExpectedException.exception; +import static de.dm.infrastructure.logcapture.ExpectedKeyValue.keyValue; import static de.dm.infrastructure.logcapture.ExpectedLoggerName.logger; import static de.dm.infrastructure.logcapture.ExpectedMarker.marker; import static de.dm.infrastructure.logcapture.ExpectedMdcEntry.mdc; @@ -21,6 +25,7 @@ import static de.dm.infrastructure.logcapture.LogExpectation.warn; import static java.lang.System.lineSeparator; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; @Slf4j @@ -718,4 +723,137 @@ void combinedLogExpectationsOnlyOutputMismatch() { lineSeparator() + " actual exception: message: \"an exception that was logged\", message: java.lang.RuntimeException" + lineSeparator()); } + + @Nested + class ExpectedKeyValues { + @Test + void failsWithDetailsNotMatchingExistingKeyValues() { + log.atInfo().setMessage("hello") + .addKeyValue("key1", 1) + .addKeyValue("key2", "value2") + .log(); + + var assertionError = assertThrows(AssertionError.class, () -> + logCapture.assertLogged(info("hello", keyValue("key", "a value"))) + ); + assertThat(assertionError).hasMessage(""" + Expected log message has occurred, but never with the expected key-value pair: Level: INFO, Regex: "hello" + expected key-value pair (key, a value) + actual pairs: [(key1, 1), (key2, value2)] + """); + } + + @Test + void failsWithDetailsWithNoExistingKeyValues() { + log.atInfo().setMessage("hello") + .log(); + + var assertionError = assertThrows(AssertionError.class, () -> + logCapture.assertLogged(info("hello", keyValue("key", "a value"))) + ); + assertThat(assertionError).hasMessage(""" + Expected log message has occurred, but never with the expected key-value pair: Level: INFO, Regex: "hello" + expected key-value pair (key, a value) + actual pairs: [] + """); + } + + @Test + void requiresKey() { + var assertionError = assertThrows(IllegalArgumentException.class, () -> + logCapture.assertLogged(info("hello", keyValue(null, "a value"))) + ); + assertThat(assertionError).hasMessage("key and value are required for key-value log assertion"); + } + + @Test + void requiresValue() { + var assertionError = assertThrows(IllegalArgumentException.class, () -> + logCapture.assertLogged(info("hello", keyValue("a_key", null))) + ); + assertThat(assertionError).hasMessage("key and value are required for key-value log assertion"); + } + + @Test + void succeedsWithString() { + log.atInfo().setMessage("hello") + .addKeyValue("name", "Frederick") + .log(); + + assertDoesNotThrow(() -> + logCapture.assertLogged(info("hello", keyValue("name", "Frederick"))) + ); + } + + @Test + void succeedsWithLong() { + log.atInfo().setMessage("hello") + .addKeyValue("meaning", 42L) + .log(); + + assertDoesNotThrow(() -> + logCapture.assertLogged(info("hello", keyValue("meaning", 42L))) + ); + } + + @Test + void succeedsWithLongAndInt() { + log.atInfo().setMessage("hello") + .addKeyValue("meaning", 42) + .log(); + + assertDoesNotThrow(() -> + logCapture.assertLogged(info("hello", keyValue("meaning", 42L))) + ); + } + + @Test + void succeedsWithIntAndLong() { + log.atInfo().setMessage("hello") + .addKeyValue("meaning", 42L) + .log(); + + assertDoesNotThrow(() -> + logCapture.assertLogged(info("hello", keyValue("meaning", 42))) + ); + } + + @Test + void succeedsWithIntAndAtomicInt() { + log.atInfo().setMessage("hello") + .addKeyValue("meaning", new AtomicInteger(42)) + .log(); + + assertDoesNotThrow(() -> + logCapture.assertLogged(info("hello", keyValue("meaning", 42))) + ); + } + + @Test + void failsWithBigDecimalAndPrecisionAndInt() { + log.atInfo().setMessage("hello") + .addKeyValue("meaning", new BigDecimal("42.00")) + .log(); + + var assertionError = assertThrows(AssertionError.class, () -> + logCapture.assertLogged(info("hello", keyValue("meaning", 42))) + ); + assertThat(assertionError).hasMessage(""" + Expected log message has occurred, but never with the expected key-value pair: Level: INFO, Regex: "hello" + expected key-value pair (meaning, 42) + actual pairs: [(meaning, 42.00)] + """); + } + + @Test + void succeedsWithBigDecimalAndInt() { + log.atInfo().setMessage("hello") + .addKeyValue("meaning", new BigDecimal("42")) + .log(); + + assertDoesNotThrow(() -> + logCapture.assertLogged(info("hello", keyValue("meaning", 42))) + ); + } + } } From b6712ac53bf3ac1998e51cab009b0d3645c2862f Mon Sep 17 00:00:00 2001 From: Daniel Flassak Date: Fri, 24 Nov 2023 16:46:31 +0100 Subject: [PATCH 8/8] prepare README for 4.0.0 --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 34882d5..6ef6e1f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ logCapture.assertLoggedInOrder( * [MDC content](#mdc-content) * [Exceptions](#exceptions) * [Markers](#markers) + * [Key-Value](#key-value) * [Logger name](#logger-name) * [Examples](#examples) * [Unit Test Example:](#unit-test-example) @@ -49,6 +50,7 @@ logCapture.assertLoggedInOrder( * [Cucumber example](#cucumber-example) * [Cucumber feature file](#cucumber-feature-file) * [Changes](#changes) + * [4.0.0](#400) * [3.6.1](#361) * [3.6.0](#360) * [3.5.0](#350) @@ -73,7 +75,7 @@ Add log-capture as a test dependency to your project. If you use Maven, add this de.dm.infrastructure log-capture - 3.6.1 + 4.0.0 test ``` @@ -130,6 +132,18 @@ log.info(MarkerFactory.getMarker("my-marker"), "hello with marker"); logCapture.assertLogged(info("hello with marker", marker("my-marker"))); ``` +#### Key-Value + +```java +import static de.dm.infrastructure.logcapture.ExpectedKeyValue.keyValue; + +... + +log.atInfo().setMessage("hello").addKeyValue("meaning", 42).log(); + +logCapture.assertLogged(info("hello", keyValue("meaning", 42))) +``` + #### Logger name ```java @@ -309,6 +323,13 @@ And with MDC logging context ## Changes +### 4.0.0 + +* **breaking change:** log-capture now requires Java 17 +* **breaking change:** all deprecated parts have been removed +* added a new log event matcher [for key-value content](#key-value) +* lots of dependency updates + ### 3.6.1 * Fixed a misleading and wrong assertion message. The assertion itself was correct, but the message always said all matchers did not match when only a subset did not match.