Skip to content

Commit cfb8701

Browse files
committed
feat: add support for JUL
1 parent 37da5f6 commit cfb8701

File tree

8 files changed

+351
-9
lines changed

8 files changed

+351
-9
lines changed

README.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,36 @@ A Java library for unit-testing logging.
1010

1111
Sometimes, writing specific information into its log(file) is an important part of an application's functionality. As such, it's probably a good idea to cover that behavior in the application's unit tests.
1212

13-
Although there are other solutions to achieve this (like e.g. using mocks and test for the methods to be called), LogUnit aims at testing on another level: right within the logging framework, so you can still see all your logs while testing. No matter how you generate your log messages in your project, if they end up in the popular [Slf4j](https://www.slf4j.org) library, you can use LogUnit (\*).
13+
Although there are other solutions to achieve this (like e.g. using mocks and test for the methods to be called), LogUnit aims at testing on another level: right within the logging framework, so you can still see all your logs while testing. No matter how you generate your log messages in your project, if they end up in the popular [Slf4j](https://www.slf4j.org) library or in java.util.logging, you can use LogUnit (\*).
1414

1515

1616
## Requirements
1717

1818
- Java 8 or above
1919
- JUnit 5
20-
- You must be using Slf4j for logging
21-
- You must be using Logback as log binding (\*)
20+
- You must be using Slf4j or java.util.logging for logging
21+
- You must be using Logback or java.util.logging as log binding (\*)
2222

2323

2424
## Installation
2525

2626
Add LogUnit to your project's dependencies.
2727

2828
* Declare `logunit-core` as compile-time dependency
29-
* Declare the binding-specific module (e.g. `logunit-logback` (\*) as test-runtime dependency
29+
* Declare the binding-specific module (e.g. `logunit-logback` or `logunit-jul` (\*)) as test-runtime dependency
3030

3131
```
3232
dependencies {
3333
...
34-
testImplementation("io.github.netmikey.logunit:logunit-core:1.0.1")
35-
testRuntimeOnly("io.github.netmikey.logunit:logunit-logback:1.0.1")
34+
testImplementation("io.github.netmikey.logunit:logunit-core:1.1.0")
35+
36+
// Choose one (and only one) of the following:
37+
38+
// for Logback:
39+
// testRuntimeOnly("io.github.netmikey.logunit:logunit-logback:1.1.0")
40+
41+
// for JUL:
42+
// testRuntimeOnly("io.github.netmikey.logunit:logunit-jul:1.1.0")
3643
}
3744
```
3845

@@ -92,9 +99,9 @@ See [LogCapturerWithLogbackTest.java](https://github.com/netmikey/logunit/blob/m
9299

93100
LogUnit wants to remain as transparent as possible. That means your unit tests' logs should stay the way they are (or at least as close as possible). As such, we don't want to bring and force our own Slf4j binding implementation onto consuming projects.
94101

95-
Therefor, LogUnit's architecture is similar to Slf4j's: At it's core, it uses the Slf4j API but in order to work at runtime, it provides binding-specific modules for the most popular logging frameworks (\*).
102+
Therefor, LogUnit's architecture is similar to Slf4j's: At it's core, it uses the Slf4j API but in order to work at runtime, it provides binding-specific modules for hooking into the most popular logging frameworks (\*).
96103

97104

98105
## Limitations
99106

100-
(\*) Currently, only Logback is supported.
107+
(\*) Currently, only Logback and java.util.logging are supported.

logunit-jul/build.gradle

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
description = "LogUnit's JUL (java.util.logging) implementation."
3+
4+
apply from: new File(rootProject.projectDir, 'publishing-build.gradle')
5+
6+
dependencies {
7+
api project(':logunit-core')
8+
9+
implementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
10+
11+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
12+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package io.github.netmikey.logunit.jul;
2+
3+
import java.util.Collections;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.logging.Level;
8+
import java.util.logging.LogRecord;
9+
import java.util.logging.Logger;
10+
import java.util.stream.Collectors;
11+
import java.util.stream.StreamSupport;
12+
13+
import org.junit.jupiter.api.extension.ExtensionContext;
14+
import org.slf4j.Marker;
15+
import org.slf4j.event.LoggingEvent;
16+
17+
import io.github.netmikey.logunit.api.LogCapturer;
18+
import io.github.netmikey.logunit.api.LogProvider;
19+
20+
/**
21+
* {@link LogCapturer} implementation based on JUL.
22+
*/
23+
public class JulLogProvider implements LogProvider {
24+
25+
private static final Map<org.slf4j.event.Level, java.util.logging.Level> LEVEL_MAPPING;
26+
27+
private static final Map<java.util.logging.Level, org.slf4j.event.Level> LEVEL_MAPPING_REVERSE;
28+
29+
static {
30+
Map<org.slf4j.event.Level, java.util.logging.Level> levelMapping = new HashMap<>();
31+
levelMapping.put(org.slf4j.event.Level.TRACE, java.util.logging.Level.FINEST);
32+
levelMapping.put(org.slf4j.event.Level.DEBUG, java.util.logging.Level.FINE);
33+
levelMapping.put(org.slf4j.event.Level.INFO, java.util.logging.Level.INFO);
34+
levelMapping.put(org.slf4j.event.Level.WARN, java.util.logging.Level.WARNING);
35+
levelMapping.put(org.slf4j.event.Level.ERROR, java.util.logging.Level.SEVERE);
36+
37+
LEVEL_MAPPING = Collections.unmodifiableMap(levelMapping);
38+
39+
Map<java.util.logging.Level, org.slf4j.event.Level> levelMappingReverse = new HashMap<>();
40+
levelMapping.forEach((key, value) -> levelMappingReverse.put(value, key));
41+
levelMappingReverse.put(java.util.logging.Level.CONFIG, org.slf4j.event.Level.INFO);
42+
levelMappingReverse.put(java.util.logging.Level.FINER, org.slf4j.event.Level.DEBUG);
43+
44+
LEVEL_MAPPING_REVERSE = Collections.unmodifiableMap(levelMappingReverse);
45+
}
46+
47+
private final ListHandler listHandler = new ListHandler();
48+
49+
private final Map<String, Level> loggerNames = new HashMap<>();
50+
51+
private final Map<String, Level> originalLevels = new HashMap<>();
52+
53+
@Override
54+
public void provideForType(Class<?> type, org.slf4j.event.Level level) {
55+
provideForLogger(type.getName(), level);
56+
}
57+
58+
@Override
59+
public void provideForLogger(String name, org.slf4j.event.Level level) {
60+
if (loggerNames.containsKey(name)) {
61+
throw new IllegalArgumentException("LogProvider already providing LogEvents for Logger with name "
62+
+ name + ". Each logger must only be captured once!");
63+
}
64+
loggerNames.put(name, mapLevel(level));
65+
}
66+
67+
@Override
68+
public List<LoggingEvent> getEvents() {
69+
return StreamSupport.stream(listHandler.spliterator(), false)
70+
.map(this::mapEvent)
71+
.collect(Collectors.toList());
72+
}
73+
74+
@Override
75+
public void beforeTestExecution(ExtensionContext context) {
76+
addAppenderToLoggingSources();
77+
}
78+
79+
@Override
80+
public void afterTestExecution(ExtensionContext context) {
81+
listHandler.flush();
82+
listHandler.close();
83+
detachAppenderFromLoggingSources();
84+
}
85+
86+
private void addAppenderToLoggingSources() {
87+
for (Map.Entry<String, Level> logSource : loggerNames.entrySet()) {
88+
addAppenderToLogger(logSource.getKey(), logSource.getValue());
89+
}
90+
}
91+
92+
private void detachAppenderFromLoggingSources() {
93+
for (Map.Entry<String, Level> logSource : loggerNames.entrySet()) {
94+
detachAppenderFromLogger(logSource.getKey());
95+
}
96+
}
97+
98+
private void addAppenderToLogger(String name, Level level) {
99+
addAppenderToLogger((Logger) Logger.getLogger(name), level);
100+
}
101+
102+
private void addAppenderToLogger(Logger logger, Level level) {
103+
logger.addHandler(listHandler);
104+
originalLevels.put(logger.getName(), logger.getLevel());
105+
logger.setLevel(level);
106+
}
107+
108+
private void detachAppenderFromLogger(String name) {
109+
detachAppenderFromLogger((Logger) Logger.getLogger(name));
110+
}
111+
112+
private void detachAppenderFromLogger(Logger logger) {
113+
logger.removeHandler(listHandler);
114+
Level originalLevel = originalLevels.get(logger.getName());
115+
if (originalLevel != null) {
116+
logger.setLevel(originalLevel);
117+
}
118+
}
119+
120+
private LoggingEvent mapEvent(LogRecord record) {
121+
return new LoggingEvent() {
122+
123+
@Override
124+
public long getTimeStamp() {
125+
return record.getMillis();
126+
}
127+
128+
@Override
129+
public Throwable getThrowable() {
130+
return record.getThrown();
131+
}
132+
133+
@Override
134+
public String getThreadName() {
135+
return String.valueOf(record.getThreadID());
136+
}
137+
138+
@Override
139+
public String getMessage() {
140+
return record.getMessage();
141+
}
142+
143+
@Override
144+
public Marker getMarker() {
145+
return null;
146+
}
147+
148+
@Override
149+
public String getLoggerName() {
150+
return record.getLoggerName();
151+
}
152+
153+
@Override
154+
public org.slf4j.event.Level getLevel() {
155+
return mapLevel(record.getLevel());
156+
}
157+
158+
@Override
159+
public Object[] getArgumentArray() {
160+
return record.getParameters();
161+
}
162+
};
163+
}
164+
165+
private Level mapLevel(org.slf4j.event.Level level) {
166+
Level result = LEVEL_MAPPING.get(level);
167+
if (result == null) {
168+
throw new IllegalArgumentException("Cannot map log level " + level + " to a JUL log level");
169+
}
170+
return result;
171+
}
172+
173+
private org.slf4j.event.Level mapLevel(Level level) {
174+
org.slf4j.event.Level result = LEVEL_MAPPING_REVERSE.get(level);
175+
if (result == null) {
176+
throw new IllegalArgumentException("Cannot map JUL log level " + level + " to an slf4j log level");
177+
}
178+
return result;
179+
}
180+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.github.netmikey.logunit.jul;
2+
3+
import io.github.netmikey.logunit.api.LogProvider;
4+
import io.github.netmikey.logunit.api.LogProviderFactory;
5+
6+
/**
7+
* JUL implementation of the {@link LogProviderFactory} SPI.
8+
*/
9+
public class JulLogProviderFactory implements LogProviderFactory {
10+
11+
@Override
12+
public LogProvider create() {
13+
return new JulLogProvider();
14+
}
15+
16+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package io.github.netmikey.logunit.jul;
2+
3+
import java.util.Spliterator;
4+
import java.util.concurrent.ConcurrentLinkedQueue;
5+
import java.util.logging.Handler;
6+
import java.util.logging.LogRecord;
7+
8+
/**
9+
* A {@link Handler} implementation that captures {@link LogRecord}s in a
10+
* thread-safe list.
11+
*/
12+
public class ListHandler extends Handler {
13+
14+
private ConcurrentLinkedQueue<LogRecord> list = new ConcurrentLinkedQueue<>();
15+
16+
@Override
17+
public void publish(LogRecord record) {
18+
list.add(record);
19+
}
20+
21+
@Override
22+
public void flush() {
23+
// Nothing to do.
24+
}
25+
26+
@Override
27+
public void close() throws SecurityException {
28+
// Nothing to do.
29+
}
30+
31+
/**
32+
* Get the items.
33+
*
34+
* @return A {@link Spliterator} over the items.
35+
*/
36+
public Spliterator<LogRecord> spliterator() {
37+
return list.spliterator();
38+
}
39+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
io.github.netmikey.logunit.jul.JulLogProviderFactory
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package io.github.netmikey.logunit.jul;
2+
3+
import java.util.logging.Logger;
4+
5+
import org.junit.jupiter.api.Assertions;
6+
import org.junit.jupiter.api.MethodOrderer.Alphanumeric;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.TestMethodOrder;
9+
import org.junit.jupiter.api.extension.RegisterExtension;
10+
import org.slf4j.event.Level;
11+
12+
import io.github.netmikey.logunit.api.LogCapturer;
13+
14+
/**
15+
* Unit test that uses JUL, applies {@link LogCapturer}s and validates their
16+
* behavior.
17+
*/
18+
@TestMethodOrder(Alphanumeric.class)
19+
public class LogCapturerWithJulTest {
20+
21+
@RegisterExtension
22+
LogCapturer testLoggerInfoCapturer = LogCapturer.create().captureForType(LogCapturerWithJulTest.class);
23+
24+
@RegisterExtension
25+
LogCapturer namedLoggerWarnCapturer = LogCapturer.create().captureForLogger(LOGGER_NAME, Level.WARN);
26+
27+
private static final String LOGGER_NAME = "CUSTOM_LOGGER";
28+
29+
private Logger testLogger = Logger.getLogger(LogCapturerWithJulTest.class.getName());
30+
31+
private Logger namedLogger = Logger.getLogger(LOGGER_NAME);
32+
33+
/**
34+
* Test
35+
* <ul>
36+
* <li>that the testLogger (by logger type) captures the INFO level and
37+
* above when no level is specified</li>
38+
* <li>that the namedLogger (by logger name) captures only the WARN level
39+
* and above as specified</li>
40+
* <li>both loggers and their capturers don't affeact each other</li>
41+
* </ul>
42+
*/
43+
@Test
44+
public void test1CaptureMessages() {
45+
logEverythingOnce(testLogger);
46+
logEverythingOnce(namedLogger);
47+
48+
Assertions.assertEquals(3, testLoggerInfoCapturer.size(),
49+
"should contain each one of INFO, WARNING and SEVERE");
50+
testLoggerInfoCapturer.assertDoesNotContain("finest");
51+
testLoggerInfoCapturer.assertDoesNotContain("finer");
52+
testLoggerInfoCapturer.assertDoesNotContain("fine");
53+
testLoggerInfoCapturer.assertDoesNotContain("config");
54+
testLoggerInfoCapturer.assertContains("info message");
55+
testLoggerInfoCapturer.assertContains("Some warning");
56+
testLoggerInfoCapturer.assertContains("severe");
57+
58+
Assertions.assertEquals(2, namedLoggerWarnCapturer.size(), "should contain each one of WARNING and SEVERE");
59+
namedLoggerWarnCapturer.assertDoesNotContain("finest");
60+
namedLoggerWarnCapturer.assertDoesNotContain("finer");
61+
namedLoggerWarnCapturer.assertDoesNotContain("fine");
62+
namedLoggerWarnCapturer.assertDoesNotContain("config");
63+
namedLoggerWarnCapturer.assertDoesNotContain("info message");
64+
namedLoggerWarnCapturer.assertContains("Some warning");
65+
namedLoggerWarnCapturer.assertContains("severe");
66+
}
67+
68+
/**
69+
* Test that {@link LogCapturer}s are being reset after each test.
70+
*/
71+
@Test
72+
public void test2CapturerReset() {
73+
Assertions.assertEquals(0, testLoggerInfoCapturer.size());
74+
Assertions.assertEquals(0, namedLoggerWarnCapturer.size());
75+
}
76+
77+
private void logEverythingOnce(Logger logger) {
78+
logger.finest("Some finest message");
79+
logger.finer("Some finer message");
80+
logger.fine("Some fine message");
81+
logger.config("Some config message");
82+
logger.info("Some info message");
83+
logger.warning("Some warning message");
84+
logger.severe("Some severe message");
85+
}
86+
}

0 commit comments

Comments
 (0)