/* * Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com * The software in this package is published under the terms of the CPAL v1.0 * license, a copy of which has been included with this distribution in the * LICENSE.txt file. */ package org.mule.tck.junit4; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.collection.IsEmptyCollection.empty; import static org.junit.Assert.assertThat; import static org.mule.runtime.core.api.construct.Flow.builder; import static org.mule.runtime.core.api.lifecycle.LifecycleUtils.setMuleContextIfNeeded; import static org.mule.runtime.core.api.lifecycle.LifecycleUtils.stopIfNeeded; import static org.mule.runtime.core.util.FileUtils.deleteTree; import static org.mule.runtime.core.util.FileUtils.newFile; import static org.mule.tck.MuleTestUtils.getTestFlow; import static org.mule.tck.junit4.TestsLogConfigurationHelper.clearLoggingConfig; import static org.mule.tck.junit4.TestsLogConfigurationHelper.configureLoggingForTest; import static org.slf4j.LoggerFactory.getLogger; import org.mule.runtime.api.exception.MuleException; import org.mule.runtime.api.message.Message; import org.mule.runtime.api.metadata.DataType; import org.mule.runtime.api.scheduler.Scheduler; import org.mule.runtime.core.DefaultEventContext; import org.mule.runtime.core.api.Event; import org.mule.runtime.core.api.Event.Builder; import org.mule.runtime.core.api.MuleContext; import org.mule.runtime.core.api.TransformationService; import org.mule.runtime.core.api.component.JavaComponent; import org.mule.runtime.core.api.config.ConfigurationBuilder; import org.mule.runtime.core.api.config.MuleConfiguration; import org.mule.runtime.core.api.construct.Flow; import org.mule.runtime.core.api.construct.FlowConstruct; import org.mule.runtime.core.api.context.MuleContextAware; import org.mule.runtime.core.api.context.MuleContextBuilder; import org.mule.runtime.core.api.context.MuleContextFactory; import org.mule.runtime.core.api.context.notification.MuleContextNotificationListener; import org.mule.runtime.core.api.el.ExpressionExecutor; import org.mule.runtime.core.api.processor.Processor; import org.mule.runtime.core.api.registry.RegistrationException; import org.mule.runtime.core.api.scheduler.SchedulerService; import org.mule.runtime.core.api.serialization.JavaObjectSerializer; import org.mule.runtime.core.api.serialization.ObjectSerializer; import org.mule.runtime.core.component.DefaultJavaComponent; import org.mule.runtime.core.config.DefaultMuleConfiguration; import org.mule.runtime.core.config.builders.DefaultsConfigurationBuilder; import org.mule.runtime.core.config.builders.SimpleConfigurationBuilder; import org.mule.runtime.core.context.DefaultMuleContextBuilder; import org.mule.runtime.core.context.DefaultMuleContextFactory; import org.mule.runtime.core.context.notification.MuleContextNotification; import org.mule.runtime.core.object.SingletonObjectFactory; import org.mule.runtime.core.util.ClassUtils; import org.mule.runtime.core.util.StringUtils; import org.mule.runtime.core.util.concurrent.Latch; import org.mule.service.http.api.HttpService; import org.mule.tck.SensingNullMessageProcessor; import org.mule.tck.SimpleUnitTestSupportSchedulerService; import org.mule.tck.TriggerableMessageSource; import org.mule.tck.config.TestServicesConfigurationBuilder; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.rules.TemporaryFolder; import org.slf4j.Logger; /** * Extends {@link AbstractMuleTestCase} providing access to a {@link MuleContext} instance and tools for manage it. */ public abstract class AbstractMuleContextTestCase extends AbstractMuleTestCase { private static final Logger LOGGER = getLogger(AbstractMuleContextTestCase.class); public static final String WORKING_DIRECTORY_SYSTEM_PROPERTY_KEY = "workingDirectory"; public static final String REACTOR_BLOCK_TIMEOUT_EXCEPTION_MESSAGE = "Timeout on Mono blocking read"; public TestServicesConfigurationBuilder testServicesConfigurationBuilder; public Supplier<TestServicesConfigurationBuilder> testServicesConfigurationBuilderSupplier = () -> new TestServicesConfigurationBuilder(mockHttpService(), mockExprExecutorService()); public TemporaryFolder workingDirectory = new TemporaryFolder(); /** * Top-level directories under <code>.mule</code> which are not deleted on each test case recycle. This is required, e.g. to * play nice with transaction manager recovery service object store. */ public static final String[] IGNORED_DOT_MULE_DIRS = new String[] {"transaction-log"}; /** * The context used to run this test. Context will be created per class or per method depending on * {@link #disposeContextPerClass}. The context will be started only when {@link #startContext} is true. */ protected static MuleContext muleContext; /** * Start the muleContext once it's configured (defaults to false for AbstractMuleTestCase, true for FunctionalTestCase). */ private boolean startContext = false; /** * Convenient test message for unit testing. */ public static final String TEST_MESSAGE = "Test Message"; /** * Default timeout for multithreaded tests (using CountDownLatch, WaitableBoolean, etc.), in milliseconds. The higher this * value, the more reliable the test will be, so it should be set high for Continuous Integration. However, this can waste time * during day-to-day development cycles, so you may want to temporarily lower it while debugging. */ public static final long LOCK_TIMEOUT = 30000; /** * Default timeout for waiting for responses */ public static final int RECEIVE_TIMEOUT = 5000; /** * Default timeout used when blocking on {@link org.reactivestreams.Publisher} completion. */ public static final int BLOCK_TIMEOUT = 200; /** * Use this as a semaphore to the unit test to indicate when a callback has successfully been called. */ protected Latch callbackCalled; /** * Indicates if the context should be instantiated per context. Default is false, which means that a context will be * instantiated per test method. */ private boolean disposeContextPerClass; private static boolean logConfigured; protected boolean isDisposeContextPerClass() { return disposeContextPerClass; } protected void setDisposeContextPerClass(boolean val) { disposeContextPerClass = val; } @Before public final void setUpMuleContext() throws Exception { if (!logConfigured) { configureLoggingForTest(getClass()); logConfigured = true; } workingDirectory.create(); String workingDirectoryOldValue = System.setProperty(WORKING_DIRECTORY_SYSTEM_PROPERTY_KEY, workingDirectory.getRoot().getAbsolutePath()); try { doSetUpBeforeMuleContextCreation(); muleContext = createMuleContext(); if (isStartContext() && muleContext != null && !muleContext.isStarted()) { startMuleContext(); } doSetUp(); } finally { if (workingDirectoryOldValue != null) { System.setProperty(WORKING_DIRECTORY_SYSTEM_PROPERTY_KEY, workingDirectoryOldValue); } else { System.clearProperty(WORKING_DIRECTORY_SYSTEM_PROPERTY_KEY); } } } protected void doSetUpBeforeMuleContextCreation() throws Exception {} private void startMuleContext() throws MuleException, InterruptedException { final AtomicReference<Latch> contextStartedLatch = new AtomicReference<>(); contextStartedLatch.set(new Latch()); // Do not inline it, otherwise the type of the listener is lost final MuleContextNotificationListener<MuleContextNotification> listener = new MuleContextNotificationListener<MuleContextNotification>() { @Override public boolean isBlocking() { return false; } @Override public void onNotification(MuleContextNotification notification) { contextStartedLatch.get().countDown(); } }; muleContext.registerListener(listener); muleContext.start(); contextStartedLatch.get().await(20, SECONDS); } /** * Enables the adding of extra behavior on the set up stage of a test right after the creation of the mule context in * {@link #setUpMuleContext}. * <p> * Under normal circumstances this method could be replaced by a <code>@Before</code> annotated method. * * @throws Exception if something fails that should halt the test case */ protected void doSetUp() throws Exception { // template method } protected MuleContext createMuleContext() throws Exception { // Should we set up the manager for every method? MuleContext context; if (isDisposeContextPerClass() && muleContext != null) { context = muleContext; } else { final ClassLoader executionClassLoader = getExecutionClassLoader(); final ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(executionClassLoader); MuleContextFactory muleContextFactory = new DefaultMuleContextFactory(); List<ConfigurationBuilder> builders = new ArrayList<>(); builders.add(new SimpleConfigurationBuilder(getStartUpProperties())); addBuilders(builders); builders.add(getBuilder()); MuleContextBuilder contextBuilder = new DefaultMuleContextBuilder(); DefaultMuleConfiguration muleConfiguration = new DefaultMuleConfiguration(); String workingDirectory = this.workingDirectory.getRoot().getAbsolutePath(); LOGGER.info("Using working directory for test: " + workingDirectory); muleConfiguration.setWorkingDirectory(workingDirectory); muleConfiguration.setId(this.getClass().getSimpleName() + "#" + name.getMethodName()); contextBuilder.setMuleConfiguration(muleConfiguration); contextBuilder.setExecutionClassLoader(executionClassLoader); contextBuilder.setObjectSerializer(getObjectSerializer()); configureMuleContext(contextBuilder); context = muleContextFactory.createMuleContext(builders, contextBuilder); recordSchedulersOnInit(context); if (!isGracefulShutdown()) { ((DefaultMuleConfiguration) context.getConfiguration()).setShutdownTimeout(0); } } finally { Thread.currentThread().setContextClassLoader(originalContextClassLoader); } } return context; } /** * @return the {@link ObjectSerializer} to use on the test's {@link MuleContext} */ protected ObjectSerializer getObjectSerializer() { return new JavaObjectSerializer(); } protected ClassLoader getExecutionClassLoader() { return this.getClass().getClassLoader(); } // This shouldn't be needed by Test cases but can be used by base testcases that wish to add further builders when // creating the MuleContext. protected void addBuilders(List<ConfigurationBuilder> builders) { testServicesConfigurationBuilder = testServicesConfigurationBuilderSupplier.get(); builders.add(testServicesConfigurationBuilder); } /** * Defines if a mock should be used for the {@link HttpService}. If {@code false} an implementation will need to be provided. * * @return whether or not the {@link HttpService} should be mocked. */ protected boolean mockHttpService() { return true; } /** * Defines if a mock should be used for the {@link ExpressionExecutor}. If {@code false} an implementation will need to be * provided. * * @return whether or not the {@link ExpressionExecutor} should be mocked. */ protected boolean mockExprExecutorService() { return false; } /** * Override this method to set properties of the MuleContextBuilder before it is used to create the MuleContext. */ protected void configureMuleContext(MuleContextBuilder contextBuilder) {} protected ConfigurationBuilder getBuilder() throws Exception { return new DefaultsConfigurationBuilder(); } protected String getConfigurationResources() { return StringUtils.EMPTY; } protected Properties getStartUpProperties() { return null; } @After public final void disposeContextPerTest() throws Exception { doTearDown(); if (!isDisposeContextPerClass()) { if (isStartContext() && muleContext != null && muleContext.isStarted()) { muleContext.stop(); } disposeContext(); if (testServicesConfigurationBuilder != null) { testServicesConfigurationBuilder.stopServices(); } doTearDownAfterMuleContextDispose(); } // When an Assumption fails then junit doesn't call @Before methods so we need to avoid // executing delete if there's no root folder. workingDirectory.delete(); } protected void doTearDownAfterMuleContextDispose() throws Exception {} @AfterClass public static void disposeContext() throws RegistrationException, MuleException { try { if (muleContext != null && !(muleContext.isDisposed() || muleContext.isDisposing())) { try { muleContext.dispose(); } catch (IllegalStateException e) { // Ignore LOGGER.warn(e + " : " + e.getMessage()); } verifyAndStopSchedulers(); MuleConfiguration configuration = muleContext.getConfiguration(); if (configuration != null) { final String workingDir = configuration.getWorkingDirectory(); // do not delete TM recovery object store, everything else is good to // go deleteTree(newFile(workingDir), IGNORED_DOT_MULE_DIRS); } } deleteTree(newFile("./ActiveMQ")); } finally { muleContext = null; clearLoggingConfig(); } } private static List<Scheduler> schedulersOnInit; protected static void recordSchedulersOnInit(MuleContext context) { if (context != null) { final SchedulerService serviceImpl = context.getSchedulerService(); schedulersOnInit = serviceImpl.getSchedulers(); } else { schedulersOnInit = emptyList(); } } protected static void verifyAndStopSchedulers() throws MuleException { final SchedulerService serviceImpl = muleContext.getSchedulerService(); final List<Scheduler> schedulers = new ArrayList<>(serviceImpl.getSchedulers()); schedulers.removeAll(schedulersOnInit); try { assertThat(schedulers, empty()); } finally { schedulers.forEach(sched -> sched.shutdownNow()); if (serviceImpl instanceof SimpleUnitTestSupportSchedulerService) { stopIfNeeded(serviceImpl); } } } /** * Enables the adding of extra behavior on the tear down stage of a test before the mule context is disposed in * {@link #disposeContextPerTest}. * <p> * Under normal circumstances this method could be replace with a <code>@After</code> annotated method. * * @throws Exception if something fails that should halt the testcase */ protected void doTearDown() throws Exception { // template method } public static Flow getTestFlowWithComponent(String name, Class<?> clazz) throws Exception { final SingletonObjectFactory of = new SingletonObjectFactory(clazz, null); of.initialise(); final JavaComponent component = new DefaultJavaComponent(of); ((MuleContextAware) component).setMuleContext(muleContext); Flow flow = builder(name, muleContext).messageProcessors(singletonList(component)).build(); muleContext.getRegistry().registerFlowConstruct(flow); return flow; } /** * Creates a basic event builder with its context already set. * * @return a basic event builder with its context already set. */ protected static Builder eventBuilder() throws MuleException { FlowConstruct flowConstruct = getTestFlow(muleContext); return Event.builder(DefaultEventContext.create(flowConstruct, TEST_CONNECTOR_LOCATION)).flow(flowConstruct); } protected boolean isStartContext() { return startContext; } protected void setStartContext(boolean startContext) { this.startContext = startContext; } /** * Determines if the test case should perform graceful shutdown or not. Default is false so that tests run more quickly. */ protected boolean isGracefulShutdown() { return false; } /** * Create an object of instance <code>clazz</code>. It will then register the object with the registry so that any dependencies * are injected and then the object will be initialised. Note that if the object needs to be configured with additional state * that cannot be passed into the constructor you should create an instance first set any additional data on the object then * call {@link #initialiseObject(Object)}. * * @param clazz the class to create an instance of. * @param <T> Object of this type will be returned * @return an initialised instance of <code>class</code> * @throws Exception if there is a problem creating or initializing the object */ protected <T extends Object> T createObject(Class<T> clazz) throws Exception { return createObject(clazz, ClassUtils.NO_ARGS); } /** * Create an object of instance <code>clazz</code>. It will then register the object with the registry so that any dependencies * are injected and then the object will be initialised. Note that if the object needs to be configured with additional state * that cannot be passed into the constructor you should create an instance first set any additional data on the object then * call {@link #initialiseObject(Object)}. * * @param clazz the class to create an instance of. * @param args constructor parameters * @param <T> Object of this type will be returned * @return an initialised instance of <code>class</code> * @throws Exception if there is a problem creating or initializing the object */ @SuppressWarnings("unchecked") protected <T extends Object> T createObject(Class<T> clazz, Object... args) throws Exception { if (args == null) { args = ClassUtils.NO_ARGS; } Object o = ClassUtils.instanciateClass(clazz, args); muleContext.getRegistry().registerObject(String.valueOf(o.hashCode()), o); return (T) o; } /** * A convenience method that will register an object in the registry using its hashcode as the key. This will cause the object * to have any objects injected and lifecycle methods called. Note that the object lifecycle will be called to the same current * lifecycle as the MuleContext * * @param o the object to register and initialise it * @throws org.mule.runtime.core.api.registry.RegistrationException */ protected void initialiseObject(Object o) throws RegistrationException { muleContext.getRegistry().registerObject(String.valueOf(o.hashCode()), o); } public SensingNullMessageProcessor getSensingNullMessageProcessor() { return new SensingNullMessageProcessor(); } public TriggerableMessageSource getTriggerableMessageSource() { return new TriggerableMessageSource(); } /** * @return the mule application working directory where the app data is stored */ protected File getWorkingDirectory() { return workingDirectory.getRoot(); } /** * @param fileName name of the file. Can contain subfolders separated by {@link java.io.File#separator} * @return a File inside the working directory of the application. */ protected File getFileInsideWorkingDirectory(String fileName) { return new File(getWorkingDirectory(), fileName); } /** * Uses {@link TransformationService} to get a {@link String} representation of a message. * * @param message message to get payload from * @return String representation of the message payload * @throws Exception if there is an unexpected error obtaining the payload representation */ protected String getPayloadAsString(org.mule.runtime.api.message.Message message) throws Exception { return (String) getPayload(message, DataType.STRING); } /** * Uses {@link TransformationService} to get byte[] representation of a message. * * @param message message to get payload from * @return byte[] representation of the message payload * @throws Exception if there is an unexpected error obtaining the payload representation */ protected byte[] getPayloadAsBytes(Message message) throws Exception { return (byte[]) getPayload(message, DataType.BYTE_ARRAY); } /** * Uses {@link TransformationService} to get representation of a message for a given {@link DataType} * * @param message message to get payload from * @param dataType dataType to be transformed to * @return representation of the message payload of the required dataType * @throws Exception if there is an unexpected error obtaining the payload representation */ protected Object getPayload(Message message, DataType dataType) throws Exception { return muleContext.getTransformationService().transform(message, dataType).getPayload().getValue(); } /** * Uses {@link TransformationService} to get representation of a message for a given {@link Class} * * @param message message to get payload from * @param clazz type of the payload to be transformed to * @return representation of the message payload of the required class * @throws Exception if there is an unexpected error obtaining the payload representation */ protected <T> T getPayload(Message message, Class<T> clazz) throws Exception { return (T) getPayload(message, DataType.fromType(clazz)); } protected Event process(Processor processor, Event event) throws Exception { setMuleContextIfNeeded(processor, muleContext); return processor.process(event); } }