aboutsummaryrefslogtreecommitdiffstats
path: root/framework/src/ant/apache-ant-1.9.6/src/main/org/apache/tools/ant/taskdefs/optional/testing/Funtest.java
blob: 2eb357a39d30447e3edb11481ae29b1184963749 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
/*
 *  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 {

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

    /** {@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";

    /**
     * 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(role + " task is not bound to the project" + 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.
     */
    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 {
        public boolean eval() {
            if (countConditions() != 1) {
                throw new BuildException(
                    "A single nested condition is required.");
            }
            return ((Condition) (getConditions().nextElement())).eval();
        }
    }
}