xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/com/ibm/icu/dev/test/TestFmwk.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 // Copied from ICU4J 57.1
2 /*
3  *******************************************************************************
4  * Copyright (C) 1996-2015, International Business Machines Corporation and    *
5  * others. All Rights Reserved.                                                *
6  *******************************************************************************
7  */
8 package com.ibm.icu.dev.test;
9 
10 import com.ibm.icu.util.TimeZone;
11 import com.ibm.icu.util.ULocale;
12 import java.io.ByteArrayOutputStream;
13 import java.io.CharArrayWriter;
14 import java.io.File;
15 import java.io.IOException;
16 import java.io.OutputStream;
17 import java.io.PrintStream;
18 import java.io.PrintWriter;
19 import java.io.Writer;
20 import java.lang.reflect.Field;
21 import java.lang.reflect.InvocationTargetException;
22 import java.lang.reflect.Method;
23 import java.nio.file.Files;
24 import java.nio.file.Path;
25 import java.nio.file.attribute.BasicFileAttributes;
26 import java.text.DecimalFormat;
27 import java.text.NumberFormat;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Comparator;
31 import java.util.HashMap;
32 import java.util.Locale;
33 import java.util.MissingResourceException;
34 import java.util.NoSuchElementException;
35 import java.util.Random;
36 import java.util.concurrent.ConcurrentHashMap;
37 import java.util.logging.Logger;
38 import java.util.stream.Stream;
39 import org.unicode.cldr.util.CLDRPaths;
40 import org.unicode.cldr.util.Pair;
41 
42 /**
43  * TestFmwk is a base class for tests that can be run conveniently from the command line as well as
44  * under the Java test harness.
45  *
46  * <p>Sub-classes implement a set of methods named Test <something>. Each of these methods performs
47  * some test. Test methods should indicate errors by calling either err or errln. This will
48  * increment the errorCount field and may optionally print a message to the log. Debugging
49  * information may also be added to the log via the log and logln methods. These methods will add
50  * their arguments to the log only if the test is being run in verbose mode.
51  */
52 public class TestFmwk extends AbstractTestLog {
53 
54     /** If true, use GitHub annotations on error messages. */
55     private static boolean CLDR_GITHUB_ANNOTATIONS =
56             (Boolean.parseBoolean(System.getProperty("CLDR_GITHUB_ANNOTATIONS", "false")));
57 
58     private Logger logger = null;
59 
60     /**
61      * Get a Logger suitable for use with this test class.
62      *
63      * @return
64      */
getLogger()65     protected synchronized Logger getLogger() {
66         if (logger == null) {
67             logger = Logger.getLogger(getClass().getName());
68         }
69         return logger;
70     }
71 
72     /** The default time zone for all of our tests. Used in Target.run(); */
73     private static final TimeZone defaultTimeZone = TimeZone.getTimeZone("America/Los_Angeles");
74 
75     /** The default locale used for all of our tests. Used in Target.run(); */
76     private static final Locale defaultLocale = Locale.US;
77 
78     public static final class TestFmwkException extends Exception {
79         /** For serialization */
80         private static final long serialVersionUID = -3051148210247229194L;
81 
TestFmwkException(String msg)82         TestFmwkException(String msg) {
83             super(msg);
84         }
85     }
86 
87     static final class ICUTestError extends RuntimeException {
88         /** For serialization */
89         private static final long serialVersionUID = 6170003850185143046L;
90 
ICUTestError(String msg)91         ICUTestError(String msg) {
92             super(msg);
93         }
94     }
95 
96     // Handling exception thrown during text execution (not including
97     // RuntimeException thrown by errln).
handleException(Throwable e)98     protected void handleException(Throwable e) {
99         Throwable ex = e.getCause();
100         if (ex == null) {
101             ex = e;
102         }
103         if (ex instanceof OutOfMemoryError) {
104             // Once OOM happens, it does not make sense to run
105             // the rest of test cases.
106             throw new RuntimeException(ex);
107         }
108         if (ex instanceof ICUTestError) {
109             // ICUTestError is one produced by errln.
110             // We don't need to include useless stack trace information for
111             // such case.
112             return;
113         }
114         if (ex instanceof ExceptionInInitializerError) {
115             ex = ((ExceptionInInitializerError) ex).getException();
116         }
117 
118         // Stack trace
119         CharArrayWriter caw = new CharArrayWriter();
120         PrintWriter pw = new PrintWriter(caw);
121         ex.printStackTrace(pw);
122         pw.close();
123         String msg = caw.toString();
124 
125         // System.err.println("TF handleException msg: " + msg);
126         if (ex instanceof MissingResourceException
127                 || ex instanceof NoClassDefFoundError
128                 || msg.indexOf("java.util.MissingResourceException") >= 0) {
129             if (params.warnings || params.nodata) {
130                 warnln(ex.toString() + '\n' + msg);
131             } else {
132                 errln(ex.toString() + '\n' + msg);
133             }
134         } else {
135             errln(sourceLocation(ex) + ex.toString() + '\n' + msg);
136         }
137     }
138     // use this instead of new random so we get a consistent seed
139     // for our tests
createRandom()140     protected Random createRandom() {
141         return new Random(params.seed);
142     }
143 
144     /**
145      * A test that has no test methods itself, but instead runs other tests.
146      *
147      * <p>This overrides methods are getTargets and getSubtest from TestFmwk.
148      *
149      * <p>If you want the default behavior, pass an array of class names and an optional description
150      * to the constructor. The named classes must extend TestFmwk. If a provided name doesn't
151      * include a ".", package name is prefixed to it (the package of the current test is used if
152      * none was provided in the constructor). The resulting full name is used to instantiate an
153      * instance of the class using the default constructor.
154      *
155      * <p>Class names are resolved to classes when getTargets or getSubtest is called. This allows
156      * instances of TestGroup to be compiled and run without all the targets they would normally
157      * invoke being available.
158      */
159     public abstract static class TestGroup extends TestFmwk {
160         private String defaultPackage;
161         private String[] names;
162         private String description;
163 
164         private Class[] tests; // deferred init
165 
166         /**
167          * Constructor that takes a default package name and a list of class names. Adopts and
168          * modifies the classname list
169          */
TestGroup(String defaultPackage, String[] classnames, String description)170         protected TestGroup(String defaultPackage, String[] classnames, String description) {
171             if (classnames == null) {
172                 throw new IllegalStateException("classnames must not be null");
173             }
174 
175             if (defaultPackage == null) {
176                 defaultPackage = getClass().getPackage().getName();
177             }
178             defaultPackage = defaultPackage + ".";
179 
180             this.defaultPackage = defaultPackage;
181             this.names = classnames;
182             this.description = description;
183         }
184 
185         /**
186          * Constructor that takes a list of class names and a description, and uses the package for
187          * this class as the default package.
188          */
TestGroup(String[] classnames, String description)189         protected TestGroup(String[] classnames, String description) {
190             this(null, classnames, description);
191         }
192 
193         /**
194          * Constructor that takes a list of class names, and uses the package for this class as the
195          * default package.
196          */
TestGroup(String[] classnames)197         protected TestGroup(String[] classnames) {
198             this(null, classnames, null);
199         }
200 
201         @Override
getDescription()202         protected String getDescription() {
203             return description;
204         }
205 
206         @Override
getTargets(String targetName)207         protected Target getTargets(String targetName) {
208             Target target = null;
209             if (targetName != null) {
210                 finishInit(); // hmmm, want to get subtest without initializing
211                 // all tests
212 
213                 try {
214                     TestFmwk test = getSubtest(targetName);
215                     if (test != null) {
216                         target = test.new ClassTarget();
217                     } else {
218                         target = this.new Target(targetName);
219                     }
220                 } catch (TestFmwkException e) {
221                     target = this.new Target(targetName);
222                 }
223             } else if (params.doRecurse()) {
224                 finishInit();
225                 boolean groupOnly = params.doRecurseGroupsOnly();
226                 for (int i = names.length; --i >= 0; ) {
227                     Target newTarget = null;
228                     Class cls = tests[i];
229                     if (cls == null) { // hack no warning for missing tests
230                         if (params.warnings) {
231                             continue;
232                         }
233                         newTarget = this.new Target(names[i]);
234                     } else {
235                         TestFmwk test = getSubtest(i, groupOnly);
236                         if (test != null) {
237                             newTarget = test.new ClassTarget();
238                         } else {
239                             if (groupOnly) {
240                                 newTarget = this.new EmptyTarget(names[i]);
241                             } else {
242                                 newTarget = this.new Target(names[i]);
243                             }
244                         }
245                     }
246                     if (newTarget != null) {
247                         newTarget.setNext(target);
248                         target = newTarget;
249                     }
250                 }
251             }
252 
253             return target;
254         }
255 
256         @Override
getSubtest(String testName)257         protected TestFmwk getSubtest(String testName) throws TestFmwkException {
258             finishInit();
259 
260             for (int i = 0; i < names.length; ++i) {
261                 if (names[i].equalsIgnoreCase(testName)) { // allow
262                     // case-insensitive
263                     // matching
264                     return getSubtest(i, false);
265                 }
266             }
267             throw new TestFmwkException(testName);
268         }
269 
getSubtest(int i, boolean groupOnly)270         private TestFmwk getSubtest(int i, boolean groupOnly) {
271             Class cls = tests[i];
272             if (cls != null) {
273                 if (groupOnly && !TestGroup.class.isAssignableFrom(cls)) {
274                     return null;
275                 }
276 
277                 try {
278                     TestFmwk subtest = (TestFmwk) cls.newInstance();
279                     subtest.params = params;
280                     return subtest;
281                 } catch (InstantiationException e) {
282                     throw new IllegalStateException(e.getMessage());
283                 } catch (IllegalAccessException e) {
284                     throw new IllegalStateException(e.getMessage());
285                 }
286             }
287             return null;
288         }
289 
finishInit()290         private void finishInit() {
291             if (tests == null) {
292                 tests = new Class[names.length];
293 
294                 for (int i = 0; i < names.length; ++i) {
295                     String name = names[i];
296                     if (name.indexOf('.') == -1) {
297                         name = defaultPackage + name;
298                     }
299                     try {
300                         Class cls = Class.forName(name);
301                         if (!TestFmwk.class.isAssignableFrom(cls)) {
302                             throw new IllegalStateException(
303                                     "class " + name + " does not extend TestFmwk");
304                         }
305 
306                         tests[i] = cls;
307                         names[i] = getClassTargetName(cls);
308                     } catch (ClassNotFoundException e) {
309                         // leave tests[i] null and name as classname
310                     }
311                 }
312             }
313         }
314     }
315 
316     /** The default target is invalid. */
317     public class Target {
318         private Target next;
319         public final String name;
320 
Target(String name)321         public Target(String name) {
322             this.name = name;
323         }
324 
setNext(Target next)325         public Target setNext(Target next) {
326             this.next = next;
327             return this;
328         }
329 
getNext()330         public Target getNext() {
331             return next;
332         }
333 
append(Target targets)334         public Target append(Target targets) {
335             Target t = this;
336             while (t.next != null) {
337                 t = t.next;
338             }
339             t.next = targets;
340             return this;
341         }
342 
run()343         public void run() throws Exception {
344             int f = filter();
345             if (f == -1) {
346                 ++params.invalidCount;
347             } else {
348                 Locale.setDefault(defaultLocale);
349                 TimeZone.setDefault(defaultTimeZone);
350 
351                 if (!validate()) {
352                     params.writeTestInvalid(name, params.nodata);
353                 } else {
354                     params.push(name, getDescription(), f == 1);
355                     execute();
356                     params.pop();
357                 }
358             }
359         }
360 
filter()361         protected int filter() {
362             return params.filter(name);
363         }
364 
validate()365         protected boolean validate() {
366             return false;
367         }
368 
getDescription()369         protected String getDescription() {
370             return null;
371         }
372 
execute()373         protected void execute() throws Exception {}
374     }
375 
376     public class EmptyTarget extends Target {
EmptyTarget(String name)377         public EmptyTarget(String name) {
378             super(name);
379         }
380 
381         @Override
validate()382         protected boolean validate() {
383             return true;
384         }
385     }
386 
387     public class MethodTarget extends Target {
388         private Method testMethod;
389 
MethodTarget(String name, Method method)390         public MethodTarget(String name, Method method) {
391             super(name);
392             testMethod = method;
393         }
394 
395         @Override
validate()396         protected boolean validate() {
397             return testMethod != null && validateMethod(name);
398         }
399 
400         @Override
getDescription()401         protected String getDescription() {
402             return getMethodDescription(name);
403         }
404 
405         @Override
execute()406         protected void execute() throws Exception {
407             if (params.inDocMode()) {
408                 // nothing to execute
409             } else if (!params.stack.included) {
410                 ++params.invalidCount;
411             } else {
412                 final Object[] NO_ARGS = new Object[0];
413                 try {
414                     ++params.testCount;
415                     init();
416                     testMethod.invoke(TestFmwk.this, NO_ARGS);
417                 } catch (IllegalAccessException e) {
418                     errln("Can't access test method " + testMethod.getName());
419                 } catch (Exception e) {
420                     handleException(e);
421                 }
422             }
423             // If non-exhaustive, check if the method target
424             // takes excessive time.
425             if (params.inclusion <= 5) {
426                 double deltaSec =
427                         (double) (System.currentTimeMillis() - params.stack.millis) / 1000;
428                 if (deltaSec > params.maxTargetSec) {
429                     if (params.timeLog == null) {
430                         params.timeLog = new StringBuffer();
431                     }
432                     params.stack.appendPath(params.timeLog);
433                     params.timeLog.append(" (" + deltaSec + "s" + ")\n");
434                 }
435             }
436         }
437 
getStackTrace(InvocationTargetException e)438         protected String getStackTrace(InvocationTargetException e) {
439             ByteArrayOutputStream bs = new ByteArrayOutputStream();
440             PrintStream ps = new PrintStream(bs);
441             e.getTargetException().printStackTrace(ps);
442             return bs.toString();
443         }
444     }
445 
446     public class ClassTarget extends Target {
447         String targetName;
448 
ClassTarget()449         public ClassTarget() {
450             this(null);
451         }
452 
ClassTarget(String targetName)453         public ClassTarget(String targetName) {
454             super(getClassTargetName(TestFmwk.this.getClass()));
455             this.targetName = targetName;
456         }
457 
458         @Override
validate()459         protected boolean validate() {
460             return TestFmwk.this.validate();
461         }
462 
463         @Override
getDescription()464         protected String getDescription() {
465             return TestFmwk.this.getDescription();
466         }
467 
468         @Override
execute()469         protected void execute() throws Exception {
470             params.indentLevel++;
471             Target target = randomize(getTargets(targetName));
472             while (target != null) {
473                 target.run();
474                 target = target.next;
475             }
476             params.indentLevel--;
477         }
478 
randomize(Target t)479         private Target randomize(Target t) {
480             if (t != null && t.getNext() != null) {
481                 ArrayList list = new ArrayList();
482                 while (t != null) {
483                     list.add(t);
484                     t = t.getNext();
485                 }
486 
487                 Target[] arr = (Target[]) list.toArray(new Target[list.size()]);
488 
489                 if (true) { // todo - add to params?
490                     // different jvms return class methods in different orders,
491                     // so we sort them (always, and then randomize them, so that
492                     // forcing a seed will also work across jvms).
493                     Arrays.sort(
494                             arr,
495                             new Comparator() {
496                                 @Override
497                                 public int compare(Object lhs, Object rhs) {
498                                     // sort in reverse order, later we link up in
499                                     // forward order
500                                     return ((Target) rhs).name.compareTo(((Target) lhs).name);
501                                 }
502                             });
503 
504                     // t is null to start, ends up as first element
505                     // (arr[arr.length-1])
506                     for (int i = 0; i < arr.length; ++i) {
507                         t = arr[i].setNext(t); // relink in forward order
508                     }
509                 }
510 
511                 if (params.random != null) {
512                     t = null; // reset t to null
513                     Random r = params.random;
514                     for (int i = arr.length; --i >= 1; ) {
515                         int x = r.nextInt(i + 1);
516                         t = arr[x].setNext(t);
517                         arr[x] = arr[i];
518                     }
519 
520                     t = arr[0].setNext(t); // new first element
521                 }
522             }
523 
524             return t;
525         }
526     }
527 
528     // ------------------------------------------------------------------------
529     // Everything below here is boilerplate code that makes it possible
530     // to add a new test by simply adding a function to an existing class
531     // ------------------------------------------------------------------------
532 
TestFmwk()533     protected TestFmwk() {}
534 
init()535     protected void init() throws Exception {}
536 
537     /**
538      * Parse arguments into a TestParams object and a collection of target paths. If there was an
539      * error parsing the TestParams, print usage and exit with -1. Otherwise, call
540      * resolveTarget(TestParams, String) for each path, and run the returned target. After the last
541      * test returns, if prompt is set, prompt and wait for input from stdin. Finally, exit with
542      * number of errors.
543      *
544      * <p>This method never returns, since it always exits with System.exit();
545      */
run(String[] args)546     public void run(String[] args) {
547         System.exit(run(args, new PrintWriter(System.out)));
548     }
549 
550     /**
551      * Like run(String[]) except this allows you to specify the error log. Unlike run(String[]) this
552      * returns the error code as a result instead of calling System.exit().
553      */
run(String[] args, PrintWriter log)554     public int run(String[] args, PrintWriter log) {
555         boolean prompt = false;
556         int wx = 0;
557         for (int i = 0; i < args.length; ++i) {
558             String arg = args[i];
559             if (arg.equals("-p") || arg.equals("-prompt")) {
560                 prompt = true;
561             } else {
562                 if (wx < i) {
563                     args[wx] = arg;
564                 }
565                 wx++;
566             }
567         }
568         while (wx < args.length) {
569             args[wx++] = null;
570         }
571 
572         TestParams localParams = TestParams.create(args, log);
573         if (localParams == null) {
574             return -1;
575         }
576 
577         int errorCount = runTests(localParams, args);
578 
579         if (localParams.seed != 0) {
580             localParams.log.println("-random:" + localParams.seed);
581             localParams.log.flush();
582         }
583 
584         if (localParams.timeLog != null && localParams.timeLog.length() > 0) {
585             localParams.log.println(
586                     "\nTest cases taking excessive time (>" + localParams.maxTargetSec + "s):");
587             localParams.log.println(localParams.timeLog.toString());
588         }
589 
590         if (localParams.knownIssues.printKnownIssues(localParams.log::println)) {
591             // We had to shorten the known issues.
592             // Suggest to the user that they could print all issues.
593             localParams.log.println(" (Use -allKnownIssues to show all known issue sites) ");
594         }
595 
596         if (localParams.errorSummary != null && localParams.errorSummary.length() > 0) {
597             localParams.log.println("\nError summary:");
598             localParams.log.println(localParams.errorSummary.toString());
599         }
600 
601         if (errorCount > 0) {
602             localParams.log.println("\n<< " + errorCount + " TEST(S) FAILED >>");
603         } else {
604             localParams.log.println("\n<< ALL TESTS PASSED >>");
605         }
606 
607         if (prompt) {
608             System.out.println("Hit RETURN to exit...");
609             System.out.flush();
610             try {
611                 System.in.read();
612             } catch (IOException e) {
613                 localParams.log.println("Exception: " + e.toString() + e.getMessage());
614             }
615         }
616 
617         localParams.log.flush();
618 
619         return errorCount;
620     }
621 
runTests(TestParams _params, String[] tests)622     public int runTests(TestParams _params, String[] tests) {
623         int ec = 0;
624 
625         StringBuffer summary = null;
626         try {
627             if (tests.length == 0 || tests[0] == null) { // no args
628                 _params.init();
629                 resolveTarget(_params).run();
630                 ec = _params.errorCount;
631             } else {
632                 for (int i = 0; i < tests.length; ++i) {
633                     if (tests[i] == null) continue;
634 
635                     if (i > 0) {
636                         _params.log.println();
637                     }
638 
639                     _params.init();
640                     resolveTarget(_params, tests[i]).run();
641                     ec += _params.errorCount;
642 
643                     if (_params.errorSummary != null && _params.errorSummary.length() > 0) {
644                         if (summary == null) {
645                             summary = new StringBuffer();
646                         }
647                         summary.append("\nTest Root: " + tests[i] + "\n");
648                         summary.append(_params.errorSummary());
649                     }
650                 }
651                 _params.errorSummary = summary;
652             }
653         } catch (Exception e) {
654             // We should normally not get here because
655             // MethodTarget.execute() calls handleException().
656             ec++;
657             _params.log.println("\nencountered a test failure, exiting\n" + e);
658             e.printStackTrace(_params.log);
659         }
660 
661         return ec;
662     }
663 
664     /** Return a ClassTarget for this test. Params is set on this test. */
resolveTarget(TestParams paramsArg)665     public Target resolveTarget(TestParams paramsArg) {
666         this.params = paramsArg;
667         return new ClassTarget();
668     }
669 
670     /**
671      * Resolve a path from this test to a target. If this test has subtests, and the path contains
672      * '/', the portion before the '/' is resolved to a subtest, until the path is consumed or the
673      * test has no subtests. Returns a ClassTarget created using the resolved test and remaining
674      * path (which ought to be null or a method name). Params is set on the target's test.
675      */
resolveTarget(TestParams paramsArg, String targetPath)676     public Target resolveTarget(TestParams paramsArg, String targetPath) {
677         TestFmwk test = this;
678         test.params = paramsArg;
679 
680         if (targetPath != null) {
681             if (targetPath.length() == 0) {
682                 targetPath = null;
683             } else {
684                 int p = 0;
685                 int e = targetPath.length();
686 
687                 // trim all leading and trailing '/'
688                 while (targetPath.charAt(p) == '/') {
689                     ++p;
690                 }
691                 while (e > p && targetPath.charAt(e - 1) == '/') {
692                     --e;
693                 }
694                 if (p > 0 || e < targetPath.length()) {
695                     targetPath = targetPath.substring(p, e - p);
696                     p = 0;
697                     e = targetPath.length();
698                 }
699 
700                 try {
701                     for (; ; ) {
702                         int n = targetPath.indexOf('/');
703                         String prefix = n == -1 ? targetPath : targetPath.substring(0, n);
704                         TestFmwk subtest = test.getSubtest(prefix);
705 
706                         if (subtest == null) {
707                             break;
708                         }
709 
710                         test = subtest;
711 
712                         if (n == -1) {
713                             targetPath = null;
714                             break;
715                         }
716 
717                         targetPath = targetPath.substring(n + 1);
718                     }
719                 } catch (TestFmwkException ex) {
720                     return test.new Target(targetPath);
721                 }
722             }
723         }
724 
725         return test.new ClassTarget(targetPath);
726     }
727 
728     /**
729      * Return true if we can run this test (allows test to inspect jvm, environment, params before
730      * running)
731      */
validate()732     protected boolean validate() {
733         return true;
734     }
735 
736     /**
737      * Return the targets for this test. If targetName is null, return all targets, otherwise return
738      * a target for just that name. The returned target can be null.
739      *
740      * <p>The default implementation returns a MethodTarget for each public method of the object's
741      * class whose name starts with "Test" or "test".
742      */
getTargets(String targetName)743     protected Target getTargets(String targetName) {
744         return getClassTargets(getClass(), targetName);
745     }
746 
getClassTargets(Class cls, String targetName)747     protected Target getClassTargets(Class cls, String targetName) {
748         if (cls == null) {
749             return null;
750         }
751 
752         Target target = null;
753         if (targetName != null) {
754             try {
755                 Method method = cls.getMethod(targetName, (Class[]) null);
756                 target = new MethodTarget(targetName, method);
757             } catch (NoSuchMethodException e) {
758                 if (!inheritTargets()) {
759                     return new Target(targetName); // invalid target
760                 }
761             } catch (SecurityException e) {
762                 return null;
763             }
764         } else {
765             if (params.doMethods()) {
766                 Method[] methods = cls.getDeclaredMethods();
767                 for (int i = methods.length; --i >= 0; ) {
768                     String name = methods[i].getName();
769                     if (name.startsWith("Test") || name.startsWith("test")) {
770                         target = new MethodTarget(name, methods[i]).setNext(target);
771                     }
772                 }
773             }
774         }
775 
776         if (inheritTargets()) {
777             Target parentTarget = getClassTargets(cls.getSuperclass(), targetName);
778             if (parentTarget == null) {
779                 return target;
780             }
781             if (target == null) {
782                 return parentTarget;
783             }
784             return parentTarget.append(target);
785         }
786 
787         return target;
788     }
789 
inheritTargets()790     protected boolean inheritTargets() {
791         return false;
792     }
793 
getDescription()794     protected String getDescription() {
795         return null;
796     }
797 
validateMethod(String name)798     protected boolean validateMethod(String name) {
799         return true;
800     }
801 
getMethodDescription(String name)802     protected String getMethodDescription(String name) {
803         return null;
804     }
805 
806     // method tests have no subtests, group tests override
getSubtest(String prefix)807     protected TestFmwk getSubtest(String prefix) throws TestFmwkException {
808         return null;
809     }
810 
isVerbose()811     public boolean isVerbose() {
812         return params.verbose;
813     }
814 
noData()815     public boolean noData() {
816         return params.nodata;
817     }
818 
isTiming()819     public boolean isTiming() {
820         return params.timing < Long.MAX_VALUE;
821     }
822 
isMemTracking()823     public boolean isMemTracking() {
824         return params.memusage;
825     }
826 
827     /** 0 = fewest tests, 5 is normal build, 10 is most tests */
getInclusion()828     public int getInclusion() {
829         return params.inclusion;
830     }
831 
isModularBuild()832     public boolean isModularBuild() {
833         return params.warnings;
834     }
835 
isQuick()836     public boolean isQuick() {
837         return params.inclusion == 0;
838     }
839 
840     @Override
msg(String message, int level, boolean incCount, boolean newln)841     public void msg(String message, int level, boolean incCount, boolean newln) {
842         params.msg(message, level, incCount, newln);
843     }
844 
845     /**
846      * Log the known issue. This method returns true unless -prop:logKnownIssue=no is specified in
847      * the argument list.
848      *
849      * @param ticket A ticket number string. For an ICU ticket, use "ICU-10245". For a CLDR ticket,
850      *     use "CLDR-12345". For compatibility, "1234" -> ICU-1234 and "cldrbug:456" -> CLDR-456
851      * @param comment Additional comment, or null
852      * @return true unless -prop:logKnownIssue=no is specified in the test command line argument.
853      */
logKnownIssue(String ticket, String comment)854     public boolean logKnownIssue(String ticket, String comment) {
855         if (getBooleanProperty("logKnownIssue", true)) {
856             StringBuffer path = new StringBuffer();
857             params.stack.appendPath(path);
858             params.knownIssues.logKnownIssue(path.toString(), ticket, comment);
859             return true;
860         } else {
861             return false;
862         }
863     }
864 
getErrorCount()865     protected int getErrorCount() {
866         return params.errorCount;
867     }
868 
getProperty(String key)869     public String getProperty(String key) {
870         String val = null;
871         if (key != null && key.length() > 0 && params.props != null) {
872             val = (String) params.props.get(key.toLowerCase());
873         }
874         return val;
875     }
876 
getBooleanProperty(String key, boolean defVal)877     public boolean getBooleanProperty(String key, boolean defVal) {
878         String s = getProperty(key);
879         if (s != null) {
880             if (s.equalsIgnoreCase("yes") || s.equals("true")) {
881                 return true;
882             }
883             if (s.equalsIgnoreCase("no") || s.equalsIgnoreCase("false")) {
884                 return false;
885             }
886         }
887         return defVal;
888     }
889 
safeGetTimeZone(String id)890     protected TimeZone safeGetTimeZone(String id) {
891         TimeZone tz = TimeZone.getTimeZone(id);
892         if (tz == null) {
893             // should never happen
894             errln("FAIL: TimeZone.getTimeZone(" + id + ") => null");
895         }
896         if (!tz.getID().equals(id)) {
897             warnln("FAIL: TimeZone.getTimeZone(" + id + ") => " + tz.getID());
898         }
899         return tz;
900     }
901 
902     /** Print a usage message for this test class. */
usage()903     public void usage() {
904         usage(new PrintWriter(System.out), getClass().getName());
905     }
906 
usage(PrintWriter pw, String className)907     public static void usage(PrintWriter pw, String className) {
908         pw.println("Usage: " + className + " option* target*");
909         pw.println();
910         pw.println("Options:");
911         pw.println(
912                 " -allKnownIssues  Show all known issues for each bug, not just the first lines\n");
913         pw.println(" -d[escribe] Print a short descriptive string for this test and all");
914         pw.println("       listed targets.");
915         pw.println(
916                 " -e<n> Set exhaustiveness from 0..10.  Default is 0, fewest tests.\n"
917                         + "       To run all tests, specify -e10.  Giving -e with no <n> is\n"
918                         + "       the same as -e5.");
919         pw.println(
920                 " -filter:<str> Only tests matching filter will be run or listed.\n"
921                         + "       <str> is of the form ['^']text[','['^']text].\n"
922                         + "       Each string delimited by ',' is a separate filter argument.\n"
923                         + "       If '^' is prepended to an argument, its matches are excluded.\n"
924                         + "       Filtering operates on test groups as well as tests, if a test\n"
925                         + "       group is included, all its subtests that are not excluded will\n"
926                         + "       be run.  Examples:\n"
927                         + "    -filter:A -- only tests matching A are run.  If A matches a group,\n"
928                         + "       all subtests of this group are run.\n"
929                         + "    -filter:^A -- all tests except those matching A are run.  If A matches\n"
930                         + "        a group, no subtest of that group will be run.\n"
931                         + "    -filter:A,B,^C,^D -- tests matching A or B and not C and not D are run\n"
932                         + "       Note: Filters are case insensitive.");
933         pw.println(" -h[elp] Print this help text and exit.");
934         pw.println(" -hex Display non-ASCII characters in hexadecimal format");
935         pw.println(" -l[ist] List immediate targets of this test");
936         pw.println("   -la, -listAll List immediate targets of this test, and all subtests");
937         pw.println("   -le, -listExaustive List all subtests and targets");
938         // don't know how to get useful numbers for memory usage using java API
939         // calls
940         //      pw.println(" -m[emory] print memory usage and force gc for
941         // each test");
942         pw.println(
943                 " -n[othrow] Message on test failure rather than exception.\n"
944                         + "       This is the default behavior and has no effects on ICU 55+.");
945         pw.println(" -p[rompt] Prompt before exiting");
946         pw.println(" -prop:<key>=<value> Set optional property used by this test");
947         pw.println("    Example: -prop:logKnownIssue=no to cause known issues to fail");
948         pw.println(" -q[uiet] Do not show warnings");
949         pw.println(
950                 " -r[andom][:<n>] If present, randomize targets.  If n is present,\n"
951                         + "       use it as the seed.  If random is not set, targets will\n"
952                         + "       be in alphabetical order to ensure cross-platform consistency.");
953         pw.println(" -s[ilent] No output except error summary or exceptions.");
954         pw.println(" -tfilter:<str> Transliterator Test filter of ids.");
955         pw.println(" -t[ime]:<n> Print elapsed time only for tests exceeding n milliseconds.");
956         pw.println(" -v[erbose] Show log messages");
957         pw.println(" -u[nicode] Don't escape error or log messages (Default on ICU 55+)");
958         pw.println(
959                 " -w[arning] Continue in presence of warnings, and disable missing test warnings.");
960         pw.println(" -nodata | -nd Do not warn if resource data is not present.");
961         pw.println();
962         pw.println(" If a list or describe option is provided, no tests are run.");
963         pw.println();
964         pw.println("Targets:");
965         pw.println(" If no target is specified, all targets for this test are run.");
966         pw.println(" If a target contains no '/' characters, and matches a target");
967         pw.println(" of this test, the target is run.  Otherwise, the part before the");
968         pw.println(" '/' is used to match a subtest, which then evaluates the");
969         pw.println(" remainder of the target as above.  Target matching is case-insensitive.");
970         pw.println();
971         pw.println(" If multiple targets are provided, each is executed in order.");
972         pw.flush();
973     }
974 
hex(char[] s)975     public static String hex(char[] s) {
976         StringBuffer result = new StringBuffer();
977         for (int i = 0; i < s.length; ++i) {
978             if (i != 0) result.append(',');
979             result.append(hex(s[i]));
980         }
981         return result.toString();
982     }
983 
hex(byte[] s)984     public static String hex(byte[] s) {
985         StringBuffer result = new StringBuffer();
986         for (int i = 0; i < s.length; ++i) {
987             if (i != 0) result.append(',');
988             result.append(hex(s[i]));
989         }
990         return result.toString();
991     }
992 
hex(char ch)993     public static String hex(char ch) {
994         StringBuffer result = new StringBuffer();
995         String foo = Integer.toString(ch, 16).toUpperCase();
996         for (int i = foo.length(); i < 4; ++i) {
997             result.append('0');
998         }
999         return result + foo;
1000     }
1001 
hex(int ch)1002     public static String hex(int ch) {
1003         StringBuffer result = new StringBuffer();
1004         String foo = Integer.toString(ch, 16).toUpperCase();
1005         for (int i = foo.length(); i < 4; ++i) {
1006             result.append('0');
1007         }
1008         return result + foo;
1009     }
1010 
hex(CharSequence s)1011     public static String hex(CharSequence s) {
1012         StringBuilder result = new StringBuilder();
1013         for (int i = 0; i < s.length(); ++i) {
1014             if (i != 0) result.append(',');
1015             result.append(hex(s.charAt(i)));
1016         }
1017         return result.toString();
1018     }
1019 
prettify(CharSequence s)1020     public static String prettify(CharSequence s) {
1021         StringBuilder result = new StringBuilder();
1022         int ch;
1023         for (int i = 0; i < s.length(); i += Character.charCount(ch)) {
1024             ch = Character.codePointAt(s, i);
1025             if (ch > 0xfffff) {
1026                 result.append("\\U00");
1027                 result.append(hex(ch));
1028             } else if (ch > 0xffff) {
1029                 result.append("\\U000");
1030                 result.append(hex(ch));
1031             } else if (ch < 0x20 || 0x7e < ch) {
1032                 result.append("\\u");
1033                 result.append(hex(ch));
1034             } else {
1035                 result.append((char) ch);
1036             }
1037         }
1038         return result.toString();
1039     }
1040 
1041     private static java.util.GregorianCalendar cal;
1042 
1043     /**
1044      * Return a Date given a year, month, and day of month. This is similar to new Date(y-1900, m,
1045      * d). It uses the default time zone at the time this method is first called.
1046      *
1047      * @param year use 2000 for 2000, unlike new Date()
1048      * @param month use Calendar.JANUARY etc.
1049      * @param dom day of month, 1-based
1050      * @return a Date object for the given y/m/d
1051      */
getDate(int year, int month, int dom)1052     protected static synchronized java.util.Date getDate(int year, int month, int dom) {
1053         if (cal == null) {
1054             cal = new java.util.GregorianCalendar();
1055         }
1056         cal.clear();
1057         cal.set(year, month, dom);
1058         return cal.getTime();
1059     }
1060 
1061     public static class NullWriter extends PrintWriter {
NullWriter()1062         public NullWriter() {
1063             super(System.out, false);
1064         }
1065 
1066         @Override
write(int c)1067         public void write(int c) {}
1068 
1069         @Override
write(char[] buf, int off, int len)1070         public void write(char[] buf, int off, int len) {}
1071 
1072         @Override
write(String s, int off, int len)1073         public void write(String s, int off, int len) {}
1074 
1075         @Override
println()1076         public void println() {}
1077     }
1078 
1079     public static class ASCIIWriter extends PrintWriter {
1080         private StringBuffer buffer = new StringBuffer();
1081 
1082         // Characters that we think are printable but that escapeUnprintable
1083         // doesn't
1084         private static final String PRINTABLES = "\t\n\r";
1085 
ASCIIWriter(Writer w, boolean autoFlush)1086         public ASCIIWriter(Writer w, boolean autoFlush) {
1087             super(w, autoFlush);
1088         }
1089 
ASCIIWriter(OutputStream os, boolean autoFlush)1090         public ASCIIWriter(OutputStream os, boolean autoFlush) {
1091             super(os, autoFlush);
1092         }
1093 
1094         @Override
write(int c)1095         public void write(int c) {
1096             synchronized (lock) {
1097                 buffer.setLength(0);
1098                 if (PRINTABLES.indexOf(c) < 0 && TestUtil.escapeUnprintable(buffer, c)) {
1099                     super.write(buffer.toString());
1100                 } else {
1101                     super.write(c);
1102                 }
1103             }
1104         }
1105 
1106         @Override
write(char[] buf, int off, int len)1107         public void write(char[] buf, int off, int len) {
1108             synchronized (lock) {
1109                 buffer.setLength(0);
1110                 int limit = off + len;
1111                 while (off < limit) {
1112                     int c = UTF16Util.charAt(buf, 0, buf.length, off);
1113                     off += UTF16Util.getCharCount(c);
1114                     if (PRINTABLES.indexOf(c) < 0 && TestUtil.escapeUnprintable(buffer, c)) {
1115                         super.write(buffer.toString());
1116                         buffer.setLength(0);
1117                     } else {
1118                         super.write(c);
1119                     }
1120                 }
1121             }
1122         }
1123 
1124         @Override
write(String s, int off, int len)1125         public void write(String s, int off, int len) {
1126             write(s.substring(off, off + len).toCharArray(), 0, len);
1127         }
1128     }
1129 
1130     // filters
1131     // match against the entire hierarchy
1132     // A;B;!C;!D --> (A ||B) && (!C && !D)
1133     // positive, negative, unknown matches
1134     // positive -- known to be included, negative- known to be excluded
1135     // positive only if no excludes, and matches at least one include, if any
1136     // negative only if matches at least one exclude
1137     // otherwise, we wait
1138 
1139     public static class TestParams {
1140         public boolean prompt;
1141         public boolean verbose;
1142         public boolean quiet;
1143         public int listlevel;
1144         public boolean describe;
1145         public boolean warnings;
1146         public boolean nodata;
1147         public long timing = 0;
1148         public boolean memusage;
1149         public boolean allKnownIssues = false;
1150         public int inclusion;
1151         public String filter;
1152         public long seed;
1153         public String tfilter; // for transliterator tests
1154 
1155         public State stack;
1156 
1157         public StringBuffer errorSummary = new StringBuffer();
1158         private StringBuffer timeLog;
1159 
1160         public PrintWriter log;
1161         public int indentLevel;
1162         private boolean needLineFeed;
1163         private boolean suppressIndent;
1164         public int errorCount;
1165         public int warnCount;
1166         public int invalidCount;
1167         public int testCount;
1168         private NumberFormat tformat;
1169         public Random random;
1170         public int maxTargetSec = 10;
1171         public HashMap props;
1172         private UnicodeKnownIssues knownIssues;
1173 
TestParams()1174         private TestParams() {}
1175 
create(String arglist, PrintWriter log)1176         public static TestParams create(String arglist, PrintWriter log) {
1177             String[] args = null;
1178             if (arglist != null && arglist.length() > 0) {
1179                 args = arglist.split("\\s");
1180             }
1181             return create(args, log);
1182         }
1183 
1184         /**
1185          * Create a TestParams from a list of arguments. If successful, return the params object,
1186          * else return null. Error messages will be reported on errlog if it is not null. Arguments
1187          * and values understood by this method will be removed from the args array and existing
1188          * args will be shifted down, to be filled by nulls at the end.
1189          *
1190          * @param args the list of arguments
1191          * @param log the error log, or null if no error log is desired
1192          * @return the new TestParams object, or null if error
1193          */
create(String[] args, PrintWriter log)1194         public static TestParams create(String[] args, PrintWriter log) {
1195             TestParams params = new TestParams();
1196 
1197             if (log == null) {
1198                 params.log = new NullWriter();
1199             } else {
1200                 params.log = log;
1201             }
1202 
1203             boolean usageError = false;
1204             String filter = null;
1205             String fmt = "#,##0.000s";
1206             int wx = 0; // write argets.
1207             if (args != null) {
1208                 for (int i = 0; i < args.length; i++) {
1209                     String arg = args[i];
1210                     if (arg == null || arg.length() == 0) {
1211                         continue;
1212                     }
1213                     if (arg.charAt(0) == '-') {
1214                         arg = arg.toLowerCase();
1215                         if (arg.equals("-verbose") || arg.equals("-v")) {
1216                             params.verbose = true;
1217                             params.quiet = false;
1218                         } else if (arg.equals("-quiet") || arg.equals("-q")) {
1219                             params.quiet = true;
1220                             params.verbose = false;
1221                         } else if (arg.equals("-hex")) {
1222                             params.log = new ASCIIWriter(log, true);
1223                         } else if (arg.equals("-help") || arg.equals("-h")) {
1224                             usageError = true;
1225                         } else if (arg.equals("-warning") || arg.equals("-w")) {
1226                             params.warnings = true;
1227                         } else if (arg.equals("-nodata") || arg.equals("-nd")) {
1228                             params.nodata = true;
1229                         } else if (arg.equals("-allknownissues")) {
1230                             params.allKnownIssues = true;
1231                         } else if (arg.equals("-list") || arg.equals("-l")) {
1232                             params.listlevel = 1;
1233                         } else if (arg.equals("-listall") || arg.equals("-la")) {
1234                             params.listlevel = 2;
1235                         } else if (arg.equals("-listexaustive") || arg.equals("-le")) {
1236                             params.listlevel = 3;
1237                         } else if (arg.equals("-memory") || arg.equals("-m")) {
1238                             params.memusage = true;
1239                         } else if (arg.equals("-nothrow") || arg.equals("-n")) {
1240                             // Default since ICU 55. This option has no effects.
1241                         } else if (arg.equals("-describe") || arg.equals("-d")) {
1242                             params.describe = true;
1243                         } else if (arg.startsWith("-r")) {
1244                             String s = null;
1245                             int n = arg.indexOf(':');
1246                             if (n != -1) {
1247                                 s = arg.substring(n + 1);
1248                                 arg = arg.substring(0, n);
1249                             }
1250 
1251                             if (arg.equals("-r") || arg.equals("-random")) {
1252                                 if (s == null) {
1253                                     params.seed = System.currentTimeMillis();
1254                                 } else {
1255                                     params.seed = Long.parseLong(s);
1256                                 }
1257                             } else {
1258                                 log.println("*** Error: unrecognized argument: " + arg);
1259                                 usageError = true;
1260                                 break;
1261                             }
1262                         } else if (arg.startsWith("-e")) {
1263                             // see above
1264                             params.inclusion =
1265                                     (arg.length() == 2) ? 5 : Integer.parseInt(arg.substring(2));
1266                             if (params.inclusion < 0 || params.inclusion > 10) {
1267                                 usageError = true;
1268                                 break;
1269                             }
1270                         } else if (arg.startsWith("-tfilter:")) {
1271                             params.tfilter = arg.substring(8);
1272                         } else if (arg.startsWith("-time") || arg.startsWith("-t")) {
1273                             long val = 0;
1274                             int inx = arg.indexOf(':');
1275                             if (inx > 0) {
1276                                 String num = arg.substring(inx + 1);
1277                                 try {
1278                                     val = Long.parseLong(num);
1279                                 } catch (Exception e) {
1280                                     log.println(
1281                                             "*** Error: could not parse time threshold '"
1282                                                     + num
1283                                                     + "'");
1284                                     usageError = true;
1285                                     break;
1286                                 }
1287                             }
1288                             params.timing = val;
1289                             if (val <= 10) {
1290                                 fmt = "#,##0.000s";
1291                             } else if (val <= 100) {
1292                                 fmt = "#,##0.00s";
1293                             } else if (val <= 1000) {
1294                                 fmt = "#,##0.0s";
1295                             }
1296                         } else if (arg.startsWith("-filter:")) {
1297                             String temp = arg.substring(8).toLowerCase();
1298                             filter = filter == null ? temp : filter + "," + temp;
1299                         } else if (arg.startsWith("-f:")) {
1300                             String temp = arg.substring(3).toLowerCase();
1301                             filter = filter == null ? temp : filter + "," + temp;
1302                         } else if (arg.startsWith("-s")) {
1303                             params.log = new NullWriter();
1304                         } else if (arg.startsWith("-u")) {
1305                             if (params.log instanceof ASCIIWriter) {
1306                                 params.log = log;
1307                             }
1308                         } else if (arg.startsWith("-prop:")) {
1309                             String temp = arg.substring(6);
1310                             int eql = temp.indexOf('=');
1311                             if (eql <= 0) {
1312                                 log.println(
1313                                         "*** Error: could not parse custom property '" + arg + "'");
1314                                 usageError = true;
1315                                 break;
1316                             }
1317                             if (params.props == null) {
1318                                 params.props = new HashMap();
1319                             }
1320                             params.props.put(temp.substring(0, eql), temp.substring(eql + 1));
1321                         } else {
1322                             log.println("*** Error: unrecognized argument: " + args[i]);
1323                             usageError = true;
1324                             break;
1325                         }
1326                     } else {
1327                         args[wx++] = arg; // shift down
1328                     }
1329                 }
1330 
1331                 while (wx < args.length) {
1332                     args[wx++] = null;
1333                 }
1334             }
1335 
1336             params.tformat = new DecimalFormat(fmt);
1337 
1338             if (usageError) {
1339                 usage(log, "TestAll");
1340                 return null;
1341             }
1342 
1343             if (filter != null) {
1344                 params.filter = filter.toLowerCase();
1345             }
1346 
1347             params.init();
1348 
1349             return params;
1350         }
1351 
errorSummary()1352         public String errorSummary() {
1353             return errorSummary == null ? "" : errorSummary.toString();
1354         }
1355 
init()1356         public void init() {
1357             indentLevel = 0;
1358             needLineFeed = false;
1359             suppressIndent = false;
1360             errorCount = 0;
1361             warnCount = 0;
1362             invalidCount = 0;
1363             testCount = 0;
1364             random = seed == 0 ? null : new Random(seed);
1365 
1366             knownIssues = new UnicodeKnownIssues(allKnownIssues);
1367         }
1368 
1369         public class State {
1370             State link;
1371             String name;
1372             StringBuffer buffer;
1373             int level;
1374             int ec;
1375             int wc;
1376             int ic;
1377             int tc;
1378             boolean flushed;
1379             public boolean included;
1380             long mem;
1381             long millis;
1382 
State(State link, String name, boolean included)1383             public State(State link, String name, boolean included) {
1384                 this.link = link;
1385                 this.name = name;
1386                 if (link == null) {
1387                     this.level = 0;
1388                     this.included = included;
1389                 } else {
1390                     this.level = link.level + 1;
1391                     this.included = included || link.included;
1392                 }
1393                 this.ec = errorCount;
1394                 this.wc = warnCount;
1395                 this.ic = invalidCount;
1396                 this.tc = testCount;
1397 
1398                 if (link == null || this.included) {
1399                     flush();
1400                 }
1401 
1402                 mem = getmem();
1403                 millis = System.currentTimeMillis();
1404             }
1405 
flush()1406             void flush() {
1407                 if (!flushed) {
1408                     if (link != null) {
1409                         link.flush();
1410                     }
1411 
1412                     indent(level);
1413                     log.print(name);
1414                     log.flush();
1415 
1416                     flushed = true;
1417 
1418                     needLineFeed = true;
1419                 }
1420             }
1421 
appendPath(StringBuffer buf)1422             void appendPath(StringBuffer buf) {
1423                 if (this.link != null) {
1424                     this.link.appendPath(buf);
1425                     buf.append('/');
1426                 }
1427                 buf.append(name);
1428             }
1429         }
1430 
push(String name, String description, boolean included)1431         public void push(String name, String description, boolean included) {
1432             if (inDocMode() && describe && description != null) {
1433                 name += ": " + description;
1434             }
1435             stack = new State(stack, name, included);
1436         }
1437 
pop()1438         public void pop() {
1439             if (stack != null) {
1440                 writeTestResult();
1441                 stack = stack.link;
1442             }
1443         }
1444 
inDocMode()1445         public boolean inDocMode() {
1446             return describe || listlevel != 0;
1447         }
1448 
doMethods()1449         public boolean doMethods() {
1450             return !inDocMode() || listlevel == 3 || (indentLevel == 1 && listlevel > 0);
1451         }
1452 
doRecurse()1453         public boolean doRecurse() {
1454             return !inDocMode() || listlevel > 1 || (indentLevel == 1 && listlevel > 0);
1455         }
1456 
doRecurseGroupsOnly()1457         public boolean doRecurseGroupsOnly() {
1458             return inDocMode() && (listlevel == 2 || (indentLevel == 1 && listlevel > 0));
1459         }
1460 
1461         // return 0, -1, or 1
1462         // 1: run this test
1463         // 0: might run this test, no positive include or exclude on this group
1464         // -1: exclude this test
filter(String testName)1465         public int filter(String testName) {
1466             int result = 0;
1467             if (filter == null) {
1468                 result = 1;
1469             } else {
1470                 boolean noIncludes = true;
1471                 boolean noExcludes = filter.indexOf('^') == -1;
1472                 testName = testName.toLowerCase();
1473                 int ix = 0;
1474                 while (ix < filter.length()) {
1475                     int nix = filter.indexOf(',', ix);
1476                     if (nix == -1) {
1477                         nix = filter.length();
1478                     }
1479                     if (filter.charAt(ix) == '^') {
1480                         if (testName.indexOf(filter.substring(ix + 1, nix)) != -1) {
1481                             result = -1;
1482                             break;
1483                         }
1484                     } else {
1485                         noIncludes = false;
1486                         if (testName.indexOf(filter.substring(ix, nix)) != -1) {
1487                             result = 1;
1488                             if (noExcludes) {
1489                                 break;
1490                             }
1491                         }
1492                     }
1493 
1494                     ix = nix + 1;
1495                 }
1496                 if (result == 0 && noIncludes) {
1497                     result = 1;
1498                 }
1499             }
1500             //              System.out.println("filter: " + testName + " returns: " +
1501             // result);
1502             return result;
1503         }
1504 
1505         /**
1506          * Log access.
1507          *
1508          * @param msg The string message to write
1509          */
write(String msg)1510         public void write(String msg) {
1511             write(msg, false);
1512         }
1513 
writeln(String msg)1514         public void writeln(String msg) {
1515             write(msg, true);
1516         }
1517 
write(String msg, boolean newln)1518         private void write(String msg, boolean newln) {
1519             if (!suppressIndent) {
1520                 if (needLineFeed) {
1521                     log.println();
1522                     needLineFeed = false;
1523                 }
1524                 log.print(spaces.substring(0, indentLevel * 2));
1525             }
1526             log.print(msg);
1527             if (newln) {
1528                 log.println();
1529             }
1530             log.flush();
1531             suppressIndent = !newln;
1532         }
1533 
msg(String message, int level, boolean incCount, boolean newln)1534         private void msg(String message, int level, boolean incCount, boolean newln) {
1535             int oldLevel = level;
1536             //            if (level == WARN && (!warnings && !nodata)){
1537             //                level = ERR;
1538             //            }
1539 
1540             if (incCount) {
1541                 if (level == WARN) {
1542                     warnCount++;
1543                     //                    invalidCount++;
1544                 } else if (level == ERR) {
1545                     errorCount++;
1546                 }
1547             }
1548 
1549             final SourceLocation testLocation = sourceLocation();
1550             final String[] MSGNAMES = {"", "Warning: ", "Error: "};
1551 
1552             if (newln && CLDR_GITHUB_ANNOTATIONS && (level == WARN || level == ERR)) {
1553                 // when -DCLDR_GITHUB_ANNOTATIONS=true, bypass usual output for warn and err:
1554                 final String[] GH_MSGNAMES = {"", "::warning ", "::error "};
1555                 System.out.println(); // skip indentation for github
1556                 System.out.println(
1557                         GH_MSGNAMES[oldLevel]
1558                                 + testLocation.forGitHub()
1559                                 + "::"
1560                                 + " "
1561                                 + testLocation
1562                                 + " "
1563                                 + MSGNAMES[oldLevel]
1564                                 + message);
1565                 // TODO: somehow, our github location format is not right
1566                 // For now, just repeat the location in the message.
1567                 log.println();
1568             } else if (verbose || level > (quiet ? WARN : LOG)) {
1569                 // should roll indentation stuff into log ???
1570                 if (!suppressIndent) {
1571                     indent(indentLevel + 1);
1572                     log.print(MSGNAMES[oldLevel]);
1573                 }
1574 
1575                 message = testLocation + message;
1576                 log.print(message);
1577                 if (newln) {
1578                     log.println();
1579                 }
1580                 log.flush();
1581             }
1582 
1583             if (level == ERR) {
1584                 if (!suppressIndent
1585                         && errorSummary != null
1586                         && stack != null
1587                         && (errorCount == stack.ec + 1)) {
1588                     stack.appendPath(errorSummary);
1589                     errorSummary.append("\n");
1590                 }
1591             }
1592 
1593             suppressIndent = !newln;
1594         }
1595 
writeTestInvalid(String name, boolean nodataArg)1596         private void writeTestInvalid(String name, boolean nodataArg) {
1597             //              msg("***" + name + "*** not found or not valid.", WARN, true,
1598             // true);
1599             if (inDocMode()) {
1600                 if (!warnings) {
1601                     if (stack != null) {
1602                         stack.flush();
1603                     }
1604                     log.println(" *** Target not found or not valid.");
1605                     log.flush();
1606                     needLineFeed = false;
1607                 }
1608             } else {
1609                 if (!nodataArg) {
1610                     msg("Test " + name + " not found or not valid.", WARN, true, true);
1611                 }
1612             }
1613         }
1614 
getmem()1615         long getmem() {
1616             long newmem = 0;
1617             if (memusage) {
1618                 Runtime rt = Runtime.getRuntime();
1619                 long lastmem = Long.MAX_VALUE;
1620                 do {
1621                     rt.gc();
1622                     rt.gc();
1623                     try {
1624                         Thread.sleep(50);
1625                     } catch (Exception e) {
1626                         break;
1627                     }
1628                     lastmem = newmem;
1629                     newmem = rt.totalMemory() - rt.freeMemory();
1630                 } while (newmem < lastmem);
1631             }
1632             return newmem;
1633         }
1634 
writeTestResult()1635         private void writeTestResult() {
1636             if (inDocMode()) {
1637                 if (needLineFeed) {
1638                     log.println();
1639                     log.flush();
1640                 }
1641                 needLineFeed = false;
1642                 return;
1643             }
1644 
1645             long dmem = getmem() - stack.mem;
1646             long dtime = System.currentTimeMillis() - stack.millis;
1647 
1648             int testDelta = testCount - stack.tc;
1649             if (testDelta == 0) {
1650                 if (stack.included) {
1651                     stack.flush();
1652                     indent(indentLevel);
1653                     log.println("} (0s) Empty");
1654                 }
1655                 return;
1656             }
1657 
1658             int errorDelta = errorCount - stack.ec;
1659             int warnDelta = warnCount - stack.wc;
1660             int invalidDelta = invalidCount - stack.ic;
1661 
1662             stack.flush();
1663 
1664             if (!needLineFeed) {
1665                 indent(indentLevel);
1666                 log.print("}");
1667             }
1668             needLineFeed = false;
1669 
1670             if (memusage || dtime >= timing) {
1671                 log.print(" (");
1672                 if (memusage) {
1673                     log.print("dmem: " + dmem);
1674                 }
1675                 if (dtime >= timing) {
1676                     if (memusage) {
1677                         log.print(", ");
1678                     }
1679                     log.print(tformat.format(dtime / 1000f));
1680                 }
1681                 log.print(")");
1682             }
1683 
1684             if (errorDelta != 0) {
1685                 log.println(
1686                         " FAILED ("
1687                                 + errorDelta
1688                                 + " failure(s)"
1689                                 + ((warnDelta != 0) ? ", " + warnDelta + " warning(s)" : "")
1690                                 + ((invalidDelta != 0)
1691                                         ? ", " + invalidDelta + " test(s) skipped)"
1692                                         : ")"));
1693             } else if (warnDelta != 0) {
1694                 log.println(
1695                         " ALERT ("
1696                                 + warnDelta
1697                                 + " warning(s)"
1698                                 + ((invalidDelta != 0)
1699                                         ? ", " + invalidDelta + " test(s) skipped)"
1700                                         : ")"));
1701             } else if (invalidDelta != 0) {
1702                 log.println(" Qualified (" + invalidDelta + " test(s) skipped)");
1703             } else {
1704                 log.println(" Passed");
1705             }
1706         }
1707 
indent(int distance)1708         private final void indent(int distance) {
1709             boolean idm = inDocMode();
1710             if (needLineFeed) {
1711                 if (idm) {
1712                     log.println();
1713                 } else {
1714                     log.println(" {");
1715                 }
1716                 needLineFeed = false;
1717             }
1718 
1719             log.print(spaces.substring(0, distance * (idm ? 3 : 2)));
1720 
1721             if (idm) {
1722                 log.print("-- ");
1723             }
1724         }
1725     }
1726 
getTranslitTestFilter()1727     public String getTranslitTestFilter() {
1728         return params.tfilter;
1729     }
1730 
1731     /**
1732      * Return the target name for a test class. This is either the end of the class name, or if the
1733      * class declares a public static field CLASS_TARGET_NAME, the value of that field.
1734      */
getClassTargetName(Class testClass)1735     private static String getClassTargetName(Class testClass) {
1736         String name = testClass.getName();
1737         try {
1738             Field f = testClass.getField("CLASS_TARGET_NAME");
1739             name = (String) f.get(null);
1740         } catch (IllegalAccessException e) {
1741             throw new IllegalStateException("static field CLASS_TARGET_NAME must be accessible");
1742         } catch (NoSuchFieldException e) {
1743             int n = Math.max(name.lastIndexOf('.'), name.lastIndexOf('$'));
1744             if (n != -1) {
1745                 name = name.substring(n + 1);
1746             }
1747         }
1748         return name;
1749     }
1750 
1751     /**
1752      * Check the given array to see that all the strings in the expected array are present.
1753      *
1754      * @param msg string message, for log output
1755      * @param array array of strings to check
1756      * @param expected array of strings we expect to see, or null
1757      * @return the length of 'array', or -1 on error
1758      */
checkArray(String msg, String array[], String expected[])1759     protected int checkArray(String msg, String array[], String expected[]) {
1760         int explen = (expected != null) ? expected.length : 0;
1761         if (!(explen >= 0 && explen < 31)) { // [sic] 31 not 32
1762             errln("Internal error");
1763             return -1;
1764         }
1765         int i = 0;
1766         StringBuffer buf = new StringBuffer();
1767         int seenMask = 0;
1768         for (; i < array.length; ++i) {
1769             String s = array[i];
1770             if (i != 0) buf.append(", ");
1771             buf.append(s);
1772             // check expected list
1773             for (int j = 0, bit = 1; j < explen; ++j, bit <<= 1) {
1774                 if ((seenMask & bit) == 0) {
1775                     if (s.equals(expected[j])) {
1776                         seenMask |= bit;
1777                         logln("Ok: \"" + s + "\" seen");
1778                     }
1779                 }
1780             }
1781         }
1782         logln(msg + " = [" + buf + "] (" + i + ")");
1783         // did we see all expected strings?
1784         if (((1 << explen) - 1) != seenMask) {
1785             for (int j = 0, bit = 1; j < expected.length; ++j, bit <<= 1) {
1786                 if ((seenMask & bit) == 0) {
1787                     errln("\"" + expected[j] + "\" not seen");
1788                 }
1789             }
1790         }
1791         return array.length;
1792     }
1793 
1794     /**
1795      * Check the given array to see that all the locales in the expected array are present.
1796      *
1797      * @param msg string message, for log output
1798      * @param array array of locales to check
1799      * @param expected array of locales names we expect to see, or null
1800      * @return the length of 'array'
1801      */
checkArray(String msg, Locale array[], String expected[])1802     protected int checkArray(String msg, Locale array[], String expected[]) {
1803         String strs[] = new String[array.length];
1804         for (int i = 0; i < array.length; ++i) strs[i] = array[i].toString();
1805         return checkArray(msg, strs, expected);
1806     }
1807 
1808     /**
1809      * Check the given array to see that all the locales in the expected array are present.
1810      *
1811      * @param msg string message, for log output
1812      * @param array array of locales to check
1813      * @param expected array of locales names we expect to see, or null
1814      * @return the length of 'array'
1815      */
checkArray(String msg, ULocale array[], String expected[])1816     protected int checkArray(String msg, ULocale array[], String expected[]) {
1817         String strs[] = new String[array.length];
1818         for (int i = 0; i < array.length; ++i) strs[i] = array[i].toString();
1819         return checkArray(msg, strs, expected);
1820     }
1821 
1822     // JUnit-like assertions.
1823 
assertTrue(String message, boolean condition)1824     protected boolean assertTrue(String message, boolean condition) {
1825         return handleAssert(condition, message, "true", null);
1826     }
1827 
assertFalse(String message, boolean condition)1828     protected boolean assertFalse(String message, boolean condition) {
1829         return handleAssert(!condition, message, "false", null);
1830     }
1831 
assertEquals(String message, boolean expected, boolean actual)1832     protected boolean assertEquals(String message, boolean expected, boolean actual) {
1833         return handleAssert(
1834                 expected == actual, message, String.valueOf(expected), String.valueOf(actual));
1835     }
1836 
assertEquals(String message, long expected, long actual)1837     protected boolean assertEquals(String message, long expected, long actual) {
1838         return handleAssert(
1839                 expected == actual, message, String.valueOf(expected), String.valueOf(actual));
1840     }
1841 
1842     // do NaN and range calculations to precision of float, don't rely on
1843     // promotion to double
assertEquals(String message, float expected, float actual, double error)1844     protected boolean assertEquals(String message, float expected, float actual, double error) {
1845         boolean result =
1846                 Float.isInfinite(expected)
1847                         ? expected == actual
1848                         : !(Math.abs(expected - actual) > error); // handles NaN
1849         return handleAssert(
1850                 result,
1851                 message,
1852                 String.valueOf(expected) + (error == 0 ? "" : " (within " + error + ")"),
1853                 String.valueOf(actual));
1854     }
1855 
assertEquals(String message, double expected, double actual, double error)1856     protected boolean assertEquals(String message, double expected, double actual, double error) {
1857         boolean result =
1858                 Double.isInfinite(expected)
1859                         ? expected == actual
1860                         : !(Math.abs(expected - actual) > error); // handles NaN
1861         return handleAssert(
1862                 result,
1863                 message,
1864                 String.valueOf(expected) + (error == 0 ? "" : " (within " + error + ")"),
1865                 String.valueOf(actual));
1866     }
1867 
assertEquals(String message, T[] expected, T[] actual)1868     protected <T> boolean assertEquals(String message, T[] expected, T[] actual) {
1869         // Use toString on a List to get useful, readable messages
1870         String expectedString = expected == null ? "null" : Arrays.asList(expected).toString();
1871         String actualString = actual == null ? "null" : Arrays.asList(actual).toString();
1872         return assertEquals(message, expectedString, actualString);
1873     }
1874 
assertEquals(String message, Object expected, Object actual)1875     protected boolean assertEquals(String message, Object expected, Object actual) {
1876         boolean result = expected == null ? actual == null : expected.equals(actual);
1877         return handleAssert(result, message, stringFor(expected), stringFor(actual));
1878     }
1879 
assertNotEquals(String message, Object expected, Object actual)1880     protected boolean assertNotEquals(String message, Object expected, Object actual) {
1881         boolean result = !(expected == null ? actual == null : expected.equals(actual));
1882         return handleAssert(
1883                 result, message, stringFor(expected), stringFor(actual), "not equal to", true);
1884     }
1885 
assertSame(String message, Object expected, Object actual)1886     protected boolean assertSame(String message, Object expected, Object actual) {
1887         return handleAssert(
1888                 expected == actual, message, stringFor(expected), stringFor(actual), "==", false);
1889     }
1890 
assertNotSame(String message, Object expected, Object actual)1891     protected boolean assertNotSame(String message, Object expected, Object actual) {
1892         return handleAssert(
1893                 expected != actual, message, stringFor(expected), stringFor(actual), "!=", true);
1894     }
1895 
assertNull(String message, Object actual)1896     protected boolean assertNull(String message, Object actual) {
1897         return handleAssert(actual == null, message, null, stringFor(actual));
1898     }
1899 
assertNotNull(String message, Object actual)1900     protected boolean assertNotNull(String message, Object actual) {
1901         return handleAssert(actual != null, message, null, stringFor(actual), "!=", true);
1902     }
1903 
fail()1904     protected void fail() {
1905         fail("");
1906     }
1907 
fail(String message)1908     protected void fail(String message) {
1909         if (message == null) {
1910             message = "";
1911         }
1912         if (!message.equals("")) {
1913             message = ": " + message;
1914         }
1915         errln(sourceLocation() + message);
1916     }
1917 
handleAssert(boolean result, String message, String expected, String actual)1918     private boolean handleAssert(boolean result, String message, String expected, String actual) {
1919         return handleAssert(result, message, expected, actual, null, false);
1920     }
1921 
handleAssert( boolean result, String message, Object expected, Object actual, String relation, boolean flip)1922     public boolean handleAssert(
1923             boolean result,
1924             String message,
1925             Object expected,
1926             Object actual,
1927             String relation,
1928             boolean flip) {
1929         if (!result || isVerbose()) {
1930             if (message == null) {
1931                 message = "";
1932             }
1933             if (!message.equals("")) {
1934                 message = ": " + message;
1935             }
1936             relation = relation == null ? ", got " : " " + relation + " ";
1937             if (result) {
1938                 logln("OK " + message + ": " + (flip ? expected + relation + actual : expected));
1939             } else {
1940                 // assert must assume errors are true errors and not just warnings
1941                 // so cannot warnln here
1942                 errln(
1943                         message
1944                                 + ": expected"
1945                                 + (flip
1946                                         ? relation + expected
1947                                         : " "
1948                                                 + expected
1949                                                 + (actual != null ? relation + actual : "")));
1950             }
1951         }
1952         return result;
1953     }
1954 
stringFor(Object obj)1955     private final String stringFor(Object obj) {
1956         if (obj == null) {
1957             return "null";
1958         }
1959         if (obj instanceof String) {
1960             return "\"" + obj + '"';
1961         }
1962         return obj.getClass().getName() + "<" + obj + ">";
1963     }
1964 
1965     // Return the source code location of the calling test
1966     // or "" if not found
sourceLocation()1967     public static SourceLocation sourceLocation() {
1968         return sourceLocation(new Throwable());
1969     }
1970 
1971     /**
1972      * Tuple representing the location of an error or warning.
1973      *
1974      * @see org.unicode.cldr.util.XMLSource.SourceLocation
1975      */
1976     public static final class SourceLocation {
1977         public final int lineNumber;
1978         public final String file;
1979         public final String className;
1980 
SourceLocation(int lineNumber2, String source, StackTraceElement st)1981         public SourceLocation(int lineNumber2, String source, StackTraceElement st) {
1982             this.lineNumber = lineNumber2;
1983             this.className = st.getClassName();
1984             this.file = source;
1985         }
1986 
SourceLocation()1987         public SourceLocation() {
1988             this.lineNumber = -1;
1989             this.file = null;
1990             this.className = null;
1991         }
1992 
1993         @Override
toString()1994         public String toString() {
1995             if (lineNumber == -1 && file == null) {
1996                 return "";
1997             } else {
1998                 return "(" + file + ":" + lineNumber + ") ";
1999             }
2000         }
2001 
forGitHub()2002         public String forGitHub() {
2003             return "file=" + getFullFile() + ",line=" + lineNumber;
2004         }
2005 
2006         /**
2007          * Attempt to locate the relative filename, for GitHub annotations purposes
2008          *
2009          * @return
2010          */
getFullFile()2011         public String getFullFile() {
2012             if (file == null) {
2013                 return "no-file";
2014             } else if (className == null) {
2015                 return file;
2016             } else {
2017                 try {
2018                     final String s =
2019                             locationToRelativeFile.computeIfAbsent(
2020                                     Pair.of(className, file),
2021                                     (Pair<String, String> loc) ->
2022                                             findSource(loc.getFirst(), loc.getSecond()));
2023                     if (s == null) {
2024                         return file;
2025                     }
2026                     return s;
2027                 } catch (Throwable t) {
2028                     System.err.println(
2029                             "SourceLocation: err-" + t.getMessage() + " fetching " + this);
2030                     return file;
2031                 }
2032             }
2033         }
2034 
2035         /**
2036          * Attempt to find 'org.unicode.Foo', 'Foo.class' ->
2037          * tools/cldr-code/src/test/java/org/unicode/Foo.java
2038          */
findSource(String clazz, String fyle)2039         public static final String findSource(String clazz, String fyle) {
2040             final String classSubPath = clazz.replaceAll("\\.", "/"); // a.b.c -> a/b/c
2041             final Path basePath = new File(CLDRPaths.BASE_DIRECTORY).toPath().toAbsolutePath();
2042             final Path subPath =
2043                     new File(classSubPath)
2044                             .toPath() // a/b/c/Class
2045                             .getParent() // a/b/c
2046                             .resolve(fyle); // a/b/c/Class.java
2047             try (Stream<Path> paths =
2048                     Files.find(
2049                             basePath,
2050                             Integer.MAX_VALUE,
2051                             (Path path, BasicFileAttributes attrs) ->
2052                                     path.endsWith(subPath) && Files.isReadable(path))) {
2053                 Path p = paths.findFirst().get().toAbsolutePath();
2054                 return p.subpath(basePath.getNameCount(), p.getNameCount()).toString();
2055                 // return p.toString();
2056             } catch (IOException | NoSuchElementException e) {
2057                 System.err.println(
2058                         "SourceLocation.findSource err-" + e.getMessage() + " fetching " + subPath);
2059                 if (!(e instanceof NoSuchElementException)) {
2060                     // Skip for not-found
2061                     e.printStackTrace();
2062                 }
2063                 return fyle;
2064             }
2065         }
2066 
isEmpty()2067         public boolean isEmpty() {
2068             return (file == null) || (className == null) || (lineNumber == -1);
2069         }
2070 
2071         static final ConcurrentHashMap<Pair<String, String>, String> locationToRelativeFile =
2072                 new ConcurrentHashMap<>();
2073     }
2074 
2075     // Return the source code location of the specified throwable's calling test
2076     // returns "" if not found
sourceLocation(Throwable forThrowable)2077     public static SourceLocation sourceLocation(Throwable forThrowable) {
2078         // Walk up the stack to the first call site outside this file
2079         for (StackTraceElement st : new Throwable().getStackTrace()) {
2080             String source = st.getFileName();
2081             if (source == null || source.equals("TestShim.java")) {
2082                 return new SourceLocation(); // hit the end of helpful stack traces
2083             } else if (source != null
2084                     && !source.equals("TestFmwk.java")
2085                     && !source.equals("AbstractTestLog.java")) {
2086                 String methodName = st.getMethodName();
2087                 if (methodName != null && methodName.startsWith("lambda$")) { // unpack inner lambda
2088                     methodName =
2089                             methodName.substring(
2090                                     "lambda$".length()); // lambda$TestValid$0 -> TestValid$0
2091                 }
2092                 if (methodName != null
2093                         && (methodName.startsWith("Test")
2094                                 || methodName.startsWith("test")
2095                                 || methodName.equals("main"))) {}
2096                 return new SourceLocation(st.getLineNumber(), source, st);
2097             }
2098         }
2099         return new SourceLocation(); // not found
2100     }
2101 
2102     // End JUnit-like assertions
2103 
2104     // PrintWriter support
2105 
getErrorLogPrintWriter()2106     public PrintWriter getErrorLogPrintWriter() {
2107         return new PrintWriter(new TestLogWriter(this, TestLog.ERR));
2108     }
2109 
getLogPrintWriter()2110     public PrintWriter getLogPrintWriter() {
2111         return new PrintWriter(new TestLogWriter(this, TestLog.LOG));
2112     }
2113 
2114     // end PrintWriter support
2115 
2116     protected TestParams params = null;
2117 
2118     private static final String spaces = "                                          ";
2119 }
2120