/*
 *  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
 *
 *      https://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:
  • Conditional execution
  • Application to start
  • A probe to "waitfor" before running tests
  • A tests sequence
  • A reporting sequence that runs after the tests have finished
  • A "teardown" clause that runs after the rest.
  • Automated termination of the program it executes, if a timeout is not met
  • Checking of a failure property and automatic raising of a fault (with the text in failureText) if test shutdown and reporting succeeded
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
/** * 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 {
"Overriding previous definition of "
/** {@value} */
public static final String WARN_OVERRIDING = "Overriding previous definition of ";
"Application forcibly shut down"
/** {@value} */
public static final String APPLICATION_FORCIBLY_SHUT_DOWN = "Application forcibly shut down";
"Shutdown interrupted"
/** {@value} */
public static final String SHUTDOWN_INTERRUPTED = "Shutdown interrupted";
"Condition failed -skipping tests"
/** {@value} */
public static final String SKIPPING_TESTS = "Condition failed -skipping tests";
Application exception : "Application Exception"
/** Application exception : {@value} */
public static final String APPLICATION_EXCEPTION = "Application Exception";
Teardown exception : "Teardown 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.
/** * 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
/** * 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
/** * 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
/** * The application to run */
private Sequential application;
A block that halts the tests until met.
/** * A block that halts the tests until met. */
private BlockFor block;
Tests to run
/** * Tests to run */
private Sequential tests;
Reporting only runs if the tests were executed. If the block stopped them, reporting is skipped.
/** * Reporting only runs if the tests were executed. If the block stopped * them, reporting is skipped. */
private Sequential reporting;
Any teardown operations.
/** * Any teardown operations. */
private Sequential teardown;
time for the tests to time out
/** * time for the tests to time out */
private long timeout; private long timeoutUnitMultiplier = WaitFor.ONE_MILLISECOND;
time for the execution to time out.
/** * 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
/** * Name of a property to look for */
private String failureProperty;
Message to send when tests failed
/** * 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.

In that situation, errors raised during teardown are logged but not turned into BuildFault events. Similar to catching and ignoring finally {} clauses in Java/
/** * 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)
/** * What was thrown in the test run (including reporting) */
private BuildException testException;
What got thrown during teardown
/** * What got thrown during teardown */
private BuildException teardownException;
Did the application throw an exception
/** * Did the application throw an exception */
private BuildException applicationException;
Did the task throw an exception
/** * Did the task throw an exception */
private BuildException taskException;
Log if the definition is overriding something
Params:
  • name – what is being defined
  • definition – what should be null if you don't want a warning
/** * 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.
Returns:ConditionBase.
Since:Ant 1.6.2
/** * 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.
Params:
  • sequence – the application to add.
/** * Add an application. * @param sequence the application to add. */
public void addApplication(Sequential sequence) { logOverride("application", application); application = sequence; }
Add a setup sequence.
Params:
  • sequence – the setup sequence to add.
/** * Add a setup sequence. * @param sequence the setup sequence to add. */
public void addSetup(Sequential sequence) { logOverride("setup", setup); setup = sequence; }
Add a block.
Params:
  • sequence – the block for to add.
/** * Add a block. * @param sequence the block for to add. */
public void addBlock(BlockFor sequence) { logOverride("block", block); block = sequence; }
add tests.
Params:
  • sequence – a sequence to add.
/** * add tests. * @param sequence a sequence to add. */
public void addTests(Sequential sequence) { logOverride("tests", tests); tests = sequence; }
set reporting sequence of tasks.
Params:
  • sequence – a reporting sequence to use.
/** * 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.
Params:
  • sequence – a teardown sequence to use.
/** * 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.
Params:
  • failOnTeardownErrors – the value to use.
/** * Set the failOnTeardownErrors attribute. * @param failOnTeardownErrors the value to use. */
public void setFailOnTeardownErrors(boolean failOnTeardownErrors) { this.failOnTeardownErrors = failOnTeardownErrors; }
Set the failureMessage attribute.
Params:
  • failureMessage – the value to use.
/** * Set the failureMessage attribute. * @param failureMessage the value to use. */
public void setFailureMessage(String failureMessage) { this.failureMessage = failureMessage; }
Set the failureProperty attribute.
Params:
  • failureProperty – the value to use.
/** * Set the failureProperty attribute. * @param failureProperty the value to use. */
public void setFailureProperty(String failureProperty) { this.failureProperty = failureProperty; }
Set the shutdownTime attribute.
Params:
  • shutdownTime – the value to use.
/** * Set the shutdownTime attribute. * @param shutdownTime the value to use. */
public void setShutdownTime(long shutdownTime) { this.shutdownTime = shutdownTime; }
Set the timeout attribute.
Params:
  • timeout – the value to use.
/** * Set the timeout attribute. * @param timeout the value to use. */
public void setTimeout(long timeout) { this.timeout = timeout; }
Set the timeoutunit attribute.
Params:
  • unit – the value to use.
/** * Set the timeoutunit attribute. * @param unit the value to use. */
public void setTimeoutUnit(WaitFor.Unit unit) { timeoutUnitMultiplier = unit.getMultiplier(); }
Set the shutdownunit attribute.
Params:
  • unit – the value to use.
/** * Set the shutdownunit attribute. * @param unit the value to use. */
public void setShutdownUnit(WaitFor.Unit unit) { shutdownUnitMultiplier = unit.getMultiplier(); }
Get the application exception.
Returns:the application exception.
/** * Get the application exception. * @return the application exception. */
public BuildException getApplicationException() { return applicationException; }
Get the teardown exception.
Returns:the teardown exception.
/** * Get the teardown exception. * @return the teardown exception. */
public BuildException getTeardownException() { return teardownException; }
Get the test exception.
Returns:the test exception.
/** * Get the test exception. * @return the test exception. */
public BuildException getTestException() { return testException; }
Get the task exception.
Returns:the task exception.
/** * Get the task exception. * @return the task exception. */
public BuildException getTaskException() { return taskException; }
Bind and initialise a task
Params:
  • task – task to bind
/** * 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
Params:
  • parallelTimeout – timeout
Returns:a bound and initialised parallel instance.
/** * 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
Params:
  • parallelTimeout – timeout
  • child – task
Returns:a bound and initialised parallel instance.
/** * 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
Params:
  • task – task
  • role – role of the task
/** * 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.

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.
/** * 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.

The priority is

  1. testexceptions, except those indicating a build timeout when the application itself failed. (Because often it is the application fault that is more interesting than the probe failure, which is usually triggered by the application not starting.)
  2. Application exceptions (above test timeout exceptions)
  3. Teardown exceptions -except when they are being ignored
  4. Test failures as indicated by the failure property
/** * Now faults are analysed. * <p>The priority is</p> * <ol> * <li>testexceptions, except those indicating a build timeout when the application itself * failed. (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.
Params:
  • type – name of exception
  • thrown – what was thrown
/** * 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(); } } }