001/*
002 * (C) Copyright 2012-2016 Nuxeo SA (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 *     Sun Seng David TAN <stan@nuxeo.com>, slacoin, jcarsique
018 */
019package org.nuxeo.runtime.test.runner;
020
021import java.lang.annotation.ElementType;
022import java.lang.annotation.Inherited;
023import java.lang.annotation.Retention;
024import java.lang.annotation.RetentionPolicy;
025import java.lang.annotation.Target;
026import java.util.ArrayList;
027import java.util.List;
028
029import org.apache.log4j.Appender;
030import org.apache.log4j.AppenderSkeleton;
031import org.apache.log4j.Level;
032import org.apache.log4j.Logger;
033import org.apache.log4j.spi.LoggingEvent;
034import org.junit.Assert;
035import org.junit.runners.model.FrameworkMethod;
036
037import com.google.common.base.Strings;
038
039/**
040 * Test feature to capture from a log4j appender to check that some log4j calls have been correctly called.</br>
041 * On a test class or a test method using this feature, a default filter can be configured with the annotation
042 * {@link LogCaptureFeature.FilterOn} or a custom one implementing {@link LogCaptureFeature.Filter} class can be
043 * provided with the annotation {@link LogCaptureFeature.FilterWith} to select the log events to capture.</br>
044 * A {@link LogCaptureFeature.Result} instance is to be injected with {@link Inject} as an attribute of the test.</br>
045 * The method {@link LogCaptureFeature.Result#assertHasEvent()} can then be called from test methods to check that
046 * matching log calls (events) have been captured.
047 *
048 * @since 5.7
049 */
050public class LogCaptureFeature extends SimpleFeature {
051
052    public class NoLogCaptureFilterException extends Exception {
053        private static final long serialVersionUID = 1L;
054    }
055
056    @Inherited
057    @Retention(RetentionPolicy.RUNTIME)
058    @Target({ ElementType.TYPE, ElementType.METHOD })
059    public @interface FilterWith {
060        /**
061         * Custom implementation of a filter to select event to capture.
062         */
063        Class<? extends LogCaptureFeature.Filter> value();
064    }
065
066    @Inherited
067    @Retention(RetentionPolicy.RUNTIME)
068    @Target({ ElementType.TYPE, ElementType.METHOD })
069    public @interface FilterOn {
070        /**
071         * Configuration for the default filter
072         */
073        String loggerName() default "";
074
075        String logLevel() default "";
076    }
077
078    private static class DefaultFilter implements LogCaptureFeature.Filter {
079
080        String loggerName;
081
082        Level logLevel;
083
084        public DefaultFilter(String loggerName, String logLevel) {
085            super();
086            this.loggerName = Strings.emptyToNull(loggerName);
087            if (!"".equals(logLevel)) {
088                this.logLevel = Level.toLevel(logLevel);
089            }
090        }
091
092        @Override
093        public boolean accept(LoggingEvent event) {
094            if (logLevel != null && !logLevel.equals(event.getLevel())) {
095                return false;
096            }
097            if (loggerName != null && !loggerName.equals(event.getLoggerName())) {
098                return false;
099            }
100            return true;
101        }
102    }
103
104    public class Result {
105        protected final ArrayList<LoggingEvent> caughtEvents = new ArrayList<>();
106
107        protected boolean noFilterFlag = false;
108
109        public void assertHasEvent() throws NoLogCaptureFilterException {
110            if (noFilterFlag) {
111                throw new LogCaptureFeature.NoLogCaptureFilterException();
112            }
113            Assert.assertFalse("No log result found", caughtEvents.isEmpty());
114        }
115
116        public void clear() {
117            caughtEvents.clear();
118            noFilterFlag = false;
119        }
120
121        public List<LoggingEvent> getCaughtEvents() {
122            return caughtEvents;
123        }
124
125        protected void setNoFilterFlag(boolean noFilterFlag) {
126            this.noFilterFlag = noFilterFlag;
127        }
128    }
129
130    public interface Filter {
131        /**
132         * {@link LogCaptureFeature} will capture the event if it does match the implementation condition.
133         */
134        boolean accept(LoggingEvent event);
135    }
136
137    protected Filter logCaptureFilter;
138
139    protected final Result myResult = new Result();
140
141    protected Logger rootLogger = Logger.getRootLogger();
142
143    protected Appender logAppender = new AppenderSkeleton() {
144        @Override
145        public boolean requiresLayout() {
146            return false;
147        }
148
149        @Override
150        public void close() {
151        }
152
153        @Override
154        protected void append(LoggingEvent event) {
155            if (logCaptureFilter == null) {
156                myResult.setNoFilterFlag(true);
157                return;
158            }
159            if (logCaptureFilter.accept(event)) {
160                myResult.caughtEvents.add(event);
161            }
162        }
163    };
164
165    private Filter setupCaptureFiler;
166
167    @Override
168    public void configure(FeaturesRunner runner, com.google.inject.Binder binder) {
169        binder.bind(Result.class).toInstance(myResult);
170    };
171
172    @Override
173    public void beforeSetup(FeaturesRunner runner) throws Exception {
174        super.beforeSetup(runner);
175        Filter filter;
176        FilterWith filterProvider = runner.getConfig(FilterWith.class);
177        if (filterProvider.value() == null) {
178            FilterOn defaultFilterConfig = runner.getConfig(FilterOn.class);
179            if (defaultFilterConfig == null) {
180                return;
181            } else {
182                filter = new DefaultFilter(defaultFilterConfig.loggerName(), defaultFilterConfig.logLevel());
183            }
184        } else {
185            filter = filterProvider.value().newInstance();
186        }
187        enable(filter);
188    }
189
190    @Override
191    public void afterTeardown(FeaturesRunner runner) throws Exception {
192        disable();
193    }
194
195    @Override
196    public void beforeMethodRun(FeaturesRunner runner, FrameworkMethod method, Object test) throws Exception {
197        Filter filter;
198        FilterWith filterProvider = runner.getConfig(method, FilterWith.class);
199        if (filterProvider.value() == null) {
200            FilterOn defaultFilterConfig = runner.getConfig(method, FilterOn.class);
201            if (defaultFilterConfig == null) {
202                return;
203            } else {
204                filter = new DefaultFilter(defaultFilterConfig.loggerName(), defaultFilterConfig.logLevel());
205            }
206        } else {
207            filter = filterProvider.value().newInstance();
208        }
209        enable(filter);
210    }
211
212    @Override
213    public void afterMethodRun(FeaturesRunner runner, FrameworkMethod method, Object test) throws Exception {
214        disable();
215    }
216
217    /**
218     * @param filter
219     * @since 8.4
220     */
221    protected void enable(Filter filter) throws InstantiationException, IllegalAccessException {
222
223        if (logCaptureFilter != null) {
224            setupCaptureFiler = logCaptureFilter;
225        } else {
226            rootLogger.addAppender(logAppender);
227        }
228        logCaptureFilter = filter;
229    }
230
231    /**
232     * @since 6.0
233     */
234    protected void disable() {
235        if (setupCaptureFiler != null) {
236            logCaptureFilter = setupCaptureFiler;
237            setupCaptureFiler = null;
238            return;
239        }
240        if (logCaptureFilter != null) {
241            myResult.clear();
242            rootLogger.removeAppender(logAppender);
243            logCaptureFilter = null;
244        }
245    }
246
247}