001/* 002 * #%L 003 * Netarchivesuite - archive - test 004 * %% 005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library, 006 * the National Library of France and the Austrian National Library. 007 * %% 008 * This program is free software: you can redistribute it and/or modify 009 * it under the terms of the GNU Lesser General Public License as 010 * published by the Free Software Foundation, either version 2.1 of the 011 * License, or (at your option) any later version. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Lesser Public License for more details. 017 * 018 * You should have received a copy of the GNU General Lesser Public 019 * License along with this program. If not, see 020 * <http://www.gnu.org/licenses/lgpl-2.1.html>. 021 * #L% 022 */ 023package dk.netarkivet.testutils; 024 025import java.util.ArrayList; 026import java.util.HashSet; 027import java.util.Iterator; 028import java.util.List; 029import java.util.Set; 030import java.util.regex.Pattern; 031 032import org.junit.Assert; 033import org.slf4j.LoggerFactory; 034 035import ch.qos.logback.classic.Level; 036import ch.qos.logback.classic.Logger; 037import ch.qos.logback.classic.LoggerContext; 038import ch.qos.logback.classic.spi.ILoggingEvent; 039import ch.qos.logback.core.Appender; 040import ch.qos.logback.core.filter.Filter; 041import ch.qos.logback.core.spi.FilterReply; 042 043// TODO So maybe these methods should be unit-tested... NICL 044 045/** 046 * This class implements an <code>Logback</code> appender which can be attached dynamically to an 047 * <code>SLF4J</code> context. The appender stores logging events in memory so their occurrence (or lack of) can be 048 * validated, most likely, in unit test. 049 * 050 * It can be used to test whether logging is performed. The normal usage is: 051 * <pre> 052 * <code>public void testSomething() { 053 * LogbackRecorder logRecorder = LogbackRecorder.startRecorder(); 054 * doTheTesting(); 055 * logRecorder.assertLogContains(theStringToVerifyIsInTheLog); 056 * } 057 * </code> 058 * </pre> 059 * 060 * Remember to call the stopRecorder method on the logRecorder instance when finished. The probability 061 * of doing this on a consistent basis is increased if the LogbackRecorder.startRecorder() and stopRecorder calls 062 * are made as part of the @Before and @After test methods. 063 */ 064public class LogbackRecorder extends ch.qos.logback.core.AppenderBase<ch.qos.logback.classic.spi.ILoggingEvent> { 065 066 /** The Logback context currently in use. */ 067 protected static final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); 068 069 /** The root Logback logger, used to attach appender(s). */ 070 protected static final Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); 071 072 /** This instances appender. */ 073 protected Appender<ILoggingEvent> appender; 074 075 /** List of archived logging events, can be reset any at point by calling reset(). */ 076 protected List<ILoggingEvent> events = new ArrayList<ILoggingEvent>(); 077 078 /** 079 * Constructor only for use in unit tests. 080 */ 081 protected LogbackRecorder() { 082 } 083 084 /** 085 * Create a new <code>LogbackRecorder</code> and attach it to the current logging context's root logger. 086 */ 087 public static LogbackRecorder startRecorder() { 088 LogbackRecorder lu = new LogbackRecorder(); 089 lu.setName("unit-test"); 090 lu.setContext(context); 091 lu.start(); 092 root.addAppender(lu); 093 return lu; 094 } 095 096 /** 097 * Stops recorder, clears recorded events and detaches appender from logging context's root logger. 098 * @return indication of success trying to detach appender 099 */ 100 public boolean stopRecorder() { 101 stop(); 102 events.clear(); 103 return root.detachAppender(this); 104 } 105 106 /** 107 * Reset recorder by clearing all recorder events. 108 */ 109 public void reset() { 110 events.clear(); 111 } 112 113 @Override 114 protected synchronized void append(ILoggingEvent event) { 115 events.add(event); 116 // System.out.println("#\n#" + event.getLoggerName() + "#\n"); 117 } 118 119 /** 120 * Returns boolean indicating whether any log entries have been recorded. 121 * @return boolean indicating whether any log entries have been recorded 122 */ 123 public synchronized boolean isEmpty() { 124 return events.isEmpty(); 125 } 126 127 /** 128 * Tries to find a log entry with a specific log level containing a specific string and fails the if no match is 129 * found. 130 * @param level The log level of the log to find 131 * @param logStringToLookup The string to find in the log. 132 */ 133 public synchronized void assertLogContains(Level level, String logStringToLookup) { 134 boolean matchFound = false; 135 Set<Level> matchedLevels = new HashSet<>(); 136 for (ILoggingEvent logEntry : events) { 137 if (logEntry.getFormattedMessage().indexOf(logStringToLookup) != -1) { 138 if (logEntry.getLevel() == level) { 139 matchFound = true; 140 break; 141 } else { 142 matchedLevels.add(logEntry.getLevel()); 143 } 144 } 145 } 146 if (!matchFound) { 147 StringBuilder sb = new StringBuilder(); 148 sb.append("Unable to find match level(" + level + ") in log: " + logStringToLookup); 149 if (!matchedLevels.isEmpty()) { 150 sb.append("\nFound matches for other log levels though: " + matchedLevels); 151 } 152 Assert.fail(sb.toString()); 153 } 154 } 155 156 /** 157 * Assert that there is a recorded entry than contains the supplied string. 158 * @param logStringToLookup string to match for 159 */ 160 public synchronized void assertLogContains(String logStringToLookup) { 161 assertLogContains((String) null, logStringToLookup); 162 } 163 164 /** 165 * Assert that there is a recorded entry than contains the supplied string. 166 * @param msg error message or null 167 * @param logStringToLookup string to match for 168 */ 169 public synchronized void assertLogContains(String msg, String logStringToLookup) { 170 Iterator<ILoggingEvent> iter = events.iterator(); 171 boolean bMatched = false; 172 while (!bMatched && iter.hasNext()) { 173 bMatched = (iter.next().getFormattedMessage().indexOf(logStringToLookup) != -1); 174 } 175 if (!bMatched) { 176 if (msg == null) { 177 msg = "Unable to match in log: " + logStringToLookup; 178 } 179 Assert.fail(msg); 180 } 181 } 182 183 /** 184 * Assert that there is a recorded entry than matches the supplied regular expression. 185 * @param msg error message or null 186 * @param regexToLookup regular expression to match for 187 */ 188 public synchronized void assertLogMatches(String msg, String regexToLookup) { 189 Pattern pattern = Pattern.compile(regexToLookup); 190 Iterator<ILoggingEvent> iter = events.iterator(); 191 boolean bMatched = false; 192 while (!bMatched && iter.hasNext()) { 193 bMatched = pattern.matcher((iter.next().getFormattedMessage())).find(); 194 } 195 if (!bMatched) { 196 if (msg == null) { 197 msg = "Unable to match regex in log: " + regexToLookup; 198 } 199 Assert.fail(msg); 200 } 201 } 202 203 /** 204 * Assert that there is no recorded entry with the supplied string 205 * @param logStringToLookup log message to look for 206 */ 207 public synchronized void assertLogNotContains(String logStringToLookup) { 208 assertLogNotContains(null, logStringToLookup); 209 } 210 211 /** 212 * Assert that there is no recorded entry with the supplied string 213 * @param msg error message or null 214 * @param logStringToLookup log message to look for 215 */ 216 public synchronized void assertLogNotContains(String msg, String logStringToLookup) { 217 Iterator<ILoggingEvent> iter = events.iterator(); 218 boolean bMatched = false; 219 while (!bMatched && iter.hasNext()) { 220 bMatched = (iter.next().getFormattedMessage().indexOf(logStringToLookup) != -1); 221 } 222 if (bMatched) { 223 if (msg == null) { 224 msg = "Able to match in log: " + logStringToLookup; 225 } 226 Assert.fail(msg); 227 } 228 } 229 230 /** 231 * Assert that there is no recorded log entry with the supplied log level. 232 * @param msg error message or null 233 * @param level log level 234 */ 235 public synchronized void assertLogNotContainsLevel(String msg, Level level) { 236 Iterator<ILoggingEvent> iter = events.iterator(); 237 boolean bMatched = false; 238 while (!bMatched && iter.hasNext()) { 239 bMatched = (iter.next().getLevel() == level); 240 } 241 if (bMatched) { 242 if (msg == null) { 243 msg = "Able to match level=" + level.toString() + " + in log."; 244 } 245 Assert.fail(msg); 246 } 247 } 248 249 /** 250 * Search the log entry list for a string starting from a specific index. 251 * @param logStringToLookup string to find 252 * @param fromIndex log entry list start index 253 * @return index of next occurrence or -1, if not found 254 */ 255 public synchronized int logIndexOf(String logStringToLookup, int fromIndex) { 256 boolean bMatched = false; 257 while (fromIndex >= 0 && fromIndex < events.size() && !bMatched) { 258 if (events.get(fromIndex).getFormattedMessage().indexOf(logStringToLookup) != -1) { 259 bMatched = true; 260 } else { 261 ++fromIndex; 262 } 263 } 264 if (!bMatched) { 265 fromIndex = -1; 266 } 267 return fromIndex; 268 } 269 270 /** 271 * Add filter on all appenders registered with the logger with the supplied logger name. 272 * @param filter filter to add 273 * @param loggerName name of logger 274 */ 275 public void addFilter(Filter<ILoggingEvent> filter, String loggerName) { 276 Logger logger = (Logger)LoggerFactory.getLogger(loggerName); 277 if (logger != null) { 278 Iterator<Appender<ILoggingEvent>> index = logger.iteratorForAppenders(); 279 while (index.hasNext()) { 280 appender = index.next(); 281 appender.addFilter(filter); 282 } 283 } 284 } 285 286 /** 287 * Remove all filters on all appenders registered with the logger with the supplied logger name. 288 * @param loggerName name of logger 289 */ 290 public void clearAllFilters(String loggerName) { 291 Logger logger = (Logger) LoggerFactory.getLogger(loggerName); 292 if (logger != null) { 293 Iterator<Appender<ILoggingEvent>> index = logger.iteratorForAppenders(); 294 while (index.hasNext()) { 295 appender = index.next(); 296 appender.clearAllFilters(); 297 } 298 } 299 } 300 301 /** 302 * Simple deny filter. 303 */ 304 public static class DenyFilter extends Filter<ILoggingEvent> { 305 @Override 306 public FilterReply decide(ILoggingEvent event) { 307 return FilterReply.DENY; 308 } 309 } 310 311}