View Javadoc
1   package com.atlassian.plugin.test;
2   
3   import com.google.common.base.Function;
4   import com.google.common.collect.Lists;
5   import org.apache.log4j.Appender;
6   import org.apache.log4j.AppenderSkeleton;
7   import org.apache.log4j.Level;
8   import org.apache.log4j.LogManager;
9   import org.apache.log4j.Logger;
10  import org.apache.log4j.spi.LoggingEvent;
11  import org.hamcrest.Description;
12  import org.hamcrest.Matcher;
13  import org.hamcrest.TypeSafeMatcher;
14  import org.junit.rules.ExternalResource;
15  
16  import java.util.ArrayList;
17  import java.util.List;
18  import java.util.Optional;
19  
20  import static com.atlassian.plugin.test.Matchers.containsAllStrings;
21  import static org.hamcrest.CoreMatchers.allOf;
22  import static org.hamcrest.Matchers.hasItem;
23  
24  /**
25   * A JUnit Rule for capturing and verifying log messages from a given class.
26   *
27   * This implementation is Log4J specific, but the interface is framed in SLF4J terminology. The intent is that if we need to switch
28   * to a different logger we should be able to preserve the interface, since it mirrors the SLF4J interface used by non test code.
29   * The implementation uses a Log4j appender to capture log messages for verification.
30   *
31   * @since 3.2.16
32   */
33  public class CapturedLogging extends ExternalResource {
34      private final Class logSource;
35      private Appender appender;
36      private List<LoggingEvent> loggingEvents;
37      private Logger logger;
38      private Level savedLoggerLevel;
39      private boolean savedLoggerAdditivity;
40  
41      public CapturedLogging(final Class logSource) {
42          this.logSource = logSource;
43      }
44  
45      public List<LoggingEvent> getLoggingEvents() {
46          return loggingEvents;
47      }
48  
49      @Override
50      protected void before() throws Throwable {
51          super.before();
52          loggingEvents = new ArrayList<>();
53          appender = new AppenderSkeleton() {
54              @Override
55              protected void append(final LoggingEvent event) {
56                  loggingEvents.add(event);
57              }
58  
59              @Override
60              public void close() {
61              }
62  
63              @Override
64              public boolean requiresLayout() {
65                  return false;
66              }
67          };
68          logger = LogManager.getLogger(logSource);
69          // Ensure we get all logs from the object under test, but stop them propagating
70          savedLoggerLevel = logger.getLevel();
71          savedLoggerAdditivity = logger.getAdditivity();
72          logger.setLevel(Level.ALL);
73          logger.setAdditivity(false);
74          logger.addAppender(appender);
75      }
76  
77      @Override
78      protected void after() {
79          logger.setLevel(savedLoggerLevel);
80          logger.setAdditivity(savedLoggerAdditivity);
81          logger.removeAppender(appender);
82          super.after();
83      }
84  
85      public static Matcher<CapturedLogging> didLog(final Matcher<LoggingEvent> loggingEventMatcher) {
86          return new TypeSafeMatcher<CapturedLogging>() {
87              @Override
88              protected boolean matchesSafely(final CapturedLogging capturedLogging) {
89                  return hasItem(loggingEventMatcher).matches(capturedLogging.getLoggingEvents());
90              }
91  
92              @Override
93              public void describeTo(final Description description) {
94                  description.appendText("some LoggingEvent that ");
95                  description.appendDescriptionOf(loggingEventMatcher);
96              }
97          };
98      }
99  
100     public static Matcher<CapturedLogging> didLogError(final Matcher<String> messageMatcher) {
101         return didLog(levelAndMessageMatch(Level.ERROR, messageMatcher));
102     }
103 
104     public static Matcher<CapturedLogging> didLogError(final String... substrings) {
105         return didLogError(containsAllStrings(substrings));
106     }
107 
108     public static Matcher<CapturedLogging> didLogWarn(final Matcher<String> messageMatcher) {
109         return didLog(levelAndMessageMatch(Level.WARN, messageMatcher));
110     }
111 
112     public static Matcher<CapturedLogging> didLogWarn(final String... substrings) {
113         return didLogWarn(containsAllStrings(substrings));
114     }
115 
116     public static Matcher<CapturedLogging> didLogInfo(final Matcher<String> messageMatcher) {
117         return didLog(levelAndMessageMatch(Level.INFO, messageMatcher));
118     }
119 
120     public static Matcher<CapturedLogging> didLogInfo(final String... substrings) {
121         return didLogInfo(containsAllStrings(substrings));
122     }
123 
124     public static Matcher<CapturedLogging> didLogDebug(final Matcher<String> messageMatcher) {
125         return didLog(levelAndMessageMatch(Level.DEBUG, messageMatcher));
126     }
127 
128     public static Matcher<CapturedLogging> didLogDebug(final String... substrings) {
129         return didLogDebug(containsAllStrings(substrings));
130     }
131 
132     public static Matcher<LoggingEvent> levelIs(final Level level) {
133         return new TypeSafeMatcher<LoggingEvent>() {
134             @Override
135             protected boolean matchesSafely(final LoggingEvent loggingEvent) {
136                 return level.equals(loggingEvent.getLevel());
137             }
138 
139             @Override
140             public void describeTo(final Description description) {
141                 description.appendText("has level ");
142                 description.appendValue(level);
143             }
144         };
145     }
146 
147     public static Matcher<LoggingEvent> messageMatches(final Matcher<String> stringMatcher) {
148         return new TypeSafeMatcher<LoggingEvent>() {
149             @Override
150             protected boolean matchesSafely(final LoggingEvent loggingEvent) {
151                 return stringMatcher.matches(loggingEvent.getMessage());
152             }
153 
154             @Override
155             public void describeTo(final Description description) {
156                 description.appendText("has message ");
157                 description.appendDescriptionOf(stringMatcher);
158             }
159         };
160     }
161 
162     public static Matcher<LoggingEvent> throwableMatches(final Matcher<Throwable> throwableMatcher) {
163         return new TypeSafeMatcher<LoggingEvent>() {
164             @Override
165             protected boolean matchesSafely(final LoggingEvent loggingEvent) {
166                 return Optional.ofNullable(loggingEvent.getThrowableInformation())
167                         .map(ti -> throwableMatcher.matches(ti.getThrowable()))
168                         .orElse(false);
169             }
170 
171             @Override
172             public void describeTo(final Description description) {
173                 description.appendText("has throwable which ");
174                 description.appendDescriptionOf(throwableMatcher);
175             }
176         };
177     }
178 
179     public String toString() {
180         final List<String> loggingEventsAsString = Lists.transform(loggingEvents, new Function<LoggingEvent, String>() {
181             @Override
182             public String apply(final LoggingEvent loggingEvent) {
183                 return loggingEvent.getLevel() + ":" + loggingEvent.getMessage();
184             }
185         });
186         return "CapturedLogging( " + loggingEventsAsString + ")";
187     }
188 
189     public static Matcher<LoggingEvent> levelAndMessageMatch(final Level level, final Matcher<String> messageMatcher) {
190         return allOf(levelIs(level), messageMatches(messageMatcher));
191     }
192 }