/* * 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; 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 org.springframework.util.Assert; 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; /** * * 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()) { //Raise an assert Assert.isTrue(false, 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); } }