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}