Skip to content

Commit

Permalink
Improve LoggerUtils support and add TestLoggerExtension (#3123)
Browse files Browse the repository at this point in the history
This commit improves `LoggerUtils` and adds supporting JUnit5 extension
and annotations in reactor-core tests:

`LoggerUtils` now has support to _redirect_ rather than _copy/capture_
log messages when the logger factory is installed early.

In reactor-core tests, a `TestLoggerExtension` is added that sets up a
`TestLogger` and activates capture/redirection via `LoggerUtils`:
 - before the test, it creates a `TestLogger` (configured depending on
 test annotations) and sets up `LoggerUtils` with said `TestLogger`
 - as a `ParameterResolver` it injects the `TestLogger` into the test
 - after the test it `disableCapture()`

The Extension should be applied to individual tests. Convenience
annotations are provided that fine tune the `TestLogger` that will be
injected:
 - `@Capture` when the loggers should capture log output, ie. go both
 to original logger and TestLogger
 - `@Redirect` when the loggers should redirect log output, ie. only go
 to TestLogger

This commit further simplifies the `LoggerUtils` internals, putting more
state into the `CapturingFactory` rather than as a top level static
field.

Finally, it adds a ton of test coverage to both `TestLogger` and
`LoggerUtils`.
  • Loading branch information
simonbasle committed Aug 1, 2022
1 parent 41e956d commit 6a89d1f
Show file tree
Hide file tree
Showing 18 changed files with 1,183 additions and 772 deletions.
98 changes: 98 additions & 0 deletions 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;
}
}
@@ -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.
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -82,30 +83,23 @@ protected void hookFinally(SignalType type) {
}

@Test
public void onErrorCallbackNotImplemented() {
TestLogger testLogger = new TestLogger();
LoggerUtils.enableCaptureWith(testLogger);
try {
Flux<String> flux = Flux.error(new IllegalStateException());
@TestLoggerExtension.Redirect
void onErrorCallbackNotImplemented(TestLogger testLogger) {
Flux<String> flux = Flux.error(new IllegalStateException());

flux.subscribe(new BaseSubscriber<String>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
request(1);
}
flux.subscribe(new BaseSubscriber<String>() {
@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
Expand Down Expand Up @@ -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<SignalType> checkFinally = new AtomicReference<>();

Flux.<String>error(new IllegalStateException("someError")).subscribe(new BaseSubscriber<String>() {
@Override
protected void hookOnSubscribe(Subscription subscription) {
requestUnbounded();
}
@TestLoggerExtension.Redirect
void finallyExecutesWhenHookOnErrorFails(TestLogger testLogger) {
RuntimeException error = new IllegalArgumentException("hookOnError");
AtomicReference<SignalType> checkFinally = new AtomicReference<>();

@Override
protected void hookOnNext(String value) {
}
Flux.<String>error(new IllegalStateException("someError")).subscribe(new BaseSubscriber<String>() {
@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
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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<String> 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) {
Expand Down

0 comments on commit 6a89d1f

Please sign in to comment.