001package ca.uhn.test.util;
002
003/*-
004 * #%L
005 * HAPI FHIR Test Utilities
006 * %%
007 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ch.qos.logback.classic.Level;
024import ch.qos.logback.classic.Logger;
025import ch.qos.logback.classic.spi.ILoggingEvent;
026import ch.qos.logback.core.read.ListAppender;
027import org.hamcrest.CustomTypeSafeMatcher;
028import org.hamcrest.Matcher;
029import org.junit.jupiter.api.extension.AfterEachCallback;
030import org.junit.jupiter.api.extension.BeforeEachCallback;
031import org.junit.jupiter.api.extension.ExtensionContext;
032import org.slf4j.LoggerFactory;
033
034import javax.annotation.Nonnull;
035import javax.annotation.Nullable;
036import java.util.ArrayList;
037import java.util.List;
038import java.util.function.Predicate;
039import java.util.stream.Collectors;
040
041/**
042 * Test helper to collect logback lines.
043 *
044 * The empty constructor will capture all log events, or you can name a log root to limit the noise.
045 */
046public class LogbackCaptureTestExtension implements BeforeEachCallback, AfterEachCallback {
047        private final Logger myLogger;
048        private final Level myLevel;
049        private ListAppender<ILoggingEvent> myListAppender = null;
050        private Level mySavedLevel;
051
052        /**
053         *
054         * @param theLogger the log to capture
055         */
056        public LogbackCaptureTestExtension(Logger theLogger) {
057                myLogger = theLogger;
058                myLevel = null;
059        }
060
061        /**
062         *
063         * @param theLogger the log to capture
064         * @param theTestLogLevel the log Level to set on the target logger for the duration of the test
065         */
066        public LogbackCaptureTestExtension(Logger theLogger, Level theTestLogLevel) {
067                myLogger = theLogger;
068                myLevel = theTestLogLevel;
069        }
070
071        /**
072         * @param theLoggerName the log name to capture
073         */
074        public LogbackCaptureTestExtension(String theLoggerName) {
075                this((Logger) LoggerFactory.getLogger(theLoggerName));
076        }
077
078        /**
079         * Capture the root logger - all lines.
080         */
081        public LogbackCaptureTestExtension() {
082                this(org.slf4j.Logger.ROOT_LOGGER_NAME);
083        }
084
085        public LogbackCaptureTestExtension(String theLoggerName, Level theLevel) {
086                this((Logger) LoggerFactory.getLogger(theLoggerName), theLevel);
087        }
088
089        /**
090         * Returns a copy to avoid concurrent modification errors.
091         * @return A copy of the log events so far.
092         */
093        public java.util.List<ILoggingEvent> getLogEvents() {
094                // copy to avoid concurrent mod errors
095                return new ArrayList<>(myListAppender.list);
096        }
097
098        /** Clear accumulated log events. */
099        public void clearEvents() {
100                myListAppender.list.clear();
101        }
102
103        public ListAppender<ILoggingEvent> getAppender() {
104                return myListAppender;
105        }
106
107        @Override
108        public void beforeEach(ExtensionContext context) throws Exception {
109                setUp();
110        }
111
112        /**
113         * Guts of beforeEach exposed for manual lifecycle.
114         */
115        public void setUp() {
116                myListAppender = new ListAppender<>();
117                myListAppender.start();
118                myLogger.addAppender(myListAppender);
119                if (myLevel != null) {
120                        mySavedLevel = myLogger.getLevel();
121                        myLogger.setLevel(myLevel);
122                }
123        }
124
125        @Override
126        public void afterEach(ExtensionContext context) throws Exception {
127                myLogger.detachAppender(myListAppender);
128                myListAppender.stop();
129                if (myLevel != null) {
130                        myLogger.setLevel(mySavedLevel);
131                }
132        }
133
134
135        public List<ILoggingEvent> filterLoggingEventsWithMessageEqualTo(String theMessageText){
136                return filterLoggingEventsWithPredicate(loggingEvent -> loggingEvent.getFormattedMessage().equals(theMessageText));
137        }
138
139        public List<ILoggingEvent> filterLoggingEventsWithMessageContaining(String theMessageText){
140                return filterLoggingEventsWithPredicate(loggingEvent -> loggingEvent.getFormattedMessage().contains(theMessageText));
141        }
142
143        public List<ILoggingEvent> filterLoggingEventsWithPredicate(Predicate<ILoggingEvent> theLoggingEventPredicate){
144                return getLogEvents()
145                        .stream()
146                        .filter(theLoggingEventPredicate)
147                        .collect(Collectors.toList());
148        }
149
150        //  Hamcrest matcher support
151        public static Matcher<ILoggingEvent> eventWithLevelAndMessageContains(@Nonnull Level theLevel, @Nonnull String thePartialMessage) {
152                return new LogbackEventMatcher(theLevel, thePartialMessage);
153        }
154
155        public static Matcher<ILoggingEvent> eventWithLevel(@Nonnull Level theLevel) {
156                return new LogbackEventMatcher(theLevel, null);
157        }
158
159        public static Matcher<ILoggingEvent> eventWithMessageContains(@Nonnull String thePartialMessage) {
160                return new LogbackEventMatcher(null, thePartialMessage);
161        }
162
163        /**
164         * A Hamcrest matcher for junit assertions.
165         * Matches on level and/or partial message.
166         */
167        public static class LogbackEventMatcher extends CustomTypeSafeMatcher<ILoggingEvent> {
168                @Nullable
169                private final Level myLevel;
170                @Nullable
171                private final String myString;
172
173                public LogbackEventMatcher(@Nullable Level theLevel, @Nullable String thePartialString) {
174                        this("log event", theLevel, thePartialString);
175                }
176
177                public LogbackEventMatcher(String description, @Nullable Level theLevel, @Nullable String thePartialString) {
178                        super(makeDescription(description, theLevel, thePartialString));
179                        myLevel = theLevel;
180                        myString = thePartialString;
181                }
182                @Nonnull
183                private static String makeDescription(String description, Level theLevel, String thePartialString) {
184                        String msg = description;
185                        if (theLevel != null) {
186                                msg = msg + " with level at least " + theLevel;
187                        }
188                        if (thePartialString != null) {
189                                msg = msg + " containing string \"" + thePartialString + "\"";
190
191                        }
192                        return msg;
193                }
194
195                @Override
196                protected boolean matchesSafely(ILoggingEvent item) {
197                        return (myLevel == null || item.getLevel().isGreaterOrEqual(myLevel)) &&
198                                (myString == null || item.getFormattedMessage().contains(myString));
199                }
200        }
201}