summaryrefslogtreecommitdiff
path: root/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTest.java
blob: 835c013b660c5b452bb8cb319e3ee92176fcb637 (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
/*
 *  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.junit;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.PropertyHelper;

/**
 * <p> Run a single JUnit test.
 *
 * <p> The JUnit test is actually run by {@link JUnitTestRunner}.
 * So read the doc comments for that class :)
 *
 * @since Ant 1.2
 *
 * @see JUnitTask
 * @see JUnitTestRunner
 */
public class JUnitTest extends BaseTest implements Cloneable {

    /** the name of the test case */
    private String name = null;

    /**
     * whether the list of test methods has been specified
     * @see #setMethods(java.lang.String)
     * @see #setMethods(java.lang.String[])
     */
    private boolean methodsSpecified = false;

    /** comma-separated list of names of test methods to execute */
    private String methodsList = null;

    /** the names of test methods to execute */
    private String[] methods = null;

    /** the name of the result file */
    private String outfile = null;

    // @todo this is duplicating TestResult information. Only the time is not
    // part of the result. So we'd better derive a new class from TestResult
    // and deal with it. (SB)
    private long runs, failures, errors;
    /**
    @since Ant 1.9.0
    */
    private long skips;

    private long runTime;

    private int antThreadID;

    // Snapshot of the system properties
    private Properties props = null;

    /** No arg constructor. */
    public JUnitTest() {
    }

    /**
     * Constructor with name.
     * @param name the name of the test.
     */
    public JUnitTest(String name) {
        this.name  = name;
    }

    /**
     * Constructor with options.
     * @param name the name of the test.
     * @param haltOnError if true halt the tests if there is an error.
     * @param haltOnFailure if true halt the tests if there is a failure.
     * @param filtertrace if true filter stack traces.
     */
    public JUnitTest(String name, boolean haltOnError, boolean haltOnFailure,
            boolean filtertrace) {
        this(name, haltOnError, haltOnFailure, filtertrace, null, 0);
    }

    /**
     * Constructor with options.
     * @param name the name of the test.
     * @param haltOnError if true halt the tests if there is an error.
     * @param haltOnFailure if true halt the tests if there is a failure.
     * @param filtertrace if true filter stack traces.
     * @param methods if non-null run only these test methods
     * @since 1.8.2
     */
    public JUnitTest(String name, boolean haltOnError, boolean haltOnFailure,
                     boolean filtertrace, String[] methods) {
        this(name, haltOnError, haltOnFailure, filtertrace, methods, 0);
    }

    /**
     * Constructor with options.
     * @param name the name of the test.
     * @param haltOnError if true halt the tests if there is an error.
     * @param haltOnFailure if true halt the tests if there is a failure.
     * @param filtertrace if true filter stack traces.
     * @param methods if non-null run only these test methods
     * @param thread Ant thread ID in which test is currently running
     * @since 1.9.4
     */
    public JUnitTest(String name, boolean haltOnError, boolean haltOnFailure,
                     boolean filtertrace, String[] methods, int thread) {
        this.name  = name;
        this.haltOnError = haltOnError;
        this.haltOnFail = haltOnFailure;
        this.filtertrace = filtertrace;
        this.methodsSpecified = methods != null;
        this.methods = methodsSpecified ? (String[]) methods.clone() : null;
        this.antThreadID = thread;
    }

    /**
     * Sets names of individual test methods to be executed.
     * @param value comma-separated list of names of individual test methods
     *              to be executed,
     *              or <code>null</code> if all test methods should be executed
     * @since 1.8.2
     */
    public void setMethods(String value) {
        methodsList = value;
        methodsSpecified = (value != null);
        methods = null;
    }

    /**
     * Sets names of individual test methods to be executed.
     * @param value non-empty array of names of test methods to be executed
     * @see #setMethods(String)
     * @since 1.8.2
     */
    void setMethods(String[] value) {
        methods = value;
        methodsSpecified = (value != null);
        methodsList = null;
    }

    /**
     * Set the name of the test class.
     * @param value the name to use.
     */
    public void setName(String value) {
        name = value;
    }

    /**
     * Set the thread id
     * @param thread the Ant id of the thread running this test
     * (this is not the system process or thread id)
     * (this will be 0 in single-threaded mode).
     * @since Ant 1.9.4
     */
    public void setThread(int thread) {
        this.antThreadID = thread;
    }

    /**
     * Set the name of the output file.
     * @param value the name of the output file to use.
     */
    public void setOutfile(String value) {
        outfile = value;
    }

    /**
     * Informs whether a list of test methods has been specified in this test.
     * @return <code>true</code> if test methods to be executed have been
     *         specified, <code>false</code> otherwise
     * @see #setMethods(java.lang.String)
     * @see #setMethods(java.lang.String[])
     * @since 1.8.2
     */
    boolean hasMethodsSpecified() {
        return methodsSpecified;
    }

    /**
     * Get names of individual test methods to be executed.
     *
     * @return array of names of the individual test methods to be executed,
     *         or <code>null</code> if all test methods in the suite
     *         defined by the test class will be executed
     * @since 1.8.2
     */
    String[] getMethods() {
        if (methodsSpecified && (methods == null)) {
            resolveMethods();
        }
        return methods;
    }

    /**
     * Gets a comma-separated list of names of methods that are to be executed
     * by this test.
     * @return the comma-separated list of test method names, or an empty
     *         string of no method is to be executed, or <code>null</code>
     *         if no method is specified
     * @since 1.8.2
     */
    String getMethodsString() {
        if ((methodsList == null) && methodsSpecified) {
            if (methods.length == 0) {
                methodsList = "";
            } else if (methods.length == 1) {
                methodsList = methods[0];
            } else {
                StringBuffer buf = new StringBuffer(methods.length * 16);
                buf.append(methods[0]);
                for (int i = 1; i < methods.length; i++) {
                    buf.append(',').append(methods[i]);
                }
                methodsList = buf.toString();
            }
        }
        return methodsList;
    }

    /**
     * Computes the value of the {@link #methods} field from the value
     * of the {@link #methodsList} field, if it has not been computed yet.
     * @exception BuildException if the value of the {@link #methodsList} field
     *                           was invalid
     * @since 1.8.2
     */
    void resolveMethods() {
        if ((methods == null) && methodsSpecified) {
            try {
                methods = parseTestMethodNamesList(methodsList);
            } catch (IllegalArgumentException ex) {
                throw new BuildException(
                        "Invalid specification of test methods: \""
                            + methodsList
                            + "\"; expected: comma-separated list of valid Java identifiers",
                        ex);
            }
        }
    }

    /**
     * Parses a comma-separated list of method names and check their validity.
     * @param methodNames comma-separated list of method names to be parsed
     * @return array of individual test method names
     * @exception  java.lang.IllegalArgumentException
     *             if the given string is <code>null</code> or if it is not
     *             a comma-separated list of valid Java identifiers;
     *             an empty string is acceptable and is handled as an empty
     *             list
     * @since 1.8.2
     */
    public static String[] parseTestMethodNamesList(String methodNames)
                                            throws IllegalArgumentException {
        if (methodNames == null) {
            throw new IllegalArgumentException("methodNames is <null>");
        }

        methodNames = methodNames.trim();

        int length = methodNames.length();
        if (length == 0) {
            return new String[0];
        }

        /* strip the trailing comma, if any */
        if (methodNames.charAt(length - 1) == ',') {
            methodNames = methodNames.substring(0, length - 1).trim();
            length = methodNames.length();
            if (length == 0) {
                throw new IllegalArgumentException("Empty method name");
            }
        }

        final char[] chars = methodNames.toCharArray();
        /* easy detection of one particular case of illegal string: */
        if (chars[0] == ',') {
            throw new IllegalArgumentException("Empty method name");
        }
        /* count number of method names: */
        int wordCount = 1;
        for (int i = 1; i < chars.length; i++) {
            if (chars[i] == ',') {
                wordCount++;
            }
        }
        /* prepare the resulting array: */
        String[] result = new String[wordCount];
        /* parse the string: */
        final int stateBeforeWord = 1;
        final int stateInsideWord = 2;
        final int stateAfterWord = 3;
        //
        int state = stateBeforeWord;
        int wordStartIndex = -1;
        int wordIndex = 0;
        for (int i = 0; i < chars.length; i++) {
            char c = chars[i];
            switch (state) {
                case stateBeforeWord:
                    if (c == ',') {
                        throw new IllegalArgumentException("Empty method name");
                    } else if (c == ' ') {
                        // remain in the same state
                    } else if (Character.isJavaIdentifierStart(c)) {
                        wordStartIndex = i;
                        state = stateInsideWord;
                    } else {
                        throw new IllegalArgumentException("Illegal start of method name: " + c);
                    }
                    break;
                case stateInsideWord:
                    if (c == ',') {
                        result[wordIndex++] = methodNames.substring(wordStartIndex, i);
                        state = stateBeforeWord;
                    } else if (c == ' ') {
                        result[wordIndex++] = methodNames.substring(wordStartIndex, i);
                        state = stateAfterWord;
                    } else if (Character.isJavaIdentifierPart(c)) {
                        // remain in the same state
                    } else {
                        throw new IllegalArgumentException("Illegal character in method name: " + c);
                    }
                    break;
                case stateAfterWord:
                    if (c == ',') {
                        state = stateBeforeWord;
                    } else if (c == ' ') {
                        // remain in the same state
                    } else {
                        throw new IllegalArgumentException("Space in method name");
                    }
                    break;
                default:
                    // this should never happen
            }
        }
        switch (state) {
            case stateBeforeWord:
            case stateAfterWord:
                break;
            case stateInsideWord:
                result[wordIndex++] = methodNames.substring(wordStartIndex, chars.length);
                break;
            default:
                // this should never happen
        }
        return result;
    }

    /**
     * Get the name of the test class.
     * @return the name of the test.
     */
    public String getName() {
        return name;
    }

    /**
     * Get the Ant id of the thread running the test.
     * @return the thread id
     */
    public int getThread() {
        return antThreadID;
    }

    /**
     * Get the name of the output file
     *
     * @return the name of the output file.
     */
    public String getOutfile() {
        return outfile;
    }

    /**
     * Set the number of runs, failures, errors, and skipped tests.
     * @param runs     the number of runs.
     * @param failures the number of failures.
     * @param errors   the number of errors.
     * Kept for backward compatibility with Ant 1.8.4
     */
    public void setCounts(long runs, long failures, long errors) {
        this.runs = runs;
        this.failures = failures;
        this.errors = errors;
    }
    /**
     * Set the number of runs, failures, errors, and skipped tests.
     * @param runs     the number of runs.
     * @param failures the number of failures.
     * @param errors   the number of errors.
     * @param skips   the number of skipped tests.
     * @since Ant 1.9.0
     */
    public void setCounts(long runs, long failures, long errors, long skips) {
        this.runs = runs;
        this.failures = failures;
        this.errors = errors;
        this.skips = skips;
    }

    /**
     * Set the runtime.
     * @param runTime the time in milliseconds.
     */
    public void setRunTime(long runTime) {
        this.runTime = runTime;
    }

    /**
     * Get the number of runs.
     * @return the number of runs.
     */
    public long runCount() {
        return runs;
    }

    /**
     * Get the number of failures.
     * @return the number of failures.
     */
    public long failureCount() {
        return failures;
    }

    /**
     * Get the number of errors.
     * @return the number of errors.
     */
    public long errorCount() {
        return errors;
    }

    /**
     * Get the number of skipped tests.
     * @return the number of skipped tests.
     */
    public long skipCount() {
        return skips;
    }

    /**
     * Get the run time.
     * @return the run time in milliseconds.
     */
    public long getRunTime() {
        return runTime;
    }

    /**
     * Get the properties used in the test.
     * @return the properties.
     */
    public Properties getProperties() {
        return props;
    }

    /**
     * Set the properties to be used in the test.
     * @param p the properties.
     *          This is a copy of the projects ant properties.
     */
    public void setProperties(Hashtable p) {
        props = new Properties();
        for (Enumeration e = p.keys(); e.hasMoreElements();) {
            Object key = e.nextElement();
            props.put(key, p.get(key));
        }
    }

    /**
     * Check if this test should run based on the if and unless
     * attributes.
     * @param p the project to use to check if the if and unless
     *          properties exist in.
     * @return true if this test or testsuite should be run.
     */
    public boolean shouldRun(Project p) {
        PropertyHelper ph = PropertyHelper.getPropertyHelper(p);
        return ph.testIfCondition(getIfCondition())
            && ph.testUnlessCondition(getUnlessCondition());
    }

    /**
     * Get the formatters set for this test.
     * @return the formatters as an array.
     */
    public FormatterElement[] getFormatters() {
        FormatterElement[] fes = new FormatterElement[formatters.size()];
        formatters.copyInto(fes);
        return fes;
    }

    /**
     * Convenient method to add formatters to a vector
     */
    void addFormattersTo(Vector v) {
        final int count = formatters.size();
        for (int i = 0; i < count; i++) {
            v.addElement(formatters.elementAt(i));
        }
    }

    /**
     * @since Ant 1.5
     * @return a clone of this test.
     */
    @Override
    public Object clone() {
        try {
            JUnitTest t = (JUnitTest) super.clone();
            t.props = props == null ? null : (Properties) props.clone();
            t.formatters = (Vector) formatters.clone();
            return t;
        } catch (CloneNotSupportedException e) {
            // plain impossible
            return this;
        }
    }
}