package com.sugarcrm.candybean.candybeanRunner; import com.sugarcrm.candybean.automation.Candybean; import com.sugarcrm.candybean.exceptions.CandybeanException; import org.junit.Ignore; import org.junit.internal.AssumptionViolatedException; import org.junit.internal.runners.model.EachTestNotifier; import org.junit.internal.runners.model.ReflectiveCallable; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; import org.junit.runner.notification.StoppedByUserException; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import javax.swing.plaf.nimbus.State; import java.lang.reflect.Method; import java.util.concurrent.*; /** * CandybeanRunner is a test runner designed to give more control over the test runs. * It provides features that while currently available in JUnit, are implemented in a * slightly different manner. * <p> * To use the runner, annotate your tests with @RunWith(CandybeanRunner.class) * <p> * <b>Test Retries</b> * Set the value "runner.retryCount = x" in candybean.config where x is the maximum * number of times to retry a test if the first run fails (default 0). If any of the * runs pass, the test is marked as a pass. This differs from setting * -DrerunFailingTestsCount as that will mark tests as a flake, which does not play * well with external reporting tools such as Allure. Allure currently has a PR in * to fix that in which retries may become unnecessary. * <a href=https://github.com/allure-framework/allure-core/issues/611>Link</a> * <p> * <b>Test Timeouts</b> * Set the value "runner.timeout = x" in candybean.config where x is the maximum time in * milliseconds that a test (including its @Before and @After methods) should run before * being killed. This setting allows a \@Test('timeout=xxx') like functionality to be set * on a class level rather than on a per test level i.e. if the test fails, its \@After * method will still be ran, unlike setting a timeout @Rule which will hard kill tests * without running \@After. * Additionally, the timeout applies to the \@Before, \@Test, and \@After methods together, * <p> * The precise order of actions which are run is @Before -> @Test -> @After. If the timeout * occurs during any of the methods, the test will be killed, and the runner will attempt to * run @Before and @After to clean up the environment, depending on candybean.config. If * the cleaning up fails, the runner will simply continue on, notifying that the test failed. * <p> * <b>Additional Settings</b> * runner.rerunIfTimedOut = [true|false] If false, do not rerun if a test fails due to timeout * runner.cleanupTimeout = xxx The max timeout in milliseconds the runner will attempt to * clean up after timing out (Defaults to runner.timeout) * runner.runBeforeIfTimedOut = [true|false] If true, run the @Before method on timeout * runner.runAfterIfTimedOut = [true|false] If true, run the @After method on timeout * * Since I expect that those editing this file don't want to have to go read the docs on * how to extend JUnit runners, I've attempted to keep the documentation to a level that * anyone can figure out what's going on without reading the docs (even though you should * anyways). */ public class CandybeanRunner extends BlockJUnit4ClassRunner { protected final Candybean candybean = Candybean.getInstance(); protected final int retryCount = Integer.parseInt(candybean.config.getValue("runner.retryCount", "0")); protected final int timeout = Integer.parseInt(candybean.config.getValue("runner.timeout", "0")); protected final int cleanupTimeout = Integer.parseInt( candybean.config.getValue("runner.cleanupTimeout", String.valueOf(timeout))); protected final boolean rerunIfTimedOut = Boolean.parseBoolean(candybean.config.getValue("runner.rerunIfTimedOut", "false")); protected final boolean runBeforeIfTimedOut = Boolean.parseBoolean(candybean.config.getValue("runner.runBeforeIfTimedOut", "true")); protected final boolean runAfterIfTimedOut = Boolean.parseBoolean(candybean.config.getValue("runner.runAfterIfTimedOut", "true")); final boolean INTERRUPT = true; /** * Creates a BlockJUnit4ClassRunner to run klass * * @param klass The class to initialize * @throws InitializationError */ public CandybeanRunner(Class<?> klass) throws InitializationError, CandybeanException { super(klass); } /** * @{inheritDoc} */ @Override public void run(final RunNotifier notifier) { /* Construct statement that when evaluated: * Runs @BeforeClass * Runs All children tests with runChild * Runs @AfterClass * * If the test fails, it notifies the test runner * * This method is called on each test class. Evaluating * the statement then runs runChild on each test within * the class. */ final Statement statement = classBlock(notifier); try { statement.evaluate(); } catch (StoppedByUserException e) { throw e; } catch (Throwable e) { notifier.fireTestFailure(new Failure(getDescription(), e)); } } /** * Runs each test using retries and timeouts. The general idea of this * method is that it constructs a Callable that when run, runs the test. It * attempts to retrieve the results of the callable after a certain timeout. * If the test wasn't ready, or the test failed, it reruns the callable if * applicable up to some specified number of times. * * @param method The test method to run * @param notifier The test notifier to update */ @Override protected void runChild(final FrameworkMethod method, RunNotifier notifier) { final Description description = describeChild(method); // Check the annotations, it the test is marked ignore, ignore it. // If you wanted add a check for custom annotations, perhaps you // wanted an @DoNotReRun annotation, you would check for it here if (method.getAnnotation(Ignore.class) != null) { notifier.fireTestIgnored(description); return; } // Get a statement representing a single test final Statement statement = methodBlock(method); // Construct a callable to run a single test final Callable<Void> runTest = new Callable<Void>() { public Void call() throws Exception { try { // this code throws a Throwable... statement.evaluate(); } catch (Error | Exception e) { // ...if it's an Error or Exception, that's fine... throw e; } catch (Throwable e) { // ...if not, wrap it in an Exception before throwing. throw new Exception(e); } return null; } }; final EachTestNotifier eachTestNotifier = new EachTestNotifier(notifier, description); eachTestNotifier.fireTestStarted(); TestResult result; int attempts = 0; do { result = attemptTest(description, runTest, method); if (result.failed() && attempts != retryCount){ candybean.getLogger().warning("Caught \"" + result.getThrowable() + "\" while running " + description + ", but have not yet exceeded retries. Retrying test..."); } } while (result.failed() && attempts++ < retryCount && !(result.getThrowable() instanceof TimeoutException && !rerunIfTimedOut)); if (result.failed()) { eachTestNotifier.addFailure(result.getThrowable()); } eachTestNotifier.fireTestFinished(); } /** * Attempt to run the test, and handle the exceptions that a test can throw * * @param description The test Description * @param runTest The callable that runs the test * @param method The FrameworkMethod of the test to run * @return A TestResult representing the result */ protected TestResult attemptTest(Description description, Callable runTest, FrameworkMethod method) { final ExecutorService executorService = Executors.newCachedThreadPool(); Future task = executorService.submit(runTest); try { // Create task to run the test, and attempt to retrieve it within // the time limit, else throw a timeout exception task.get(timeout, TimeUnit.MILLISECONDS); } catch (TimeoutException te) { candybean.log.severe(description + " exceeded allocated runtime of " + timeout + "ms, killing now"); // Interrupt the task and shutdown the task executor task.cancel(INTERRUPT); executorService.shutdownNow(); // If we catch a timeout exception, attempt to run @Before and @After, if enabled if (runAfterIfTimedOut || runBeforeIfTimedOut) { cleanUpState(method); } return TestResult.Fail(new TimeoutException(description + " exceeded allocated runtime of " + timeout + "ms")); } catch (ExecutionException e) { // If the test failed, unwrap the ExecutionException executorService.shutdownNow(); return TestResult.Fail(e.getCause()); } catch (AssumptionViolatedException | InterruptedException t) { executorService.shutdownNow(); return TestResult.Fail(t); } return TestResult.Success(); } /** * Attempts to cleanup the testing environment using the Afters and Befores of method * * @param method The method to use to cleanup state */ protected void cleanUpState(final FrameworkMethod method) { Future cleanupTask = null; final ExecutorService executorService = Executors.newCachedThreadPool(); try { // Construct a new empty test final Object test = new ReflectiveCallable() { @Override protected Object runReflectiveCall() throws Throwable { return createTest(); } }.run(); // Attach the befores and afters to the empty test final Statement emptyStatement = new Statement() { @Override public void evaluate() throws Throwable {} }; final Callable<Void> runAfter = new Callable<Void>() { public Void call() throws Exception { try { /* * In order to run the befores and/or the afters without * running the actual test, we construct an empty Statement * and add the before/afters to it */ if (runBeforeIfTimedOut && runAfterIfTimedOut) { withBefores(method, test, withAfters(method, test, emptyStatement)).evaluate(); } else if (runBeforeIfTimedOut) { withBefores(method, test, emptyStatement).evaluate(); } else { withAfters(method, test, emptyStatement).evaluate(); } } catch (Error | Exception e) { throw e; } catch (Throwable t) { throw new Exception(t); } return null; } }; cleanupTask = executorService.submit(runAfter); cleanupTask.get(cleanupTimeout, TimeUnit.MILLISECONDS); } catch (ExecutionException e) { // If cleaning up failed, log a warning, but continue on, we use the original error to // notify the test runner, not this one candybean.log.warning("Cleaning up" + getDescription() + "failed with " + e.getCause()); } catch (TimeoutException e) { // Similar to if cleaning up failed, we alert if the clean up timed out, but continue on // after killing the cleanup task candybean.log.warning(getDescription() + "' @After method exceeded allocated runtime of " + cleanupTimeout + "ms, killing now"); if (cleanupTask != null) { cleanupTask.cancel(INTERRUPT); } } catch (Throwable t) { candybean.log.warning("Cleaning up" + getDescription() + "failed with " + t); } finally { executorService.shutdownNow(); } } /** * Inner class to represent a test result. A test result * contains the result of the test, passed: true, or failed: false. * If the result is failed, it also contains the throwable the test * created. */ protected static class TestResult { private boolean result; private Throwable throwable; private TestResult(boolean result, Throwable throwable) { this.result = result; this.throwable = throwable; } /** * Create an instance of a passing TestResult * @return A passing TestResult */ public static TestResult Success() { return new TestResult(true, null); } /** * Create an instance of a failing TestResult * @param t The throwable the test threw * @return A failing TestResult */ public static TestResult Fail(Throwable t) { return new TestResult(false, t); } /** * Get the result of the test * @return true if the test passed */ public boolean passed() {return result;} /** * Get the result of the test * @return true if the test failed */ public boolean failed() {return !result;} /** * The failure reason of the test * @return The throwable created by the test */ public Throwable getThrowable() { if (result) { throw new RuntimeException("Cannot access throwable of passed test"); } return throwable; } } }