/
JUnit4.java
1923 lines (1681 loc) · 67.3 KB
/
JUnit4.java
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
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package com.carrotsearch.ant.tasks.junit4;
import static com.carrotsearch.randomizedtesting.SysGlobals.*;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.SimpleDateFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.carrotsearch.ant.tasks.junit4.forked.ForkedMain;
import com.carrotsearch.ant.tasks.junit4.forked.ForkedMainSafe;
import com.carrotsearch.ant.tasks.junit4.runlisteners.RunListenerClass;
import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectComponent;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.types.Assertions;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.CommandlineJava;
import org.apache.tools.ant.types.Environment;
import org.apache.tools.ant.types.Environment.Variable;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.PropertySet;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.Resources;
import org.apache.tools.ant.util.LoaderUtils;
import org.junit.runner.Description;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import com.carrotsearch.ant.tasks.junit4.SuiteBalancer.Assignment;
import com.carrotsearch.ant.tasks.junit4.balancers.RoundRobinBalancer;
import com.carrotsearch.ant.tasks.junit4.balancers.SuiteHint;
import com.carrotsearch.ant.tasks.junit4.events.BootstrapEvent;
import com.carrotsearch.ant.tasks.junit4.events.QuitEvent;
import com.carrotsearch.ant.tasks.junit4.events.aggregated.AggregatedQuitEvent;
import com.carrotsearch.ant.tasks.junit4.events.aggregated.AggregatedStartEvent;
import com.carrotsearch.ant.tasks.junit4.events.aggregated.AggregatingListener;
import com.carrotsearch.ant.tasks.junit4.events.aggregated.ChildBootstrap;
import com.carrotsearch.ant.tasks.junit4.events.aggregated.JvmOutputEvent;
import com.carrotsearch.ant.tasks.junit4.listeners.AggregatedEventListener;
import com.carrotsearch.randomizedtesting.ClassGlobFilter;
import com.carrotsearch.randomizedtesting.FilterExpressionParser;
import com.carrotsearch.randomizedtesting.FilterExpressionParser.Node;
import com.carrotsearch.randomizedtesting.MethodGlobFilter;
import com.carrotsearch.randomizedtesting.RandomizedRunner;
import com.carrotsearch.randomizedtesting.SeedUtils;
import com.carrotsearch.randomizedtesting.SysGlobals;
import com.carrotsearch.randomizedtesting.TeeOutputStream;
import com.carrotsearch.randomizedtesting.annotations.SuppressForbidden;
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.io.CharStreams;
import com.google.common.io.Closeables;
import com.google.common.io.Closer;
import com.google.common.io.FileWriteMode;
/**
* An ANT task to run JUnit4 tests. Differences (benefits?) compared to ANT's default JUnit task:
* <ul>
* <li>Built-in parallel test execution support (spawns multiple JVMs to avoid
* test interactions).</li>
* <li>Randomization of the order of test suites within a single JVM.</li>
* <li>Aggregates and synchronizes test events from executors. All reports run on
* the task's JVM (not on the test JVM).</li>
* <li>Fully configurable reporting via listeners (console, ANT-compliant XML, JSON).
* Report listeners use Google Guava's {@link EventBus} and receive full information
* about tests' execution (including skipped, assumption-skipped tests, streamlined
* output and error stream chunks, etc.).</li>
* <li>JUnit 4.10+ is required both for the task and for the tests classpath.
* Older versions will cause build failure.</li>
* <li>Integration with {@link RandomizedRunner} (randomization seed is passed to
* children JVMs).</li>
* </ul>
*/
public class JUnit4 extends Task {
/**
* Welcome messages.
*/
private static String [] WELCOME_MESSAGES = {
"hello!", // en
"hi!", // en
"g'day!", // en, australia
"¡Hola!", // es
"jolly good day!", // monty python
"aloha!", // en, hawaii
"cześć!", // pl
"مرحبا!", // arabic (modern)
"kaixo!", // basque
"Привет!", // bulgarian, russian
"你好!", // cn, traditional
"ahoj!", // czech
"salut!", // french
"hallo!", // german
"שלום!", // hebrew
"नमस्ते!", // hindi
"ᐊᐃ!", // inuktitut
"ciao!", // italian
"今日は!", // japanese
"olá!", // portuguese
// add more if your country/ place is not on the list ;)
};
/** Name of the antlib resource inside JUnit4 JAR. */
public static final String ANTLIB_RESOURCE_NAME = "com/carrotsearch/junit4/antlib.xml";
/** @see #setParallelism(String) */
public static final Object PARALLELISM_AUTO = "auto";
/** @see #setParallelism(String) */
public static final String PARALLELISM_MAX = "max";
/** Default value of {@link #setShuffleOnForkedJvm(boolean)}. */
public static final boolean DEFAULT_SHUFFLE_ON_FORKED_JVM = true;
/** Default value of {@link #setParallelism}. */
public static final String DEFAULT_PARALLELISM = "1";
/** Default value of {@link #setPrintSummary}. */
public static final boolean DEFAULT_PRINT_SUMMARY = true;
/** Default value of {@link #setHaltOnFailure}. */
public static final boolean DEFAULT_HALT_ON_FAILURE = true;
/** Default value of {@link #setIsolateWorkingDirectories(boolean)}. */
public static final boolean DEFAULT_ISOLATE_WORKING_DIRECTORIES = true;
/** Default valkue of {@link #setOnNonEmptyWorkDirectory}. */
public static final NonEmptyWorkDirectoryAction DEFAULT_NON_EMPTY_WORKDIR_ACTION = NonEmptyWorkDirectoryAction.FAIL;
/** Default value of {@link #setDynamicAssignmentRatio(float)} */
public static final float DEFAULT_DYNAMIC_ASSIGNMENT_RATIO = .25f;
/** Default value of {@link #setSysouts}. */
public static final boolean DEFAULT_SYSOUTS = false;
/** Default value of {@link #setDebugStream}. */
public static final boolean DEFAULT_DEBUGSTREAM = false;
/** Default value of {@link #setUniqueSuiteNames(boolean)} */
public static final boolean DEFAULT_UNIQUE_SUITE_NAME = true;
/** System property passed to forked VMs: current working directory (absolute). */
private static final String CHILDVM_SYSPROP_CWD = "junit4.childvm.cwd";
/**
* System property passed to forked VMs: junit4's temporary folder location
* (must have read/write access if security manager is used).
*/
private static final String SYSPROP_TEMPDIR = "junit4.tempDir";
/** What to do on JVM output? */
public static enum JvmOutputAction {
PIPE,
IGNORE,
FAIL,
WARN,
LISTENERS
}
/** What to do when there were no executed tests (all ignored or none at all?). */
public static enum NoTestsAction {
IGNORE,
FAIL,
WARN
}
/**
* @see #setJvmOutputAction(String)
*/
public EnumSet<JvmOutputAction> jvmOutputAction = EnumSet.of(
JvmOutputAction.LISTENERS,
JvmOutputAction.WARN);
/**
* @see #setSysouts
*/
private boolean sysouts = DEFAULT_SYSOUTS;
/**
* @see #setDebugStream
*/
private boolean debugStream = DEFAULT_DEBUGSTREAM;
/**
* Forked JVM command line.
*/
private CommandlineJava forkedJvmCommandLine = new CommandlineJava();
/**
* Set new environment for the forked process?
*/
private boolean newEnvironment;
/**
* @see #setUniqueSuiteNames
*/
private boolean uniqueSuiteNames = DEFAULT_UNIQUE_SUITE_NAME;
/**
* Environment variables to use in the forked JVM.
*/
private Environment env = new Environment();
/**
* Directory to invoke forked VMs in.
*/
private Path dir;
/**
* Test names.
*/
private final Resources resources;
/**
* Stop the build process if there were errors?
*/
private boolean haltOnFailure = DEFAULT_HALT_ON_FAILURE;
/**
* Print summary of all tests at the end.
*/
private boolean printSummary = DEFAULT_PRINT_SUMMARY;
/**
* Property to set if there were test failures or errors.
*/
private String failureProperty;
/**
* A folder to store temporary files in. Defaults to {@link #dir} or
* the project's basedir.
*/
private Path tempDir;
/**
* Listeners listening on the event bus.
*/
private List<Object> listeners = new ArrayList<>();
/**
* User-defined {@link org.junit.runner.notification.RunListener}s.
*/
private List<RunListenerClass> runListeners = new ArrayList<>();
/**
* Balancers scheduling tests for individual JVMs in parallel mode.
*/
private List<SuiteBalancer> balancers = new ArrayList<>();
/**
* Class loader used to resolve annotations and classes referenced from annotations
* when {@link Description}s containing them are passed from forked JVMs.
*/
private AntClassLoader testsClassLoader;
/**
* @see #setParallelism(String)
*/
private String parallelism = DEFAULT_PARALLELISM;
/**
* Set to true to leave temporary files (for diagnostics).
*/
private boolean leaveTemporary;
/**
* A list of temporary files to leave or remove if build passes.
*/
private List<Path> temporaryFiles = Collections.synchronizedList(new ArrayList<Path>());
/**
* @see #setSeed(String)
*/
private String random;
/**
* @see #setIsolateWorkingDirectories(boolean)
*/
private boolean isolateWorkingDirectories = DEFAULT_ISOLATE_WORKING_DIRECTORIES;
/**
* @see #setIsolateWorkingDirectories(boolean)
*/
private NonEmptyWorkDirectoryAction nonEmptyWorkDirAction = DEFAULT_NON_EMPTY_WORKDIR_ACTION;
/**
* Multiple path resolution in {@link CommandlineJava#getCommandline()} is very slow
* so we construct and canonicalize paths.
*/
private org.apache.tools.ant.types.Path classpath;
private org.apache.tools.ant.types.Path bootclasspath;
/**
* @see #setDynamicAssignmentRatio(float)
*/
private float dynamicAssignmentRatio = DEFAULT_DYNAMIC_ASSIGNMENT_RATIO;
/**
* @see #setShuffleOnForkedJvm(boolean)
*/
private boolean shuffleOnForkedJvm = DEFAULT_SHUFFLE_ON_FORKED_JVM;
/**
* @see #setHeartbeat
*/
private long heartbeat;
/**
* @see #setIfNoTests
*/
private NoTestsAction ifNoTests = NoTestsAction.IGNORE;
/**
* @see #setStatsPropertyPrefix
*/
private String statsPropertyPrefix;
/**
*
*/
public JUnit4() {
resources = new Resources();
}
/**
* What should be done on unexpected JVM output? JVM may write directly to the
* original descriptors, bypassing redirections of System.out and System.err. Typically,
* these messages will be important and should fail the build (permgen space exceeded,
* compiler errors, crash dumps). However, certain legitimate logs (gc activity, class loading
* logs) are also printed to these streams so sometimes the output can be ignored.
*
* <p>Allowed values (any comma-delimited combination of): {@link JvmOutputAction}
* constants.
*/
public void setJvmOutputAction(String jvmOutputActions) {
EnumSet<JvmOutputAction> actions = EnumSet.noneOf(JvmOutputAction.class);
for (String s : jvmOutputActions.split("[\\,\\ ]+")) {
s = s.trim().toUpperCase(Locale.ROOT);
actions.add(JvmOutputAction.valueOf(s));
}
this.jvmOutputAction = actions;
}
/**
* If set to true, any sysout and syserr calls will be written to original
* output and error streams (and in effect will appear as "jvm output". By default
* sysout and syserrs are captured and proxied to the event stream to be synchronized
* with other test events but occasionally one may want to synchronize them with direct
* JVM output (to synchronize with compiler output or GC output for example).
*/
public void setSysouts(boolean sysouts) {
this.sysouts = sysouts;
}
/**
* Enables a debug stream from each forked JVM. This will create an additional file
* next to each events file. For debugging the framework only, not a general-purpose setting.
*/
public void setDebugStream(boolean debugStream) {
this.debugStream = debugStream;
}
/**
* Allow or disallow duplicate suite names in resource collections. By default this option
* is <code>true</code> because certain ANT-compatible report types (like XML reports)
* will have a problem with duplicate suite names (will overwrite files).
*/
public void setUniqueSuiteNames(boolean uniqueSuiteNames) {
this.uniqueSuiteNames = uniqueSuiteNames;
}
/**
* @see #setUniqueSuiteNames(boolean)
*/
public boolean isUniqueSuiteNames() {
return uniqueSuiteNames;
}
/**
* Specifies the ratio of suites moved to dynamic assignment list. A dynamic
* assignment list dispatches suites to the first idle forked JVM. Theoretically
* this is an optimal strategy, but it is usually better to have some static assignments
* to avoid communication costs.
*
* <p>A ratio of 0 means only static assignments are used. A ratio of 1 means
* only dynamic assignments are used.
*
* <p>The list of dynamic assignments is sorted by decreasing cost (always) and
* is inherently prone to race conditions in distributing suites. Should there
* be an error based on suite-dependency it will not be directly repeatable. In such
* case use the per-forked-jvm list of suites file dumped to disk for each forked JVM.
* (see {@link #setLeaveTemporary(boolean)}).
*/
public void setDynamicAssignmentRatio(float ratio) {
if (ratio < 0 || ratio > 1) {
throw new IllegalArgumentException("Dynamic assignment ratio must be " +
"between 0 (only static assignments) to 1 (fully dynamic assignments).");
}
this.dynamicAssignmentRatio = ratio;
}
/**
* The number of parallel forked JVMs. Can be set to a constant "max" for the
* number of cores returned from {@link Runtime#availableProcessors()} or
* "auto" for sensible defaults depending on the number of cores.
* The default is a single subprocess.
*
* <p>Note that this setting forks physical JVM processes so it multiplies the
* requirements for heap memory, IO, etc.
*/
public void setParallelism(String parallelism) {
this.parallelism = parallelism;
}
/**
* Property to set to "true" if there is a failure in a test.
*/
public void setFailureProperty(String failureProperty) {
this.failureProperty = failureProperty;
}
/**
* Do not propagate the old environment when new environment variables are specified.
*/
public void setNewEnvironment(boolean v) {
this.newEnvironment = v;
}
/**
* Initial random seed used for shuffling test suites and other sources
* of pseudo-randomness. If not set, any random value is set.
*
* <p>The seed's format is compatible with {@link RandomizedRunner} so that
* seed can be fixed for suites and methods alike.
*/
public void setSeed(String randomSeed) {
if (!Strings.isNullOrEmpty(getProject().getUserProperty(SYSPROP_RANDOM_SEED()))) {
String userProperty = getProject().getUserProperty(SYSPROP_RANDOM_SEED());
if (!userProperty.equals(randomSeed)) {
log("Ignoring seed attribute because it is overridden by user properties.", Project.MSG_WARN);
}
} else if (!Strings.isNullOrEmpty(randomSeed)) {
this.random = randomSeed;
}
}
/**
* Initializes custom prefix for all junit4 properties. This must be consistent
* across all junit4 invocations if done from the same classpath. Use only when REALLY needed.
*/
public void setPrefix(String prefix) {
if (!Strings.isNullOrEmpty(getProject().getUserProperty(SYSPROP_PREFIX()))) {
log("Ignoring prefix attribute because it is overridden by user properties.", Project.MSG_WARN);
} else {
SysGlobals.initializeWith(prefix);
}
}
/**
* @see #setSeed(String)
*/
public String getSeed() {
return random;
}
/**
* Predictably shuffle tests order after balancing. This will help in spreading
* lighter and heavier tests over a single forked JVM execution timeline while
* still keeping the same tests order depending on the seed.
*
* @deprecated Use {@link #setShuffleOnForkedJvm(boolean)}
*/
@Deprecated()
public void setShuffleOnSlave(boolean shuffle) {
setShuffleOnForkedJvm(shuffle);
}
/**
* Predictably shuffle tests order after balancing. This will help in spreading
* lighter and heavier tests over a single forked JVM execution timeline while
* still keeping the same tests order depending on the seed.
*/
public void setShuffleOnForkedJvm(boolean shuffle) {
this.shuffleOnForkedJvm = shuffle;
}
/*
*
*/
@Override
public void setProject(Project project) {
super.setProject(project);
this.resources.setProject(project);
this.classpath = new org.apache.tools.ant.types.Path(getProject());
this.bootclasspath = new org.apache.tools.ant.types.Path(getProject());
}
/**
* Prints the summary of all executed, ignored etc. tests at the end.
*/
public void setPrintSummary(boolean printSummary) {
this.printSummary = printSummary;
}
/**
* Stop the build process if there were failures or errors during test execution.
*/
public void setHaltOnFailure(boolean haltOnFailure) {
this.haltOnFailure = haltOnFailure;
}
/**
* Set the maximum memory to be used by all forked JVMs.
*
* @param max
* the value as defined by <tt>-mx</tt> or <tt>-Xmx</tt> in the java
* command line options.
*/
public void setMaxmemory(String max) {
if (!Strings.isNullOrEmpty(max)) {
getCommandline().setMaxmemory(max);
}
}
/**
* Set to true to leave temporary files for diagnostics.
*/
public void setLeaveTemporary(boolean leaveTemporary) {
this.leaveTemporary = leaveTemporary;
}
/**
* Add an additional argument to any forked JVM.
*/
public Commandline.Argument createJvmarg() {
return getCommandline().createVmArgument();
}
/**
* The directory to invoke forked VMs in.
*/
public void setDir(File dir) {
this.dir = dir.toPath();
}
/**
* The directory to store temporary files in.
*/
public void setTempDir(File tempDir) {
this.tempDir = tempDir.toPath();
}
/**
* What to do when no tests were executed (all tests were ignored)?
* @see NoTestsAction
*/
public void setIfNoTests(String value) {
try {
ifNoTests = NoTestsAction.valueOf(value.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new BuildException("Invalid value (one of "
+ Arrays.toString(NoTestsAction.values()) + " accepted): " + value);
}
}
/**
* A {@link org.apache.tools.ant.types.Environment.Variable} with an additional
* attribute specifying whether or not empty values should be propagated or ignored.
*/
public static class ExtendedVariable extends Environment.Variable {
private boolean ignoreEmptyValue = false;
public void setIgnoreEmpty(boolean ignoreEmptyValue) {
this.ignoreEmptyValue = ignoreEmptyValue;
}
public boolean shouldIgnore() {
return ignoreEmptyValue && Strings.isNullOrEmpty(getValue());
}
@Override
public String toString() {
return getContent() + " (ignoreEmpty=" + ignoreEmptyValue + ")";
}
}
/**
* Adds a system property to any forked JVM.
*/
public void addConfiguredSysproperty(ExtendedVariable sysp) {
if (!sysp.shouldIgnore()) {
getCommandline().addSysproperty(sysp);
}
}
/**
* A {@link PropertySet} with an additional
* attribute specifying whether or not empty values should be propagated or ignored.
*/
public static class ExtendedPropertySet extends PropertySet {
private boolean ignoreEmptyValue = false;
public void setIgnoreEmpty(boolean ignoreEmptyValue) {
this.ignoreEmptyValue = ignoreEmptyValue;
}
@Override
public Properties getProperties() {
Properties properties = super.getProperties();
Properties clone = new Properties();
for (String s : properties.stringPropertyNames()) {
String value = (String) properties.get(s);
if (ignoreEmptyValue && Strings.isNullOrEmpty(value)) {
continue;
} else {
clone.setProperty(s, value);
}
}
return clone;
}
}
/**
* Adds a set of properties that will be used as system properties that tests
* can access.
*
* This might be useful to transfer Ant properties to the testcases.
*/
public void addConfiguredSyspropertyset(ExtendedPropertySet sysp) {
getCommandline().addSyspropertyset(sysp);
}
/**
* The command used to invoke the Java Virtual Machine, default is 'java'. The
* command is resolved by java.lang.Runtime.exec().
*/
public void setJvm(String jvm) {
if (!Strings.isNullOrEmpty(jvm)) {
getCommandline().setVm(jvm);
}
}
/**
* If set to <code>true</code> each forked JVM gets a separate working directory
* under whatever is set in {@link #setDir(File)}. The directory naming for each forked JVM
* follows: "S<i>num</i>", where <i>num</i> is forked JVM number. Directories are created
* automatically and removed unless {@link #setLeaveTemporary(boolean)} is set to
* <code>true</code>.
*/
public void setIsolateWorkingDirectories(boolean isolateWorkingDirectories) {
this.isolateWorkingDirectories = isolateWorkingDirectories;
}
/**
* Determines the behavior on detecting non-empty existing current working
* directory for a forked JVM, before the tests commence. This action is performed
* only if work directory isolation is set to true (see {@link #setIsolateWorkingDirectories(boolean)}).
*/
public void setOnNonEmptyWorkDirectory(String value) {
try {
this.nonEmptyWorkDirAction = NonEmptyWorkDirectoryAction.valueOf(value.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("OnNonEmptyWorkDirectory accepts any of: "
+ Arrays.toString(NonEmptyWorkDirectoryAction.values()) + ", value is not valid: " + value);
}
}
/**
* Adds an environment variable; used when forking.
*/
public void addEnv(ExtendedVariable var) {
env.addVariable(var);
}
/**
* Adds a set of tests based on pattern matching.
*/
public void addFileSet(FileSet fs) {
add(fs);
if (fs.getProject() == null) {
fs.setProject(getProject());
}
}
/**
* Adds a set of tests based on pattern matching.
*/
public void add(ResourceCollection rc) {
resources.add(rc);
}
/**
* Creates a new list of listeners.
*/
public ListenersList createListeners() {
return new ListenersList(listeners);
}
/**
* Creates a new list of user-defined run listeners.
*/
public RunListenerList createRunListeners() {
return new RunListenerList(runListeners);
}
/**
* Add assertions to tests execution.
*/
public void addAssertions(Assertions asserts) {
if (getCommandline().getAssertions() != null) {
throw new BuildException("Only one assertion declaration is allowed");
}
getCommandline().setAssertions(asserts);
}
/**
* Creates a new list of balancers.
*/
public BalancersList createBalancers() {
return new BalancersList(balancers);
}
/**
* Adds path to classpath used for tests.
*
* @return reference to the classpath in the embedded java command line
*/
public org.apache.tools.ant.types.Path createClasspath() {
return classpath.createPath();
}
/**
* Adds a path to the bootclasspath.
*
* @return reference to the bootclasspath in the embedded java command line
*/
public org.apache.tools.ant.types.Path createBootclasspath() {
return bootclasspath.createPath();
}
/* ANT-junit compat only. */
public void setFork(boolean fork) {
warnUnsupported("fork");
}
public void setForkmode(String forkMode) {
warnUnsupported("forkmode");
}
public void setHaltOnError(boolean haltOnError) {
warnUnsupported("haltonerror");
}
public void setFiltertrace(boolean filterTrace) {
warnUnsupported("filtertrace");
log("Hint: report listeners have stack filtering options.", Project.MSG_WARN);
}
public void setTimeout(String v) {
warnUnsupported("timeout");
}
public void setIncludeantruntime(String v) {
warnUnsupported("includeantruntime");
}
public void setShowoutput(String v) {
warnUnsupported("showoutput");
}
public void setOutputtoformatters(String v) {
warnUnsupported("outputtoformatters");
}
public void setReloading(String v) {
warnUnsupported("reloading");
}
public void setClonevm(String v) {
warnUnsupported("clonevm");
}
public void setErrorproperty(String v) {
warnUnsupported("errorproperty");
}
public void setLogfailedtests(String v) {
warnUnsupported("logfailedtests");
}
public void setEnableTestListenerEvents(String v) {
warnUnsupported("enableTestListenerEvents");
}
public Object createFormatter() {
throw new BuildException("<formatter> elements are not supported by <junit4>. " +
"Refer to the documentation about listeners and reports.");
}
public Object createTest() {
throw new BuildException("<test> elements are not supported by <junit4>. " +
"Use regular ANT resource collections to point at individual tests or their groups.");
}
public Object createBatchtest() {
throw new BuildException("<batchtest> elements are not supported by <junit4>. " +
"Use regular ANT resource collections to point at individual tests or their groups.");
}
private void warnUnsupported(String attName) {
log("The '" + attName + "' attribute is not supported by <junit4>.", Project.MSG_WARN);
}
/**
* Sets the heartbeat used to detect inactive/ hung forked tests (JVMs) to the given
* number of seconds. The heartbeat detects
* no-event intervals and will report them to listeners. Notably, text report report will
* emit heartbeat information (to a file or console).
*
* <p>Setting the heartbeat to zero means no detection.
*/
public void setHeartbeat(long heartbeat) {
this.heartbeat = heartbeat;
}
/**
* Sets the property prefix to which test statistics are saved.
*/
public void setStatsPropertyPrefix(String statsPropertyPrefix) {
this.statsPropertyPrefix = statsPropertyPrefix;
}
@Override
public void execute() throws BuildException {
validateJUnit4();
validateArguments();
// Initialize random if not already provided.
if (random == null) {
this.random = MoreObjects.firstNonNull(
Strings.emptyToNull(getProject().getProperty(SYSPROP_RANDOM_SEED())),
SeedUtils.formatSeed(new Random().nextLong()));
}
mainSeed();
// Say hello and continue.
log("<JUnit4> says " +
RandomPicks.randomFrom(new Random(mainSeed()), WELCOME_MESSAGES) +
" Main seed: " + getSeed(), Project.MSG_INFO);
// Pass the random seed property.
createJvmarg().setValue("-D" + SYSPROP_PREFIX() + "=" + CURRENT_PREFIX());
createJvmarg().setValue("-D" + SYSPROP_RANDOM_SEED() + "=" + random);
// Resolve paths first.
this.classpath = resolveFiles(classpath);
this.bootclasspath = resolveFiles(bootclasspath);
getCommandline().createClasspath(getProject()).add(classpath);
getCommandline().createBootclasspath(getProject()).add(bootclasspath);
// Setup a class loader over test classes. This will be used for loading annotations
// and referenced classes. This is kind of ugly, but mirroring annotation content will
// be even worse and Description carries these.
// TODO: [GH-211] we should NOT be using any actual classes, annotations, etc.
// from client code. Everything should be a mirror.
testsClassLoader = new AntClassLoader(
this.getClass().getClassLoader(),
getProject(),
getCommandline().getClasspath(),
true);
// Pass method filter if any.
String testMethodFilter = Strings.emptyToNull(getProject().getProperty(SYSPROP_TESTMETHOD()));
if (testMethodFilter != null) {
Environment.Variable v = new Environment.Variable();
v.setKey(SYSPROP_TESTMETHOD());
v.setValue(testMethodFilter);
getCommandline().addSysproperty(v);
}
// Process test classes and resources.
final TestsCollection testCollection = processTestResources();
final EventBus aggregatedBus = new EventBus("aggregated");
final TestsSummaryEventListener summaryListener = new TestsSummaryEventListener();
aggregatedBus.register(summaryListener);
for (Object o : listeners) {
if (o instanceof ProjectComponent) {
((ProjectComponent) o).setProject(getProject());
}
if (o instanceof AggregatedEventListener) {
((AggregatedEventListener) o).setOuter(this);
}
aggregatedBus.register(o);
}
if (testCollection.testClasses.isEmpty()) {
aggregatedBus.post(new AggregatedQuitEvent());
} else {
long start = System.currentTimeMillis();
// Check if we allow duplicate suite names. Some reports (ANT compatible XML
// reports) will have a problem with duplicate suite names, for example.
if (uniqueSuiteNames) {
testCollection.onlyUniqueSuiteNames();
}
final int jvmCount = determineForkedJvmCount(testCollection);
final List<ForkedJvmInfo> forkedJvmInfos = new ArrayList<>();
for (int jvmid = 0; jvmid < jvmCount; jvmid++) {
final ForkedJvmInfo forkedJvmInfo = new ForkedJvmInfo(jvmid, jvmCount);
forkedJvmInfos.add(forkedJvmInfo);
}
if (jvmCount > 1 && uniqueSuiteNames && testCollection.hasReplicatedSuites()) {
throw new BuildException(String.format(Locale.ROOT,
"There are test suites that request JVM replication and the number of forked JVMs %d is larger than 1. Run on a single JVM.",
jvmCount));
}
// Prepare a pool of suites dynamically dispatched to forked JVMs as they become idle.
final Deque<String> stealingQueue =
new ArrayDeque<String>(loadBalanceSuites(forkedJvmInfos, testCollection, balancers));
aggregatedBus.register(new Object() {
@Subscribe
public void onForkedJvmIdle(ForkedJvmIdle forkedJvmIdle) {
if (stealingQueue.isEmpty()) {
forkedJvmIdle.finished();
} else {
String suiteName = stealingQueue.pop();
forkedJvmIdle.newSuite(suiteName);
}
}
});
// Check for filtering expressions.
Vector<Variable> vv = getCommandline().getSystemProperties().getVariablesVector();
for (Variable v : vv) {
if (SysGlobals.SYSPROP_TESTFILTER().equals(v.getKey())) {
try {
Node root = new FilterExpressionParser().parse(v.getValue());
log("Parsed test filtering expression: " + root.toExpression(), Project.MSG_INFO);
} catch (Exception e) {
log("Could not parse filtering expression: " + v.getValue(), e, Project.MSG_WARN);
}
}
}