This is an automated email from the ASF dual-hosted git repository. aldettinger pushed a commit to branch CAMEL-13342-JUNIT5-EXPLORATORY in repository https://gitbox.apache.org/repos/asf/camel.git
commit ce5a74373a90cbd01fe9e82a712328bb775b9975 Author: aldettinger <aldettin...@gmail.com> AuthorDate: Thu Jul 11 10:12:37 2019 +0200 CAMEL-13342: Implemented a first version of a proof-of-concept for camel-testcontainers-spring with JUnit 5 --- .../SpringConsulDefaultServiceCallRouteTest.java | 12 +- ...SpringConsulExpressionServiceCallRouteTest.java | 12 +- .../SpringConsulRibbonServiceCallRouteTest.java | 12 +- .../cloud/SpringConsulServiceCallRouteTest.java | 8 +- .../junit5/spring/CamelAnnotationsHandler.java | 367 ++++++++++++++ .../spring/CamelSpringBootExecutionListener.java | 95 ++++ .../spring/CamelSpringBootJUnit4ClassRunner.java | 33 ++ .../test/junit5/spring/CamelSpringBootRunner.java | 87 ++++ .../CamelSpringDelegatingTestContextLoader.java | 138 ++++++ .../test/junit5/spring/CamelSpringRunner.java | 83 ++++ .../spring/CamelSpringTestContextLoader.java | 551 +++++++++++++++++++++ ...ringTestContextLoaderTestExecutionListener.java | 50 ++ .../test/junit5/spring/CamelSpringTestHelper.java | 109 ++++ .../test/junit5/spring/CamelSpringTestSupport.java | 212 ++++++++ .../spring/CamelTestContextBootstrapper.java | 31 ++ .../camel/test/junit5/spring/DisableJmx.java | 43 ++ .../spring/DisableJmxTestExecutionListener.java | 39 ++ .../test/junit5/spring/EnableRouteCoverage.java | 41 ++ .../camel/test/junit5/spring/ExcludeRoutes.java | 44 ++ .../camel/test/junit5/spring/MockEndpoints.java | 43 ++ .../test/junit5/spring/MockEndpointsAndSkip.java | 43 ++ .../test/junit5/spring/ProvidesBreakpoint.java | 36 ++ .../test/junit5/spring/RouteCoverageDumper.java | 82 +++ .../junit5/spring/RouteCoverageEventNotifier.java | 51 ++ .../camel/test/junit5/spring/ShutdownTimeout.java | 49 ++ .../spring/StopWatchTestExecutionListener.java | 62 +++ .../camel/test/junit5/spring/UseAdviceWith.java | 49 ++ ...eOverridePropertiesWithPropertiesComponent.java | 34 ++ .../spring/ContainerAwareSpringTestSupport.java | 112 +++++ .../spring/ContainerAwareSpringTestSupportIT.java | 61 +++ 30 files changed, 2567 insertions(+), 22 deletions(-) diff --git a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulDefaultServiceCallRouteTest.java b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulDefaultServiceCallRouteTest.java index e0cbcb9..bbe67f2 100644 --- a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulDefaultServiceCallRouteTest.java +++ b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulDefaultServiceCallRouteTest.java @@ -20,8 +20,8 @@ import java.util.List; import org.apache.camel.component.ribbon.cloud.RibbonServiceLoadBalancer; import org.apache.camel.impl.cloud.DefaultServiceCallProcessor; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -35,9 +35,9 @@ public class SpringConsulDefaultServiceCallRouteTest extends SpringConsulService public void testServiceCallConfiguration() throws Exception { List<DefaultServiceCallProcessor> processors = findServiceCallProcessors(); - Assert.assertFalse(processors.isEmpty()); - Assert.assertEquals(2, processors.size()); - Assert.assertFalse(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer); - Assert.assertFalse(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer); + Assertions.assertFalse(processors.isEmpty()); + Assertions.assertEquals(2, processors.size()); + Assertions.assertFalse(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer); + Assertions.assertFalse(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer); } } diff --git a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulExpressionServiceCallRouteTest.java b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulExpressionServiceCallRouteTest.java index 21995d7..f1f937f 100644 --- a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulExpressionServiceCallRouteTest.java +++ b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulExpressionServiceCallRouteTest.java @@ -20,8 +20,8 @@ import java.util.List; import org.apache.camel.component.ribbon.cloud.RibbonServiceLoadBalancer; import org.apache.camel.impl.cloud.DefaultServiceCallProcessor; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -35,9 +35,9 @@ public class SpringConsulExpressionServiceCallRouteTest extends SpringConsulServ public void testServiceCallConfiguration() throws Exception { List<DefaultServiceCallProcessor> processors = findServiceCallProcessors(); - Assert.assertFalse(processors.isEmpty()); - Assert.assertEquals(2, processors.size()); - Assert.assertFalse(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer); - Assert.assertFalse(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer); + Assertions.assertFalse(processors.isEmpty()); + Assertions.assertEquals(2, processors.size()); + Assertions.assertFalse(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer); + Assertions.assertFalse(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer); } } diff --git a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulRibbonServiceCallRouteTest.java b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulRibbonServiceCallRouteTest.java index 230d178..b8cd79b 100644 --- a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulRibbonServiceCallRouteTest.java +++ b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulRibbonServiceCallRouteTest.java @@ -20,8 +20,8 @@ import java.util.List; import org.apache.camel.component.ribbon.cloud.RibbonServiceLoadBalancer; import org.apache.camel.impl.cloud.DefaultServiceCallProcessor; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; @@ -35,9 +35,9 @@ public class SpringConsulRibbonServiceCallRouteTest extends SpringConsulServiceC public void testServiceCallConfiguration() throws Exception { List<DefaultServiceCallProcessor> processors = findServiceCallProcessors(); - Assert.assertFalse(processors.isEmpty()); - Assert.assertEquals(2, processors.size()); - Assert.assertTrue(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer); - Assert.assertTrue(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer); + Assertions.assertFalse(processors.isEmpty()); + Assertions.assertEquals(2, processors.size()); + Assertions.assertTrue(processors.get(0).getLoadBalancer() instanceof RibbonServiceLoadBalancer); + Assertions.assertTrue(processors.get(1).getLoadBalancer() instanceof RibbonServiceLoadBalancer); } } diff --git a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulServiceCallRouteTest.java b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulServiceCallRouteTest.java index 68de60c..83a4608 100644 --- a/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulServiceCallRouteTest.java +++ b/components/camel-consul/src/test/java/org/apache/camel/component/consul/cloud/SpringConsulServiceCallRouteTest.java @@ -31,9 +31,9 @@ import org.apache.camel.component.consul.ConsulTestSupport; import org.apache.camel.impl.cloud.DefaultServiceCallProcessor; import org.apache.camel.processor.ChoiceProcessor; import org.apache.camel.processor.FilterProcessor; -import org.apache.camel.test.testcontainers.spring.ContainerAwareSpringTestSupport; -import org.junit.Assert; -import org.junit.Test; +import org.apache.camel.test.junit5.testcontainers.spring.ContainerAwareSpringTestSupport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.testcontainers.containers.GenericContainer; public abstract class SpringConsulServiceCallRouteTest extends ContainerAwareSpringTestSupport { @@ -130,7 +130,7 @@ public abstract class SpringConsulServiceCallRouteTest extends ContainerAwareSpr protected List<DefaultServiceCallProcessor> findServiceCallProcessors() { Route route = context().getRoute("scall"); - Assert.assertNotNull("ServiceCall Route should be present", route); + Assertions.assertNotNull(route, "ServiceCall Route should be present"); return findServiceCallProcessors(new ArrayList<>(), route.navigate()); } diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelAnnotationsHandler.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelAnnotationsHandler.java new file mode 100644 index 0000000..8bc45b2 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelAnnotationsHandler.java @@ -0,0 +1,367 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import org.apache.camel.ExtendedCamelContext; +import org.apache.camel.api.management.JmxSystemPropertyKeys; +import org.apache.camel.api.management.ManagedCamelContext; +import org.apache.camel.api.management.mbean.ManagedCamelContextMBean; +import org.apache.camel.component.properties.PropertiesComponent; +import org.apache.camel.impl.engine.InterceptSendToMockEndpointStrategy; +import org.apache.camel.processor.interceptor.DefaultDebugger; +import org.apache.camel.spi.Breakpoint; +import org.apache.camel.spi.Debugger; +import org.apache.camel.spi.EventNotifier; +import org.apache.camel.spring.SpringCamelContext; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.apache.camel.util.CollectionStringBuffer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; + +import static org.apache.camel.test.spring.CamelSpringTestHelper.getAllMethods; + +public final class CamelAnnotationsHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(CamelAnnotationsHandler.class); + + private CamelAnnotationsHandler() { + } + + /** + * Handles @ExcludeRoutes to make it easier to exclude other routes when testing with Spring Boot. + * + * @param testClass the test class being executed + */ + public static void handleExcludeRoutesForSpringBoot(Class<?> testClass) { + if (testClass.isAnnotationPresent(ExcludeRoutes.class)) { + Class[] routes = testClass.getAnnotation(ExcludeRoutes.class).value(); + // need to setup this as a JVM system property + CollectionStringBuffer csb = new CollectionStringBuffer(","); + for (Class clazz : routes) { + csb.append(clazz.getName()); + } + String key = "CamelTestSpringExcludeRoutes"; + String value = csb.toString(); + + String exists = System.getProperty(key); + if (exists != null) { + LOGGER.warn("Cannot use @ExcludeRoutes as JVM property " + key + " has already been set."); + } else { + LOGGER.info("@ExcludeRoutes annotation found. Setting up JVM property {}={}", key, value); + System.setProperty(key, value); + } + } + } + + /** + * Handles disabling of JMX on Camel contexts based on {@link DisableJmx}. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + public static void handleDisableJmx(ConfigurableApplicationContext context, Class<?> testClass) { + CamelSpringTestHelper.setOriginalJmxDisabledValue(System.getProperty(JmxSystemPropertyKeys.DISABLED)); + + if (testClass.isAnnotationPresent(DisableJmx.class)) { + if (testClass.getAnnotation(DisableJmx.class).value()) { + LOGGER.info("Disabling Camel JMX globally as DisableJmx annotation was found and disableJmx is set to true."); + System.setProperty(JmxSystemPropertyKeys.DISABLED, "true"); + + } else { + LOGGER.info("Enabling Camel JMX as DisableJmx annotation was found and disableJmx is set to false."); + System.clearProperty(JmxSystemPropertyKeys.DISABLED); + } + } else { + LOGGER.info("Disabling Camel JMX globally for tests by default. Use the DisableJMX annotation to override the default setting."); + System.setProperty(JmxSystemPropertyKeys.DISABLED, "true"); + } + } + + /** + * Handles disabling of JMX on Camel contexts based on {@link DisableJmx}. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + public static void handleRouteCoverage(ConfigurableApplicationContext context, Class<?> testClass, Function testMethod) throws Exception { + if (testClass.isAnnotationPresent(EnableRouteCoverage.class)) { + System.setProperty(CamelTestSupport.ROUTE_COVERAGE_ENABLED, "true"); + + CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() { + + @Override + public void execute(String contextName, SpringCamelContext camelContext) throws Exception { + LOGGER.info("Enabling RouteCoverage"); + EventNotifier notifier = new RouteCoverageEventNotifier(testClass.getName(), testMethod); + camelContext.addService(notifier, true); + camelContext.getManagementStrategy().addEventNotifier(notifier); + } + }); + } + } + + public static void handleRouteCoverageDump(ConfigurableApplicationContext context, Class<?> testClass, Function testMethod) throws Exception { + if (testClass.isAnnotationPresent(EnableRouteCoverage.class)) { + CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() { + + @Override + public void execute(String contextName, SpringCamelContext camelContext) throws Exception { + LOGGER.debug("Dumping RouteCoverage"); + + String testMethodName = (String) testMethod.apply(this); + RouteCoverageDumper.dumpRouteCoverage(camelContext, testClass.getName(), testMethodName); + + // reset JMX statistics + ManagedCamelContextMBean managedCamelContext = camelContext.getExtension(ManagedCamelContext.class).getManagedCamelContext(); + if (managedCamelContext != null) { + LOGGER.debug("Resetting JMX statistics for RouteCoverage"); + managedCamelContext.reset(true); + } + + // turn off dumping one more time by removing the event listener (which would dump as well when Camel is stopping) + // but this method was explicit invoked to dump such as from afterTest callbacks from JUnit. + RouteCoverageEventNotifier eventNotifier = camelContext.hasService(RouteCoverageEventNotifier.class); + if (eventNotifier != null) { + camelContext.getManagementStrategy().removeEventNotifier(eventNotifier); + camelContext.removeService(eventNotifier); + } + } + }); + } + } + + public static void handleProvidesBreakpoint(ConfigurableApplicationContext context, Class<?> testClass) throws Exception { + Collection<Method> methods = getAllMethods(testClass); + final List<Breakpoint> breakpoints = new LinkedList<>(); + + for (Method method : methods) { + if (AnnotationUtils.findAnnotation(method, ProvidesBreakpoint.class) != null) { + Class<?>[] argTypes = method.getParameterTypes(); + if (argTypes.length != 0) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with ProvidesBreakpoint but is not a no-argument method."); + } else if (!Breakpoint.class.isAssignableFrom(method.getReturnType())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with ProvidesBreakpoint but does not return a Breakpoint."); + } else if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with ProvidesBreakpoint but is not static."); + } else if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with ProvidesBreakpoint but is not public."); + } + + try { + breakpoints.add((Breakpoint) method.invoke(null)); + } catch (Exception e) { + throw new RuntimeException("Method [" + method.getName() + + "] threw exception during evaluation.", e); + } + } + } + + if (breakpoints.size() != 0) { + CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() { + + public void execute(String contextName, SpringCamelContext camelContext) + throws Exception { + Debugger debugger = camelContext.getDebugger(); + if (debugger == null) { + debugger = new DefaultDebugger(); + camelContext.setDebugger(debugger); + } + + for (Breakpoint breakpoint : breakpoints) { + LOGGER.info("Adding Breakpoint [{}] to CamelContext with name [{}].", breakpoint, contextName); + debugger.addBreakpoint(breakpoint); + } + } + }); + } + } + + /** + * Handles updating shutdown timeouts on Camel contexts based on {@link ShutdownTimeout}. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + public static void handleShutdownTimeout(ConfigurableApplicationContext context, Class<?> testClass) throws Exception { + final int shutdownTimeout; + final TimeUnit shutdownTimeUnit; + if (testClass.isAnnotationPresent(ShutdownTimeout.class)) { + shutdownTimeout = testClass.getAnnotation(ShutdownTimeout.class).value(); + shutdownTimeUnit = testClass.getAnnotation(ShutdownTimeout.class).timeUnit(); + } else { + shutdownTimeout = 10; + shutdownTimeUnit = TimeUnit.SECONDS; + } + + CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() { + + public void execute(String contextName, SpringCamelContext camelContext) + throws Exception { + LOGGER.info("Setting shutdown timeout to [{} {}] on CamelContext with name [{}].", shutdownTimeout, shutdownTimeUnit, contextName); + camelContext.getShutdownStrategy().setTimeout(shutdownTimeout); + camelContext.getShutdownStrategy().setTimeUnit(shutdownTimeUnit); + } + }); + } + + /** + * Handles auto-intercepting of endpoints with mocks based on {@link MockEndpoints}. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + public static void handleMockEndpoints(ConfigurableApplicationContext context, Class<?> testClass) throws Exception { + if (testClass.isAnnotationPresent(MockEndpoints.class)) { + final String mockEndpoints = testClass.getAnnotation(MockEndpoints.class).value(); + CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() { + + public void execute(String contextName, SpringCamelContext camelContext) + throws Exception { + LOGGER.info("Enabling auto mocking of endpoints matching pattern [{}] on CamelContext with name [{}].", mockEndpoints, contextName); + camelContext.adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(mockEndpoints)); + } + }); + } + } + + /** + * Handles auto-intercepting of endpoints with mocks based on {@link MockEndpointsAndSkip} and skipping the + * original endpoint. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + public static void handleMockEndpointsAndSkip(ConfigurableApplicationContext context, Class<?> testClass) throws Exception { + if (testClass.isAnnotationPresent(MockEndpointsAndSkip.class)) { + final String mockEndpoints = testClass.getAnnotation(MockEndpointsAndSkip.class).value(); + CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() { + + public void execute(String contextName, SpringCamelContext camelContext) + throws Exception { + // resolve the property place holders of the mockEndpoints + String mockEndpointsValue = camelContext.resolvePropertyPlaceholders(mockEndpoints); + LOGGER.info("Enabling auto mocking and skipping of endpoints matching pattern [{}] on CamelContext with name [{}].", mockEndpointsValue, contextName); + camelContext.adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(mockEndpointsValue, true)); + } + }); + } + } + + /** + * Handles override this method to include and override properties with the Camel {@link org.apache.camel.component.properties.PropertiesComponent}. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + public static void handleUseOverridePropertiesWithPropertiesComponent(ConfigurableApplicationContext context, Class<?> testClass) throws Exception { + Collection<Method> methods = getAllMethods(testClass); + final List<Properties> properties = new LinkedList<>(); + + for (Method method : methods) { + if (AnnotationUtils.findAnnotation(method, UseOverridePropertiesWithPropertiesComponent.class) != null) { + Class<?>[] argTypes = method.getParameterTypes(); + if (argTypes.length > 0) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not a no-argument method."); + } else if (!Properties.class.isAssignableFrom(method.getReturnType())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with UseOverridePropertiesWithPropertiesComponent but does not return a java.util.Properties."); + } else if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not static."); + } else if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not public."); + } + + try { + properties.add((Properties) method.invoke(null)); + } catch (Exception e) { + throw new RuntimeException("Method [" + method.getName() + + "] threw exception during evaluation.", e); + } + } + } + + if (properties.size() != 0) { + CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() { + public void execute(String contextName, SpringCamelContext camelContext) throws Exception { + PropertiesComponent pc = camelContext.getComponent("properties", PropertiesComponent.class); + Properties extra = new Properties(); + for (Properties prop : properties) { + extra.putAll(prop); + } + if (!extra.isEmpty()) { + LOGGER.info("Using {} properties to override any existing properties on the PropertiesComponent on CamelContext with name [{}].", extra.size(), contextName); + pc.setOverrideProperties(extra); + } + } + }); + } + } + + /** + * Handles starting of Camel contexts based on {@link UseAdviceWith} and other state in the JVM. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + public static void handleCamelContextStartup(ConfigurableApplicationContext context, Class<?> testClass) throws Exception { + boolean skip = "true".equalsIgnoreCase(System.getProperty("skipStartingCamelContext")); + if (skip) { + LOGGER.info("Skipping starting CamelContext(s) as system property skipStartingCamelContext is set to be true."); + } else if (testClass.isAnnotationPresent(UseAdviceWith.class)) { + if (testClass.getAnnotation(UseAdviceWith.class).value()) { + LOGGER.info("Skipping starting CamelContext(s) as UseAdviceWith annotation was found and isUseAdviceWith is set to true."); + skip = true; + } else { + LOGGER.info("Starting CamelContext(s) as UseAdviceWith annotation was found, but isUseAdviceWith is set to false."); + skip = false; + } + } + + if (!skip) { + CamelSpringTestHelper.doToSpringCamelContexts(context, new CamelSpringTestHelper.DoToSpringCamelContextsStrategy() { + public void execute(String contextName, + SpringCamelContext camelContext) throws Exception { + if (!camelContext.isStarted()) { + LOGGER.info("Starting CamelContext with name [{}].", contextName); + camelContext.start(); + } else { + LOGGER.debug("CamelContext with name [{}] already started.", contextName); + } + } + }); + } + } + +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootExecutionListener.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootExecutionListener.java new file mode 100644 index 0000000..4192e5e --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootExecutionListener.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import org.apache.camel.spring.SpringCamelContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +public class CamelSpringBootExecutionListener extends AbstractTestExecutionListener { + + protected static ThreadLocal<ConfigurableApplicationContext> threadApplicationContext = new ThreadLocal<>(); + + private static final Logger LOG = LoggerFactory.getLogger(CamelSpringBootExecutionListener.class); + + @Override + public void prepareTestInstance(TestContext testContext) throws Exception { + LOG.info("@RunWith(CamelSpringBootRunner.class) preparing: {}", testContext.getTestClass()); + + Class<?> testClass = testContext.getTestClass(); + + // need to prepare this before we load spring application context + CamelAnnotationsHandler.handleExcludeRoutesForSpringBoot(testClass); + + // we are customizing the Camel context with + // CamelAnnotationsHandler so we do not want to start it + // automatically, which would happen when SpringCamelContext + // is added to Spring ApplicationContext, so we set the flag + // not to start it just yet + SpringCamelContext.setNoStart(true); + System.setProperty("skipStartingCamelContext", "true"); + ConfigurableApplicationContext context = (ConfigurableApplicationContext) testContext.getApplicationContext(); + + // Post CamelContext(s) instantiation but pre CamelContext(s) start setup + CamelAnnotationsHandler.handleProvidesBreakpoint(context, testClass); + CamelAnnotationsHandler.handleShutdownTimeout(context, testClass); + CamelAnnotationsHandler.handleMockEndpoints(context, testClass); + CamelAnnotationsHandler.handleMockEndpointsAndSkip(context, testClass); + CamelAnnotationsHandler.handleUseOverridePropertiesWithPropertiesComponent(context, testClass); + + System.clearProperty("skipStartingCamelContext"); + SpringCamelContext.setNoStart(false); + } + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + LOG.info("@RunWith(CamelSpringBootRunner.class) before: {}.{}", testContext.getTestClass(), testContext.getTestMethod().getName()); + + Class<?> testClass = testContext.getTestClass(); + String testName = testContext.getTestMethod().getName(); + + ConfigurableApplicationContext context = (ConfigurableApplicationContext) testContext.getApplicationContext(); + threadApplicationContext.set(context); + + // mark Camel to be startable again and start Camel + System.clearProperty("skipStartingCamelContext"); + + // route coverage need to know the test method + CamelAnnotationsHandler.handleRouteCoverage(context, testClass, s -> testName); + + LOG.info("Initialized CamelSpringBootRunner now ready to start CamelContext"); + CamelAnnotationsHandler.handleCamelContextStartup(context, testClass); + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + LOG.info("@RunWith(CamelSpringBootRunner.class) after: {}.{}", testContext.getTestClass(), testContext.getTestMethod().getName()); + + Class<?> testClass = testContext.getTestClass(); + String testName = testContext.getTestMethod().getName(); + + ConfigurableApplicationContext context = threadApplicationContext.get(); + if (context != null && context.isRunning()) { + // dump route coverage for each test method so its accurate statistics + // even if spring application context is running (i.e. its not dirtied per test method) + CamelAnnotationsHandler.handleRouteCoverageDump(context, testClass, s -> testName); + } + } +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootJUnit4ClassRunner.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootJUnit4ClassRunner.java new file mode 100644 index 0000000..d96c903 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootJUnit4ClassRunner.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import org.junit.runners.model.InitializationError; + +/** + * The class {@link CamelSpringBootJUnit4ClassRunner} has been renamed to {@link CamelSpringBootRunner} + * which is a shorter and easier to remember name. + * + * @deprecated use {@link CamelSpringBootRunner} + */ +@Deprecated +public class CamelSpringBootJUnit4ClassRunner extends CamelSpringBootRunner { + + public CamelSpringBootJUnit4ClassRunner(Class<?> clazz) throws InitializationError { + super(clazz); + } +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootRunner.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootRunner.java new file mode 100644 index 0000000..fbd3a2e --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringBootRunner.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.util.List; + +import org.junit.runners.model.InitializationError; +import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * An implementation bringing the functionality of {@link CamelSpringTestSupport} to + * Spring Boot Test based test cases. This approach allows developers to implement tests + * for their Spring Boot based applications/routes using the typical Spring Test conventions + * for test development. + */ +public class CamelSpringBootRunner extends SpringJUnit4ClassRunner { + + public CamelSpringBootRunner(Class<?> clazz) throws InitializationError { + super(clazz); + } + + /** + * Returns the specialized manager instance that provides tight integration between Camel testing + * features and Spring. + * + * @return a new instance of {@link CamelTestContextManager}. + */ + @Override + protected TestContextManager createTestContextManager(Class<?> clazz) { + return new CamelTestContextManager(clazz); + } + + /** + * An implementation providing additional integration between Spring Test and Camel + * testing features. + */ + public static final class CamelTestContextManager extends TestContextManager { + + public CamelTestContextManager(Class<?> testClass) { + super(testClass); + + // turn off auto starting spring as we need to do this later + System.setProperty("skipStartingCamelContext", "true"); + + // is Camel already registered + if (!alreadyRegistered()) { + // inject Camel first, and then disable jmx and add the stop-watch + List<TestExecutionListener> list = getTestExecutionListeners(); + list.add(0, new CamelSpringTestContextLoaderTestExecutionListener()); + list.add(1, new DisableJmxTestExecutionListener()); + list.add(2, new CamelSpringBootExecutionListener()); + list.add(3, new StopWatchTestExecutionListener()); + } + } + + private boolean alreadyRegistered() { + List<TestExecutionListener> list = getTestExecutionListeners(); + if (list != null) { + for (TestExecutionListener listener : list) { + if (listener instanceof CamelSpringTestContextLoaderTestExecutionListener) { + return true; + } + } + } + + return false; + } + + } + +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringDelegatingTestContextLoader.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringDelegatingTestContextLoader.java new file mode 100644 index 0000000..ce486e5 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringDelegatingTestContextLoader.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.reflect.Method; + +import org.apache.camel.api.management.JmxSystemPropertyKeys; +import org.apache.camel.spring.SpringCamelContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.support.DelegatingSmartContextLoader; + +/** + * CamelSpringDelegatingTestContextLoader which fixes issues in Camel's JavaConfigContextLoader. (adds support for Camel's test annotations) + * <br> + * <em>This loader can handle either classes or locations for configuring the context.</em> + * <br> + * NOTE: This TestContextLoader doesn't support the annotation of ExcludeRoutes now. + * + * @deprecated use {@link CamelSpringRunner} or {@link CamelSpringBootRunner} instead. + */ +@Deprecated +public class CamelSpringDelegatingTestContextLoader extends DelegatingSmartContextLoader { + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + @Override + public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { + + Class<?> testClass = getTestClass(); + + if (logger.isDebugEnabled()) { + logger.debug("Loading ApplicationContext for merged context configuration [{}].", mergedConfig); + } + + // Pre CamelContext(s) instantiation setup + CamelAnnotationsHandler.handleDisableJmx(null, testClass); + + try { + SpringCamelContext.setNoStart(true); + System.setProperty("skipStartingCamelContext", "true"); + ConfigurableApplicationContext context = (ConfigurableApplicationContext) super.loadContext(mergedConfig); + SpringCamelContext.setNoStart(false); + System.clearProperty("skipStartingCamelContext"); + return loadContext(context, testClass); + } finally { + cleanup(testClass); + } + } + + /** + * Performs the bulk of the Spring application context loading/customization. + * + * @param context the partially configured context. The context should have the bean definitions loaded, but nothing else. + * @param testClass the test class being executed + * @return the initialized (refreshed) Spring application context + * + * @throws Exception if there is an error during initialization/customization + */ + public ApplicationContext loadContext(ConfigurableApplicationContext context, Class<?> testClass) + throws Exception { + + AnnotationConfigUtils.registerAnnotationConfigProcessors((BeanDefinitionRegistry) context); + + // Post CamelContext(s) instantiation but pre CamelContext(s) start setup + CamelAnnotationsHandler.handleRouteCoverage(context, testClass, s -> getTestMethod().getName()); + CamelAnnotationsHandler.handleProvidesBreakpoint(context, testClass); + CamelAnnotationsHandler.handleShutdownTimeout(context, testClass); + CamelAnnotationsHandler.handleMockEndpoints(context, testClass); + CamelAnnotationsHandler.handleMockEndpointsAndSkip(context, testClass); + CamelAnnotationsHandler.handleUseOverridePropertiesWithPropertiesComponent(context, testClass); + + // CamelContext(s) startup + CamelAnnotationsHandler.handleCamelContextStartup(context, testClass); + + return context; + } + + /** + * Cleanup/restore global state to defaults / pre-test values after the test setup + * is complete. + * + * @param testClass the test class being executed + */ + protected void cleanup(Class<?> testClass) { + SpringCamelContext.setNoStart(false); + + if (testClass.isAnnotationPresent(DisableJmx.class)) { + if (CamelSpringTestHelper.getOriginalJmxDisabled() == null) { + System.clearProperty(JmxSystemPropertyKeys.DISABLED); + } else { + System.setProperty(JmxSystemPropertyKeys.DISABLED, + CamelSpringTestHelper.getOriginalJmxDisabled()); + } + } + } + + /** + * Returns the class under test in order to enable inspection of annotations while the + * Spring context is being created. + * + * @return the test class that is being executed + * @see CamelSpringTestHelper + */ + protected Class<?> getTestClass() { + return CamelSpringTestHelper.getTestClass(); + } + + /** + * Returns the test method under test. + * + * @return the method that is being executed + * @see CamelSpringTestHelper + */ + protected Method getTestMethod() { + return CamelSpringTestHelper.getTestMethod(); + } + +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringRunner.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringRunner.java new file mode 100644 index 0000000..d64ce29 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringRunner.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.util.List; + +import org.junit.runners.model.InitializationError; +import org.springframework.test.context.TestContextManager; +import org.springframework.test.context.TestExecutionListener; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * An implementation bringing the functionality of {@link org.apache.camel.test.spring.CamelSpringTestSupport} to + * Spring Test based test cases. This approach allows developers to implement tests + * for their Spring based applications/routes using the typical Spring Test conventions + * for test development. + */ +public class CamelSpringRunner extends SpringJUnit4ClassRunner { + + public CamelSpringRunner(Class<?> clazz) throws InitializationError { + super(clazz); + } + + /** + * Returns the specialized manager instance that provides tight integration between Camel testing + * features and Spring. + * + * @return a new instance of {@link CamelTestContextManager}. + */ + @Override + protected TestContextManager createTestContextManager(Class<?> clazz) { + return new CamelTestContextManager(clazz); + } + + /** + * An implementation providing additional integration between Spring Test and Camel + * testing features. + */ + public static final class CamelTestContextManager extends TestContextManager { + + public CamelTestContextManager(Class<?> testClass) { + super(testClass); + + // is Camel already registered + if (!alreadyRegistered()) { + // inject Camel first, and then disable jmx and add the stop-watch + List<TestExecutionListener> list = getTestExecutionListeners(); + list.add(0, new CamelSpringTestContextLoaderTestExecutionListener()); + list.add(1, new DisableJmxTestExecutionListener()); + list.add(2, new StopWatchTestExecutionListener()); + } + } + + private boolean alreadyRegistered() { + List<TestExecutionListener> list = getTestExecutionListeners(); + if (list != null) { + for (TestExecutionListener listener : list) { + if (listener instanceof CamelSpringTestContextLoaderTestExecutionListener) { + return true; + } + } + } + + return false; + } + + } + +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoader.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoader.java new file mode 100644 index 0000000..99e52e0 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoader.java @@ -0,0 +1,551 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import org.apache.camel.CamelContext; +import org.apache.camel.ExtendedCamelContext; +import org.apache.camel.api.management.JmxSystemPropertyKeys; +import org.apache.camel.impl.engine.InterceptSendToMockEndpointStrategy; +import org.apache.camel.processor.interceptor.DefaultDebugger; +import org.apache.camel.spi.Breakpoint; +import org.apache.camel.spi.Debugger; +import org.apache.camel.spi.EventNotifier; +import org.apache.camel.spi.PropertiesComponent; +import org.apache.camel.spring.SpringCamelContext; +import org.apache.camel.test.ExcludingPackageScanClassResolver; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.apache.camel.test.junit5.spring.CamelSpringTestHelper.DoToSpringCamelContextsStrategy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.test.context.support.AbstractContextLoader; +import org.springframework.test.context.support.AbstractGenericContextLoader; +import org.springframework.test.context.support.GenericXmlContextLoader; +import org.springframework.util.StringUtils; + +import static org.apache.camel.test.spring.CamelSpringTestHelper.getAllMethods; + +/** + * Replacement for the default {@link GenericXmlContextLoader} that provides hooks for + * processing some class level Camel related test annotations. + */ +public class CamelSpringTestContextLoader extends AbstractContextLoader { + + private static final Logger LOG = LoggerFactory.getLogger(CamelSpringTestContextLoader.class); + + /** + * Modeled after the Spring implementation in {@link AbstractGenericContextLoader}, + * this method creates and refreshes the application context while providing for + * processing of additional Camel specific post-refresh actions. We do not provide the + * pre-post hooks for customization seen in {@link AbstractGenericContextLoader} because + * they probably are unnecessary for 90+% of users. + * <p/> + * For some functionality, we cannot use {@link org.springframework.test.context.TestExecutionListener} because we need + * to both produce the desired outcome during application context loading, and also cleanup + * after ourselves even if the test class never executes. Thus the listeners, which + * only run if the application context is successfully initialized are insufficient to + * provide the behavior described above. + */ + @Override + public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception { + Class<?> testClass = getTestClass(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Loading ApplicationContext for merged context configuration [{}].", mergedConfig); + } + + try { + GenericApplicationContext context = createContext(testClass, mergedConfig); + prepareContext(context, mergedConfig); + loadBeanDefinitions(context, mergedConfig); + return loadContext(context, testClass); + } finally { + cleanup(testClass); + } + } + + /** + * Modeled after the Spring implementation in {@link AbstractGenericContextLoader}, + * this method creates and refreshes the application context while providing for + * processing of additional Camel specific post-refresh actions. We do not provide the + * pre-post hooks for customization seen in {@link AbstractGenericContextLoader} because + * they probably are unnecessary for 90+% of users. + * <p/> + * For some functionality, we cannot use {@link org.springframework.test.context.TestExecutionListener} because we need + * to both produce the desired outcome during application context loading, and also cleanup + * after ourselves even if the test class never executes. Thus the listeners, which + * only run if the application context is successfully initialized are insufficient to + * provide the behavior described above. + */ + @Override + public ApplicationContext loadContext(String... locations) throws Exception { + + Class<?> testClass = getTestClass(); + + if (LOG.isDebugEnabled()) { + LOG.debug("Loading ApplicationContext for locations [" + StringUtils.arrayToCommaDelimitedString(locations) + "]."); + } + + try { + GenericApplicationContext context = createContext(testClass, null); + loadBeanDefinitions(context, locations); + return loadContext(context, testClass); + } finally { + cleanup(testClass); + } + } + + /** + * Returns "<code>-context.xml</code>". + */ + @Override + public String getResourceSuffix() { + return "-context.xml"; + } + + /** + * Performs the bulk of the Spring application context loading/customization. + * + * @param context the partially configured context. The context should have the bean definitions loaded, but nothing else. + * @param testClass the test class being executed + * @return the initialized (refreshed) Spring application context + * + * @throws Exception if there is an error during initialization/customization + */ + protected ApplicationContext loadContext(GenericApplicationContext context, Class<?> testClass) throws Exception { + + AnnotationConfigUtils.registerAnnotationConfigProcessors(context); + + // Pre CamelContext(s) instantiation setup + handleDisableJmx(context, testClass); + handleUseOverridePropertiesWithPropertiesComponent(context, testClass); + + // Temporarily disable CamelContext start while the contexts are instantiated. + SpringCamelContext.setNoStart(true); + context.refresh(); + context.registerShutdownHook(); + // Turn CamelContext startup back on since the context's have now been instantiated. + SpringCamelContext.setNoStart(false); + + // Post CamelContext(s) instantiation but pre CamelContext(s) start setup + handleRouteCoverage(context, testClass); + handleProvidesBreakpoint(context, testClass); + handleShutdownTimeout(context, testClass); + handleMockEndpoints(context, testClass); + handleMockEndpointsAndSkip(context, testClass); + + // CamelContext(s) startup + handleCamelContextStartup(context, testClass); + + return context; + } + + /** + * Cleanup/restore global state to defaults / pre-test values after the test setup + * is complete. + * + * @param testClass the test class being executed + */ + protected void cleanup(Class<?> testClass) { + SpringCamelContext.setNoStart(false); + + if (testClass.isAnnotationPresent(DisableJmx.class)) { + if (CamelSpringTestHelper.getOriginalJmxDisabled() == null) { + System.clearProperty(JmxSystemPropertyKeys.DISABLED); + } else { + System.setProperty(JmxSystemPropertyKeys.DISABLED, + CamelSpringTestHelper.getOriginalJmxDisabled()); + } + } + } + + protected void loadBeanDefinitions(GenericApplicationContext context, MergedContextConfiguration mergedConfig) { + (new XmlBeanDefinitionReader(context)).loadBeanDefinitions(mergedConfig.getLocations()); + } + + protected void loadBeanDefinitions(GenericApplicationContext context, String... locations) { + (new XmlBeanDefinitionReader(context)).loadBeanDefinitions(locations); + } + + /** + * Creates and starts the Spring context while optionally starting any loaded Camel contexts. + * + * @param testClass the test class that is being executed + * @return the loaded Spring context + */ + protected GenericApplicationContext createContext(Class<?> testClass, MergedContextConfiguration mergedConfig) { + ApplicationContext parentContext = null; + GenericApplicationContext routeExcludingContext = null; + + if (mergedConfig != null) { + parentContext = mergedConfig.getParentApplicationContext(); + } + + if (testClass.isAnnotationPresent(ExcludeRoutes.class)) { + Class<?>[] excludedClasses = testClass.getAnnotation(ExcludeRoutes.class).value(); + + if (excludedClasses.length > 0) { + if (LOG.isDebugEnabled()) { + LOG.debug("Setting up package scanning excluded classes as ExcludeRoutes " + + "annotation was found. Excluding [" + StringUtils.arrayToCommaDelimitedString(excludedClasses) + "]."); + } + + if (parentContext == null) { + routeExcludingContext = new GenericApplicationContext(); + } else { + routeExcludingContext = new GenericApplicationContext(parentContext); + } + routeExcludingContext.registerBeanDefinition("excludingResolver", new RootBeanDefinition(ExcludingPackageScanClassResolver.class)); + routeExcludingContext.refresh(); + + ExcludingPackageScanClassResolver excludingResolver = routeExcludingContext.getBean("excludingResolver", ExcludingPackageScanClassResolver.class); + List<Class<?>> excluded = Arrays.asList(excludedClasses); + excludingResolver.setExcludedClasses(new HashSet<>(excluded)); + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("Not enabling package scanning excluded classes as ExcludeRoutes " + + "annotation was found but no classes were excluded."); + } + } + } + + GenericApplicationContext context; + + if (routeExcludingContext != null) { + context = new GenericApplicationContext(routeExcludingContext); + } else { + if (parentContext != null) { + context = new GenericApplicationContext(parentContext); + } else { + context = new GenericApplicationContext(); + } + } + + return context; + } + + /** + * Handles disabling of JMX on Camel contexts based on {@link DisableJmx}. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + protected void handleDisableJmx(GenericApplicationContext context, Class<?> testClass) { + CamelSpringTestHelper.setOriginalJmxDisabledValue(System.getProperty(JmxSystemPropertyKeys.DISABLED)); + + if (testClass.isAnnotationPresent(DisableJmx.class)) { + if (testClass.getAnnotation(DisableJmx.class).value()) { + LOG.info("Disabling Camel JMX globally as DisableJmx annotation was found and disableJmx is set to true."); + System.setProperty(JmxSystemPropertyKeys.DISABLED, "true"); + } else { + LOG.info("Enabling Camel JMX as DisableJmx annotation was found and disableJmx is set to false."); + System.clearProperty(JmxSystemPropertyKeys.DISABLED); + } + } else if (!testClass.isAnnotationPresent(EnableRouteCoverage.class)) { + // route coverage need JMX so do not disable it by default + LOG.info("Disabling Camel JMX globally for tests by default. Use the DisableJMX annotation to override the default setting."); + System.setProperty(JmxSystemPropertyKeys.DISABLED, "true"); + } + } + + /** + * Handles disabling of JMX on Camel contexts based on {@link DisableJmx}. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + private void handleRouteCoverage(GenericApplicationContext context, Class<?> testClass) throws Exception { + if (testClass.isAnnotationPresent(EnableRouteCoverage.class)) { + System.setProperty(CamelTestSupport.ROUTE_COVERAGE_ENABLED, "true"); + + CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() { + + @Override + public void execute(String contextName, SpringCamelContext camelContext) throws Exception { + LOG.info("Enabling RouteCoverage"); + EventNotifier notifier = new RouteCoverageEventNotifier(testClass.getName(), s -> getTestMethod().getName()); + camelContext.addService(notifier, true); + camelContext.getManagementStrategy().addEventNotifier(notifier); + } + }); + } + } + + /** + * Handles the processing of the {@link ProvidesBreakpoint} annotation on a test class. Exists here + * as it is needed in + * + * @param context the initialized Spring context containing the Camel context(s) to insert breakpoints into + * @param testClass the test class being processed + * + * @throws Exception if there is an error processing the class + */ + protected void handleProvidesBreakpoint(GenericApplicationContext context, Class<?> testClass) throws Exception { + Collection<Method> methods = getAllMethods(testClass); + final List<Breakpoint> breakpoints = new LinkedList<>(); + + for (Method method : methods) { + if (AnnotationUtils.findAnnotation(method, ProvidesBreakpoint.class) != null) { + Class<?>[] argTypes = method.getParameterTypes(); + if (argTypes.length != 0) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with ProvidesBreakpoint but is not a no-argument method."); + } else if (!Breakpoint.class.isAssignableFrom(method.getReturnType())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with ProvidesBreakpoint but does not return a Breakpoint."); + } else if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with ProvidesBreakpoint but is not static."); + } else if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with ProvidesBreakpoint but is not public."); + } + + try { + breakpoints.add((Breakpoint) method.invoke(null)); + } catch (Exception e) { + throw new RuntimeException("Method [" + method.getName() + + "] threw exception during evaluation.", e); + } + } + } + + if (breakpoints.size() != 0) { + CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() { + + @Override + public void execute(String contextName, SpringCamelContext camelContext) + throws Exception { + Debugger debugger = camelContext.getDebugger(); + if (debugger == null) { + debugger = new DefaultDebugger(); + camelContext.setDebugger(debugger); + } + + for (Breakpoint breakpoint : breakpoints) { + LOG.info("Adding Breakpoint [{}] to CamelContext with name [{}].", breakpoint, contextName); + debugger.addBreakpoint(breakpoint); + } + } + }); + } + } + + + /** + * Handles updating shutdown timeouts on Camel contexts based on {@link ShutdownTimeout}. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + protected void handleShutdownTimeout(GenericApplicationContext context, Class<?> testClass) throws Exception { + final int shutdownTimeout; + final TimeUnit shutdownTimeUnit; + if (testClass.isAnnotationPresent(ShutdownTimeout.class)) { + shutdownTimeout = testClass.getAnnotation(ShutdownTimeout.class).value(); + shutdownTimeUnit = testClass.getAnnotation(ShutdownTimeout.class).timeUnit(); + } else { + shutdownTimeout = 10; + shutdownTimeUnit = TimeUnit.SECONDS; + } + + CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() { + + @Override + public void execute(String contextName, SpringCamelContext camelContext) + throws Exception { + LOG.info("Setting shutdown timeout to [{} {}] on CamelContext with name [{}].", shutdownTimeout, shutdownTimeUnit, contextName); + camelContext.getShutdownStrategy().setTimeout(shutdownTimeout); + camelContext.getShutdownStrategy().setTimeUnit(shutdownTimeUnit); + } + }); + } + + /** + * Handles auto-intercepting of endpoints with mocks based on {@link MockEndpoints}. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + protected void handleMockEndpoints(GenericApplicationContext context, Class<?> testClass) throws Exception { + if (testClass.isAnnotationPresent(MockEndpoints.class)) { + final String mockEndpoints = testClass.getAnnotation(MockEndpoints.class).value(); + CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() { + + @Override + public void execute(String contextName, SpringCamelContext camelContext) + throws Exception { + LOG.info("Enabling auto mocking of endpoints matching pattern [{}] on CamelContext with name [{}].", mockEndpoints, contextName); + camelContext.adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(mockEndpoints)); + } + }); + } + } + + /** + * Handles auto-intercepting of endpoints with mocks based on {@link MockEndpointsAndSkip} and skipping the + * original endpoint. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + protected void handleMockEndpointsAndSkip(GenericApplicationContext context, Class<?> testClass) throws Exception { + if (testClass.isAnnotationPresent(MockEndpointsAndSkip.class)) { + final String mockEndpoints = testClass.getAnnotation(MockEndpointsAndSkip.class).value(); + CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() { + + @Override + public void execute(String contextName, SpringCamelContext camelContext) + throws Exception { + // resovle the property place holders of the mockEndpoints + String mockEndpointsValue = camelContext.resolvePropertyPlaceholders(mockEndpoints); + LOG.info("Enabling auto mocking and skipping of endpoints matching pattern [{}] on CamelContext with name [{}].", mockEndpointsValue, contextName); + camelContext.adapt(ExtendedCamelContext.class).registerEndpointCallback(new InterceptSendToMockEndpointStrategy(mockEndpointsValue, true)); + } + }); + } + } + + /** + * Sets property overrides for the Camel {@link org.apache.camel.component.properties.PropertiesComponent}. + * + * @param context the pre-refresh Spring context + * @param testClass the test class being executed + */ + protected void handleUseOverridePropertiesWithPropertiesComponent(ConfigurableApplicationContext context, Class<?> testClass) throws Exception { + Collection<Method> methods = getAllMethods(testClass); + final List<Properties> properties = new LinkedList<>(); + + for (Method method : methods) { + if (AnnotationUtils.findAnnotation(method, UseOverridePropertiesWithPropertiesComponent.class) != null) { + Class<?>[] argTypes = method.getParameterTypes(); + if (argTypes.length > 0) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not a no-argument method."); + } else if (!Properties.class.isAssignableFrom(method.getReturnType())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with UseOverridePropertiesWithPropertiesComponent but does not return a java.util.Properties."); + } else if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not static."); + } else if (!Modifier.isPublic(method.getModifiers())) { + throw new IllegalArgumentException("Method [" + method.getName() + + "] is annotated with UseOverridePropertiesWithPropertiesComponent but is not public."); + } + + try { + properties.add((Properties) method.invoke(null)); + } catch (Exception e) { + throw new RuntimeException("Method [" + method.getName() + + "] threw exception during evaluation.", e); + } + } + } + + Properties extra = new Properties(); + for (Properties prop : properties) { + extra.putAll(prop); + } + + if (!extra.isEmpty()) { + context.addBeanFactoryPostProcessor(beanFactory -> beanFactory.addBeanPostProcessor(new BeanPostProcessor() { + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof CamelContext) { + CamelContext camelContext = (CamelContext) bean; + PropertiesComponent pc = camelContext.getPropertiesComponent(true); + LOG.info("Using {} properties to override any existing properties on the PropertiesComponent on CamelContext with name [{}].", extra.size(), camelContext.getName()); + pc.setOverrideProperties(extra); + } + return bean; + } + })); + } + } + + /** + * Handles starting of Camel contexts based on {@link UseAdviceWith} and other state in the JVM. + * + * @param context the initialized Spring context + * @param testClass the test class being executed + */ + protected void handleCamelContextStartup(GenericApplicationContext context, Class<?> testClass) throws Exception { + boolean skip = "true".equalsIgnoreCase(System.getProperty("skipStartingCamelContext")); + if (skip) { + LOG.info("Skipping starting CamelContext(s) as system property skipStartingCamelContext is set to be true."); + } else if (testClass.isAnnotationPresent(UseAdviceWith.class)) { + if (testClass.getAnnotation(UseAdviceWith.class).value()) { + LOG.info("Skipping starting CamelContext(s) as UseAdviceWith annotation was found and isUseAdviceWith is set to true."); + skip = true; + } else { + LOG.info("Starting CamelContext(s) as UseAdviceWith annotation was found, but isUseAdviceWith is set to false."); + skip = false; + } + } + + if (!skip) { + CamelSpringTestHelper.doToSpringCamelContexts(context, new DoToSpringCamelContextsStrategy() { + + @Override + public void execute(String contextName, + SpringCamelContext camelContext) throws Exception { + LOG.info("Starting CamelContext with name [{}].", contextName); + camelContext.start(); + } + }); + } + } + + /** + * Returns the class under test in order to enable inspection of annotations while the + * Spring context is being created. + * + * @return the test class that is being executed + * @see CamelSpringTestHelper + */ + protected Class<?> getTestClass() { + return CamelSpringTestHelper.getTestClass(); + } + + /** + * Returns the test method under test. + * + * @return the method that is being executed + * @see CamelSpringTestHelper + */ + protected Method getTestMethod() { + return CamelSpringTestHelper.getTestMethod(); + } +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoaderTestExecutionListener.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoaderTestExecutionListener.java new file mode 100644 index 0000000..a749104 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestContextLoaderTestExecutionListener.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import org.springframework.core.Ordered; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +/** + * Helper for {@link CamelSpringTestContextLoader} that sets the test class state + * in {@link CamelSpringTestHelper} almost immediately before the loader initializes + * the Spring context. + * <p/> + * Implemented as a listener as the state can be set on a {@code ThreadLocal} and we are pretty sure + * that the same thread will be used to initialize the Spring context. + */ +public class CamelSpringTestContextLoaderTestExecutionListener extends AbstractTestExecutionListener { + + /** + * The default implementation returns {@link org.springframework.core.Ordered#LOWEST_PRECEDENCE}, + * thereby ensuring that custom listeners are ordered after default + * listeners supplied by the framework. Can be overridden by subclasses + * as necessary. + */ + @Override + public int getOrder() { + //set Camel first + return Ordered.HIGHEST_PRECEDENCE; + } + + @Override + public void prepareTestInstance(TestContext testContext) throws Exception { + CamelSpringTestHelper.setTestClass(testContext.getTestClass()); + CamelSpringTestHelper.setTestContext(testContext); + } +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestHelper.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestHelper.java new file mode 100644 index 0000000..a9ed288 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestHelper.java @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.camel.spring.SpringCamelContext; + +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.TestContext; + +/** + * Helper that provides state information across the levels of Spring Test that do not expose the + * necessary context/state for integration of Camel testing features into Spring test. Also + * provides utility methods. + * <p/> + * Note that this class makes use of {@link ThreadLocal}s to maintain some state. It is imperative + * that the state setters and getters are accessed within the scope of a single thread in order + * for this class to work right. + */ +public final class CamelSpringTestHelper { + + private static ThreadLocal<String> originalJmxDisabledValue = new ThreadLocal<>(); + private static ThreadLocal<Class<?>> testClazz = new ThreadLocal<>(); + private static ThreadLocal<TestContext> testContext = new ThreadLocal<>(); + + private CamelSpringTestHelper() { + } + + public static String getOriginalJmxDisabled() { + return originalJmxDisabledValue.get(); + } + + public static void setOriginalJmxDisabledValue(String originalValue) { + originalJmxDisabledValue.set(originalValue); + } + + public static Class<?> getTestClass() { + return testClazz.get(); + } + + public static void setTestClass(Class<?> testClass) { + testClazz.set(testClass); + } + + public static Method getTestMethod() { + return testContext.get().getTestMethod(); + } + + public static void setTestContext(TestContext context) { + testContext.set(context); + } + + /** + * Returns all methods defined in {@code clazz} and its superclasses/interfaces. + */ + public static Collection<Method> getAllMethods(Class<?> clazz) { + Set<Method> methods = new LinkedHashSet<>(); + Class<?> currentClass = clazz; + + while (currentClass != null) { + methods.addAll(Arrays.asList(clazz.getMethods())); + currentClass = currentClass.getSuperclass(); + } + + return methods; + } + + /** + * Executes {@code strategy} against all {@link SpringCamelContext}s found in the Spring context. + * This method reduces the amount of repeated find and loop code throughout this class. + * + * @param context the Spring context to search + * @param strategy the strategy to execute against the found {@link SpringCamelContext}s + * + * @throws Exception if there is an error executing any of the strategies + */ + public static void doToSpringCamelContexts(ApplicationContext context, DoToSpringCamelContextsStrategy strategy) throws Exception { + Map<String, SpringCamelContext> contexts = context.getBeansOfType(SpringCamelContext.class); + + for (Entry<String, SpringCamelContext> entry : contexts.entrySet()) { + strategy.execute(entry.getKey(), entry.getValue()); + } + } + + public interface DoToSpringCamelContextsStrategy { + void execute(String contextName, SpringCamelContext camelContext) throws Exception; + } +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestSupport.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestSupport.java new file mode 100644 index 0000000..839f675 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelSpringTestSupport.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import org.apache.camel.CamelContext; +import org.apache.camel.ExtendedCamelContext; +import org.apache.camel.spring.SpringCamelContext; +import org.apache.camel.test.ExcludingPackageScanClassResolver; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.apache.camel.util.IOHelper; +import org.apache.camel.util.ObjectHelper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.GenericApplicationContext; + +/** + * Base test-class for classic Spring application such as standalone, web applications. + * Do <tt>not</tt> use this class for Spring Boot testing, instead use <code>@RunWith(CamelSpringBootRunner.class)</code>. + */ +public abstract class CamelSpringTestSupport extends CamelTestSupport { + protected static ThreadLocal<AbstractApplicationContext> threadAppContext = new ThreadLocal<>(); + protected static Object lock = new Object(); + + protected AbstractApplicationContext applicationContext; + protected abstract AbstractApplicationContext createApplicationContext(); + + @Override + public void postProcessTest() throws Exception { + if (isCreateCamelContextPerClass()) { + applicationContext = threadAppContext.get(); + } + super.postProcessTest(); + } + + @Override + public void doPreSetup() throws Exception { + if (!"true".equalsIgnoreCase(System.getProperty("skipStartingCamelContext"))) { + // tell camel-spring it should not trigger starting CamelContext, since we do that later + // after we are finished setting up the unit test + synchronized (lock) { + SpringCamelContext.setNoStart(true); + if (isCreateCamelContextPerClass()) { + applicationContext = threadAppContext.get(); + if (applicationContext == null) { + applicationContext = doCreateApplicationContext(); + threadAppContext.set(applicationContext); + } + } else { + applicationContext = doCreateApplicationContext(); + } + SpringCamelContext.setNoStart(false); + } + } else { + log.info("Skipping starting CamelContext as system property skipStartingCamelContext is set to be true."); + } + } + + private AbstractApplicationContext doCreateApplicationContext() { + AbstractApplicationContext context = createApplicationContext(); + Assertions.assertNotNull(context, "Should have created a valid Spring application context"); + + String[] profiles = activeProfiles(); + if (profiles != null && profiles.length > 0) { + // the context must not be active + if (context.isActive()) { + throw new IllegalStateException("Cannot active profiles: " + Arrays.asList(profiles) + " on active Spring application context: " + context + + ". The code in your createApplicationContext() method should be adjusted to create the application context with refresh = false as parameter"); + } + log.info("Spring activating profiles: {}", Arrays.asList(profiles)); + context.getEnvironment().setActiveProfiles(profiles); + } + + // ensure the context has been refreshed at least once + if (!context.isActive()) { + context.refresh(); + } + + return context; + } + + @Override + @AfterEach + public void tearDown() throws Exception { + super.tearDown(); + + if (!isCreateCamelContextPerClass()) { + IOHelper.close(applicationContext); + applicationContext = null; + } + } + + @Override + public void doPostTearDown() throws Exception { + super.doPostTearDown(); + + if (threadAppContext.get() != null) { + IOHelper.close(threadAppContext.get()); + threadAppContext.remove(); + } + } + + /** + * Create a parent context that initializes a + * {@link org.apache.camel.spi.PackageScanClassResolver} to exclude a set of given classes from + * being resolved. Typically this is used at test time to exclude certain routes, + * which might otherwise be just noisy, from being discovered and initialized. + * <p/> + * To use this filtering mechanism it is necessary to provide the + * {@link org.springframework.context.ApplicationContext} returned from here as the parent context to + * your test context e.g. + * + * <pre> + * protected AbstractXmlApplicationContext createApplicationContext() { + * return new ClassPathXmlApplicationContext(new String[] {"test-context.xml"}, getRouteExcludingApplicationContext()); + * } + * </pre> + * + * This will, in turn, call the template methods <code>excludedRoutes</code> + * and <code>excludedRoute</code> to determine the classes to be excluded from scanning. + * + * @return ApplicationContext a parent {@link org.springframework.context.ApplicationContext} configured + * to exclude certain classes from package scanning + */ + protected ApplicationContext getRouteExcludingApplicationContext() { + GenericApplicationContext routeExcludingContext = new GenericApplicationContext(); + routeExcludingContext.registerBeanDefinition("excludingResolver", new RootBeanDefinition(ExcludingPackageScanClassResolver.class)); + routeExcludingContext.refresh(); + + ExcludingPackageScanClassResolver excludingResolver = routeExcludingContext.getBean("excludingResolver", ExcludingPackageScanClassResolver.class); + List<Class<?>> excluded = Arrays.asList(excludeRoutes()); + excludingResolver.setExcludedClasses(new HashSet<>(excluded)); + + return routeExcludingContext; + } + + /** + * Template method used to exclude {@link org.apache.camel.Route} from the test time context + * route scanning + * + * @return Class[] the classes to be excluded from test time context route scanning + */ + protected Class<?>[] excludeRoutes() { + Class<?> excludedRoute = excludeRoute(); + return excludedRoute != null ? new Class[] {excludedRoute} : new Class[0]; + } + + /** + * Template method used to exclude a {@link org.apache.camel.Route} from the test camel context + */ + protected Class<?> excludeRoute() { + return null; + } + + /** + * Looks up the mandatory spring bean of the given name and type, failing if + * it is not present or the correct type + */ + public <T> T getMandatoryBean(Class<T> type, String name) { + Object value = applicationContext.getBean(name); + Assertions.assertNotNull(value, "No spring bean found for name <" + name + ">"); + if (type.isInstance(value)) { + return type.cast(value); + } else { + Assertions.fail("Spring bean <" + name + "> is not an instanceof " + type.getName() + " but is of type " + ObjectHelper.className(value)); + return null; + } + } + + /** + * Which active profiles should be used. + * <p/> + * <b>Important:</b> When using active profiles, then the code in {@link #createApplicationContext()} should create + * the Spring {@link org.springframework.context.support.AbstractApplicationContext} without refreshing. For example creating an + * {@link org.springframework.context.support.ClassPathXmlApplicationContext} you would need to pass in + * <tt>false</tt> in the refresh parameter, in the constructor. + * Camel will thrown an {@link IllegalStateException} if this is not correct stating this problem. + * The reason is that we cannot active profiles <b>after</b> a Spring application context has already + * been refreshed, and is active. + * + * @return an array of active profiles to use, use <tt>null</tt> to not use any active profiles. + */ + protected String[] activeProfiles() { + return null; + } + + @Override + protected CamelContext createCamelContext() throws Exception { + // don't start the springCamelContext if we + return SpringCamelContext.springCamelContext(applicationContext, false); + } +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelTestContextBootstrapper.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelTestContextBootstrapper.java new file mode 100644 index 0000000..aef6e19 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/CamelTestContextBootstrapper.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import org.springframework.test.context.ContextLoader; +import org.springframework.test.context.support.DefaultTestContextBootstrapper; + +/** + * To bootstrap Camel for testing with Spring 4.1 onwards. + */ +public class CamelTestContextBootstrapper extends DefaultTestContextBootstrapper { + + @Override + protected Class<? extends ContextLoader> getDefaultContextLoaderClass(Class<?> testClass) { + return CamelSpringTestContextLoader.class; + } +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmx.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmx.java new file mode 100644 index 0000000..b3f44c1 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmx.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates if JMX should be globally disabled in the {@code CamelContext}s that are bootstrapped + * during the test through the use of Spring Test loaded application contexts. Note that the + * presence of this annotation will result in the manipulation of System Properties that + * will affect Camel contexts constructed outside of the Spring Test loaded application contexts. + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface DisableJmx { + + /** + * Whether the test annotated with this annotation should be run with JMX disabled in Camel. + * Defaults to {@code true}. + */ + boolean value() default true; +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmxTestExecutionListener.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmxTestExecutionListener.java new file mode 100644 index 0000000..f3aaee6 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/DisableJmxTestExecutionListener.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import org.apache.camel.api.management.JmxSystemPropertyKeys; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +/** + * Provides reset to pre-test state behavior for global enable/disable of JMX + * support in Camel through the use of {@link DisableJmx}. + * Tries to ensure that the pre-test value is restored. + */ +public class DisableJmxTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public void afterTestClass(TestContext testContext) throws Exception { + if (CamelSpringTestHelper.getOriginalJmxDisabled() == null) { + System.clearProperty(JmxSystemPropertyKeys.DISABLED); + } else { + System.setProperty(JmxSystemPropertyKeys.DISABLED, CamelSpringTestHelper.getOriginalJmxDisabled()); + } + } + +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/EnableRouteCoverage.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/EnableRouteCoverage.java new file mode 100644 index 0000000..9b846b9 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/EnableRouteCoverage.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Enables dumping route coverage statistic. + * The route coverage status is written as xml files in the <tt>target/camel-route-coverage</tt> directory after the test has finished. + * <p/> + * This allows tooling or manual inspection of the stats, so you can generate a route trace diagram of which EIPs + * have been in use and which have not. Similar concepts as a code coverage report. + * <p/> + * You can also turn on route coverage globally via setting JVM system property <tt>CamelTestRouteCoverage=true</tt>. + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface EnableRouteCoverage { + +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ExcludeRoutes.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ExcludeRoutes.java new file mode 100644 index 0000000..eab25f0 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ExcludeRoutes.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.camel.RoutesBuilder; + +/** + * Indicates if certain route builder classes should be excluded from discovery. + * Initializes a {@link org.apache.camel.spi.PackageScanClassResolver} to exclude a set of given + * classes from being resolved. Typically this is used at test time to exclude certain routes, + * which might otherwise be noisy, from being discovered and initialized. + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface ExcludeRoutes { + + /** + * The classes to exclude from resolution when using package scanning. + */ + Class<? extends RoutesBuilder>[] value() default {}; +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpoints.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpoints.java new file mode 100644 index 0000000..2e9215a --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpoints.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.camel.impl.engine.InterceptSendToMockEndpointStrategy; + +/** + * Triggers the auto-mocking of endpoints whose URIs match the provided filter. The default + * filter is "*" which matches all endpoints. See {@link InterceptSendToMockEndpointStrategy} for + * more details on the registration of the mock endpoints. + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface MockEndpoints { + + /** + * The pattern to use for matching endpoints to enable mocking on. + */ + String value() default "*"; +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpointsAndSkip.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpointsAndSkip.java new file mode 100644 index 0000000..4e88a3c --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/MockEndpointsAndSkip.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.camel.impl.engine.InterceptSendToMockEndpointStrategy; + +/** + * Triggers the auto-mocking of endpoints whose URIs match the provided filter with the added provision + * that the endpoints are also skipped. The default filter is "*" which matches all endpoints. + * See {@link InterceptSendToMockEndpointStrategy} for more details on the registration of the mock endpoints. + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface MockEndpointsAndSkip { + + /** + * The pattern to use for matching endpoints to enable mocking on. + */ + String value() default "*"; +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ProvidesBreakpoint.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ProvidesBreakpoint.java new file mode 100644 index 0000000..088b313 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ProvidesBreakpoint.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +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.apache.camel.spi.Breakpoint; + +/** + * Indicates that the annotated method returns a {@link Breakpoint} for use in the test. Useful for intercepting + * traffic to all endpoints or simply for setting a break point in an IDE for debugging. The method must + * be {@code public}, {@code static}, take no arguments, and return {@link Breakpoint}. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface ProvidesBreakpoint { +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageDumper.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageDumper.java new file mode 100644 index 0000000..11b11b2 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageDumper.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.camel.CamelContext; +import org.apache.camel.api.management.ManagedCamelContext; +import org.apache.camel.api.management.mbean.ManagedCamelContextMBean; +import org.apache.camel.util.IOHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper to dump route coverage when using {@link EnableRouteCoverage}. + */ +public final class RouteCoverageDumper { + + private static final Logger LOG = LoggerFactory.getLogger(RouteCoverageDumper.class); + + private RouteCoverageDumper() { + } + + public static void dumpRouteCoverage(CamelContext context, String testClassName, String testName) { + try { + String dir = "target/camel-route-coverage"; + String name = testClassName + "-" + testName + ".xml"; + + ManagedCamelContextMBean managedCamelContext = context.getExtension(ManagedCamelContext.class).getManagedCamelContext(); + if (managedCamelContext == null) { + LOG.warn("Cannot dump route coverage to file as JMX is not enabled. Override useJmx() method to enable JMX in the unit test classes."); + } else { + String xml = managedCamelContext.dumpRoutesCoverageAsXml(); + String combined = "<camelRouteCoverage>\n" + gatherTestDetailsAsXml(testClassName, testName) + xml + "\n</camelRouteCoverage>"; + + File file = new File(dir); + // ensure dir exists + file.mkdirs(); + file = new File(dir, name); + + LOG.info("Dumping route coverage to file: " + file); + InputStream is = new ByteArrayInputStream(combined.getBytes()); + OutputStream os = new FileOutputStream(file, false); + IOHelper.copyAndCloseInput(is, os); + IOHelper.close(os); + } + } catch (Exception e) { + LOG.warn("Error during dumping route coverage statistic. This exception is ignored.", e); + } + + } + + /** + * Gathers test details as xml + */ + private static String gatherTestDetailsAsXml(String testClassName, String testName) { + StringBuilder sb = new StringBuilder(); + sb.append("<test>\n"); + sb.append(" <class>").append(testClassName).append("</class>\n"); + sb.append(" <method>").append(testName).append("</method>\n"); + sb.append("</test>\n"); + return sb.toString(); + } +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageEventNotifier.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageEventNotifier.java new file mode 100644 index 0000000..7d7df3d --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/RouteCoverageEventNotifier.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.util.function.Function; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.CamelEvent; +import org.apache.camel.spi.CamelEvent.CamelContextEvent; +import org.apache.camel.spi.CamelEvent.CamelContextStoppingEvent; +import org.apache.camel.support.EventNotifierSupport; + +public class RouteCoverageEventNotifier extends EventNotifierSupport { + + private final String testClassName; + private final Function testMethodName; + + public RouteCoverageEventNotifier(String testClassName, Function testMethodName) { + this.testClassName = testClassName; + this.testMethodName = testMethodName; + setIgnoreCamelContextEvents(false); + setIgnoreExchangeEvents(true); + } + + @Override + public boolean isEnabled(CamelEvent event) { + return event instanceof CamelContextStoppingEvent; + } + + @Override + public void notify(CamelEvent event) throws Exception { + CamelContext context = ((CamelContextStoppingEvent) event).getContext(); + String testName = (String) testMethodName.apply(this); + RouteCoverageDumper.dumpRouteCoverage(context, testClassName, testName); + } + +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ShutdownTimeout.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ShutdownTimeout.java new file mode 100644 index 0000000..8e35d58 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/ShutdownTimeout.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * Indicates to set the shutdown timeout of all {@code CamelContext}s instantiated through the + * use of Spring Test loaded application contexts. If no annotation is used, the timeout is + * automatically reduced to 10 seconds by the test framework. If the annotation is present the + * shutdown timeout is set based on the value of {@link #value()}. + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface ShutdownTimeout { + + /** + * The shutdown timeout to set on the {@code CamelContext}(s). + * Defaults to {@code 10} seconds. + */ + int value() default 10; + + /** + * The time unit that {@link #value()} is in. + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/StopWatchTestExecutionListener.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/StopWatchTestExecutionListener.java new file mode 100644 index 0000000..535a7dc --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/StopWatchTestExecutionListener.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import org.apache.camel.util.StopWatch; +import org.apache.camel.util.TimeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +/** + * An execution listener that simulates the timing output built in to {@link org.apache.camel.test.junit4.CamelTestSupport}. + */ +public class StopWatchTestExecutionListener extends AbstractTestExecutionListener { + + protected static ThreadLocal<StopWatch> threadStopWatch = new ThreadLocal<>(); + + /** + * Exists primarily for testing purposes, but allows for access to the underlying stop watch instance for a test. + */ + public static StopWatch getStopWatch() { + return threadStopWatch.get(); + } + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + StopWatch stopWatch = new StopWatch(); + threadStopWatch.set(stopWatch); + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + StopWatch watch = threadStopWatch.get(); + if (watch != null) { + long time = watch.taken(); + Logger log = LoggerFactory.getLogger(testContext.getTestClass()); + + log.info("********************************************************************************"); + log.info("Testing done: " + testContext.getTestMethod().getName() + "(" + testContext.getTestClass().getName() + ")"); + log.info("Took: " + TimeUtils.printDuration(time) + " (" + time + " millis)"); + log.info("********************************************************************************"); + + threadStopWatch.remove(); + } + } + +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseAdviceWith.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseAdviceWith.java new file mode 100644 index 0000000..9e09deb --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseAdviceWith.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.camel.CamelContext; + +/** + * Indicates the use of {@code adviceWith()} within the test class. If a class is annotated with + * this annotation and {@link UseAdviceWith#value()} returns true, any + * {@code CamelContext}s bootstrapped during the test through the use of Spring Test loaded + * application contexts will not be started automatically. The test author is responsible for + * injecting the Camel contexts into the test and executing {@link CamelContext#start()} on them + * at the appropriate time after any advice has been applied to the routes in the Camel context(s). + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface UseAdviceWith { + + /** + * Whether the test annotated with this annotation should be treated as if + * {@code adviceWith()} is in use in the test and the Camel contexts should not be started + * automatically. + * Defaults to {@code true}. + */ + boolean value() default true; +} diff --git a/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseOverridePropertiesWithPropertiesComponent.java b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseOverridePropertiesWithPropertiesComponent.java new file mode 100644 index 0000000..89a71a5 --- /dev/null +++ b/components/camel-test-spring/src/main/java/org/apache/camel/test/junit5/spring/UseOverridePropertiesWithPropertiesComponent.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.spring; + +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; + +/** + * Indicates that the annotated method returns a {@link java.util.Properties} for use in the test, and that + * those properties override any existing properties configured on the {@link org.apache.camel.component.properties.PropertiesComponent}. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface UseOverridePropertiesWithPropertiesComponent { + +} diff --git a/components/camel-testcontainers-spring/src/main/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupport.java b/components/camel-testcontainers-spring/src/main/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupport.java new file mode 100644 index 0000000..74130af --- /dev/null +++ b/components/camel-testcontainers-spring/src/main/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupport.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.testcontainers.spring; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import org.apache.camel.CamelContext; +import org.apache.camel.component.properties.PropertiesComponent; +import org.apache.camel.test.junit5.spring.CamelSpringTestSupport; +import org.apache.camel.test.testcontainers.ContainerPropertiesFunction; +import org.apache.camel.test.testcontainers.Containers; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; + +public abstract class ContainerAwareSpringTestSupport extends CamelSpringTestSupport { + private List<GenericContainer<?>> containers = new CopyOnWriteArrayList<>(); + + // ****************** + // Setup + // ****************** + + @Override + protected void setupResources() throws Exception { + super.setupResources(); + + containers.clear(); + containers.addAll(createContainers()); + + final Network network = containerNetwork(); + final long timeout = containersStartupTimeout(); + + Containers.start(containers, network, timeout); + } + + @Override + protected void cleanupResources() throws Exception { + super.cleanupResources(); + + Containers.stop(containers, containerShutdownTimeout()); + } + + @Override + protected CamelContext createCamelContext() throws Exception { + final CamelContext context = super.createCamelContext(); + final PropertiesComponent pc = context.getComponent("properties", PropertiesComponent.class); + + pc.addFunction(new ContainerPropertiesFunction(containers)); + + return context; + } + + // ****************** + // Containers set-up + // ****************** + + protected GenericContainer<?> createContainer() { + return null; + } + + protected List<GenericContainer<?>> createContainers() { + GenericContainer<?> container = createContainer(); + + return container == null + ? Collections.emptyList() + : Collections.singletonList(container); + } + + protected long containersStartupTimeout() { + return TimeUnit.MINUTES.toSeconds(1); + } + + protected long containerShutdownTimeout() { + return TimeUnit.MINUTES.toSeconds(1); + } + + protected Network containerNetwork() { + return null; + } + + // ****************** + // Helpers + // ****************** + + protected GenericContainer<?> getContainer(String containerName) { + return Containers.lookup(containers, containerName); + } + + protected String getContainerHost(String containerName) { + return getContainer(containerName).getContainerIpAddress(); + } + + protected int getContainerPort(String containerName, int originalPort) { + return getContainer(containerName).getMappedPort(originalPort); + } +} diff --git a/components/camel-testcontainers-spring/src/test/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupportIT.java b/components/camel-testcontainers-spring/src/test/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupportIT.java new file mode 100644 index 0000000..fef4342 --- /dev/null +++ b/components/camel-testcontainers-spring/src/test/java/org/apache/camel/test/junit5/testcontainers/spring/ContainerAwareSpringTestSupportIT.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.camel.test.junit5.testcontainers.spring; + +import org.apache.camel.test.testcontainers.Wait; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.testcontainers.containers.GenericContainer; + +public class ContainerAwareSpringTestSupportIT extends ContainerAwareSpringTestSupport { + @Override + protected AbstractApplicationContext createApplicationContext() { + return new ClassPathXmlApplicationContext("org/apache/camel/test/testcontainers/spring/ContainerAwareSpringTestSupportTest.xml"); + } + + @Test + public void testPropertyPlaceholders() throws Exception { + final GenericContainer<?> container = getContainer("myconsul"); + + final String host = context.resolvePropertyPlaceholders("{{container:host:myconsul}}"); + Assertions.assertThat(host).isEqualTo(container.getContainerIpAddress()); + + final String port = context.resolvePropertyPlaceholders("{{container:port:8500@myconsul}}"); + Assertions.assertThat(port).isEqualTo("" + container.getMappedPort(8500)); + } + + @Override + protected GenericContainer<?> createContainer() { + return new GenericContainer("consul:1.5.1") + .withNetworkAliases("myconsul") + .withExposedPorts(8500) + .waitingFor(Wait.forLogMessageContaining("Synced node info", 1)) + .withCommand( + "agent", + "-dev", + "-server", + "-bootstrap", + "-client", + "0.0.0.0", + "-log-level", + "trace" + ); + } + +}