001/*
002 * (C) Copyright 2017-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Thomas Roger
018 */
019package org.nuxeo.runtime.test.runner;
020
021import java.lang.annotation.Annotation;
022import java.lang.annotation.ElementType;
023import java.util.ArrayList;
024import java.util.List;
025import java.util.Map;
026import java.util.concurrent.ConcurrentHashMap;
027
028import org.apache.commons.lang3.StringUtils;
029import org.apache.logging.log4j.Level;
030import org.apache.logging.log4j.core.Logger;
031import org.apache.logging.log4j.core.LoggerContext;
032import org.apache.logging.log4j.core.appender.ConsoleAppender;
033import org.apache.logging.log4j.core.appender.ConsoleAppender.Target;
034import org.apache.logging.log4j.core.config.Configurator;
035import org.apache.logging.log4j.core.filter.ThresholdFilter;
036import org.junit.runners.model.FrameworkMethod;
037
038/**
039 * @since 9.3
040 */
041public class LogFeature implements RunnerFeature {
042
043    protected static final String CONSOLE_APPENDER = "CONSOLE";
044
045    protected static final String CONSOLE_LOG_FEATURE_APPENDER = "CONSOLE_LOG_FEATURE";
046
047    protected ConsoleAppender consoleAppender;
048
049    protected ConsoleAppender hiddenAppender;
050
051    /**
052     * Stores the original log level for a given logger name, which allows us to restore the level as defined before
053     * launching the tests.
054     *
055     * @since 11.1
056     */
057    protected Map<LoggerLevelKey, Level> originalLevelByLogger = new ConcurrentHashMap<>();
058
059    /**
060     * {@inheritDoc}
061     * <p>
062     *
063     * @since 11.1
064     */
065    @Override
066    public void beforeRun(FeaturesRunner runner) {
067        originalLevelByLogger.clear();
068        addOrUpdateLoggerLevel(runner, null);
069        addConsoleThresholdLogLevel(runner, null);
070    }
071
072    /**
073     * {@inheritDoc}
074     * <p>
075     *
076     * @since 11.1
077     */
078    @Override
079    public void afterRun(FeaturesRunner runner) {
080        restoreLoggerLevel(runner, null);
081        restoreConsoleThresholdLogLevel(runner, null);
082    }
083
084    /**
085     * {@inheritDoc}
086     * <p>
087     *
088     * @since 11.1
089     */
090    @Override
091    public void beforeMethodRun(FeaturesRunner runner, FrameworkMethod method, Object test) {
092        addOrUpdateLoggerLevel(runner, method);
093        addConsoleThresholdLogLevel(runner, method);
094    }
095
096    /**
097     * {@inheritDoc}
098     * <p>
099     *
100     * @since 11.1
101     */
102    @Override
103    public void afterMethodRun(FeaturesRunner runner, FrameworkMethod method, Object test) {
104        restoreLoggerLevel(runner, method);
105        restoreConsoleThresholdLogLevel(runner, method);
106    }
107
108    /**
109     * @deprecated since 11.1. Use {@link ConsoleLogLevelThreshold} with {@link ConsoleLogLevelThreshold#value()} set to
110     *             {@code ERROR}.
111     */
112    @Deprecated(since = "11.1", forRemoval = true)
113    public void hideWarningFromConsoleLog() {
114        setConsoleLogThreshold(Level.ERROR.toString());
115    }
116
117    /**
118     * @deprecated since 11.1. Use {@link ConsoleLogLevelThreshold} with {@link ConsoleLogLevelThreshold#value()} set to
119     *             {@code FATAL}.
120     * @since 9.10
121     */
122    @Deprecated(since = "11.1", forRemoval = true)
123    public void hideErrorFromConsoleLog() {
124        setConsoleLogThreshold(Level.FATAL.toString());
125    }
126
127    /**
128     * @since 9.10
129     */
130    public void setConsoleLogThreshold(String level) {
131        if (consoleAppender != null) {
132            return;
133        }
134
135        Logger rootLogger = LoggerContext.getContext(false).getRootLogger();
136        consoleAppender = (ConsoleAppender) rootLogger.getAppenders().get(CONSOLE_APPENDER);
137        rootLogger.removeAppender(consoleAppender);
138        ConsoleAppender newAppender = ConsoleAppender.newBuilder()
139                .withName(CONSOLE_LOG_FEATURE_APPENDER)
140                .setTarget(Target.SYSTEM_OUT)
141                .withFilter(ThresholdFilter.createFilter(Level.toLevel(level),
142                        null, null))
143                .build();
144        newAppender.start();
145        rootLogger.addAppender(newAppender);
146        hiddenAppender = newAppender;
147    }
148
149    public void restoreConsoleLog() {
150        if (consoleAppender == null) {
151            return;
152        }
153
154        Logger rootLogger = LoggerContext.getContext(false).getRootLogger();
155        rootLogger.removeAppender(hiddenAppender);
156        rootLogger.addAppender(consoleAppender);
157        consoleAppender = null;
158        hiddenAppender = null;
159    }
160
161    /**
162     * Adds the console threshold log level. To be proceed a {@code Class} / {@code Method} should be annotated by
163     * @see ConsoleLogLevelThreshold
164     * <p>
165     * @see #setConsoleLogThreshold(String)
166     *
167     * @param runner the feature runner, cannot be {@code null}
168     * @param method the framework method, can be {@code null}
169     * @since 11.1
170     */
171    protected void addConsoleThresholdLogLevel(FeaturesRunner runner, FrameworkMethod method) {
172        // Remove the previous threshold if any.
173        restoreConsoleLog();
174        // Set the new threshold
175        ConsoleLogLevelThreshold consoleLogThreshold = getAnnotation(runner, method, ConsoleLogLevelThreshold.class);
176        if (consoleLogThreshold.value() != null) {
177            setConsoleLogThreshold(consoleLogThreshold.value());
178        }
179    }
180
181    /**
182     * Restores the console threshold log level. Based on if {@code Class} or {@code Method} is annotated by
183     * {@link ConsoleLogLevelThreshold}.
184     * <p>
185     * {@link #restoreConsoleLog()}
186     *
187     * @since 11.1
188     */
189    protected void restoreConsoleThresholdLogLevel(FeaturesRunner runner, FrameworkMethod method) {
190        ConsoleLogLevelThreshold consoleLogThreshold = getAnnotation(runner, method, ConsoleLogLevelThreshold.class);
191        if (consoleLogThreshold.value() != null) {
192            restoreConsoleLog();
193        }
194    }
195
196    /**
197     * Adds or updates the logger level.
198     * <p>
199     * The definition of {@link LoggerLevel} can be done on a given {@code Class} / {@code Method} test. At the end of
200     * the test each overriding logger must be restored to its original value for this the purpose we should save the
201     * original level.
202     * <p>
203     * {@link #restoreLoggerLevel(FeaturesRunner, FrameworkMethod)} to see how the restore part will be happened.
204     *
205     * @param runner the feature runner, cannot be {@code null}
206     * @param method the framework method, can be {@code null}
207     * @since 11.1
208     */
209    protected void addOrUpdateLoggerLevel(FeaturesRunner runner, FrameworkMethod method) {
210        for (LoggerLevel logger : getLoggers(runner, method)) {
211            if (logger.level() != null) {
212                String loggerName = getLoggerName(logger);
213                LoggerContext context = LoggerContext.getContext(false);
214                // Initialize the undefined logger to simplify the restoring step. There is no way to remove / delete
215                // a logger this is why we turned it to OFF.
216                if (!context.hasLogger(loggerName)) {
217                    Configurator.setLevel(loggerName, Level.OFF);
218                }
219
220                // Save the original value.
221                originalLevelByLogger.put(buildKey(logger, method), context.getLogger(loggerName).getLevel());
222                // Set the new level that we want
223                Configurator.setLevel(loggerName, Level.toLevel(logger.level()));
224            }
225        }
226    }
227
228    /**
229     * Restores the original value of the logger level.
230     * <p>
231     * {@link #addOrUpdateLoggerLevel(FeaturesRunner, FrameworkMethod)}} to see how we store the original value and set
232     * the new one.
233     *
234     * @param runner the feature runner, cannot be {@code null}
235     * @param method the framework method, can be {@code null}
236     * @since 11.1
237     */
238    protected void restoreLoggerLevel(FeaturesRunner runner, FrameworkMethod method) {
239        for (LoggerLevel logger : getLoggers(runner, method)) {
240            if (logger.level() != null) {
241                String loggerName = getLoggerName(logger);
242                Level level = originalLevelByLogger.remove(buildKey(logger, method));
243                Configurator.setLevel(loggerName, level);
244            }
245        }
246    }
247
248    /**
249     * Retrieves the {@link LoggerLevel}, a {@code Class} or a {@code Method} can be annotated by one or more Logger.
250     *
251     * @since 11.1
252     */
253    protected List<LoggerLevel> getLoggers(FeaturesRunner runner, FrameworkMethod method) {
254        // Unique annotation LoggerLevel.
255        List<LoggerLevel> loggers = new ArrayList<>(List.of(getAnnotation(runner, method, LoggerLevel.class)));
256
257        // Repeatable annotation LoggerLevel using LoggerLevels.
258        LoggerLevels configs = getAnnotation(runner, method, LoggerLevels.class);
259        if (configs.value() != null) {
260            loggers.addAll(List.of(configs.value()));
261        }
262
263        return loggers;
264    }
265
266    /**
267     * Gets the annotation for a given {@code FeaturesRunner}, {@code FrameworkMethod} and a {@code Class} type.
268     *
269     * @since 11.1
270     */
271    protected <T extends Annotation> T getAnnotation(FeaturesRunner runner, FrameworkMethod method, Class<T> type) {
272        return method != null ? runner.getConfig(method, type) : runner.getConfig(type);
273
274    }
275
276    /**
277     * Gets the logger name from a given {@link LoggerLevel}.
278     *
279     * @since 11.1
280     */
281    protected String getLoggerName(LoggerLevel logLevel) {
282        return StringUtils.defaultIfBlank(logLevel.name(), logLevel.klass().getName());
283    }
284
285    /**
286     * Builds the logger key.
287     *
288     * @since 11.1
289     */
290    protected LoggerLevelKey buildKey(LoggerLevel logger, FrameworkMethod method) {
291        ElementType type = method != null ? ElementType.METHOD : ElementType.TYPE;
292        String loggerName = getLoggerName(logger);
293        return new LoggerLevelKey(type, loggerName);
294    }
295}