Skip to content

Commit dba68fd

Browse files
committed
Add JUnit 5 test-method-scoped LoggerContexts
1 parent 98c3ceb commit dba68fd

File tree

6 files changed

+281
-0
lines changed

6 files changed

+281
-0
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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.core.test.junit;
18+
19+
import org.apache.logging.log4j.Level;
20+
import org.apache.logging.log4j.core.LogEvent;
21+
import org.apache.logging.log4j.core.LoggerContext;
22+
import org.apache.logging.log4j.core.appender.AbstractAppender;
23+
import org.apache.logging.log4j.core.config.LoggerConfig;
24+
import org.apache.logging.log4j.core.impl.MutableLogEvent;
25+
import org.apache.logging.log4j.core.layout.PatternLayout;
26+
27+
import java.util.ArrayList;
28+
import java.util.Collections;
29+
import java.util.List;
30+
import java.util.concurrent.atomic.AtomicInteger;
31+
32+
public final class Log4jEventRecorder implements AutoCloseable {
33+
34+
private static final String LOGGER_CONTEXT_NAME_PREFIX = Log4jEventRecorder.class.getSimpleName() + "-LoggerContext-";
35+
36+
private static final AtomicInteger LOGGER_CONTEXT_COUNTER = new AtomicInteger(0);
37+
38+
private final InternalLog4jAppender appender;
39+
40+
private final LoggerContext loggerContext;
41+
42+
private static final class InternalLog4jAppender extends AbstractAppender {
43+
44+
private static final PatternLayout LAYOUT = PatternLayout.createDefaultLayout();
45+
46+
private final List<LogEvent> events;
47+
48+
private InternalLog4jAppender() {
49+
super("ListAppender", null, LAYOUT, false, null);
50+
this.events = Collections.synchronizedList(new ArrayList<>());
51+
start();
52+
}
53+
54+
@Override
55+
public void append(final LogEvent event) {
56+
final LogEvent copySafeEvent = event instanceof MutableLogEvent
57+
? ((MutableLogEvent) event).createMemento()
58+
: event;
59+
events.add(copySafeEvent);
60+
}
61+
62+
}
63+
64+
Log4jEventRecorder() {
65+
this.appender = new InternalLog4jAppender();
66+
this.loggerContext = new LoggerContext(LOGGER_CONTEXT_NAME_PREFIX + LOGGER_CONTEXT_COUNTER.getAndIncrement());
67+
final LoggerConfig rootConfig = loggerContext.getConfiguration().getRootLogger();
68+
rootConfig.setLevel(Level.ALL);
69+
rootConfig.getAppenders().values().forEach(appender -> rootConfig.removeAppender(appender.getName()));
70+
rootConfig.addAppender(appender, Level.ALL, null);
71+
}
72+
73+
public org.apache.logging.log4j.spi.LoggerContext getLoggerContext() {
74+
return loggerContext;
75+
}
76+
77+
public List<LogEvent> getEvents() {
78+
return appender.events;
79+
}
80+
81+
@Override
82+
public void close() {
83+
loggerContext.close();
84+
}
85+
86+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.core.test.junit;
18+
19+
import org.junit.jupiter.api.extension.ExtensionContext;
20+
import org.junit.jupiter.api.extension.ParameterContext;
21+
22+
import java.util.Collection;
23+
import java.util.LinkedHashMap;
24+
import java.util.Map;
25+
26+
final class Log4jEventRecorderAnchor {
27+
28+
private Log4jEventRecorderAnchor() {}
29+
30+
static Log4jEventRecorder recorder(
31+
final ExtensionContext extensionContext,
32+
final ParameterContext parameterContext) {
33+
return recorderByParameterName(extensionContext).computeIfAbsent(
34+
parameterContext.getParameter().getName(),
35+
ignored -> new Log4jEventRecorder());
36+
}
37+
38+
static Collection<Log4jEventRecorder> recorders(final ExtensionContext extensionContext) {
39+
return recorderByParameterName(extensionContext).values();
40+
}
41+
42+
private static Map<String, Log4jEventRecorder> recorderByParameterName(final ExtensionContext extensionContext) {
43+
ExtensionContext.Namespace namespace = ExtensionContext.Namespace.create(
44+
Log4jEventRecorder.class,
45+
extensionContext.getRequiredTestClass(),
46+
extensionContext.getRequiredTestMethod());
47+
final ExtensionContext.Store store = extensionContext.getStore(namespace);
48+
@SuppressWarnings("unchecked")
49+
final Map<String, Log4jEventRecorder> recorderByParameterName = store.getOrComputeIfAbsent(
50+
"recorderByParameterName",
51+
ignored -> new LinkedHashMap<>(),
52+
Map.class);
53+
return recorderByParameterName;
54+
}
55+
56+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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.core.test.junit;
18+
19+
import org.junit.jupiter.api.extension.ExtendWith;
20+
21+
import java.lang.annotation.ElementType;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
/**
27+
* Enables JUnit support resolving test method parameters of type {@link Log4jEventRecorder}.
28+
*/
29+
@Target({ElementType.TYPE, ElementType.METHOD})
30+
@Retention(RetentionPolicy.RUNTIME)
31+
@ExtendWith({Log4jEventRecorderTerminator.class, Log4jEventRecorderParameterResolver.class})
32+
public @interface Log4jEventRecorderEnabled {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.core.test.junit;
18+
19+
import org.junit.jupiter.api.extension.ExtensionContext;
20+
import org.junit.jupiter.api.extension.ParameterContext;
21+
import org.junit.jupiter.api.extension.ParameterResolutionException;
22+
import org.junit.jupiter.api.extension.support.TypeBasedParameterResolver;
23+
24+
public final class Log4jEventRecorderParameterResolver extends TypeBasedParameterResolver<Log4jEventRecorder> {
25+
26+
@Override
27+
public Log4jEventRecorder resolveParameter(
28+
final ParameterContext parameterContext,
29+
final ExtensionContext extensionContext
30+
) throws ParameterResolutionException {
31+
return Log4jEventRecorderAnchor.recorder(extensionContext, parameterContext);
32+
}
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.core.test.junit;
18+
19+
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
20+
import org.junit.jupiter.api.extension.ExtensionContext;
21+
22+
public final class Log4jEventRecorderTerminator implements AfterTestExecutionCallback {
23+
24+
@Override
25+
public void afterTestExecution(final ExtensionContext extensionContext) {
26+
Log4jEventRecorderAnchor.recorders(extensionContext).forEach(Log4jEventRecorder::close);
27+
}
28+
29+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package org.apache.logging.log4j.core.test.junit;
2+
3+
import org.apache.logging.log4j.Level;
4+
import org.apache.logging.log4j.Logger;
5+
import org.apache.logging.log4j.core.LogEvent;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.util.List;
9+
import java.util.stream.Collectors;
10+
import java.util.stream.IntStream;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
@Log4jEventRecorderEnabled
15+
class Log4jEventRecorderTest {
16+
17+
@Test
18+
void should_succeed_when_run_even_in_parallel(final Log4jEventRecorder eventRecorder) {
19+
20+
// Log events
21+
final int eventCount = 3;//1 + (int) (Math.random() * 1000D);
22+
final Logger logger = eventRecorder.getLoggerContext().getLogger(Log4jEventRecorderTest.class);
23+
for (int eventIndex = 0; eventIndex < eventCount; eventIndex++) {
24+
logger.trace("test message {}", eventIndex);
25+
}
26+
27+
// Verify logged levels
28+
final List<LogEvent> events = eventRecorder.getEvents();
29+
assertThat(events).allMatch(event -> Level.TRACE.equals(event.getLevel()));
30+
31+
// Verify logged messages
32+
final List<String> expectedMessages = IntStream
33+
.range(0, eventCount)
34+
.mapToObj(eventIndex -> String.format("test message %d", eventIndex))
35+
.collect(Collectors.toList());
36+
final List<String> actualMessages = events
37+
.stream()
38+
.map(event -> event.getMessage().getFormattedMessage())
39+
.collect(Collectors.toList());
40+
assertThat(actualMessages).containsExactlyElementsOf(expectedMessages);
41+
42+
}
43+
44+
}

0 commit comments

Comments
 (0)