/* * 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.tools.ant.taskdefs.optional.testing; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.Task; import org.apache.tools.ant.TaskAdapter; import org.apache.tools.ant.taskdefs.Parallel; import org.apache.tools.ant.taskdefs.Sequential; import org.apache.tools.ant.taskdefs.WaitFor; import org.apache.tools.ant.taskdefs.condition.Condition; import org.apache.tools.ant.taskdefs.condition.ConditionBase; import org.apache.tools.ant.util.WorkerAnt; /** * Task to provide functional testing under Ant, with a fairly complex workflow of: * * <ul> * <li>Conditional execution</li> * <li>Application to start</li> * <li>A probe to "waitfor" before running tests</li> * <li>A tests sequence</li> * <li>A reporting sequence that runs after the tests have finished</li> * <li>A "teardown" clause that runs after the rest.</li> * <li>Automated termination of the program it executes, if a timeout is not met</li> * <li>Checking of a failure property and automatic raising of a fault * (with the text in failureText) * if test shutdown and reporting succeeded</li> * </ul> * * The task is designed to be framework neutral; it will work with JUnit, * TestNG and other test frameworks That can be * executed from Ant. It bears a resemblance to the FunctionalTest task from * SmartFrog, as the attribute names were * chosen to make migration easier. However, this task benefits from the * ability to tweak Ant's internals, and so * simplify the workflow, and from the experience of using the SmartFrog task. * No code has been shared. * * @since Ant 1.8 */ public class Funtest extends Task { /** {@value} */ public static final String WARN_OVERRIDING = "Overriding previous definition of "; /** {@value} */ public static final String APPLICATION_FORCIBLY_SHUT_DOWN = "Application forcibly shut down"; /** {@value} */ public static final String SHUTDOWN_INTERRUPTED = "Shutdown interrupted"; /** {@value} */ public static final String SKIPPING_TESTS = "Condition failed -skipping tests"; /** Application exception : {@value} */ public static final String APPLICATION_EXCEPTION = "Application Exception"; /** Teardown exception : {@value} */ public static final String TEARDOWN_EXCEPTION = "Teardown Exception"; /** * A condition that must be true before the tests are run. This makes it * easier to define complex tests that only * run if certain conditions are met, such as OS or network state. */ private NestedCondition condition; /** * Used internally to set the workflow up */ private Parallel timedTests; /** * Setup runs if the condition is met. Once setup is complete, teardown * will be run when the task finishes */ private Sequential setup; /** * The application to run */ private Sequential application; /** * A block that halts the tests until met. */ private BlockFor block; /** * Tests to run */ private Sequential tests; /** * Reporting only runs if the tests were executed. If the block stopped * them, reporting is skipped. */ private Sequential reporting; /** * Any teardown operations. */ private Sequential teardown; /** * time for the tests to time out */ private long timeout; private long timeoutUnitMultiplier = WaitFor.ONE_MILLISECOND; /** * time for the execution to time out. */ private long shutdownTime = 10 * WaitFor.ONE_SECOND; private long shutdownUnitMultiplier = WaitFor.ONE_MILLISECOND; /** * Name of a property to look for */ private String failureProperty; /** * Message to send when tests failed */ private String failureMessage = "Tests failed"; /** * Flag to set to true if you don't care about any shutdown errors. * <p/> * In that situation, errors raised during teardown are logged but not * turned into BuildFault events. Similar to catching and ignoring * <code>finally {}</code> clauses in Java/ */ private boolean failOnTeardownErrors = true; /** * What was thrown in the test run (including reporting) */ private BuildException testException; /** * What got thrown during teardown */ private BuildException teardownException; /** * Did the application throw an exception */ private BuildException applicationException; /** * Did the task throw an exception */ private BuildException taskException; /** * Log if the definition is overriding something * * @param name what is being defined * @param definition what should be null if you don't want a warning */ private void logOverride(String name, Object definition) { if (definition != null) { log(WARN_OVERRIDING + '<' + name + '>', Project.MSG_INFO); } } /** * Add a condition element. * @return <code>ConditionBase</code>. * @since Ant 1.6.2 */ public ConditionBase createCondition() { logOverride("condition", condition); condition = new NestedCondition(); return condition; } /** * Add an application. * @param sequence the application to add. */ public void addApplication(Sequential sequence) { logOverride("application", application); application = sequence; } /** * Add a setup sequence. * @param sequence the setup sequence to add. */ public void addSetup(Sequential sequence) { logOverride("setup", setup); setup = sequence; } /** * Add a block. * @param sequence the block for to add. */ public void addBlock(BlockFor sequence) { logOverride("block", block); block = sequence; } /** * add tests. * @param sequence a sequence to add. */ public void addTests(Sequential sequence) { logOverride("tests", tests); tests = sequence; } /** * set reporting sequence of tasks. * @param sequence a reporting sequence to use. */ public void addReporting(Sequential sequence) { logOverride("reporting", reporting); reporting = sequence; } /** * set teardown sequence of tasks. * @param sequence a teardown sequence to use. */ public void addTeardown(Sequential sequence) { logOverride("teardown", teardown); teardown = sequence; } /** * Set the failOnTeardownErrors attribute. * @param failOnTeardownErrors the value to use. */ public void setFailOnTeardownErrors(boolean failOnTeardownErrors) { this.failOnTeardownErrors = failOnTeardownErrors; } /** * Set the failureMessage attribute. * @param failureMessage the value to use. */ public void setFailureMessage(String failureMessage) { this.failureMessage = failureMessage; } /** * Set the failureProperty attribute. * @param failureProperty the value to use. */ public void setFailureProperty(String failureProperty) { this.failureProperty = failureProperty; } /** * Set the shutdownTime attribute. * @param shutdownTime the value to use. */ public void setShutdownTime(long shutdownTime) { this.shutdownTime = shutdownTime; } /** * Set the timeout attribute. * @param timeout the value to use. */ public void setTimeout(long timeout) { this.timeout = timeout; } /** * Set the timeoutunit attribute. * @param unit the value to use. */ public void setTimeoutUnit(WaitFor.Unit unit) { timeoutUnitMultiplier = unit.getMultiplier(); } /** * Set the shutdownunit attribute. * @param unit the value to use. */ public void setShutdownUnit(WaitFor.Unit unit) { shutdownUnitMultiplier = unit.getMultiplier(); } /** * Get the application exception. * @return the application exception. */ public BuildException getApplicationException() { return applicationException; } /** * Get the teardown exception. * @return the teardown exception. */ public BuildException getTeardownException() { return teardownException; } /** * Get the test exception. * @return the test exception. */ public BuildException getTestException() { return testException; } /** * Get the task exception. * @return the task exception. */ public BuildException getTaskException() { return taskException; } /** * Bind and initialise a task * @param task task to bind */ private void bind(Task task) { task.bindToOwner(this); task.init(); } /** * Create a newly bound parallel instance * @param parallelTimeout timeout * @return a bound and initialised parallel instance. */ private Parallel newParallel(long parallelTimeout) { Parallel par = new Parallel(); bind(par); par.setFailOnAny(true); par.setTimeout(parallelTimeout); return par; } /** * Create a newly bound parallel instance with one child * @param parallelTimeout timeout * @param child task * @return a bound and initialised parallel instance. */ private Parallel newParallel(long parallelTimeout, Task child) { Parallel par = newParallel(parallelTimeout); par.addTask(child); return par; } /** * Add any task validation needed to ensure internal code quality * @param task task * @param role role of the task */ private void validateTask(Task task, String role) { if (task != null && task.getProject() == null) { throw new BuildException("%s task is not bound to the project %s", role, task); } } /** * Run the functional test sequence. * <p> * This is a fairly complex workflow -what is going on is that we try to clean up * no matter how the run ended, and to retain the innermost exception that got thrown * during cleanup. That is, if teardown fails after the tests themselves failed, it is the * test failing that is more important. * @throws BuildException if something was caught during the run or teardown. */ @Override public void execute() throws BuildException { //validation validateTask(setup, "setup"); validateTask(application, "application"); validateTask(tests, "tests"); validateTask(reporting, "reporting"); validateTask(teardown, "teardown"); //check the condition //and bail out if it is defined but not true if (condition != null && !condition.eval()) { //we are skipping the test log(SKIPPING_TESTS); return; } long timeoutMillis = timeout * timeoutUnitMultiplier; //set up the application to run in a separate thread Parallel applicationRun = newParallel(timeoutMillis); //with a worker which we can use to manage it WorkerAnt worker = new WorkerAnt(applicationRun, null); if (application != null) { applicationRun.addTask(application); } //The test run consists of the block followed by the tests. long testRunTimeout = 0; Sequential testRun = new Sequential(); bind(testRun); if (block != null) { //waitfor is not a task, it needs to be adapted TaskAdapter ta = new TaskAdapter(block); ta.bindToOwner(this); validateTask(ta, "block"); testRun.addTask(ta); //add the block time to the total test run timeout testRunTimeout = block.calculateMaxWaitMillis(); } //add the tests and more delay if (tests != null) { testRun.addTask(tests); testRunTimeout += timeoutMillis; } //add the reporting and more delay if (reporting != null) { testRun.addTask(reporting); testRunTimeout += timeoutMillis; } //wrap this in a parallel purely to set up timeouts for the //test run timedTests = newParallel(testRunTimeout, testRun); try { //run any setup task if (setup != null) { Parallel setupRun = newParallel(timeoutMillis, setup); setupRun.execute(); } //start the worker thread and leave it running worker.start(); //start the probe+test sequence timedTests.execute(); } catch (BuildException e) { //Record the exception and continue testException = e; } finally { //teardown always runs; its faults are filed away if (teardown != null) { try { Parallel teardownRun = newParallel(timeoutMillis, teardown); teardownRun.execute(); } catch (BuildException e) { teardownException = e; } } } //we get here whether or not the tests/teardown have thrown a BuildException. //do a forced shutdown of the running application, before processing the faults try { //wait for the worker to have finished long shutdownTimeMillis = shutdownTime * shutdownUnitMultiplier; worker.waitUntilFinished(shutdownTimeMillis); if (worker.isAlive()) { //then, if it is still running, interrupt it a second time. log(APPLICATION_FORCIBLY_SHUT_DOWN, Project.MSG_WARN); worker.interrupt(); worker.waitUntilFinished(shutdownTimeMillis); } } catch (InterruptedException e) { //success, something interrupted the shutdown. There may be a leaked //worker; log(SHUTDOWN_INTERRUPTED, e, Project.MSG_VERBOSE); } applicationException = worker.getBuildException(); //Now faults are analysed processExceptions(); } /** * Now faults are analysed. * <p> The priority is * <ol> * <li>testexceptions, except those indicating a build timeout when the application itself failed.<br> (because often it is the application fault that is more interesting than the probe failure, which is usually triggered by the application not starting </li><li> Application exceptions (above test timeout exceptions) </li><li> Teardown exceptions -except when they are being ignored </li><li> Test failures as indicated by the failure property </li></ol> */ protected void processExceptions() { taskException = testException; //look for an application fault if (applicationException != null) { if (taskException == null || taskException instanceof BuildTimeoutException) { taskException = applicationException; } else { ignoringThrowable(APPLICATION_EXCEPTION, applicationException); } } //now look for teardown faults, which may be ignored if (teardownException != null) { if (taskException == null && failOnTeardownErrors) { taskException = teardownException; } else { //don't let the cleanup exception get in the way of any other failure ignoringThrowable(TEARDOWN_EXCEPTION, teardownException); } } //now, analyse the tests if (failureProperty != null && getProject().getProperty(failureProperty) != null) { //we've failed log(failureMessage); if (taskException == null) { taskException = new BuildException(failureMessage); } } //at this point taskException is null or not. //if not, throw the exception if (taskException != null) { throw taskException; } } /** * log that we are ignoring something rather than rethrowing it. * @param type name of exception * @param thrown what was thrown */ protected void ignoringThrowable(String type, Throwable thrown) { log(type + ": " + thrown.toString(), thrown, Project.MSG_WARN); } private static class NestedCondition extends ConditionBase implements Condition { @Override public boolean eval() { if (countConditions() != 1) { throw new BuildException( "A single nested condition is required."); } return getConditions().nextElement().eval(); } } }