diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/AvroTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/AvroTests.java index 245fd560ca..8e414f5905 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/AvroTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/AvroTests.java @@ -17,9 +17,15 @@ package org.springframework.integration.transformer; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -28,6 +34,8 @@ import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.integration.test.condition.LogLevels; +import org.springframework.integration.test.util.TestUtils; import org.springframework.integration.transformer.support.AvroHeaders; import org.springframework.messaging.Message; import org.springframework.messaging.PollableChannel; @@ -40,11 +48,15 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; * */ @SpringJUnitConfig +@LogLevels(categories = "foo", level = "DEBUG") public class AvroTests { @Test + @LogLevels(classes = DirectChannel.class, categories = "bar", level = "DEBUG") void testTransformers(@Autowired Config config) { AvroTestClass1 test = new AvroTestClass1("baz", "fiz"); + Log spied = spy(TestUtils.getPropertyValue(config.in1(), "logger", Log.class)); + new DirectFieldAccessor(config.in1()).setPropertyValue("logger", spied); config.in1().send(new GenericMessage<>(test)); assertThat(config.tapped().receive(0)) .isNotNull() @@ -57,6 +69,10 @@ public class AvroTests { .isEqualTo(test) .isNotSameAs(test); assertThat(received.getHeaders().get("flow")).isEqualTo("flow1"); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(spied, atLeastOnce()).debug(captor.capture()); + assertThat(captor.getAllValues()).anyMatch(s -> s.contains("preSend on channel")); + assertThat(captor.getAllValues()).anyMatch(s -> s.contains("postSend (sent=true) on channel")); } @Test diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/condition/LogLevels.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/condition/LogLevels.java new file mode 100644 index 0000000000..bb8f9c203d --- /dev/null +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/condition/LogLevels.java @@ -0,0 +1,60 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.test.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Test classes annotated with this will change logging levels between tests. It can also + * be applied to individual test methods. If both class-level and method-level annotations + * are present, the method-level annotation is used. + * + * @author Gary Russell + * @since 5.2 + * + */ +@ExtendWith(LogLevelsCondition.class) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface LogLevels { + + /** + * Classes representing Log4j categories to change. + * @return the classes. + */ + Class[] classes() default {}; + + /** + * Category names representing Log4j categories to change. + * @return the names. + */ + String[] categories() default {}; + + /** + * The Log4j level name to switch the categories to during the test. + * @return the level. + */ + String level() default ""; + +} diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/condition/LogLevelsCondition.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/condition/LogLevelsCondition.java new file mode 100644 index 0000000000..20b5c61641 --- /dev/null +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/condition/LogLevelsCondition.java @@ -0,0 +1,108 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.test.condition; + +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.Optional; + +import org.apache.logging.log4j.Level; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.integration.test.util.TestUtils.LevelsContainer; + +/** + * JUnit condition that adjusts and reverts log levels before/after each test. + * + * @author Gary Russell + * @since 5.2 + * + */ +public class LogLevelsCondition + implements ExecutionCondition, BeforeEachCallback, AfterEachCallback, AfterAllCallback { + + private static final String STORE_ANNOTATION_KEY = "logLevelsAnnotation"; + + private static final String STORE_CONTAINER_KEY = "logLevelsContainer"; + + private static final ConditionEvaluationResult ENABLED = + ConditionEvaluationResult.enabled("@LogLevels always enabled"); + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + Optional element = context.getElement(); + MergedAnnotations annotations = MergedAnnotations.from(element.get(), + MergedAnnotations.SearchStrategy.TYPE_HIERARCHY); + MergedAnnotation mergedAnnotation = annotations.get(LogLevels.class); + if (mergedAnnotation.isPresent()) { + LogLevels loglevels = mergedAnnotation.synthesize(); + Store store = context.getStore(Namespace.create(getClass(), context)); + store.put(STORE_ANNOTATION_KEY, loglevels); + } + return ENABLED; + } + + @Override + public void beforeEach(ExtensionContext context) { + Store store = context.getStore(Namespace.create(getClass(), context)); + LogLevels logLevels = store.get(STORE_ANNOTATION_KEY, LogLevels.class); + if (logLevels == null) { + ExtensionContext parent = context.getParent().get(); + store = parent.getStore(Namespace.create(getClass(), parent)); + logLevels = store.get(STORE_ANNOTATION_KEY, LogLevels.class); + } + store.put(STORE_CONTAINER_KEY, TestUtils.adjustLogLevels(context.getDisplayName(), + Arrays.asList((logLevels.classes())), + Arrays.asList(logLevels.categories()), + Level.toLevel(logLevels.level()))); + } + + @Override + public void afterEach(ExtensionContext context) { + Store store = context.getStore(Namespace.create(getClass(), context)); + LevelsContainer container = store.get(STORE_CONTAINER_KEY, LevelsContainer.class); + boolean parentStore = false; + if (container == null) { + ExtensionContext parent = context.getParent().get(); + store = parent.getStore(Namespace.create(getClass(), parent)); + container = store.get(STORE_CONTAINER_KEY, LevelsContainer.class); + parentStore = true; + } + TestUtils.revertLogLevels(context.getDisplayName(), container); + store.remove(STORE_CONTAINER_KEY); + if (!parentStore) { + store.remove(STORE_ANNOTATION_KEY); + } + } + + @Override + public void afterAll(ExtensionContext context) { + Store store = context.getStore(Namespace.create(getClass(), context)); + store.remove(STORE_ANNOTATION_KEY); + } + +} diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/rule/Log4j2LevelAdjuster.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/rule/Log4j2LevelAdjuster.java index 92a9614570..660d817c7f 100644 --- a/spring-integration-test-support/src/main/java/org/springframework/integration/test/rule/Log4j2LevelAdjuster.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/rule/Log4j2LevelAdjuster.java @@ -17,21 +17,17 @@ package org.springframework.integration.test.rule; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.LoggerConfig; import org.junit.rules.MethodRule; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.Statement; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.integration.test.util.TestUtils.LevelsContainer; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -81,75 +77,18 @@ public final class Log4j2LevelAdjuster implements MethodRule { @Override public void evaluate() throws Throwable { - LoggerContext ctx = (LoggerContext) LogManager.getContext(false); - Configuration config = ctx.getConfiguration(); - - Map, Level> classLevels = new HashMap<>(); - for (Class cls : Log4j2LevelAdjuster.this.classes) { - String className = cls.getName(); - LoggerConfig loggerConfig = config.getLoggerConfig(className); - LoggerConfig specificConfig = loggerConfig; - - // We need a specific configuration for this logger, - // otherwise we would change the level of all other loggers - // having the original configuration as parent as well - - if (!loggerConfig.getName().equals(className)) { - specificConfig = new LoggerConfig(className, Log4j2LevelAdjuster.this.level, true); - specificConfig.setParent(loggerConfig); - config.addLogger(className, specificConfig); - } - - classLevels.put(cls, specificConfig.getLevel()); - specificConfig.setLevel(Log4j2LevelAdjuster.this.level); - } - - Map categoryLevels = new HashMap<>(); - for (String category : Log4j2LevelAdjuster.this.categories) { - LoggerConfig loggerConfig = config.getLoggerConfig(category); - LoggerConfig specificConfig = loggerConfig; - - // We need a specific configuration for this logger, - // otherwise we would change the level of all other loggers - // having the original configuration as parent as well - - if (!loggerConfig.getName().equals(category)) { - specificConfig = new LoggerConfig(category, Log4j2LevelAdjuster.this.level, true); - specificConfig.setParent(loggerConfig); - config.addLogger(category, specificConfig); - } - - categoryLevels.put(category, specificConfig.getLevel()); - specificConfig.setLevel(Log4j2LevelAdjuster.this.level); - } - - ctx.updateLoggers(); - - logger.debug("++++++++++++++++++++++++++++ " - + "Overridden log level setting for: " + Arrays.toString(Log4j2LevelAdjuster.this.classes) - + " and " + Arrays.toString(Log4j2LevelAdjuster.this.categories) - + " for test " + method.getName()); - + LevelsContainer container = null; try { + container = TestUtils.adjustLogLevels(method.getName(), + Arrays.asList(Log4j2LevelAdjuster.this.classes), + Arrays.asList(Log4j2LevelAdjuster.this.categories), + Log4j2LevelAdjuster.this.level); base.evaluate(); } finally { - logger.debug("++++++++++++++++++++++++++++ " - + "Restoring log level setting for: " + Arrays.toString(Log4j2LevelAdjuster.this.classes) - + " and " + Arrays.toString(Log4j2LevelAdjuster.this.categories) - + " for test " + method.getName()); - - for (Class cls : Log4j2LevelAdjuster.this.classes) { - LoggerConfig loggerConfig = config.getLoggerConfig(cls.getName()); - loggerConfig.setLevel(classLevels.get(cls)); + if (container != null) { + TestUtils.revertLogLevels(method.getName(), container); } - - for (String category : Log4j2LevelAdjuster.this.categories) { - LoggerConfig loggerConfig = config.getLoggerConfig(category); - loggerConfig.setLevel(categoryLevels.get(category)); - } - - ctx.updateLoggers(); } } } diff --git a/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/TestUtils.java b/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/TestUtils.java index ae4fb4f3fe..77daf65f14 100644 --- a/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/TestUtils.java +++ b/spring-integration-test-support/src/main/java/org/springframework/integration/test/util/TestUtils.java @@ -18,7 +18,9 @@ package org.springframework.integration.test.util; import java.io.File; import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy; @@ -26,6 +28,11 @@ import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanFactory; @@ -301,4 +308,94 @@ public abstract class TestUtils { } + public static LevelsContainer adjustLogLevels(String methodName, List> classes, List categories, + Level level) { + + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + Configuration config = ctx.getConfiguration(); + + Map, Level> classLevels = new HashMap<>(); + for (Class cls : classes) { + String className = cls.getName(); + LoggerConfig loggerConfig = config.getLoggerConfig(className); + LoggerConfig specificConfig = loggerConfig; + + // We need a specific configuration for this logger, + // otherwise we would change the level of all other loggers + // having the original configuration as parent as well + + if (!loggerConfig.getName().equals(className)) { + specificConfig = new LoggerConfig(className, loggerConfig.getLevel(), true); + specificConfig.setParent(loggerConfig); + config.addLogger(className, specificConfig); + } + + classLevels.put(cls, specificConfig.getLevel()); + specificConfig.setLevel(level); + } + + Map categoryLevels = new HashMap<>(); + for (String category : categories) { + LoggerConfig loggerConfig = config.getLoggerConfig(category); + LoggerConfig specificConfig = loggerConfig; + + // We need a specific configuration for this logger, + // otherwise we would change the level of all other loggers + // having the original configuration as parent as well + + if (!loggerConfig.getName().equals(category)) { + specificConfig = new LoggerConfig(category, loggerConfig.getLevel(), true); + specificConfig.setParent(loggerConfig); + config.addLogger(category, specificConfig); + } + + categoryLevels.put(category, specificConfig.getLevel()); + specificConfig.setLevel(level); + } + + ctx.updateLoggers(); + + LOGGER.warn("++++++++++++++++++++++++++++ " + + "Overridden log level setting for: " + classes + + " and " + categories + + " for test " + methodName); + + return new LevelsContainer(classLevels, categoryLevels); + } + + public static void revertLogLevels(String methodName, LevelsContainer container) { + LOGGER.warn("++++++++++++++++++++++++++++ " + + "Restoring log level setting for: " + container.classLevels.keySet() + + " and " + container.categoryLevels.keySet() + + " for test " + methodName); + + LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + Configuration config = ctx.getConfiguration(); + + container.classLevels.entrySet().forEach(entry -> { + LoggerConfig loggerConfig = config.getLoggerConfig(entry.getKey().getName()); + loggerConfig.setLevel(entry.getValue()); + }); + + container.categoryLevels.entrySet().forEach(entry -> { + LoggerConfig loggerConfig = config.getLoggerConfig(entry.getKey()); + loggerConfig.setLevel(entry.getValue()); + }); + + ctx.updateLoggers(); + } + + public static class LevelsContainer { + + final Map, Level> classLevels; + + final Map categoryLevels; + + public LevelsContainer(Map, Level> classLevels, Map categoryLevels) { + this.classLevels = classLevels; + this.categoryLevels = categoryLevels; + } + + } + }