diff --git a/reactor-core/src/test/java/reactor/core/TestLoggerExtension.java b/reactor-core/src/test/java/reactor/core/TestLoggerExtension.java new file mode 100644 index 0000000000..8a006db15c --- /dev/null +++ b/reactor-core/src/test/java/reactor/core/TestLoggerExtension.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2022 VMware Inc. or its affiliates, All Rights Reserved. + * + * 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 + * + * https://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 reactor.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; + +import reactor.test.util.LoggerUtils; +import reactor.test.util.TestLogger; +import reactor.util.Logger; +import reactor.util.annotation.Nullable; + +/** + * A JUnit5 extension that installs a {@link TestLogger} as the capturing instance via {@link LoggerUtils#enableCaptureWith(Logger)}, + * {@link LoggerUtils#disableCapture() disable capture} at the end of the test and injects the {@link TestLogger} + * into the test case (by implementing {@link ParameterResolver}). + * + * @author Simon Baslé + */ +public class TestLoggerExtension implements ParameterResolver, AfterEachCallback, BeforeEachCallback { + + /** + * Set up a default {@link TestLoggerExtension}, unless @{@link Redirect} annotation is also present. + * By default loggers will route the log messages to both the original logger and the injected + * {@link TestLogger}, and in the latter there won't be automatic inclusion of thread names. + * + * @see Redirect + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.METHOD }) + @ExtendWith(TestLoggerExtension.class) + public @interface Capture { } + + /** + * Set up a {@link TestLoggerExtension} that routes log messages only to the injected {@link TestLogger}, + * suppressing the logs from the original logger. Messages don't include thread names. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.METHOD }) + @ExtendWith(TestLoggerExtension.class) + public @interface Redirect { } + + TestLogger logger; + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + if (context.getElement().isPresent()) { + boolean suppressOriginal = context.getElement().get().isAnnotationPresent(Redirect.class); + this.logger = new TestLogger(false); + LoggerUtils.enableCaptureWith(logger, !suppressOriginal); + } + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + LoggerUtils.disableCapture(); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.getParameter().getType() == TestLogger.class; + } + + @Override + @Nullable + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + if (parameterContext.getParameter().getType() == TestLogger.class) { + return this.logger; + } + return null; + } +} diff --git a/reactor-core/src/test/java/reactor/core/publisher/BaseSubscriberTest.java b/reactor-core/src/test/java/reactor/core/publisher/BaseSubscriberTest.java index 8c6c55df32..08e1fac34b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/BaseSubscriberTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/BaseSubscriberTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.reactivestreams.Subscription; import reactor.core.Disposable; +import reactor.core.TestLoggerExtension; import reactor.test.util.LoggerUtils; import reactor.test.util.TestLogger; @@ -82,30 +83,23 @@ protected void hookFinally(SignalType type) { } @Test - public void onErrorCallbackNotImplemented() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - Flux flux = Flux.error(new IllegalStateException()); + @TestLoggerExtension.Redirect + void onErrorCallbackNotImplemented(TestLogger testLogger) { + Flux flux = Flux.error(new IllegalStateException()); - flux.subscribe(new BaseSubscriber() { - @Override - protected void hookOnSubscribe(Subscription subscription) { - request(1); - } + flux.subscribe(new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + request(1); + } - @Override - protected void hookOnNext(String value) { - //NO-OP - } - }); - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException"); - } - finally { - LoggerUtils.disableCapture(); - } + @Override + protected void hookOnNext(String value) { + //NO-OP + } + }); + Assertions.assertThat(testLogger.getErrContent()) + .startsWith("[ERROR] Operator called default onErrorDropped - reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException"); } @Test @@ -277,41 +271,33 @@ protected void hookFinally(SignalType type) { } @Test - public void finallyExecutesWhenHookOnErrorFails() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - RuntimeException error = new IllegalArgumentException("hookOnError"); - AtomicReference checkFinally = new AtomicReference<>(); - - Flux.error(new IllegalStateException("someError")).subscribe(new BaseSubscriber() { - @Override - protected void hookOnSubscribe(Subscription subscription) { - requestUnbounded(); - } + @TestLoggerExtension.Redirect + void finallyExecutesWhenHookOnErrorFails(TestLogger testLogger) { + RuntimeException error = new IllegalArgumentException("hookOnError"); + AtomicReference checkFinally = new AtomicReference<>(); - @Override - protected void hookOnNext(String value) { - } + Flux.error(new IllegalStateException("someError")).subscribe(new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + requestUnbounded(); + } - @Override - protected void hookOnError(Throwable throwable) { - throw error; - } + @Override + protected void hookOnNext(String value) { + } - @Override - protected void hookFinally(SignalType type) { - checkFinally.set(type); - } - }); - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains(error.getMessage()); - assertThat(checkFinally).hasValue(SignalType.ON_ERROR); - } - finally { - LoggerUtils.disableCapture(); - } + @Override + protected void hookOnError(Throwable throwable) { + throw error; + } + + @Override + protected void hookFinally(SignalType type) { + checkFinally.set(type); + } + }); + Assertions.assertThat(testLogger.getErrContent()) + .startsWith("[ERROR] Operator called default onErrorDropped - java.lang.IllegalArgumentException: hookOnError"); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxDoFinallyTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxDoFinallyTest.java index bd63d6e5b2..17557f055d 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxDoFinallyTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxDoFinallyTest.java @@ -30,6 +30,7 @@ import reactor.core.CoreSubscriber; import reactor.core.Exceptions; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.test.StepVerifier; import reactor.test.util.LoggerUtils; import reactor.test.util.TestLogger; @@ -353,28 +354,22 @@ public void gh951_withConsumerInSubscribe() { } @Test + @TestLoggerExtension.Redirect //see https://github.com/reactor/reactor-core/issues/951 - public void gh951_withoutDoOnError() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - List events = new ArrayList<>(); - - Mono.just(true) - .map(this::throwError) - .doFinally(any -> events.add("doFinally " + any.toString())) - .subscribe(); - - Assertions.assertThat(events) - .as("withoutDoOnError") - .containsExactly("doFinally onError"); - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException: boom"); - } - finally { - LoggerUtils.disableCapture(); - } + void gh951_withoutDoOnError(TestLogger testLogger) { + List events = new ArrayList<>(); + + Mono.just(true) + .map(this::throwError) + .doFinally(any -> events.add("doFinally " + any.toString())) + .subscribe(); + + Assertions.assertThat(events) + .as("withoutDoOnError") + .containsExactly("doFinally onError"); + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalStateException: boom"); } private Boolean throwError(Boolean x) { diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxFlatMapTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxFlatMapTest.java index 6cb710cb7e..7176da5845 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxFlatMapTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxFlatMapTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.core.publisher.FluxPeekFuseableTest.AssertQueueSubscription; import reactor.core.scheduler.Schedulers; import reactor.test.util.LoggerUtils; @@ -780,70 +781,55 @@ public void ignoreDoubleOnSubscribeInner() { } @Test - public void failDoubleError() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - StepVerifier.create(Flux.from(s -> { - s.onSubscribe(Operators.emptySubscription()); - s.onError(new Exception("test")); - s.onError(new Exception("test2")); - }) - .flatMap(Flux::just)) - .verifyErrorMessage("test"); - - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("java.lang.Exception: test2"); - } finally { - LoggerUtils.disableCapture(); - } + @TestLoggerExtension.Redirect + void failDoubleError(TestLogger testLogger) { + StepVerifier.create(Flux.from(s -> { + s.onSubscribe(Operators.emptySubscription()); + s.onError(new Exception("test")); + s.onError(new Exception("test2")); + }) + .flatMap(Flux::just)) + .verifyErrorMessage("test"); + + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("java.lang.Exception: test2"); } @Test //FIXME use Violation.NO_CLEANUP_ON_TERMINATE - public void failDoubleErrorTerminated() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - StepVerifier.create(Flux.from(s -> { - s.onSubscribe(Operators.emptySubscription()); - Exceptions.terminate(FluxFlatMap.FlatMapMain.ERROR, (FluxFlatMap.FlatMapMain) s); - ((FluxFlatMap.FlatMapMain) s).done = true; - ((FluxFlatMap.FlatMapMain) s).drain(null); - s.onError(new Exception("test")); - }) - .flatMap(Flux::just)) - .verifyComplete(); - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("java.lang.Exception: test"); - } finally { - LoggerUtils.disableCapture(); - } + @TestLoggerExtension.Redirect + void failDoubleErrorTerminated(TestLogger testLogger) { + StepVerifier.create(Flux.from(s -> { + s.onSubscribe(Operators.emptySubscription()); + Exceptions.terminate(FluxFlatMap.FlatMapMain.ERROR, (FluxFlatMap.FlatMapMain) s); + ((FluxFlatMap.FlatMapMain) s).done = true; + ((FluxFlatMap.FlatMapMain) s).drain(null); + s.onError(new Exception("test")); + }) + .flatMap(Flux::just)) + .verifyComplete(); + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("java.lang.Exception: test"); } @Test //FIXME use Violation.NO_CLEANUP_ON_TERMINATE - public void failDoubleErrorTerminatedInner() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - StepVerifier.create(Flux.just(1) - .hide() - .flatMap(f -> Flux.from(s -> { - s.onSubscribe(Operators.emptySubscription()); - Exceptions.terminate(FluxFlatMap.FlatMapMain.ERROR, - ((FluxFlatMap.FlatMapInner) s).parent); - s.onError(new Exception("test")); - }))) - .verifyComplete(); - - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("java.lang.Exception: test"); - } finally { - LoggerUtils.disableCapture(); - } + @TestLoggerExtension.Redirect + void failDoubleErrorTerminatedInner(TestLogger testLogger) { + StepVerifier.create(Flux.just(1) + .hide() + .flatMap(f -> Flux.from(s -> { + s.onSubscribe(Operators.emptySubscription()); + Exceptions.terminate(FluxFlatMap.FlatMapMain.ERROR, + ((FluxFlatMap.FlatMapInner) s).parent); + s.onError(new Exception("test")); + }))) + .verifyComplete(); + + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("java.lang.Exception: test"); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxPeekFuseableTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxPeekFuseableTest.java index 6ef1eaf225..65f625e229 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxPeekFuseableTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxPeekFuseableTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.core.publisher.FluxPeekFuseable.PeekConditionalSubscriber; import reactor.core.publisher.FluxPeekFuseable.PeekFuseableConditionalSubscriber; import reactor.core.publisher.FluxPeekFuseable.PeekFuseableSubscriber; @@ -322,41 +323,34 @@ public void errorCallbackErrorWithParallel() { } @Test - public void afterTerminateCallbackErrorDoesNotInvokeOnError() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - - IllegalStateException error = new IllegalStateException("test"); - AtomicReference errorCallbackCapture = new AtomicReference<>(); + @TestLoggerExtension.Redirect + void afterTerminateCallbackErrorDoesNotInvokeOnError(TestLogger testLogger) { + IllegalStateException error = new IllegalStateException("test"); + AtomicReference errorCallbackCapture = new AtomicReference<>(); - FluxPeekFuseable flux = new FluxPeekFuseable<>(Flux.empty(), - null, - null, - errorCallbackCapture::set, - null, - () -> { - throw error; - }, - null, - null); + FluxPeekFuseable flux = new FluxPeekFuseable<>(Flux.empty(), + null, + null, + errorCallbackCapture::set, + null, + () -> { + throw error; + }, + null, + null); - AssertSubscriber ts = AssertSubscriber.create(); + AssertSubscriber ts = AssertSubscriber.create(); - flux.subscribe(ts); - ts.assertNoValues(); - ts.assertComplete(); + flux.subscribe(ts); + ts.assertNoValues(); + ts.assertComplete(); - //the onError wasn't invoked: - assertThat(errorCallbackCapture.get()).isNull(); + //the onError wasn't invoked: + assertThat(errorCallbackCapture.get()).isNull(); - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains(error.getMessage()); - } - finally { - LoggerUtils.disableCapture(); - } + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains(error.getMessage()); } @Test @@ -404,73 +398,59 @@ public void afterTerminateCallbackFatalIsThrownDirectly() { } @Test - public void afterTerminateCallbackErrorAndErrorCallbackError() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - - IllegalStateException error = new IllegalStateException("expected afterTerminate"); - IllegalArgumentException error2 = new IllegalArgumentException("error"); + @TestLoggerExtension.Redirect + void afterTerminateCallbackErrorAndErrorCallbackError(TestLogger testLogger) { + IllegalStateException error = new IllegalStateException("expected afterTerminate"); + IllegalArgumentException error2 = new IllegalArgumentException("error"); + + FluxPeekFuseable flux = + new FluxPeekFuseable<>(Flux.empty(), null, null, e -> { + throw error2; + }, null, () -> { + throw error; + }, null, null); - FluxPeekFuseable flux = - new FluxPeekFuseable<>(Flux.empty(), null, null, e -> { - throw error2; - }, null, () -> { - throw error; - }, null, null); - - AssertSubscriber ts = AssertSubscriber.create(); + AssertSubscriber ts = AssertSubscriber.create(); - flux.subscribe(ts); - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains(error.getMessage()); - assertThat(error2.getSuppressed()).isEmpty(); - //error2 is never thrown - ts.assertNoValues(); - ts.assertComplete(); - } - finally { - LoggerUtils.disableCapture(); - } + flux.subscribe(ts); + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains(error.getMessage()); + assertThat(error2.getSuppressed()).isEmpty(); + //error2 is never thrown + ts.assertNoValues(); + ts.assertComplete(); } @Test - public void afterTerminateCallbackErrorAndErrorCallbackError2() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - - IllegalStateException afterTerminate = new IllegalStateException("afterTerminate"); - IllegalArgumentException error = new IllegalArgumentException("error"); - NullPointerException error2 = new NullPointerException(); + @TestLoggerExtension.Redirect + void afterTerminateCallbackErrorAndErrorCallbackError2(TestLogger testLogger) { + IllegalStateException afterTerminate = new IllegalStateException("afterTerminate"); + IllegalArgumentException error = new IllegalArgumentException("error"); + NullPointerException error2 = new NullPointerException(); + + FluxPeekFuseable flux = + new FluxPeekFuseable<>(Flux.error(error2), null, null, e -> { + throw error; + }, null, () -> { + throw afterTerminate; + }, null, null); - FluxPeekFuseable flux = - new FluxPeekFuseable<>(Flux.error(error2), null, null, e -> { - throw error; - }, null, () -> { - throw afterTerminate; - }, null, null); - - AssertSubscriber ts = AssertSubscriber.create(); + AssertSubscriber ts = AssertSubscriber.create(); - flux.subscribe(ts); - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains(afterTerminate.getMessage()); - //afterTerminate suppressed error which itself suppressed original error2 - assertThat(afterTerminate.getSuppressed().length).isEqualTo(1); - assertThat(afterTerminate.getSuppressed()[0]).isEqualTo(error); - - assertThat(error.getSuppressed().length).isEqualTo(1); - assertThat(error.getSuppressed()[0]).isEqualTo(error2); - ts.assertNoValues(); - //the subscriber still sees the 'error' message since actual.onError is called before the afterTerminate callback - ts.assertErrorMessage("error"); - } - finally { - LoggerUtils.disableCapture(); - } + flux.subscribe(ts); + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains(afterTerminate.getMessage()); + //afterTerminate suppressed error which itself suppressed original error2 + assertThat(afterTerminate.getSuppressed().length).isEqualTo(1); + assertThat(afterTerminate.getSuppressed()[0]).isEqualTo(error); + + assertThat(error.getSuppressed().length).isEqualTo(1); + assertThat(error.getSuppressed()[0]).isEqualTo(error2); + ts.assertNoValues(); + //the subscriber still sees the 'error' message since actual.onError is called before the afterTerminate callback + ts.assertErrorMessage("error"); } diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxPeekTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxPeekTest.java index feed2dd46d..2832e2d36b 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxPeekTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxPeekTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.test.util.LoggerUtils; import reactor.test.StepVerifier; import reactor.test.publisher.FluxOperatorTest; @@ -494,39 +495,34 @@ public void errorCallbackErrorWithParallel() { } @Test - public void afterTerminateCallbackErrorDoesNotInvokeOnError() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - IllegalStateException e = new IllegalStateException("test"); - AtomicReference errorCallbackCapture = new AtomicReference<>(); - - FluxPeek flux = new FluxPeek<>(Flux.empty(), - null, - null, - errorCallbackCapture::set, - null, - () -> { - throw e; - }, - null, - null); - - AssertSubscriber ts = AssertSubscriber.create(); + @TestLoggerExtension.Redirect + void afterTerminateCallbackErrorDoesNotInvokeOnError(TestLogger testLogger) { + IllegalStateException e = new IllegalStateException("test"); + AtomicReference errorCallbackCapture = new AtomicReference<>(); - flux.subscribe(ts); + FluxPeek flux = new FluxPeek<>(Flux.empty(), + null, + null, + errorCallbackCapture::set, + null, + () -> { + throw e; + }, + null, + null); - ts.assertNoValues(); - ts.assertComplete(); + AssertSubscriber ts = AssertSubscriber.create(); - assertThat(errorCallbackCapture.get()).isNull(); + flux.subscribe(ts); - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains(e.toString()); - } finally { - LoggerUtils.disableCapture(); - } + ts.assertNoValues(); + ts.assertComplete(); + + assertThat(errorCallbackCapture.get()).isNull(); + + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains(e.toString()); } @Test @@ -573,68 +569,57 @@ public void afterTerminateCallbackFatalIsThrownDirectly() { } @Test - public void afterTerminateCallbackErrorAndErrorCallbackError() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - IllegalStateException error1 = new IllegalStateException("afterTerminate"); - IllegalArgumentException error2 = new IllegalArgumentException("error"); - - FluxPeek flux = new FluxPeek<>(Flux.empty(), null, null, e -> { - throw error2; - }, null, () -> { - throw error1; - }, null, null); + @TestLoggerExtension.Redirect + void afterTerminateCallbackErrorAndErrorCallbackError(TestLogger testLogger) { + IllegalStateException error1 = new IllegalStateException("afterTerminate"); + IllegalArgumentException error2 = new IllegalArgumentException("error"); + + FluxPeek flux = new FluxPeek<>(Flux.empty(), null, null, e -> { + throw error2; + }, null, () -> { + throw error1; + }, null, null); - AssertSubscriber ts = AssertSubscriber.create(); + AssertSubscriber ts = AssertSubscriber.create(); - flux.subscribe(ts); - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains(error1.getMessage()); - assertThat(error2.getSuppressed()).hasSize(0); - //error2 is never thrown - ts.assertNoValues(); - ts.assertComplete(); - } - finally { - LoggerUtils.disableCapture(); - } + flux.subscribe(ts); + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains(error1.getMessage()); + assertThat(error2.getSuppressed()).hasSize(0); + //error2 is never thrown + ts.assertNoValues(); + ts.assertComplete(); } @Test - public void afterTerminateCallbackErrorAndErrorCallbackError2() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - IllegalStateException afterTerminate = new IllegalStateException("afterTerminate"); - IllegalArgumentException error = new IllegalArgumentException("error"); - NullPointerException ex = new NullPointerException(); - - FluxPeek flux = new FluxPeek<>(Flux.error(ex), null, null, e -> { - throw error; - }, null, () -> { - throw afterTerminate; - }, null, null); + @TestLoggerExtension.Redirect + void afterTerminateCallbackErrorAndErrorCallbackError2(TestLogger testLogger) { + IllegalStateException afterTerminate = new IllegalStateException("afterTerminate"); + IllegalArgumentException error = new IllegalArgumentException("error"); + NullPointerException ex = new NullPointerException(); + + FluxPeek flux = new FluxPeek<>(Flux.error(ex), null, null, e -> { + throw error; + }, null, () -> { + throw afterTerminate; + }, null, null); - AssertSubscriber ts = AssertSubscriber.create(); + AssertSubscriber ts = AssertSubscriber.create(); - flux.subscribe(ts); - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains(afterTerminate.getMessage()); - //afterTerminate suppressed error which itself suppressed original err - assertThat(afterTerminate.getSuppressed()).hasSize(1); - assertThat(afterTerminate).hasSuppressedException(error); - - assertThat(error.getSuppressed()).hasSize(1); - assertThat(error).hasSuppressedException(ex); - ts.assertNoValues(); - //the subscriber still sees the 'error' message since actual.onError is called before the afterTerminate callback - ts.assertErrorMessage("error"); - } finally { - LoggerUtils.disableCapture(); - } + flux.subscribe(ts); + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains(afterTerminate.getMessage()); + //afterTerminate suppressed error which itself suppressed original err + assertThat(afterTerminate.getSuppressed()).hasSize(1); + assertThat(afterTerminate).hasSuppressedException(error); + + assertThat(error.getSuppressed()).hasSize(1); + assertThat(error).hasSuppressedException(ex); + ts.assertNoValues(); + //the subscriber still sees the 'error' message since actual.onError is called before the afterTerminate callback + ts.assertErrorMessage("error"); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxUsingWhenTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxUsingWhenTest.java index 26368ddb17..72e03fd69c 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxUsingWhenTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxUsingWhenTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ import reactor.core.Disposable; import reactor.core.Fuseable; import reactor.core.Scannable.Attr; +import reactor.core.TestLoggerExtension; import reactor.core.publisher.FluxUsingWhen.ResourceSubscriber; import reactor.core.publisher.FluxUsingWhen.UsingWhenSubscriber; import reactor.test.ParameterizedTestWithName; @@ -398,64 +399,53 @@ public void cancelWithHandler(Flux source) { @ParameterizedTestWithName @MethodSource("sources01") - public void cancelWithHandlerFailure(Flux source) { + @TestLoggerExtension.Redirect + void cancelWithHandlerFailure(Flux source, TestLogger testLogger) { TestResource testResource = new TestResource(); - final TestLogger tl = new TestLogger(); - Loggers.useCustomLoggers(name -> tl); - - try { - Flux test = Flux.usingWhen(Mono.just(testResource), - tr -> source, - TestResource::commit, - TestResource::rollback, - r -> r.cancel() - //immediate error to trigger the logging within the test - .concatWith(Mono.error(new IllegalStateException("cancel error"))) + Flux test = Flux.usingWhen(Mono.just(testResource), + tr -> source, + TestResource::commit, + TestResource::rollback, + r -> r.cancel() + //immediate error to trigger the logging within the test + .concatWith(Mono.error(new IllegalStateException("cancel error"))) ) - .take(2); + .take(2); - StepVerifier.create(test) - .expectNext("0", "1") - .verifyComplete(); + StepVerifier.create(test) + .expectNext("0", "1") + .verifyComplete(); - testResource.commitProbe.assertWasNotSubscribed(); - testResource.rollbackProbe.assertWasNotSubscribed(); - testResource.cancelProbe.assertWasSubscribed(); - } - finally { - Loggers.resetLoggerFactory(); - } - assertThat(tl.getErrContent()) + testResource.commitProbe.assertWasNotSubscribed(); + testResource.rollbackProbe.assertWasNotSubscribed(); + testResource.cancelProbe.assertWasSubscribed(); + + assertThat(testLogger.getErrContent()) .contains("Async resource cleanup failed after cancel") .contains("java.lang.IllegalStateException: cancel error"); } @ParameterizedTestWithName @MethodSource("sources01") - public void cancelWithHandlerGenerationFailureLogs(Flux source) throws InterruptedException { - TestLogger tl = new TestLogger(); - Loggers.useCustomLoggers(name -> tl); + @TestLoggerExtension.Redirect + void cancelWithHandlerGenerationFailureLogs(Flux source, TestLogger tl) throws InterruptedException { TestResource testResource = new TestResource(); - try { - Flux test = Flux.usingWhen(Mono.just(testResource), - tr -> source, - TestResource::commit, - TestResource::rollback, - r -> null) - .take(2); + Flux test = Flux.usingWhen(Mono.just(testResource), + tr -> source, + TestResource::commit, + TestResource::rollback, + r -> null) + .take(2); - StepVerifier.create(test) - .expectNext("0", "1") - .verifyComplete(); + StepVerifier.create(test) + .expectNext("0", "1") + .verifyComplete(); + + testResource.commitProbe.assertWasNotSubscribed(); + testResource.cancelProbe.assertWasNotSubscribed(); + testResource.rollbackProbe.assertWasNotSubscribed(); - testResource.commitProbe.assertWasNotSubscribed(); - testResource.cancelProbe.assertWasNotSubscribed(); - testResource.rollbackProbe.assertWasNotSubscribed(); - } - finally { - Loggers.resetLoggerFactory(); - } assertThat(tl.getErrContent()) .contains("Error generating async resource cleanup during onCancel") .contains("java.lang.NullPointerException"); @@ -488,39 +478,33 @@ public void cancelWithoutHandlerAppliesCommit(Flux source) { @ParameterizedTestWithName @MethodSource("sources01") + @TestLoggerExtension.Redirect @Deprecated - public void cancelDefaultHandlerFailure(Flux source) { + void cancelDefaultHandlerFailure(Flux source, TestLogger tl) { TestResource testResource = new TestResource(); - final TestLogger tl = new TestLogger(); - Loggers.useCustomLoggers(name -> tl); - - try { - Function> completeOrCancel = r -> { - return r.commit() - //immediate error to trigger the logging within the test - .concatWith(Mono.error(new IllegalStateException("commit error"))); - }; - Flux test = Flux - .usingWhen( - Mono.just(testResource), - tr -> source, - completeOrCancel, - (r, e) -> r.rollback(new RuntimeException("placeholder ignored rollback exception")), - completeOrCancel - ) - .take(2); - - StepVerifier.create(test) - .expectNext("0", "1") - .verifyComplete(); + Function> completeOrCancel = r -> { + return r.commit() + //immediate error to trigger the logging within the test + .concatWith(Mono.error(new IllegalStateException("commit error"))); + }; + Flux test = Flux + .usingWhen( + Mono.just(testResource), + tr -> source, + completeOrCancel, + (r, e) -> r.rollback(new RuntimeException("placeholder ignored rollback exception")), + completeOrCancel + ) + .take(2); + + StepVerifier.create(test) + .expectNext("0", "1") + .verifyComplete(); + + testResource.commitProbe.assertWasSubscribed(); + testResource.cancelProbe.assertWasNotSubscribed(); + testResource.rollbackProbe.assertWasNotSubscribed(); - testResource.commitProbe.assertWasSubscribed(); - testResource.cancelProbe.assertWasNotSubscribed(); - testResource.rollbackProbe.assertWasNotSubscribed(); - } - finally { - Loggers.resetLoggerFactory(); - } assertThat(tl.getErrContent()) .contains("Async resource cleanup failed after cancel") .contains("java.lang.IllegalStateException: commit error"); @@ -723,76 +707,63 @@ public void apiCancel(Flux transactionToCancel) { @ParameterizedTestWithName @MethodSource("sourcesFullTransaction") - public void apiCancelFailure(Flux transaction) { - TestLogger testLogger = new TestLogger(); - Loggers.useCustomLoggers(s -> testLogger); - try { - final AtomicReference ref = new AtomicReference<>(); - Flux flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), - d -> { - ref.set(d); - return transaction; - }, - TestResource::commit, - TestResource::rollback, - TestResource::cancelError); + @TestLoggerExtension.Redirect + void apiCancelFailure(Flux transaction, TestLogger testLogger) { + final AtomicReference ref = new AtomicReference<>(); + Flux flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), + d -> { + ref.set(d); + return transaction; + }, + TestResource::commit, + TestResource::rollback, + TestResource::cancelError); StepVerifier.create(flux.take(1), 1) .expectNext("Transaction started") .verifyComplete(); - assertThat(ref.get()) - .isNotNull() - .matches(tr -> !tr.commitProbe.wasSubscribed(), "no commit") - .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback") - .matches(tr -> tr.cancelProbe.wasSubscribed(), "cancel method used"); - - //since the CancelInner is subscribed in a fire-and-forget fashion, the log comes later - //the test must be done before the finally, lest the error message be printed too late for TestLogger to catch it - Awaitility.await().atMost(1, TimeUnit.SECONDS) - .untilAsserted(() -> - assertThat(testLogger.getErrContent()) - .startsWith("[ WARN]") - .contains("Async resource cleanup failed after cancel - java.lang.ArithmeticException: / by zero")); - } - finally { - Loggers.resetLoggerFactory(); - } + assertThat(ref.get()) + .isNotNull() + .matches(tr -> !tr.commitProbe.wasSubscribed(), "no commit") + .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback") + .matches(tr -> tr.cancelProbe.wasSubscribed(), "cancel method used"); + + //since the CancelInner is subscribed in a fire-and-forget fashion, the log comes later + //the test must be done before the finally, lest the error message be printed too late for TestLogger to catch it + Awaitility.await().atMost(1, TimeUnit.SECONDS) + .untilAsserted(() -> + assertThat(testLogger.getErrContent()) + .startsWith("[ WARN]") + .contains("Async resource cleanup failed after cancel - java.lang.ArithmeticException: / by zero")); } @ParameterizedTestWithName @MethodSource("sourcesFullTransaction") - public void apiCancelGeneratingNullLogs(Flux transactionToCancel) { - TestLogger testLogger = new TestLogger(); - Loggers.useCustomLoggers(s -> testLogger); - try { - final AtomicReference ref = new AtomicReference<>(); - Flux flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), - d -> { - ref.set(d); - return transactionToCancel; - }, - TestResource::commit, - TestResource::rollback, - TestResource::cancelNull); + @TestLoggerExtension.Redirect + void apiCancelGeneratingNullLogs(Flux transactionToCancel, TestLogger testLogger) { + final AtomicReference ref = new AtomicReference<>(); + Flux flux = Flux.usingWhen(Mono.fromCallable(TestResource::new), + d -> { + ref.set(d); + return transactionToCancel; + }, + TestResource::commit, + TestResource::rollback, + TestResource::cancelNull); StepVerifier.create(flux.take(1), 1) .expectNext("Transaction started") .verifyComplete(); - assertThat(ref.get()) - .isNotNull() - .matches(tr -> !tr.commitProbe.wasSubscribed(), "no commit") - .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback") - .matches(tr -> !tr.cancelProbe.wasSubscribed(), "cancel method short-circuited"); + assertThat(ref.get()) + .isNotNull() + .matches(tr -> !tr.commitProbe.wasSubscribed(), "no commit") + .matches(tr -> !tr.rollbackProbe.wasSubscribed(), "no rollback") + .matches(tr -> !tr.cancelProbe.wasSubscribed(), "cancel method short-circuited"); - } - finally { - Loggers.resetLoggerFactory(); - } assertThat(testLogger.getErrContent()) - .contains("[ WARN] (" + Thread.currentThread().getName() + ") " + - "Error generating async resource cleanup during onCancel - java.lang.NullPointerException"); + .contains("[ WARN] Error generating async resource cleanup during onCancel - java.lang.NullPointerException"); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/FluxZipTest.java b/reactor-core/src/test/java/reactor/core/publisher/FluxZipTest.java index 39118aa516..1f16f27ba3 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/FluxZipTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/FluxZipTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import reactor.core.CoreSubscriber; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.test.ParameterizedTestWithName; import reactor.test.StepVerifier; import reactor.test.publisher.FluxOperatorTest; @@ -715,47 +716,37 @@ public void ignoreDoubleComplete() { } @Test //FIXME use Violation.NO_CLEANUP_ON_TERMINATE - public void failDoubleError() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - StepVerifier.create(Flux.zip(obj -> 0, Flux.just(1), Flux.never(), s -> { + @TestLoggerExtension.Redirect + void failDoubleError(TestLogger testLogger) { + StepVerifier.create(Flux.zip(obj -> 0, Flux.just(1), Flux.never(), s -> { s.onSubscribe(Operators.emptySubscription()); s.onError(new Exception("test")); s.onError(new Exception("test2")); })) - .verifyErrorMessage("test"); + .verifyErrorMessage("test"); - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("test2"); - } finally { - LoggerUtils.disableCapture(); - } + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("test2"); } @Test //FIXME use Violation.NO_CLEANUP_ON_TERMINATE - public void failDoubleError3() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - StepVerifier.create(Flux.zip(obj -> 0, - Flux.just(1) - .hide(), - Flux.never(), - s -> { - s.onSubscribe(Operators.emptySubscription()); - s.onError(new Exception("test")); - s.onError(new Exception("test2")); - })) - .verifyErrorMessage("test"); - - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("test2"); - } finally { - LoggerUtils.disableCapture(); - } + @TestLoggerExtension.Redirect + void failDoubleError3(TestLogger testLogger) { + StepVerifier.create(Flux.zip(obj -> 0, + Flux.just(1) + .hide(), + Flux.never(), + s -> { + s.onSubscribe(Operators.emptySubscription()); + s.onError(new Exception("test")); + s.onError(new Exception("test2")); + })) + .verifyErrorMessage("test"); + + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("test2"); } @Test //FIXME use Violation.NO_CLEANUP_ON_TERMINATE @@ -771,27 +762,22 @@ public void failDoubleErrorSilent() { } @Test //FIXME use Violation.NO_CLEANUP_ON_TERMINATE - public void failDoubleErrorHide() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - StepVerifier.create(Flux.zip(obj -> 0, - Flux.just(1) - .hide(), - Flux.never(), - s -> { - s.onSubscribe(Operators.emptySubscription()); - s.onError(new Exception("test")); - s.onError(new Exception("test2")); - })) - .verifyErrorMessage("test"); - - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("test2"); - } finally { - LoggerUtils.disableCapture(); - } + @TestLoggerExtension.Redirect + void failDoubleErrorHide(TestLogger testLogger) { + StepVerifier.create(Flux.zip(obj -> 0, + Flux.just(1) + .hide(), + Flux.never(), + s -> { + s.onSubscribe(Operators.emptySubscription()); + s.onError(new Exception("test")); + s.onError(new Exception("test2")); + })) + .verifyErrorMessage("test"); + + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("test2"); } @Test @@ -810,27 +796,22 @@ public void failDoubleTerminalPublisher() { } @Test //FIXME use Violation.NO_CLEANUP_ON_TERMINATE - public void failDoubleError2() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - StepVerifier.create(Flux.zip(obj -> 0, - Flux.just(1) - .hide(), - Flux.never(), - s -> { - s.onSubscribe(Operators.emptySubscription()); - s.onError(new Exception("test")); - s.onError(new Exception("test2")); - })) - .verifyErrorMessage("test"); - - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("test2"); - } finally { - LoggerUtils.disableCapture(); - } + @TestLoggerExtension.Redirect + void failDoubleError2(TestLogger testLogger) { + StepVerifier.create(Flux.zip(obj -> 0, + Flux.just(1) + .hide(), + Flux.never(), + s -> { + s.onSubscribe(Operators.emptySubscription()); + s.onError(new Exception("test")); + s.onError(new Exception("test2")); + })) + .verifyErrorMessage("test"); + + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("test2"); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/LambdaMonoSubscriberTest.java b/reactor-core/src/test/java/reactor/core/publisher/LambdaMonoSubscriberTest.java index 5f1e26ecbe..13dc1a34f1 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/LambdaMonoSubscriberTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/LambdaMonoSubscriberTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.reactivestreams.Subscription; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.test.util.LoggerUtils; import reactor.test.util.TestLogger; import reactor.util.context.Context; @@ -176,56 +177,44 @@ public void onNextConsumerExceptionTriggersCancellation() { } @Test - public void onNextConsumerExceptionNonFatalTriggersCancellation() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - LambdaMonoSubscriber tested = new LambdaMonoSubscriber<>( - value -> { throw new IllegalArgumentException(); }, - null, //no errorConsumer so that we use onErrorDropped - () -> { }, null); + @TestLoggerExtension.Redirect + void onNextConsumerExceptionNonFatalTriggersCancellation(TestLogger testLogger) { + LambdaMonoSubscriber tested = new LambdaMonoSubscriber<>( + value -> { throw new IllegalArgumentException(); }, + null, //no errorConsumer so that we use onErrorDropped + () -> { }, null); - TestSubscription testSubscription = new TestSubscription(); - tested.onSubscribe(testSubscription); + TestSubscription testSubscription = new TestSubscription(); + tested.onSubscribe(testSubscription); - //as Mono is single-value, it cancels early on onNext. this leads to an exception - //during onNext to be bubbled up as a BubbledException, not propagated through onNext - tested.onNext("foo"); - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("IllegalArgumentException"); + //as Mono is single-value, it cancels early on onNext. this leads to an exception + //during onNext to be bubbled up as a BubbledException, not propagated through onNext + tested.onNext("foo"); + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("IllegalArgumentException"); - assertThat(testSubscription.isCancelled).as("subscription isCancelled") - .isTrue(); - } - finally { - LoggerUtils.disableCapture(); - } + assertThat(testSubscription.isCancelled).as("subscription isCancelled") + .isTrue(); } @Test - public void onNextConsumerFatalDoesntTriggerCancellation() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - LambdaMonoSubscriber tested = new LambdaMonoSubscriber<>( - value -> { throw new OutOfMemoryError(); }, - null, //no errorConsumer so that we use onErrorDropped - () -> { }, null); + @TestLoggerExtension.Redirect + void onNextConsumerFatalDoesntTriggerCancellation(TestLogger testLogger) { + LambdaMonoSubscriber tested = new LambdaMonoSubscriber<>( + value -> { throw new OutOfMemoryError(); }, + null, //no errorConsumer so that we use onErrorDropped + () -> { }, null); - TestSubscription testSubscription = new TestSubscription(); - tested.onSubscribe(testSubscription); + TestSubscription testSubscription = new TestSubscription(); + tested.onSubscribe(testSubscription); - //the error is expected to be thrown as it is fatal, so it doesn't go through onErrorDropped - assertThatExceptionOfType(OutOfMemoryError.class).isThrownBy(() -> tested.onNext("foo")); - Assertions.assertThat(testLogger.getErrContent()).isEmpty(); + //the error is expected to be thrown as it is fatal, so it doesn't go through onErrorDropped + assertThatExceptionOfType(OutOfMemoryError.class).isThrownBy(() -> tested.onNext("foo")); + Assertions.assertThat(testLogger.getErrContent()).isEmpty(); - assertThat(testSubscription.isCancelled).as("subscription isCancelled") - .isFalse(); - } - finally { - LoggerUtils.disableCapture(); - } + assertThat(testSubscription.isCancelled).as("subscription isCancelled") + .isFalse(); } @Test @@ -276,22 +265,16 @@ public void completeHookErrorDropped() { } @Test - public void noErrorHookThrowsCallbackNotImplemented() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - RuntimeException boom = new IllegalArgumentException("boom"); - Mono.error(boom) - .subscribe(v -> { - }); - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains( - "reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: boom"); - } - finally { - LoggerUtils.disableCapture(); - } + @TestLoggerExtension.Redirect + void noErrorHookThrowsCallbackNotImplemented(TestLogger testLogger) { + RuntimeException boom = new IllegalArgumentException("boom"); + Mono.error(boom) + .subscribe(v -> { + }); + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains( + "reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.IllegalArgumentException: boom"); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/MonoPeekTest.java b/reactor-core/src/test/java/reactor/core/publisher/MonoPeekTest.java index e91dc32830..66206130ba 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/MonoPeekTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/MonoPeekTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import org.reactivestreams.Subscription; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.test.util.LoggerUtils; import reactor.test.StepVerifier; import reactor.test.util.TestLogger; @@ -165,22 +166,16 @@ public void onMonoSuccessNullDoOnSuccess() { } @Test - public void testErrorWithDoOnSuccess() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - Mono.error(new NullPointerException("boom")) - .doOnSuccess(aValue -> { - }) - .subscribe(); - - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.NullPointerException: boom"); - } - finally { - LoggerUtils.disableCapture(); - } + @TestLoggerExtension.Redirect + void testErrorWithDoOnSuccess(TestLogger testLogger) { + Mono.error(new NullPointerException("boom")) + .doOnSuccess(aValue -> { + }) + .subscribe(); + + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.NullPointerException: boom"); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java b/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java index 604881f413..9c9ea7f369 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/NextProcessorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2015-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.test.util.LoggerUtils; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; @@ -476,40 +477,28 @@ void mapError() { } @Test - void doubleError() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - NextProcessor mp = new NextProcessor<>(null); + @TestLoggerExtension.Redirect + void doubleError(TestLogger testLogger) { + NextProcessor mp = new NextProcessor<>(null); - mp.onError(new Exception("test")); - mp.onError(new Exception("test2")); - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("test2"); - } - finally { - LoggerUtils.disableCapture(); - } + mp.onError(new Exception("test")); + mp.onError(new Exception("test2")); + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("test2"); } @Test - void doubleSignal() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - NextProcessor mp = new NextProcessor<>(null); + @TestLoggerExtension.Redirect + void doubleSignal(TestLogger testLogger) { + NextProcessor mp = new NextProcessor<>(null); - mp.onNext("test"); - mp.onError(new Exception("test2")); + mp.onNext("test"); + mp.onError(new Exception("test2")); - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") .contains("test2"); - } - finally { - LoggerUtils.disableCapture(); - } } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/OperatorsTest.java b/reactor-core/src/test/java/reactor/core/publisher/OperatorsTest.java index 1143208008..fcc6088fea 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/OperatorsTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/OperatorsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.core.publisher.Operators.CancelledSubscription; import reactor.core.publisher.Operators.DeferredSubscription; import reactor.core.publisher.Operators.EmptySubscription; @@ -977,20 +978,14 @@ public void reportThrowInSubscribeWithFuseableErrorResumed() { } @Test - public void onDiscardCallbackErrorsLog() { + @TestLoggerExtension.Redirect + void onDiscardCallbackErrorsLog(TestLogger testLogger) { Context context = Operators.enableOnDiscard(Context.empty(), t -> { throw new RuntimeException("Boom"); }); - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - Operators.onDiscard("Foo", context); - assertThat(testLogger.getErrContent()).contains("Error in discard hook - java.lang.RuntimeException: Boom"); - } - finally { - LoggerUtils.disableCapture(); - } + Operators.onDiscard("Foo", context); + assertThat(testLogger.getErrContent()).contains("Error in discard hook - java.lang.RuntimeException: Boom"); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/SinkOneMulticastTest.java b/reactor-core/src/test/java/reactor/core/publisher/SinkOneMulticastTest.java index 659d90459e..9894f2510a 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/SinkOneMulticastTest.java +++ b/reactor-core/src/test/java/reactor/core/publisher/SinkOneMulticastTest.java @@ -28,6 +28,7 @@ import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.core.publisher.Sinks.EmitResult; import reactor.test.StepVerifier; import reactor.test.util.LoggerUtils; @@ -210,40 +211,28 @@ void nullFulfill() { } @Test - void doubleError() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - SinkOneMulticast sink = new SinkOneMulticast<>(); + @TestLoggerExtension.Redirect + void doubleError(TestLogger testLogger) { + SinkOneMulticast sink = new SinkOneMulticast<>(); - sink.emitError(new Exception("test"), Sinks.EmitFailureHandler.FAIL_FAST); - sink.emitError(new Exception("test2"), Sinks.EmitFailureHandler.FAIL_FAST); - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("test2"); - } - finally { - LoggerUtils.disableCapture(); - } + sink.emitError(new Exception("test"), Sinks.EmitFailureHandler.FAIL_FAST); + sink.emitError(new Exception("test2"), Sinks.EmitFailureHandler.FAIL_FAST); + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("test2"); } @Test - void doubleSignal() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - SinkOneMulticast sink = new SinkOneMulticast<>(); + @TestLoggerExtension.Redirect + void doubleSignal(TestLogger testLogger) { + SinkOneMulticast sink = new SinkOneMulticast<>(); - sink.emitValue("test", Sinks.EmitFailureHandler.FAIL_FAST); - sink.emitError(new Exception("test2"), Sinks.EmitFailureHandler.FAIL_FAST); + sink.emitValue("test", Sinks.EmitFailureHandler.FAIL_FAST); + sink.emitError(new Exception("test2"), Sinks.EmitFailureHandler.FAIL_FAST); - Assertions.assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("test2"); - } - finally { - LoggerUtils.disableCapture(); - } + Assertions.assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("test2"); } @Test diff --git a/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java b/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java index adf77331e2..e8fd1823ec 100644 --- a/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java +++ b/reactor-core/src/test/java/reactor/core/publisher/scenarios/FluxTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2016-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -59,6 +59,7 @@ import reactor.core.Exceptions; import reactor.core.Fuseable; import reactor.core.Scannable; +import reactor.core.TestLoggerExtension; import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; @@ -1195,25 +1196,20 @@ public void subscribeOnDispatchOn() throws InterruptedException { latch.await(30, TimeUnit.SECONDS); assertThat(latch.getCount()).as("dispatch count").isEqualTo(0L); } + @Test - public void unimplementedErrorCallback() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); - try { - Flux.error(new Exception("forced1")) - .log("error") - .subscribe(); + @TestLoggerExtension.Capture + public void unimplementedErrorCallback(TestLogger testLogger) { + Flux.error(new Exception("forced1")) + .log("error") + .subscribe(); - Flux.error(new Exception("forced2")) - .subscribe(); + Flux.error(new Exception("forced2")) + .subscribe(); - assertThat(testLogger.getErrContent()) - .contains("Operator called default onErrorDropped") - .contains("reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.Exception: forced2"); - } - finally { - LoggerUtils.disableCapture(); - } + assertThat(testLogger.getErrContent()) + .contains("Operator called default onErrorDropped") + .contains("reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.Exception: forced2"); } @Test @@ -1446,9 +1442,8 @@ public void multiplexUsingDispatchersAndSplit() { } @Test - public void testThrowWithoutOnErrorShowsUpInSchedulerHandler() { - TestLogger testLogger = new TestLogger(); - LoggerUtils.enableCaptureWith(testLogger); + @TestLoggerExtension.Capture + void testThrowWithoutOnErrorShowsUpInSchedulerHandler(TestLogger testLogger) { AtomicReference failure = new AtomicReference<>(null); AtomicBoolean handled = new AtomicBoolean(false); @@ -1488,7 +1483,6 @@ public void testThrowWithoutOnErrorShowsUpInSchedulerHandler() { fail(e.toString()); } finally { - LoggerUtils.disableCapture(); Thread.setDefaultUncaughtExceptionHandler(null); Schedulers.resetOnHandleError(); Schedulers.resetOnScheduleHook("test"); diff --git a/reactor-test/build.gradle b/reactor-test/build.gradle index 3eef114bc1..0ee9ec6bbf 100644 --- a/reactor-test/build.gradle +++ b/reactor-test/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,9 +46,10 @@ dependencies { testImplementation platform(libs.junit.bom) testImplementation "org.junit.jupiter:junit-jupiter-api" + testImplementation "org.junit.jupiter:junit-jupiter-params" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" - testRuntimeOnly libs.logback + testRuntimeOnly libs.logback testImplementation libs.assertj testImplementation libs.mockito } diff --git a/reactor-test/src/main/java/reactor/test/util/LoggerUtils.java b/reactor-test/src/main/java/reactor/test/util/LoggerUtils.java index e51ac583ed..182244c4a1 100644 --- a/reactor-test/src/main/java/reactor/test/util/LoggerUtils.java +++ b/reactor-test/src/main/java/reactor/test/util/LoggerUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,6 @@ package reactor.test.util; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.function.Function; import reactor.core.Disposable; @@ -34,7 +32,7 @@ public final class LoggerUtils { @Nullable - private static Logger testLogger; + static CapturingFactory currentCapturingFactory; private LoggerUtils() { } @@ -55,18 +53,19 @@ public static Disposable useCurrentLoggersWithCapture() throws IllegalStateExcep try { Field lfField = Loggers.class.getDeclaredField("LOGGER_FACTORY"); lfField.setAccessible(true); - Object originalFactory = lfField.get(Loggers.class); - if (originalFactory instanceof CapturingFactory) { - return (Disposable) originalFactory; + Object originalFactoryInstance = lfField.get(Loggers.class); + if (originalFactoryInstance instanceof CapturingFactory) { + return (Disposable) originalFactoryInstance; } - Method originalFactoryMethod = originalFactory.getClass().getMethod("apply", String.class); - originalFactoryMethod.setAccessible(true); - - CapturingFactory capturingFactory = new CapturingFactory(originalFactory, originalFactoryMethod); + @SuppressWarnings("unchecked") + final Function originalFactory = + (Function) originalFactoryInstance; + CapturingFactory capturingFactory = new CapturingFactory(originalFactory); + currentCapturingFactory = capturingFactory; Loggers.useCustomLoggers(capturingFactory); return capturingFactory; } - catch (NoSuchFieldException | IllegalAccessException | NoSuchMethodException e) { + catch (NoSuchFieldException | IllegalAccessException e) { throw new IllegalStateException("Could not install custom logger", e); } } @@ -74,46 +73,92 @@ public static Disposable useCurrentLoggersWithCapture() throws IllegalStateExcep /** * Set the logger used for capturing. * - * @throws IllegalStateException if a previous logger has been set but not cleared via {@link #disableCapture()} + * @param testLogger the {@link Logger} in which to copy logs + * @throws IllegalStateException if no capturing factory is installed or a previous logger has been set but not + * cleared via {@link #disableCapture()} */ public static void enableCaptureWith(Logger testLogger) { - if (LoggerUtils.testLogger != null) { - throw new IllegalStateException("A logger was already set, maybe from a previous run. Don't forget to call disableCapture()"); + CapturingFactory f = currentCapturingFactory; + if (f == null) { + throw new IllegalStateException("LoggerUtils#useCurrentLoggerWithCapture() hasn't been called"); + } + f.enableRedirection(testLogger, true); //throws ISE also + } + + /** + * Set the logger used for capturing, an optionally suppress log messages from original logger. + * + * @param testLogger the {@link TestLogger} in which to copy or redirect logs + * @param redirectToOriginal whether log messages should also go to the original logging infrastructure + * @throws IllegalStateException if no capturing factory is installed or a previous logger has been set but not + * cleared via {@link #disableCapture()} + */ + public static void enableCaptureWith(Logger testLogger, boolean redirectToOriginal) { + CapturingFactory f = currentCapturingFactory; + if (f == null) { + throw new IllegalStateException("LoggerUtils#useCurrentLoggerWithCapture() hasn't been called"); } - LoggerUtils.testLogger = testLogger; + f.enableRedirection(testLogger, redirectToOriginal); //throws ISE also } /** * Disable capturing, forgetting about the logger set via {@link #enableCaptureWith(Logger)}. */ public static void disableCapture() { - LoggerUtils.testLogger = null; + CapturingFactory f = currentCapturingFactory; + if (f == null) { + throw new IllegalStateException("LoggerUtils#useCurrentLoggerWithCapture() hasn't been called"); + } + f.disableRedirection(); } - private static class CapturingFactory implements Function, Disposable { + static final class CapturingFactory implements Function, Disposable { - private final Object originalFactory; - private final Method originalFactoryMethod; + final Function originalFactory; - private CapturingFactory(Object factory, Method method) { - originalFactory = factory; - originalFactoryMethod = method; + @Nullable + Logger capturingLogger; + boolean redirectToOriginal; + + CapturingFactory(Function originalFactory) { + this.originalFactory = originalFactory; + disableRedirection(); + } + + void disableRedirection() { + this.redirectToOriginal = true; + this.capturingLogger = null; + } + + void enableRedirection(Logger captureLogger, boolean redirectToOriginal) { + if (this.capturingLogger != null) { + throw new IllegalStateException("A logger was already set, maybe from a previous run. Don't forget to call disableCapture()"); + } + this.redirectToOriginal = redirectToOriginal; + this.capturingLogger = captureLogger; + } + + @Nullable + Logger getCapturingLogger() { + return this.capturingLogger; + } + + boolean isRedirectToOriginal() { + return this.redirectToOriginal; } @Override public Logger apply(String category) { - try { - Logger original = (Logger) originalFactoryMethod.invoke(originalFactory, category); - return new DivertingLogger(original); - } - catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } + Logger original = originalFactory.apply(category); + return new DivertingLogger(original, this); } @Override public void dispose() { + if (LoggerUtils.currentCapturingFactory == this) { + LoggerUtils.currentCapturingFactory = null; + } try { Field lfField = Loggers.class.getDeclaredField("LOGGER_FACTORY"); lfField.setAccessible(true); @@ -131,14 +176,17 @@ public void dispose() { } /** - * A Logger that behaves like its {@link #delegate} but also logs to {@link LoggerUtils#testLogger} if it is set. + * A Logger that behaves like its {@link #delegate} but also logs to its parent {@link CapturingFactory} + * {@link CapturingFactory#getCapturingLogger() capturing logger} if it is set. */ - private static class DivertingLogger implements reactor.util.Logger { + static class DivertingLogger implements reactor.util.Logger { private final reactor.util.Logger delegate; + private final CapturingFactory parent; - private DivertingLogger(Logger delegate) { + DivertingLogger(Logger delegate, CapturingFactory parent) { this.delegate = delegate; + this.parent = parent; } @Override @@ -148,167 +196,197 @@ public String getName() { @Override public boolean isTraceEnabled() { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); return delegate.isTraceEnabled() || (logger != null && logger.isTraceEnabled()); } @Override public void trace(String msg) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.trace(msg); } - delegate.trace(msg); + if (parent.isRedirectToOriginal()) { + delegate.trace(msg); + } } @Override public void trace(String format, Object... arguments) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.trace(format, arguments); } - delegate.trace(format, arguments); + if (parent.isRedirectToOriginal()) { + delegate.trace(format, arguments); + } } @Override public void trace(String msg, Throwable t) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.trace(msg, t); } - delegate.trace(msg, t); + if (parent.isRedirectToOriginal()) { + delegate.trace(msg, t); + } } @Override public boolean isDebugEnabled() { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); return delegate.isDebugEnabled() || (logger != null && logger.isDebugEnabled()); } @Override public void debug(String msg) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.debug(msg); } - delegate.debug(msg); + if (parent.isRedirectToOriginal()) { + delegate.debug(msg); + } } @Override public void debug(String format, Object... arguments) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.debug(format, arguments); } - delegate.debug(format, arguments); + if (parent.isRedirectToOriginal()) { + delegate.debug(format, arguments); + } } @Override public void debug(String msg, Throwable t) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.debug(msg, t); } - delegate.debug(msg, t); + if (parent.isRedirectToOriginal()) { + delegate.debug(msg, t); + } } @Override public boolean isInfoEnabled() { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); return delegate.isInfoEnabled() || (logger != null && logger.isInfoEnabled()); } @Override public void info(String msg) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.info(msg); } - delegate.info(msg); + if (parent.isRedirectToOriginal()) { + delegate.info(msg); + } } @Override public void info(String format, Object... arguments) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.info(format, arguments); } - delegate.info(format, arguments); + if (parent.isRedirectToOriginal()) { + delegate.info(format, arguments); + } } @Override public void info(String msg, Throwable t) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.info(msg, t); } - delegate.info(msg, t); + if (parent.isRedirectToOriginal()) { + delegate.info(msg, t); + } } @Override public boolean isWarnEnabled() { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); return delegate.isWarnEnabled() || (logger != null && logger.isWarnEnabled()); } @Override public void warn(String msg) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.warn(msg); } - delegate.warn(msg); + if (parent.isRedirectToOriginal()) { + delegate.warn(msg); + } } @Override public void warn(String format, Object... arguments) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.warn(format, arguments); } - delegate.warn(format, arguments); + if (parent.isRedirectToOriginal()) { + delegate.warn(format, arguments); + } } @Override public void warn(String msg, Throwable t) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.warn(msg, t); } - delegate.warn(msg, t); + if (parent.isRedirectToOriginal()) { + delegate.warn(msg, t); + } } @Override public boolean isErrorEnabled() { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); return delegate.isErrorEnabled() || (logger != null && logger.isErrorEnabled()); } @Override public void error(String msg) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.error(msg); } - delegate.error(msg); + if (parent.isRedirectToOriginal()) { + delegate.error(msg); + } } @Override public void error(String format, Object... arguments) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.error(format, arguments); } - delegate.error(format, arguments); + if (parent.isRedirectToOriginal()) { + delegate.error(format, arguments); + } } @Override public void error(String msg, Throwable t) { - Logger logger = LoggerUtils.testLogger; + Logger logger = parent.getCapturingLogger(); if (logger != null) { logger.error(msg, t); } - delegate.error(msg, t); + if (parent.isRedirectToOriginal()) { + delegate.error(msg, t); + } } } } diff --git a/reactor-test/src/test/java/reactor/test/util/LoggerUtilsTest.java b/reactor-test/src/test/java/reactor/test/util/LoggerUtilsTest.java index 761daa34a4..7d2300de6e 100644 --- a/reactor-test/src/test/java/reactor/test/util/LoggerUtilsTest.java +++ b/reactor-test/src/test/java/reactor/test/util/LoggerUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,44 +18,90 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Function; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import reactor.core.Disposable; +import reactor.core.Exceptions; import reactor.util.Logger; import reactor.util.Loggers; import static org.assertj.core.api.Assertions.*; +@Isolated class LoggerUtilsTest { + /** + * Disposable added to this list will be disposed at end of test, ignoring exceptions + */ + static final List SAFE_DISPOSE = new ArrayList<>(); + + static Disposable disposeAfterTest(Disposable d) { + SAFE_DISPOSE.add(d); + return d; + } + + @AfterEach + void safelyDispose() { + for (Disposable disposable : SAFE_DISPOSE) { + try { + disposable.dispose(); + } + catch (Exception e) { + //NO-OP + } + } + SAFE_DISPOSE.clear(); + } + @Test void installsFactory() { - Disposable disposable = LoggerUtils.useCurrentLoggersWithCapture(); + Disposable disposable = disposeAfterTest(LoggerUtils.useCurrentLoggersWithCapture()); TestLogger testLogger = new TestLogger(); - try { - Logger frameworkLogger = Loggers.getLogger("category"); // simulates an early creation of a logger - - LoggerUtils.enableCaptureWith(testLogger); - frameworkLogger.debug("Look ma!, I'm debugging!"); - assertThat(testLogger.getOutContent()).contains("Look ma!, I'm debugging!"); - LoggerUtils.disableCapture(); - frameworkLogger.debug("This won't be captured"); - assertThat(testLogger.getOutContent()).doesNotContain("This won't be captured"); - } finally { - disposable.dispose(); + Logger frameworkLogger = Loggers.getLogger("category"); // simulates an early creation of a logger - // The following tests that once disposed, capturing is no longer in effect - LoggerUtils.enableCaptureWith(testLogger); - Logger otherLogger = Loggers.getLogger("another"); - otherLogger.debug("This won't be captured either"); - assertThat(testLogger.getOutContent()).doesNotContain("This won't be captured either"); - } + LoggerUtils.enableCaptureWith(testLogger); + frameworkLogger.debug("Look ma!, I'm debugging!"); + assertThat(testLogger.getOutContent()).contains("Look ma!, I'm debugging!"); + + LoggerUtils.disableCapture(); + frameworkLogger.debug("This won't be captured"); + assertThat(testLogger.getOutContent()).doesNotContain("This won't be captured"); + + LoggerUtils.enableCaptureWith(testLogger); + frameworkLogger.debug("I've reactivated redirection"); + assertThat(testLogger.getOutContent()).contains("I've reactivated redirection"); + + disposable.dispose(); + + // The following tests that once disposed, capturing is no longer in effect + Logger otherLogger = Loggers.getLogger("another"); + otherLogger.debug("This won't be captured either"); + assertThat(testLogger.getOutContent()).doesNotContain("This won't be captured either"); + } + + @Test + void disposeUninstallsFromLoggersAndLoggerUtils() { + Disposable d = disposeAfterTest(LoggerUtils.useCurrentLoggersWithCapture()); + + assertThat(Loggers.getLogger("foo")).as("when factory installed").isInstanceOf(LoggerUtils.DivertingLogger.class); + assertThat(LoggerUtils.currentCapturingFactory).as("when currentCapturingFactory").isSameAs(d); + + d.dispose(); + + assertThat(Loggers.getLogger("foo")).as("after uninstall").isNotInstanceOf(LoggerUtils.DivertingLogger.class); + assertThat(LoggerUtils.currentCapturingFactory).as("not currentCapturingFactory").isNull(); } @Test - void disposeOnlyUninstallsItself() { - Disposable disposable = LoggerUtils.useCurrentLoggersWithCapture(); + void disposeOnlyUninstallsItselfFromLoggersFactory() { + Disposable disposable = disposeAfterTest(LoggerUtils.useCurrentLoggersWithCapture()); assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> { Loggers.resetLoggerFactory(); // Overwrites our custom logger disposable.dispose(); @@ -68,7 +114,7 @@ void continuouslyInstallingFactoryDoesntCauseStackOverflow() { final int LOOPS = 2000; List disposables = new ArrayList<>(LOOPS); for (int i = 0; i < LOOPS; i++) { - disposables.add(LoggerUtils.useCurrentLoggersWithCapture()); + disposables.add(disposeAfterTest(LoggerUtils.useCurrentLoggersWithCapture())); } final Logger test = Loggers.getLogger("test"); @@ -78,4 +124,221 @@ void continuouslyInstallingFactoryDoesntCauseStackOverflow() { assertThatCode(() -> test.error("expected error message")).doesNotThrowAnyException(); } + @Test + void enableCaptureWithThrowsIfNotCapturing() { + assertThatIllegalStateException() + .isThrownBy(() -> LoggerUtils.enableCaptureWith(new TestLogger())) + .withMessage("LoggerUtils#useCurrentLoggerWithCapture() hasn't been called"); + } + + @Test + void enableCaptureWithNoRedirectThrowsIfNotCapturing() { + assertThatIllegalStateException() + .isThrownBy(() -> LoggerUtils.enableCaptureWith(new TestLogger(), false)) + .withMessage("LoggerUtils#useCurrentLoggerWithCapture() hasn't been called"); + } + + @Test + void enableCaptureWithThrowsIfPreviouslyEnabledCapture() { + disposeAfterTest(LoggerUtils.useCurrentLoggersWithCapture()); + + assertThatCode(() -> LoggerUtils.enableCaptureWith(new TestLogger())) + .as("first enableCapture") + .doesNotThrowAnyException(); + + assertThatIllegalStateException() + .as("redundant enableCapture") + .isThrownBy(() -> LoggerUtils.enableCaptureWith(new TestLogger())) + .withMessage("A logger was already set, maybe from a previous run. Don't forget to call disableCapture()"); + } + + @Test + void enableCaptureWithNoRedirectThrowsIfPreviouslyEnabledCapture() { + disposeAfterTest(LoggerUtils.useCurrentLoggersWithCapture()); + + assertThatCode(() -> LoggerUtils.enableCaptureWith(new TestLogger())) + .as("first enableCapture") + .doesNotThrowAnyException(); + + assertThatIllegalStateException() + .as("redundant enableCapture") + .isThrownBy(() -> LoggerUtils.enableCaptureWith(new TestLogger(), false)) + .withMessage("A logger was already set, maybe from a previous run. Don't forget to call disableCapture()"); + } + + @Nested + class DivertingLoggerTest { + + private final TestLogger INACTIVE_TEST_LOGGER = new TestLogger(false) { + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public boolean isTraceEnabled() { + return false; + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public boolean isWarnEnabled() { + return false; + } + + @Override + public boolean isErrorEnabled() { + return false; + } + }; + + @ParameterizedTest + @ValueSource(booleans = { true, false }) + void allLogMethods(boolean redirect) { + final TestLogger underlyingTestLogger = new TestLogger(false); + Function fakeFactory = s -> { + throw new UnsupportedOperationException("test shouldn't trigger the original factory"); + }; + final TestLogger capturingTestLogger = new TestLogger(false); + LoggerUtils.CapturingFactory capturingFactory = new LoggerUtils.CapturingFactory(fakeFactory); + LoggerUtils.DivertingLogger divertingLogger = new LoggerUtils.DivertingLogger(underlyingTestLogger, capturingFactory); + + //optionally activate the redirection to our capturingTestLogger, with suppression of original logs + capturingFactory.enableRedirection(capturingTestLogger, redirect); + + divertingLogger.info("info1"); + divertingLogger.info("info{}", 2); + divertingLogger.info("info3", Exceptions.TERMINATED); //TERMINATED has no stacktrace + + divertingLogger.warn("warn1"); + divertingLogger.warn("warn{}", 2); + divertingLogger.warn("warn3", Exceptions.TERMINATED); //TERMINATED has no stacktrace + + divertingLogger.error("error1"); + divertingLogger.error("error{}", 2); + divertingLogger.error("error3", Exceptions.TERMINATED); //TERMINATED has no stacktrace + + divertingLogger.debug("debug1"); + divertingLogger.debug("debug{}", 2); + divertingLogger.debug("debug3", Exceptions.TERMINATED); //TERMINATED has no stacktrace + + divertingLogger.trace("trace1"); + divertingLogger.trace("trace{}", 2); + divertingLogger.trace("trace3", Exceptions.TERMINATED); //TERMINATED has no stacktrace + + String[] expectedOut = new String[] { + "[ INFO] info1", + "[ INFO] info2", + "[ INFO] info3 - reactor.core.Exceptions$StaticThrowable: Operator has been terminated", + "[DEBUG] debug1", + "[DEBUG] debug2", + "[DEBUG] debug3 - reactor.core.Exceptions$StaticThrowable: Operator has been terminated", + "[TRACE] trace1", + "[TRACE] trace2", + "[TRACE] trace3 - reactor.core.Exceptions$StaticThrowable: Operator has been terminated" + }; + + String[] expectedErrInAnyOrder = new String[] { + "[ WARN] warn1", + "[ WARN] warn2", + "[ WARN] warn3 - reactor.core.Exceptions$StaticThrowable: Operator has been terminated", + "[ERROR] error1", + "[ERROR] error2", + "[ERROR] error3 - reactor.core.Exceptions$StaticThrowable: Operator has been terminated", + //all logging methods with Throwable also print the stacktrace to err logger + "reactor.core.Exceptions$StaticThrowable: Operator has been terminated", + "reactor.core.Exceptions$StaticThrowable: Operator has been terminated", + "reactor.core.Exceptions$StaticThrowable: Operator has been terminated", + "reactor.core.Exceptions$StaticThrowable: Operator has been terminated", + "reactor.core.Exceptions$StaticThrowable: Operator has been terminated" + }; + + assertThat(capturingTestLogger.getOutContent().split(System.lineSeparator())) + .as("captured out") + .containsExactly(expectedOut); + + assertThat(capturingTestLogger.getErrContent().split(System.lineSeparator())) + .as("captured err") + .containsExactlyInAnyOrder(expectedErrInAnyOrder); + + if (redirect) { + assertThat(underlyingTestLogger.getOutContent().split(System.lineSeparator())) + .as("redirected to original out") + .containsExactly(expectedOut); + assertThat(underlyingTestLogger.getErrContent().split(System.lineSeparator())) + .as("redirected to original err") + .containsExactlyInAnyOrder(expectedErrInAnyOrder); + } + else { + assertThat(underlyingTestLogger.getErrContent()).as("suppressed original err").isEmpty(); + assertThat(underlyingTestLogger.getOutContent()).as("suppressed original out").isEmpty(); + } + } + + @Test + void categoriesAreActiveIfActiveInOriginal() { + TestLogger original = new TestLogger(false); + TestLogger redirect = INACTIVE_TEST_LOGGER; + LoggerUtils.CapturingFactory capturingFactory = new LoggerUtils.CapturingFactory(s -> original); + capturingFactory.enableRedirection(redirect, true); + + LoggerUtils.DivertingLogger divertingLogger = new LoggerUtils.DivertingLogger(original, capturingFactory); + + assertThat(divertingLogger.isInfoEnabled()).as("isInfoEnabled").isTrue(); + assertThat(divertingLogger.isWarnEnabled()).as("isWarnEnabled").isTrue(); + assertThat(divertingLogger.isErrorEnabled()).as("isErrorEnabled").isTrue(); + assertThat(divertingLogger.isDebugEnabled()).as("isDebugEnabled").isTrue(); + assertThat(divertingLogger.isTraceEnabled()).as("isTraceEnabled").isTrue(); + } + + @Test + void categoriesAreActiveIfActiveInRedirect() { + TestLogger original = INACTIVE_TEST_LOGGER; + TestLogger redirect = new TestLogger(false); + LoggerUtils.CapturingFactory capturingFactory = new LoggerUtils.CapturingFactory(s -> original); + capturingFactory.enableRedirection(redirect, true); + + LoggerUtils.DivertingLogger divertingLogger = new LoggerUtils.DivertingLogger(original, capturingFactory); + + assertThat(divertingLogger.isInfoEnabled()).as("isInfoEnabled").isTrue(); + assertThat(divertingLogger.isWarnEnabled()).as("isWarnEnabled").isTrue(); + assertThat(divertingLogger.isErrorEnabled()).as("isErrorEnabled").isTrue(); + assertThat(divertingLogger.isDebugEnabled()).as("isDebugEnabled").isTrue(); + assertThat(divertingLogger.isTraceEnabled()).as("isTraceEnabled").isTrue(); + } + + @Test + void categoriesAreInactiveIfInactiveInOriginalAndNoRedirect() { + TestLogger original = INACTIVE_TEST_LOGGER; + LoggerUtils.CapturingFactory capturingFactory = new LoggerUtils.CapturingFactory(s -> original); + + LoggerUtils.DivertingLogger divertingLogger = new LoggerUtils.DivertingLogger(original, capturingFactory); + + assertThat(divertingLogger.isInfoEnabled()).as("isInfoEnabled").isFalse(); + assertThat(divertingLogger.isWarnEnabled()).as("isWarnEnabled").isFalse(); + assertThat(divertingLogger.isErrorEnabled()).as("isErrorEnabled").isFalse(); + assertThat(divertingLogger.isDebugEnabled()).as("isDebugEnabled").isFalse(); + assertThat(divertingLogger.isTraceEnabled()).as("isTraceEnabled").isFalse(); + } + + @Test + void categoriesAreInactiveIfInactiveInOriginalAndRedirect() { + TestLogger original = INACTIVE_TEST_LOGGER; + TestLogger redirect = INACTIVE_TEST_LOGGER; + LoggerUtils.CapturingFactory capturingFactory = new LoggerUtils.CapturingFactory(s -> original); + capturingFactory.enableRedirection(redirect, true); + + LoggerUtils.DivertingLogger divertingLogger = new LoggerUtils.DivertingLogger(original, capturingFactory); + + assertThat(divertingLogger.isInfoEnabled()).as("isInfoEnabled").isFalse(); + assertThat(divertingLogger.isWarnEnabled()).as("isWarnEnabled").isFalse(); + assertThat(divertingLogger.isErrorEnabled()).as("isErrorEnabled").isFalse(); + assertThat(divertingLogger.isDebugEnabled()).as("isDebugEnabled").isFalse(); + assertThat(divertingLogger.isTraceEnabled()).as("isTraceEnabled").isFalse(); + } + } } \ No newline at end of file diff --git a/reactor-test/src/test/java/reactor/test/util/TestLoggerTest.java b/reactor-test/src/test/java/reactor/test/util/TestLoggerTest.java index 40f8f9f47f..d619ef67b4 100644 --- a/reactor-test/src/test/java/reactor/test/util/TestLoggerTest.java +++ b/reactor-test/src/test/java/reactor/test/util/TestLoggerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 VMware Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2021-2022 VMware Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,14 +18,18 @@ import org.junit.jupiter.api.Test; +import reactor.core.Exceptions; + +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -public class TestLoggerTest { +class TestLoggerTest { @Test void returnMessageWithoutThreadNameWhenLogCurrentThreadNameIsFalse() { TestLogger testLogger = new TestLogger(false); + assertThat(testLogger.isLogCurrentThreadName()).as("isLogCurrentThreadName").isFalse(); assertEquals("[ERROR] TestMessage\n", testLogger.logContent("ERROR", "TestMessage")); } @@ -34,7 +38,145 @@ void returnMessageWithoutThreadNameWhenLogCurrentThreadNameIsFalse() { void returnMessageWithoutThreadNameWhenLogCurrentThreadNameIsTrue() { TestLogger testLogger = new TestLogger(true); + assertThat(testLogger.isLogCurrentThreadName()).as("isLogCurrentThreadName").isTrue(); assertEquals(String.format("[ERROR] (%s) TestMessage\n", Thread.currentThread().getName()), testLogger.logContent("ERROR", "TestMessage")); } + + @Test + void getName() { + assertThat(new TestLogger().getName()).isEqualTo("TestLogger"); + } + + @Test + void resetContents() { + final TestLogger testLogger = new TestLogger(false); + testLogger.info("info"); + testLogger.error("error"); + + assertThat(testLogger.getOutContent()) + .as("out before reset") + .isEqualToIgnoringNewLines("[ INFO] info"); + assertThat(testLogger.getErrContent()) + .as("err before reset") + .isEqualToIgnoringNewLines("[ERROR] error"); + + testLogger.reset(); + + assertThat(testLogger.getOutContent()) + .as("after reset") + .isEqualTo(testLogger.getErrContent()) + .isEmpty(); + } + + @Test + void allModesAreConsideredEnabled() { + TestLogger testLogger = new TestLogger(); + + assertThat(testLogger.isInfoEnabled()).as("isInfoEnabled").isTrue(); + assertThat(testLogger.isDebugEnabled()).as("isDebugEnabled").isTrue(); + assertThat(testLogger.isTraceEnabled()).as("isTraceEnabled").isTrue(); + assertThat(testLogger.isWarnEnabled()).as("isWarnEnabled").isTrue(); + assertThat(testLogger.isErrorEnabled()).as("isErrorEnabled").isTrue(); + } + + @Test + void infoNoThrowableLogsToOutContent() { + TestLogger testLogger = new TestLogger(false); + testLogger.info("msg1"); + testLogger.info("msg{}", 2); + + assertThat(testLogger.getOutContent()).as("out") + .isEqualToIgnoringNewLines("[ INFO] msg1[ INFO] msg2"); + assertThat(testLogger.getErrContent()).as("err").isEmpty(); + } + + @Test + void infoThrowableLogsToOutContentPrintStackTraceToErrContent() { + TestLogger testLogger = new TestLogger(false); + testLogger.info("msg", Exceptions.TERMINATED); + + assertThat(testLogger.getOutContent()).as("out") + .isEqualToIgnoringNewLines("[ INFO] msg - reactor.core.Exceptions$StaticThrowable: Operator has been terminated"); + assertThat(testLogger.getErrContent()).as("err") + .isEqualToIgnoringNewLines("reactor.core.Exceptions$StaticThrowable: Operator has been terminated"); + } + + @Test + void debugNoThrowableLogsToOutContent() { + TestLogger testLogger = new TestLogger(false); + testLogger.debug("msg1"); + testLogger.debug("msg{}", 2); + + assertThat(testLogger.getOutContent()).as("out") + .isEqualToIgnoringNewLines("[DEBUG] msg1[DEBUG] msg2"); + assertThat(testLogger.getErrContent()).as("err").isEmpty(); + } + + @Test + void debugThrowableLogsToOutContentPrintStackTraceToErrContent() { + TestLogger testLogger = new TestLogger(false); + testLogger.debug("msg", Exceptions.TERMINATED); + + assertThat(testLogger.getOutContent()).as("out") + .isEqualToIgnoringNewLines("[DEBUG] msg - reactor.core.Exceptions$StaticThrowable: Operator has been terminated"); + assertThat(testLogger.getErrContent()).as("err") + .isEqualToIgnoringNewLines("reactor.core.Exceptions$StaticThrowable: Operator has been terminated"); + } + + @Test + void traceNoThrowableLogsToOutContent() { + TestLogger testLogger = new TestLogger(false); + testLogger.trace("msg1"); + testLogger.trace("msg{}", 2); + + assertThat(testLogger.getOutContent()).as("out") + .isEqualToIgnoringNewLines("[TRACE] msg1[TRACE] msg2"); + assertThat(testLogger.getErrContent()).as("err").isEmpty(); + } + + @Test + void traceThrowableLogsToOutContentPrintStackTraceToErrContent() { + TestLogger testLogger = new TestLogger(false); + testLogger.trace("msg", Exceptions.TERMINATED); + + assertThat(testLogger.getOutContent()).as("out") + .isEqualToIgnoringNewLines("[TRACE] msg - reactor.core.Exceptions$StaticThrowable: Operator has been terminated"); + assertThat(testLogger.getErrContent()).as("err") + .isEqualToIgnoringNewLines("reactor.core.Exceptions$StaticThrowable: Operator has been terminated"); + } + + @Test + void warnLogsAndPrintsThrowableStackTraceToErrContent() { + TestLogger testLogger = new TestLogger(false); + testLogger.warn("msg1"); + testLogger.warn("msg{}", 2); + testLogger.warn("msg", Exceptions.TERMINATED); + + assertThat(testLogger.getOutContent()).as("out").isEmpty(); + assertThat(testLogger.getErrContent().split(System.lineSeparator())) + .as("err") + .containsExactly( + "[ WARN] msg1", + "[ WARN] msg2", + "[ WARN] msg - reactor.core.Exceptions$StaticThrowable: Operator has been terminated", + "reactor.core.Exceptions$StaticThrowable: Operator has been terminated"); + } + + @Test + void errorLogsAndPrintsThrowableStackTraceToErrContent() { + TestLogger testLogger = new TestLogger(false); + testLogger.error("msg1"); + testLogger.error("msg{}", 2); + testLogger.error("msg", Exceptions.TERMINATED); + + assertThat(testLogger.getOutContent()).as("out").isEmpty(); + assertThat(testLogger.getErrContent().split(System.lineSeparator())) + .as("err") + .containsExactly( + "[ERROR] msg1", + "[ERROR] msg2", + "[ERROR] msg - reactor.core.Exceptions$StaticThrowable: Operator has been terminated", + "reactor.core.Exceptions$StaticThrowable: Operator has been terminated"); + } }