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}