Skip to content

Instantly share code, notes, and snippets.

@jburwell
Last active April 20, 2016 20:25
Show Gist options
  • Save jburwell/98b6d94c57f409b5b778ffee6a8a12b1 to your computer and use it in GitHub Desktop.
Save jburwell/98b6d94c57f409b5b778ffee6a8a12b1 to your computer and use it in GitHub Desktop.
Test Appender
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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 com.cloud.test.util;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableMap;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static org.apache.log4j.Level.ALL;
import static org.apache.log4j.Level.DEBUG;
import static org.apache.log4j.Level.ERROR;
import static org.apache.log4j.Level.FATAL;
import static org.apache.log4j.Level.INFO;
import static org.apache.log4j.Level.OFF;
import static org.junit.Assert.fail;
/**
*
* Tracks one or more patterns to determine whether or not they have been
* logged. It uses a streaming approach to determine whether or not a message
* has a occurred to prevent unnecessary memory consumption. Instances of this
* of this class are created using the {@link TestAppenderBuilder}.
*
* To use this class, register a one or more expected patterns by level as part
* of the test setup and retain an reference to the appender instance. After the
* expected logging events have occurred in the test case, call
* {@link TestAppender#assertMessagesLogged()} which will fail the test if any of the
* expected patterns were not logged.
*
*/
public final class TestAppender extends AppenderSkeleton {
private final static String APPENDER_NAME = "test_appender";
private final ImmutableMap<Level, Set<PatternResult>> expectedPatternResults;
private TestAppender(final Map<Level, Set<PatternResult>> expectedPatterns) {
super();
expectedPatternResults = ImmutableMap.copyOf(expectedPatterns);
}
protected void append(LoggingEvent loggingEvent) {
checkArgument(loggingEvent != null, "append requires a non-null loggingEvent");
final Level level = loggingEvent.getLevel();
checkState(expectedPatternResults.containsKey(level), "level " + level + " not supported by append");
for (final PatternResult patternResult : expectedPatternResults.get(level)) {
if (patternResult.getPattern().matcher(loggingEvent.getRenderedMessage()).matches()) {
patternResult.markFound();
}
}
}
public void close() {
// Do nothing ...
}
public boolean requiresLayout() {
return false;
}
public void assertMessagesLogged() {
final List<String> unloggedPatterns = new ArrayList<>();
for (final Map.Entry<Level, Set<PatternResult>> expectedPatternResult : expectedPatternResults.entrySet()) {
for (final PatternResult patternResults : expectedPatternResult.getValue()) {
if (!patternResults.isFound()) {
unloggedPatterns.add(format("%1$s was not logged for level %2$s",
patternResults.getPattern().toString(), expectedPatternResult.getKey()));
}
}
}
if (!unloggedPatterns.isEmpty()) {
fail(Joiner.on(",").join(unloggedPatterns));
}
}
private static final class PatternResult {
private final Pattern pattern;
private boolean foundFlag = false;
private PatternResult(Pattern pattern) {
super();
this.pattern = pattern;
}
public Pattern getPattern() {
return pattern;
}
public void markFound() {
// This operation is thread-safe because the value will only ever be switched from false to true. Therefore,
// multiple threads mutating the value for a pattern will not corrupt the value ...
foundFlag = true;
}
public boolean isFound() {
return foundFlag;
}
@Override
public boolean equals(Object thatObject) {
if (this == thatObject) {
return true;
}
if (thatObject == null || getClass() != thatObject.getClass()) {
return false;
}
PatternResult thatPatternResult = (PatternResult) thatObject;
return foundFlag == thatPatternResult.foundFlag &&
Objects.equal(pattern, thatPatternResult.pattern);
}
@Override
public int hashCode() {
return Objects.hashCode(pattern, foundFlag);
}
@Override
public String toString() {
return format("Pattern Result [ pattern: %1$s, markFound: %2$s ]", pattern.toString(), foundFlag);
}
}
public static final class TestAppenderBuilder {
private final Map<Level, Set<PatternResult>> expectedPatterns;
public TestAppenderBuilder() {
super();
expectedPatterns = new HashMap<>();
expectedPatterns.put(ALL, new HashSet<PatternResult>());
expectedPatterns.put(DEBUG, new HashSet<PatternResult>());
expectedPatterns.put(ERROR, new HashSet<PatternResult>());
expectedPatterns.put(FATAL, new HashSet<PatternResult>());
expectedPatterns.put(INFO, new HashSet<PatternResult>());
expectedPatterns.put(OFF, new HashSet<PatternResult>());
}
public TestAppenderBuilder addExpectedPattern(final Level level, final String pattern) {
checkArgument(level != null, "addExpectedPattern requires a non-null level");
checkArgument(isNullOrEmpty(pattern), "addExpectedPattern requires a non-blank pattern");
checkState(expectedPatterns.containsKey(level), "level " + level + " is not supported by " + getClass().getName());
expectedPatterns.get(level).add(new PatternResult(Pattern.compile(pattern)));
return this;
}
public TestAppender build() {
return new TestAppender(expectedPatterns);
}
}
/**
*
* Attaches a {@link TestAppender} to a {@link Logger} and ensures that it is the only
* test appender attached to the logger.
*
* @param logger The logger which will be monitored by the test
* @param testAppender The test appender to attach to {@code logger}
*/
public static void safeAddAppender(Logger logger, TestAppender testAppender) {
logger.removeAppender(APPENDER_NAME);
logger.addAppender(testAppender);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment