Skip to content

Commit ce1a3a4

Browse files
authored
Make StatusLogger self-contained and testable (#2249)
1 parent 3333d00 commit ce1a3a4

File tree

50 files changed

+1046
-1190
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1046
-1190
lines changed

log4j-api-test/pom.xml

-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@
138138
<dependency>
139139
<groupId>org.mockito</groupId>
140140
<artifactId>mockito-core</artifactId>
141-
<scope>test</scope>
142141
</dependency>
143142
<dependency>
144143
<groupId>org.mockito</groupId>

log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusLoggerExtension.java log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/StatusListenerExtension.java

+15-34
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,16 @@
1616
*/
1717
package org.apache.logging.log4j.test.junit;
1818

19-
import java.io.IOException;
20-
import java.time.Instant;
21-
import java.time.ZoneId;
22-
import java.time.format.DateTimeFormatter;
2319
import java.util.ArrayList;
2420
import java.util.List;
2521
import java.util.stream.Stream;
2622
import org.apache.logging.log4j.Level;
27-
import org.apache.logging.log4j.Logger;
28-
import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
29-
import org.apache.logging.log4j.simple.SimpleLogger;
23+
import org.apache.logging.log4j.message.ParameterizedMessage;
24+
import org.apache.logging.log4j.status.StatusConsoleListener;
3025
import org.apache.logging.log4j.status.StatusData;
26+
import org.apache.logging.log4j.status.StatusListener;
3127
import org.apache.logging.log4j.status.StatusLogger;
3228
import org.apache.logging.log4j.test.ListStatusListener;
33-
import org.apache.logging.log4j.util.PropertiesUtil;
3429
import org.junit.jupiter.api.extension.BeforeAllCallback;
3530
import org.junit.jupiter.api.extension.BeforeEachCallback;
3631
import org.junit.jupiter.api.extension.ExtensionContext;
@@ -43,12 +38,12 @@
4338
import org.junit.platform.commons.support.ModifierSupport;
4439
import org.junit.platform.commons.support.ReflectionSupport;
4540

46-
class StatusLoggerExtension extends TypeBasedParameterResolver<ListStatusListener>
41+
class StatusListenerExtension extends TypeBasedParameterResolver<ListStatusListener>
4742
implements BeforeAllCallback, BeforeEachCallback, TestExecutionExceptionHandler {
4843

4944
private static final Object KEY = ListStatusListener.class;
5045

51-
public StatusLoggerExtension() {
46+
public StatusListenerExtension() {
5247
super(ListStatusListener.class);
5348
}
5449

@@ -130,33 +125,19 @@ public ListStatusListener getStatusListener() {
130125
}
131126

132127
@Override
133-
public void close() throws Throwable {
128+
public void close() {
134129
statusLogger.removeListener(statusListener);
135130
}
136131

137132
public void handleException(final ExtensionContext context, final Throwable throwable) {
138-
final Logger logger = new SimpleLogger(
139-
"StatusLoggerExtension",
140-
Level.ALL,
141-
false,
142-
false,
143-
false,
144-
false,
133+
final StatusListener listener = new StatusConsoleListener(Level.ALL, System.err);
134+
listener.log(new StatusData(
145135
null,
146-
ParameterizedNoReferenceMessageFactory.INSTANCE,
147-
PropertiesUtil.getProperties(),
148-
System.err);
149-
logger.error("Test {} failed.\nDumping status data:", context.getDisplayName());
150-
final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_TIME.withZone(ZoneId.systemDefault());
151-
statusListener.getStatusData().forEach(data -> {
152-
logger.atLevel(data.getLevel())
153-
.withThrowable(data.getThrowable())
154-
.withLocation(data.getStackTraceElement())
155-
.log(
156-
"{} {}",
157-
formatter.format(Instant.ofEpochMilli(data.getTimestamp())),
158-
data.getMessage().getFormattedMessage());
159-
});
136+
Level.ERROR,
137+
new ParameterizedMessage("Test `{}` has failed, dumping status data...", context.getDisplayName()),
138+
throwable,
139+
null));
140+
statusListener.getStatusData().forEach(listener::log);
160141
}
161142
}
162143

@@ -186,14 +167,14 @@ public Level getStatusLevel() {
186167
}
187168

188169
@Override
189-
public void close() throws IOException {
170+
public void close() {
190171
// NOP
191172
}
192173

193174
@Override
194175
public Stream<StatusData> getStatusData() {
195176
synchronized (statusData) {
196-
final List<StatusData> clone = (List<StatusData>) statusData.clone();
177+
final List<StatusData> clone = new ArrayList<>(statusData);
197178
return parent != null ? Stream.concat(parent.getStatusData(), clone.stream()) : clone.stream();
198179
}
199180
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.test.junit;
18+
19+
import static org.apache.logging.log4j.test.junit.ExtensionContextAnchor.getAttribute;
20+
import static org.apache.logging.log4j.test.junit.ExtensionContextAnchor.setAttribute;
21+
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.reset;
23+
import static org.mockito.Mockito.when;
24+
25+
import org.apache.logging.log4j.status.StatusConsoleListener;
26+
import org.apache.logging.log4j.status.StatusLogger;
27+
import org.junit.jupiter.api.extension.AfterAllCallback;
28+
import org.junit.jupiter.api.extension.BeforeAllCallback;
29+
import org.junit.jupiter.api.extension.BeforeEachCallback;
30+
import org.junit.jupiter.api.extension.ExtensionContext;
31+
32+
/**
33+
* Replaces {@link StatusLogger} static instance with a mocked one.
34+
* <p>
35+
* <b>Warning!</b>
36+
* Many classes store the result of {@link StatusLogger#getLogger()} in {@code static} field.
37+
* Hence, the mock replacement must be performed before anybody tries to access it.
38+
* Similarly, we cannot replace the mock in between tests, since it is already stored in {@code static} fields.
39+
* That is why we only reset the mocked instance before each test.
40+
* </p>
41+
*
42+
* @see UsingStatusLoggerMock
43+
*/
44+
class StatusLoggerMockExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback {
45+
46+
private static final String KEY_PREFIX = StatusLoggerMockExtension.class.getSimpleName() + '.';
47+
48+
private static final String INITIAL_STATUS_LOGGER_KEY = KEY_PREFIX + "initialStatusLogger";
49+
50+
@Override
51+
public void beforeAll(final ExtensionContext context) throws Exception {
52+
setAttribute(INITIAL_STATUS_LOGGER_KEY, StatusLogger.getLogger(), context);
53+
final StatusLogger statusLogger = mock(StatusLogger.class);
54+
stubFallbackListener(statusLogger);
55+
StatusLogger.setLogger(statusLogger);
56+
}
57+
58+
@Override
59+
public void beforeEach(final ExtensionContext context) throws Exception {
60+
final StatusLogger statusLogger = StatusLogger.getLogger();
61+
reset(statusLogger); // Stubs get reset too!
62+
stubFallbackListener(statusLogger);
63+
}
64+
65+
private static void stubFallbackListener(final StatusLogger statusLogger) {
66+
final StatusConsoleListener fallbackListener = mock(StatusConsoleListener.class);
67+
when(statusLogger.getFallbackListener()).thenReturn(fallbackListener);
68+
}
69+
70+
@Override
71+
public void afterAll(final ExtensionContext context) {
72+
final StatusLogger statusLogger = getAttribute(INITIAL_STATUS_LOGGER_KEY, StatusLogger.class, context);
73+
StatusLogger.setLogger(statusLogger);
74+
}
75+
}

log4j-api-test/src/main/java/org/apache/logging/log4j/test/junit/UsingStatusListener.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,5 @@
3737
@Documented
3838
@ExtendWith(ExtensionContextAnchor.class)
3939
@ExtendWith(TestPropertyResolver.class)
40-
@ExtendWith(StatusLoggerExtension.class)
40+
@ExtendWith(StatusListenerExtension.class)
4141
public @interface UsingStatusListener {}
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,22 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
package org.apache.logging.log4j.core.async;
17+
package org.apache.logging.log4j.test.junit;
1818

19-
import org.apache.logging.log4j.core.LoggerContext;
20-
import org.apache.logging.log4j.core.test.junit.Tags;
21-
import org.apache.logging.log4j.test.junit.SetTestProperty;
22-
import org.junit.jupiter.api.Tag;
19+
import static java.lang.annotation.ElementType.METHOD;
20+
import static java.lang.annotation.ElementType.TYPE;
21+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
22+
23+
import java.lang.annotation.Documented;
24+
import java.lang.annotation.Retention;
25+
import java.lang.annotation.Target;
26+
import org.junit.jupiter.api.extension.ExtendWith;
2327

2428
/**
25-
* Tests queue full scenarios with AsyncLoggers in configuration.
29+
* Shortcut to {@link StatusLoggerMockExtension}.
2630
*/
27-
@SetTestProperty(key = "log4j2.formatMsgAsync", value = "true")
28-
@Tag(Tags.ASYNC_LOGGERS)
29-
public class QueueFullAsyncLoggerConfigLoggingFromToStringTest2
30-
extends QueueFullAsyncLoggerConfigLoggingFromToStringTest {
31-
32-
@Override
33-
protected void checkConfig(final LoggerContext ctx) throws ReflectiveOperationException {
34-
super.checkConfig(ctx);
35-
assertFormatMessagesInBackground();
36-
}
37-
}
31+
@Retention(RUNTIME)
32+
@Target({TYPE, METHOD})
33+
@Documented
34+
@ExtendWith({ExtensionContextAnchor.class, StatusLoggerMockExtension.class})
35+
public @interface UsingStatusLoggerMock {}

log4j-api-test/src/test/java/org/apache/logging/log4j/AbstractLoggerTest.java

+3
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,12 @@
4242
import org.junit.jupiter.api.Test;
4343
import org.junit.jupiter.api.parallel.ResourceAccessMode;
4444
import org.junit.jupiter.api.parallel.ResourceLock;
45+
import org.junitpioneer.jupiter.SetSystemProperty;
4546

4647
@StatusLoggerLevel("WARN")
4748
@ResourceLock(value = Resources.MARKER_MANAGER, mode = ResourceAccessMode.READ)
49+
@SetSystemProperty(key = "log4j2.status.entries", value = "200")
50+
@SetSystemProperty(key = "log4j2.StatusLogger.level", value = "WARN")
4851
public class AbstractLoggerTest {
4952

5053
private static final StringBuilder CHAR_SEQ = new StringBuilder("CharSeq");

log4j-api-test/src/test/java/org/apache/logging/log4j/status/StatusConsoleListenerTest.java

+4-25
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,9 @@
1919
import java.io.ByteArrayOutputStream;
2020
import java.io.PrintStream;
2121
import org.apache.logging.log4j.Level;
22-
import org.apache.logging.log4j.LogBuilder;
2322
import org.apache.logging.log4j.message.Message;
2423
import org.apache.logging.log4j.message.MessageFactory;
2524
import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
26-
import org.apache.logging.log4j.simple.SimpleLogger;
2725
import org.assertj.core.api.Assertions;
2826
import org.junit.jupiter.api.Test;
2927
import org.mockito.Mockito;
@@ -33,38 +31,19 @@ public class StatusConsoleListenerTest {
3331
public static final MessageFactory MESSAGE_FACTORY = ParameterizedNoReferenceMessageFactory.INSTANCE;
3432

3533
@Test
36-
void SimpleLogger_should_be_used() {
37-
38-
// Create a mock `SimpleLoggerFactory`.
39-
final SimpleLogger logger = Mockito.mock(SimpleLogger.class);
40-
final LogBuilder logBuilder = Mockito.mock(LogBuilder.class);
41-
Mockito.when(logger.atLevel(Mockito.any())).thenReturn(logBuilder);
42-
Mockito.when(logBuilder.withThrowable(Mockito.any())).thenReturn(logBuilder);
43-
Mockito.when(logBuilder.withLocation(Mockito.any())).thenReturn(logBuilder);
44-
final SimpleLoggerFactory loggerFactory = Mockito.mock(SimpleLoggerFactory.class);
45-
Mockito.when(loggerFactory.createSimpleLogger(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()))
46-
.thenReturn(logger);
34+
void StatusData_getFormattedStatus_should_be_used() {
4735

4836
// Create the listener.
4937
final PrintStream stream = Mockito.mock(PrintStream.class);
50-
final Level level = Mockito.mock(Level.class);
51-
final StatusConsoleListener listener = new StatusConsoleListener(level, stream, loggerFactory);
38+
final StatusConsoleListener listener = new StatusConsoleListener(Level.ALL, stream);
5239

5340
// Log a message.
54-
final StackTraceElement caller = Mockito.mock(StackTraceElement.class);
5541
final Message message = Mockito.mock(Message.class);
56-
final Throwable throwable = Mockito.mock(Throwable.class);
57-
final StatusData statusData = new StatusData(caller, level, message, throwable, null);
42+
final StatusData statusData = Mockito.spy(new StatusData(null, Level.TRACE, message, null, null));
5843
listener.log(statusData);
5944

6045
// Verify the call.
61-
Mockito.verify(loggerFactory)
62-
.createSimpleLogger(
63-
Mockito.eq("StatusConsoleListener"), Mockito.same(level), Mockito.any(), Mockito.same(stream));
64-
Mockito.verify(logger).atLevel(Mockito.same(level));
65-
Mockito.verify(logBuilder).withThrowable(Mockito.same(throwable));
66-
Mockito.verify(logBuilder).withLocation(Mockito.same(caller));
67-
Mockito.verify(logBuilder).log(Mockito.same(message));
46+
Mockito.verify(statusData).getFormattedStatus();
6847
}
6948

7049
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.status;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.when;
22+
23+
import org.apache.logging.log4j.Level;
24+
import org.junit.jupiter.api.Test;
25+
26+
class StatusLoggerLevelTest {
27+
28+
@Test
29+
void effective_level_should_be_the_least_specific_one() {
30+
31+
// Verify the initial level
32+
final StatusLogger logger = StatusLogger.getLogger();
33+
final Level fallbackListenerLevel = Level.ERROR;
34+
assertThat(logger.getLevel()).isEqualTo(fallbackListenerLevel);
35+
36+
// Register a less specific listener
37+
final StatusListener listener1 = mock(StatusListener.class);
38+
Level listener1Level = Level.WARN;
39+
when(listener1.getStatusLevel()).thenReturn(listener1Level);
40+
logger.registerListener(listener1);
41+
assertThat(listener1Level).isNotEqualTo(fallbackListenerLevel); // Verify that the level is distinct
42+
assertThat(logger.getLevel()).isEqualTo(listener1Level); // Verify that the logger level is changed
43+
44+
// Register a less specific listener
45+
final StatusListener listener2 = mock(StatusListener.class);
46+
final Level listener2Level = Level.INFO;
47+
when(listener2.getStatusLevel()).thenReturn(listener2Level);
48+
logger.registerListener(listener2);
49+
assertThat(listener2Level)
50+
.isNotEqualTo(fallbackListenerLevel)
51+
.isNotEqualTo(listener1Level); // Verify that the level is distinct
52+
assertThat(logger.getLevel()).isEqualTo(listener2Level); // Verify that the logger level is changed
53+
54+
// Register a more specific listener
55+
final StatusListener listener3 = mock(StatusListener.class);
56+
final Level listener3Level = Level.ERROR;
57+
when(listener3.getStatusLevel()).thenReturn(listener3Level);
58+
logger.registerListener(listener3);
59+
assertThat(listener3Level)
60+
.isNotEqualTo(listener1Level)
61+
.isNotEqualTo(listener2Level); // Verify that the level is distinct
62+
assertThat(logger.getLevel()).isEqualTo(listener2Level); // Verify that the logger level is not changed
63+
64+
// Update a registered listener level
65+
listener1Level = Level.DEBUG;
66+
when(listener1.getStatusLevel()).thenReturn(listener1Level);
67+
assertThat(listener1Level) // Verify that the level is distinct
68+
.isNotEqualTo(fallbackListenerLevel)
69+
.isNotEqualTo(listener2Level)
70+
.isNotEqualTo(listener3Level);
71+
assertThat(logger.getLevel()).isEqualTo(listener1Level); // Verify that the logger level is changed
72+
73+
// Remove the least specific listener
74+
logger.removeListener(listener2);
75+
assertThat(logger.getLevel()).isEqualTo(listener1Level); // Verify that the level is changed
76+
77+
// Remove the most specific listener
78+
logger.removeListener(listener3);
79+
assertThat(logger.getLevel()).isEqualTo(listener1Level); // Verify that the level is not changed
80+
81+
// Remove the last listener
82+
logger.removeListener(listener1);
83+
assertThat(logger.getLevel()).isEqualTo(fallbackListenerLevel); // Verify that the level is changed
84+
}
85+
}

0 commit comments

Comments
 (0)