/* * 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.lang.String.format; import static java.lang.System.getProperty; import static java.lang.Thread.getAllStackTraces; import static java.util.Collections.sort; import static org.junit.Assume.assumeThat; import static org.mule.runtime.api.message.Message.of; import static org.mule.runtime.core.api.Event.setCurrentEvent; import static org.mule.runtime.core.util.StringMessageUtils.getBoilerPlate; import static org.mule.runtime.core.util.SystemUtils.parsePropertyDefinitions; import static org.mule.runtime.dsl.api.component.config.DefaultComponentLocation.fromSingleComponent; import static org.mule.tck.util.MuleContextUtils.eventBuilder; import static org.slf4j.LoggerFactory.getLogger; import org.mule.runtime.api.component.location.ComponentLocation; import org.mule.runtime.api.exception.MuleException; import org.mule.runtime.core.api.Event; import org.mule.runtime.core.util.StringUtils; import org.mule.runtime.core.util.SystemUtils; import org.mule.tck.junit4.rule.WarningTimeout; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.rules.DisableOnDebug; import org.junit.rules.TestName; import org.junit.rules.TestRule; import org.junit.rules.Timeout; import org.slf4j.Logger; /** * <code>AbstractMuleTestCase</code> is a base class for Mule test cases. This implementation provides services to test code for * creating mock and test objects. */ public abstract class AbstractMuleTestCase { public static final String TEST_PAYLOAD = "test"; public static final String TEST_CONNECTOR = "test"; public static final ComponentLocation TEST_CONNECTOR_LOCATION = fromSingleComponent(TEST_CONNECTOR); public static final String TESTING_MODE_PROPERTY_NAME = "mule.testingMode"; public static final int DEFAULT_TEST_TIMEOUT_SECS = 60; public static final String TEST_TIMEOUT_SYSTEM_PROPERTY = "mule.test.timeoutSecs"; /** * Indicates whether the text boxes will be logged when starting each test case. */ private static final boolean verbose; static { String muleOpts = SystemUtils.getenv("MULE_TEST_OPTS"); if (StringUtils.isNotBlank(muleOpts)) { Map<String, String> parsedOpts = parsePropertyDefinitions(muleOpts); String optVerbose = parsedOpts.get("mule.verbose"); verbose = Boolean.valueOf(optVerbose); } else { verbose = true; } System.setProperty(TESTING_MODE_PROPERTY_NAME, StringUtils.EMPTY); } private static final Logger LOGGER = getLogger(AbstractMuleTestCase.class); /** * Should be set to a string message describing any prerequisites not met. */ private boolean offline = "true".equalsIgnoreCase(System.getProperty("org.mule.offline")); private int testTimeoutSecs = getTimeoutSystemProperty(); @Rule public TestName name = new TestName(); @Rule public TestRule globalTimeout = createTestTimeoutRule(); /** * Creates the timeout rule that will be used to run the test. * * @return the rule used to check for test execution timeouts. */ protected TestRule createTestTimeoutRule() { int millisecondsTimeout = getTestTimeoutSecs() * 1000; if (isFailOnTimeout()) { return new DisableOnDebug(new Timeout(millisecondsTimeout)); } else { return new WarningTimeout(millisecondsTimeout); } } /** * Defines the number of seconds that a test has in order to run before throwing a timeout. If the property if not defined then * uses the <code>DEFAULT_MULE_TEST_TIMEOUT_SECS</code> constant. * * @return the timeout value expressed in seconds */ protected int getTimeoutSystemProperty() { String timeoutString = System.getProperty(TEST_TIMEOUT_SYSTEM_PROPERTY, null); if (timeoutString == null) { // unix style: MULE_TEST_TIMEOUTSECS String variableName = TEST_TIMEOUT_SYSTEM_PROPERTY.toUpperCase().replace(".", "_"); timeoutString = System.getenv(variableName); } int result = DEFAULT_TEST_TIMEOUT_SECS; if (timeoutString != null) { try { result = Integer.parseInt(timeoutString); } catch (NumberFormatException e) { // Uses the default value } } return result; } /** * Subclasses can override this method to skip the execution of the entire test class. * * @return <code>true</code> if the test class should not be run. */ protected boolean isDisabledInThisEnvironment() { return false; } /** * Should this test run? * * @param testMethodName name of the test method * @return whether the test should execute in the current environment */ protected boolean isDisabledInThisEnvironment(String testMethodName) { return false; } public boolean isOffline(String method) { if (offline) { LOGGER.warn(getBoilerPlate("Working offline cannot run test: " + method, '=', 80)); } return offline; } /** * Defines the timeout in seconds that will be used to run the test. * * @return the timeout in seconds */ public int getTestTimeoutSecs() { return testTimeoutSecs; } /** * Could be useful to use it in place of hardcoding the pom project version in the tests. * * @return the "project.version" maven property. */ protected static String getMavenProjectVersionProperty() { return getProperty("maven.projectVersion"); } @Before public final void initializeMuleTest() { skipTestWhenDisabledInCurrentEnvironment(); printTestHeader(); } private void printTestHeader() { if (verbose) { System.out.println(getBoilerPlate(getTestHeader(), '=', 80)); } } protected String getTestHeader() { return "Testing: " + name.getMethodName(); } private void skipTestWhenDisabledInCurrentEnvironment() { assumeThat(this, new BaseMatcher<AbstractMuleTestCase>() { @Override public boolean matches(Object o) { return !(isDisabledInThisEnvironment() || isDisabledInThisEnvironment(name.getMethodName())); } @Override public void describeTo(Description description) { description.appendText("Test " + name.getMethodName() + " disabled in this environment"); } }); } /** * Indicates whether the test should fail when a timeout is reached. * <p/> * This feature was added to support old test cases that depend on 3rd-party resources such as a public web service. In such * cases it may be desirable to not fail the test upon timeout but rather to simply log a warning. * * @return true if it must fail on timeout and false otherwise. Default value is true. */ protected boolean isFailOnTimeout() { return true; } @After public final void clearRequestContext() { setCurrentEvent(null); } protected static List<String> collectThreadNames() { List<String> threadNames = new ArrayList<>(); for (Thread t : getAllStackTraces().keySet()) { if (t.isAlive()) { threadNames.add(t.getName() + " - " + t.getId()); } } sort(threadNames); return threadNames; } private static String testCaseName; @BeforeClass public static void clearTestCaseName() { testCaseName = null; } @Before public void takeTestCaseName() { if (testCaseName == null) { testCaseName = this.getClass().getName(); } } private Event _testEvent; private Event _nullPayloadEvent; /** * Creates and caches a test {@link Event} instance for the scope of the current test method. * * @return test event. * @throws MuleException */ protected Event testEvent() throws MuleException { if (_testEvent == null) { _testEvent = newEvent(); } return _testEvent; } /** * Create a new {@link Event} for each invocation. Useful if multiple distinct event instances are needed in a single test * method. * * @return new test event. * @throws MuleException */ protected Event newEvent() throws MuleException { return eventBuilder().message(of(TEST_PAYLOAD)).build(); } protected Event nullPayloadEvent() throws MuleException { if (_nullPayloadEvent == null) { _nullPayloadEvent = eventBuilder().message(of(null)).build(); } return _nullPayloadEvent; } @After public void clearTestEvents() throws MuleException { _testEvent = null; _nullPayloadEvent = null; } @AfterClass public static void dumpFilteredThreadsInTest() { List<String> currentThreads = collectThreadNames(); int filteredThreads = 0; StringBuilder builder = new StringBuilder(); for (String threadName : currentThreads) { if (!nameIn(threadName, "[MuleRuntime]", "Finalizer", "Monitor Ctrl-Break", "Reference Handler", "Signal Dispatcher", "main")) { builder.append("\n-> ").append(threadName); filteredThreads++; } } if (filteredThreads > 0) { logThreadsResult(format("Hung threads count: %d. Test case: %s. Thread names:%s", filteredThreads, testCaseName, builder.toString())); } else { logThreadsResult(format("No hung threads. Test case: %s", testCaseName)); } } private static boolean nameIn(String threadName, String... values) { String threadNameLowercase = threadName.toLowerCase(); if (values != null) { for (String value : values) { if (threadNameLowercase.startsWith(value.toLowerCase())) { return true; } } } return false; } private static final transient String THREAD_RESULT_LINE = StringUtils.repeat('-', 80); private static void logThreadsResult(String result) { LOGGER.warn(format("\n%s\n%s\n%s\n", THREAD_RESULT_LINE, result, THREAD_RESULT_LINE)); } }