1// © 2016 and later: Unicode, Inc. and others.
2// License & terms of use: http://www.unicode.org/copyright.html#License
3/*
4 *******************************************************************************
5 * Copyright (C) 1996-2015, International Business Machines Corporation and    *
6 * others. All Rights Reserved.                                                *
7 *******************************************************************************
8 */
9package com.ibm.icu.dev.test;
10
11import java.lang.reflect.Constructor;
12import java.lang.reflect.Method;
13import java.lang.reflect.Modifier;
14import java.security.Policy;
15import java.util.ArrayList;
16import java.util.Arrays;
17import java.util.List;
18import java.util.Locale;
19import java.util.Map;
20import java.util.Properties;
21import java.util.Random;
22import java.util.TreeMap;
23
24import org.junit.After;
25import org.junit.Assert;
26import org.junit.Before;
27
28import com.ibm.icu.util.TimeZone;
29import com.ibm.icu.util.ULocale;
30
31/**
32 * TestFmwk is a base class for tests that can be run conveniently from the
33 * command line as well as under the Java test harness.
34 * <p>
35 * Sub-classes implement a set of methods named Test <something>. Each of these
36 * methods performs some test. Test methods should indicate errors by calling
37 * either err or errln. This will increment the errorCount field and may
38 * optionally print a message to the log. Debugging information may also be
39 * added to the log via the log and logln methods. These methods will add their
40 * arguments to the log only if the test is being run in verbose mode.
41 */
42abstract public class TestFmwk extends AbstractTestLog {
43    /**
44     * The default time zone for all of our tests. Used in @Before
45     */
46    private final static TimeZone defaultTimeZone = TimeZone.getTimeZone("America/Los_Angeles");
47
48    /**
49     * The default locale used for all of our tests. Used in @Before
50     */
51    private final static Locale defaultLocale = Locale.US;
52
53    private static final String EXHAUSTIVENESS = "ICU.exhaustive";
54    private static final int DEFAULT_EXHAUSTIVENESS = 0;
55    private static final int MAX_EXHAUSTIVENESS = 10;
56
57    private static final String LOGGING_LEVEL = "ICU.logging";
58    private static final int DEFAULT_LOGGING_LEVEL = 0;
59    private static final int MAX_LOGGING_LEVEL = 3;
60
61    public static final int LOGGING_NONE = 0;
62    public static final int LOGGING_WARN = 1;
63    public static final int LOGGING_INFO = 2;
64    public static final int LOGGING_DEBUG = 3;
65
66    private static final String SEED = "ICU.seed";
67    private static final String SECURITY_POLICY = "ICU.securitypolicy";
68
69    private static final TestParams testParams;
70    static {
71        testParams = TestParams.create();
72    }
73
74    protected TestFmwk() {
75    }
76
77    @Before
78    public void testInitialize() {
79        Locale.setDefault(defaultLocale);
80        TimeZone.setDefault(defaultTimeZone);
81
82        if (getParams().testSecurityManager != null) {
83            System.setSecurityManager(getParams().testSecurityManager);
84        }
85    }
86
87    @After
88    public void testTeardown() {
89        if (getParams().testSecurityManager != null) {
90            System.setSecurityManager(getParams().originalSecurityManager);
91        }
92    }
93
94    private static TestParams getParams() {
95        //return paramsReference.get();
96        return testParams;
97    }
98
99    protected static boolean isVerbose() {
100        return getParams().getLoggingLevel() >= LOGGING_INFO;
101    }
102
103    /**
104     * 0 = fewest tests, 5 is normal build, 10 is most tests
105     */
106    protected static int getExhaustiveness() {
107        return getParams().inclusion;
108    }
109
110    protected static boolean isQuick() {
111        return getParams().getInclusion() == 0;
112    }
113
114    // use this instead of new random so we get a consistent seed
115    // for our tests
116    protected Random createRandom() {
117        return new Random(getParams().getSeed());
118    }
119
120    /**
121     * Integer Random number generator, produces positive int values.
122     * Similar to C++ std::minstd_rand, with the same algorithm & constants.
123     * Provided for compatibility with ICU4C.
124     * Get & set of the seed allows for reproducible monkey tests.
125     */
126    protected class ICU_Rand {
127        private int fLast;
128
129        public ICU_Rand(int seed) {
130            seed(seed);
131        }
132
133        public int next() {
134            fLast = (int)((fLast * 48271L) % 2147483647L);
135            return fLast;
136        }
137
138        public void seed(int seed) {
139            if (seed <= 0) {
140                seed = 1;
141            }
142            seed %= 2147483647;   // = 0x7FFFFFFF
143            fLast = seed > 0 ? seed : 1;
144        }
145
146        public int getSeed() {
147            return fLast;
148        }
149
150    }
151
152    static final String ICU_TRAC_URL = "http://bugs.icu-project.org/trac/ticket/";
153    static final String CLDR_TRAC_URL = "http://unicode.org/cldr/trac/ticket/";
154    static final String CLDR_TICKET_PREFIX = "cldrbug:";
155
156    /**
157     * Log the known issue.
158     * This method returns true unless -prop:logKnownIssue=no is specified
159     * in the argument list.
160     *
161     * @param ticket A ticket number string. For an ICU ticket, use numeric characters only,
162     * such as "10245". For a CLDR ticket, use prefix "cldrbug:" followed by ticket number,
163     * such as "cldrbug:5013".
164     * @param comment Additional comment, or null
165     * @return true unless -prop:logKnownIssue=no is specified in the test command line argument.
166     */
167    protected static boolean logKnownIssue(String ticket, String comment) {
168        if (!getBooleanProperty("logKnownIssue", true)) {
169            return false;
170        }
171
172        StringBuffer descBuf = new StringBuffer();
173        // TODO(junit) : what to do about this?
174        //getParams().stack.appendPath(descBuf);
175        if (comment != null && comment.length() > 0) {
176            descBuf.append(" (" + comment + ")");
177        }
178        String description = descBuf.toString();
179
180        String ticketLink = "Unknown Ticket";
181        if (ticket != null && ticket.length() > 0) {
182            boolean isCldr = false;
183            ticket = ticket.toLowerCase(Locale.ENGLISH);
184            if (ticket.startsWith(CLDR_TICKET_PREFIX)) {
185                isCldr = true;
186                ticket = ticket.substring(CLDR_TICKET_PREFIX.length());
187            }
188            ticketLink = (isCldr ? CLDR_TRAC_URL : ICU_TRAC_URL) + ticket;
189        }
190
191        if (getParams().knownIssues == null) {
192            getParams().knownIssues = new TreeMap<String, List<String>>();
193        }
194        List<String> lines = getParams().knownIssues.get(ticketLink);
195        if (lines == null) {
196            lines = new ArrayList<String>();
197            getParams().knownIssues.put(ticketLink, lines);
198        }
199        if (!lines.contains(description)) {
200            lines.add(description);
201        }
202
203        return true;
204    }
205
206    protected static String getProperty(String key) {
207        return getParams().getProperty(key);
208    }
209
210    protected static boolean getBooleanProperty(String key) {
211        return getParams().getBooleanProperty(key);
212    }
213
214    protected static boolean getBooleanProperty(String key, boolean defVal) {
215        return getParams().getBooleanProperty(key, defVal);
216    }
217
218    protected static int getIntProperty(String key, int defVal) {
219        return getParams().getIntProperty(key, defVal);
220    }
221
222    protected static int getIntProperty(String key, int defVal, int maxVal) {
223        return getParams().getIntProperty(key, defVal, maxVal);
224    }
225
226    protected static TimeZone safeGetTimeZone(String id) {
227        TimeZone tz = TimeZone.getTimeZone(id);
228        if (tz == null) {
229            // should never happen
230            errln("FAIL: TimeZone.getTimeZone(" + id + ") => null");
231        }
232        if (!tz.getID().equals(id)) {
233            warnln("FAIL: TimeZone.getTimeZone(" + id + ") => " + tz.getID());
234        }
235        return tz;
236    }
237
238
239    // Utility Methods
240
241    protected static String hex(char[] s){
242        StringBuffer result = new StringBuffer();
243        for (int i = 0; i < s.length; ++i) {
244            if (i != 0) result.append(',');
245            result.append(hex(s[i]));
246        }
247        return result.toString();
248    }
249
250    protected static String hex(byte[] s){
251        StringBuffer result = new StringBuffer();
252        for (int i = 0; i < s.length; ++i) {
253            if (i != 0) result.append(',');
254            result.append(hex(s[i]));
255        }
256        return result.toString();
257    }
258
259    protected static String hex(char ch) {
260        StringBuffer result = new StringBuffer();
261        String foo = Integer.toString(ch, 16).toUpperCase();
262        for (int i = foo.length(); i < 4; ++i) {
263            result.append('0');
264        }
265        return result + foo;
266    }
267
268    protected static String hex(int ch) {
269        StringBuffer result = new StringBuffer();
270        String foo = Integer.toString(ch, 16).toUpperCase();
271        for (int i = foo.length(); i < 4; ++i) {
272            result.append('0');
273        }
274        return result + foo;
275    }
276
277    protected static String hex(CharSequence s) {
278        StringBuilder result = new StringBuilder();
279        for (int i = 0; i < s.length(); ++i) {
280            if (i != 0)
281                result.append(',');
282            result.append(hex(s.charAt(i)));
283        }
284        return result.toString();
285    }
286
287    protected static String prettify(CharSequence s) {
288        StringBuilder result = new StringBuilder();
289        int ch;
290        for (int i = 0; i < s.length(); i += Character.charCount(ch)) {
291            ch = Character.codePointAt(s, i);
292            if (ch > 0xfffff) {
293                result.append("\\U00");
294                result.append(hex(ch));
295            } else if (ch > 0xffff) {
296                result.append("\\U000");
297                result.append(hex(ch));
298            } else if (ch < 0x20 || 0x7e < ch) {
299                result.append("\\u");
300                result.append(hex(ch));
301            } else {
302                result.append((char) ch);
303            }
304
305        }
306        return result.toString();
307    }
308
309    private static java.util.GregorianCalendar cal;
310
311    /**
312     * Return a Date given a year, month, and day of month. This is similar to
313     * new Date(y-1900, m, d). It uses the default time zone at the time this
314     * method is first called.
315     *
316     * @param year
317     *            use 2000 for 2000, unlike new Date()
318     * @param month
319     *            use Calendar.JANUARY etc.
320     * @param dom
321     *            day of month, 1-based
322     * @return a Date object for the given y/m/d
323     */
324    protected static synchronized java.util.Date getDate(int year, int month,
325            int dom) {
326        if (cal == null) {
327            cal = new java.util.GregorianCalendar();
328        }
329        cal.clear();
330        cal.set(year, month, dom);
331        return cal.getTime();
332    }
333
334    private static class TestParams {
335
336        private int inclusion;
337        private long seed;
338        private int loggingLevel;
339
340        private String policyFileName;
341        private SecurityManager testSecurityManager;
342        private SecurityManager originalSecurityManager;
343
344        private Map<String, List<String>> knownIssues;
345
346        private Properties props;
347
348
349        private TestParams() {
350        }
351
352        static TestParams create() {
353            TestParams params = new TestParams();
354            Properties props = System.getProperties();
355            params.parseProperties(props);
356            return params;
357        }
358
359        private void parseProperties(Properties props) {
360            this.props = props;
361
362            inclusion = getIntProperty(EXHAUSTIVENESS, DEFAULT_EXHAUSTIVENESS, MAX_EXHAUSTIVENESS);
363            seed = getLongProperty(SEED, System.currentTimeMillis());
364            loggingLevel = getIntProperty(LOGGING_LEVEL, DEFAULT_LOGGING_LEVEL, MAX_LOGGING_LEVEL);
365
366            policyFileName = getProperty(SECURITY_POLICY);
367            if (policyFileName != null) {
368                String originalPolicyFileName = System.getProperty("java.security.policy");
369                originalSecurityManager = System.getSecurityManager();
370                System.setProperty("java.security.policy", policyFileName);
371                Policy.getPolicy().refresh();
372                testSecurityManager = new SecurityManager();
373                System.setProperty("java.security.policy", originalPolicyFileName==null ? "" : originalPolicyFileName);
374            }
375        }
376
377        public String getProperty(String key) {
378            String val = null;
379            if (key != null && key.length() > 0) {
380                val = props.getProperty(key);
381            }
382            return val;
383        }
384
385        public boolean getBooleanProperty(String key) {
386            return getBooleanProperty(key, false);
387        }
388
389        public boolean getBooleanProperty(String key, boolean defVal) {
390            String s = getProperty(key);
391            if (s == null) {
392                return defVal;
393            }
394            if (s.equalsIgnoreCase("yes") || s.equalsIgnoreCase("true") || s.equals("1")) {
395                return true;
396            }
397            return false;
398        }
399
400        public int getIntProperty(String key, int defVal) {
401            return getIntProperty(key, defVal, -1);
402        }
403
404        public int getIntProperty(String key, int defVal, int maxVal) {
405            String s = getProperty(key);
406            if (s == null) {
407                return defVal;
408            }
409            return (maxVal == -1) ? Integer.valueOf(s) : Math.max(Integer.valueOf(s), maxVal);
410        }
411
412        public long getLongProperty(String key, long defVal) {
413            String s = getProperty(key);
414            if (s == null) {
415                return defVal;
416            }
417            return Long.valueOf(s);
418        }
419
420        public int getInclusion() {
421            return inclusion;
422        }
423
424        public long getSeed() {
425            return seed;
426        }
427
428        public int getLoggingLevel() {
429            return loggingLevel;
430        }
431    }
432
433    /**
434     * Check the given array to see that all the strings in the expected array
435     * are present.
436     *
437     * @param msg
438     *            string message, for log output
439     * @param array
440     *            array of strings to check
441     * @param expected
442     *            array of strings we expect to see, or null
443     * @return the length of 'array', or -1 on error
444     */
445    protected static int checkArray(String msg, String array[], String expected[]) {
446        int explen = (expected != null) ? expected.length : 0;
447        if (!(explen >= 0 && explen < 31)) { // [sic] 31 not 32
448            errln("Internal error");
449            return -1;
450        }
451        int i = 0;
452        StringBuffer buf = new StringBuffer();
453        int seenMask = 0;
454        for (; i < array.length; ++i) {
455            String s = array[i];
456            if (i != 0)
457                buf.append(", ");
458            buf.append(s);
459            // check expected list
460            for (int j = 0, bit = 1; j < explen; ++j, bit <<= 1) {
461                if ((seenMask & bit) == 0) {
462                    if (s.equals(expected[j])) {
463                        seenMask |= bit;
464                        logln("Ok: \"" + s + "\" seen");
465                    }
466                }
467            }
468        }
469        logln(msg + " = [" + buf + "] (" + i + ")");
470        // did we see all expected strings?
471        if (((1 << explen) - 1) != seenMask) {
472            for (int j = 0, bit = 1; j < expected.length; ++j, bit <<= 1) {
473                if ((seenMask & bit) == 0) {
474                    errln("\"" + expected[j] + "\" not seen");
475                }
476            }
477        }
478        return array.length;
479    }
480
481    /**
482     * Check the given array to see that all the locales in the expected array
483     * are present.
484     *
485     * @param msg
486     *            string message, for log output
487     * @param array
488     *            array of locales to check
489     * @param expected
490     *            array of locales names we expect to see, or null
491     * @return the length of 'array'
492     */
493    protected static int checkArray(String msg, Locale array[], String expected[]) {
494        String strs[] = new String[array.length];
495        for (int i = 0; i < array.length; ++i) {
496            strs[i] = array[i].toString();
497        }
498        return checkArray(msg, strs, expected);
499    }
500
501    /**
502     * Check the given array to see that all the locales in the expected array
503     * are present.
504     *
505     * @param msg
506     *            string message, for log output
507     * @param array
508     *            array of locales to check
509     * @param expected
510     *            array of locales names we expect to see, or null
511     * @return the length of 'array'
512     */
513    protected static int checkArray(String msg, ULocale array[], String expected[]) {
514        String strs[] = new String[array.length];
515        for (int i = 0; i < array.length; ++i) {
516            strs[i] = array[i].toString();
517        }
518        return checkArray(msg, strs, expected);
519    }
520
521    // JUnit-like assertions.
522
523    protected static boolean assertTrue(String message, boolean condition) {
524        return handleAssert(condition, message, "true", null);
525    }
526
527    protected static boolean assertFalse(String message, boolean condition) {
528        return handleAssert(!condition, message, "false", null);
529    }
530
531    protected static boolean assertEquals(String message, boolean expected,
532            boolean actual) {
533        return handleAssert(expected == actual, message, String
534                .valueOf(expected), String.valueOf(actual));
535    }
536
537    protected static boolean assertEquals(String message, long expected, long actual) {
538        return handleAssert(expected == actual, message, String
539                .valueOf(expected), String.valueOf(actual));
540    }
541
542    // do NaN and range calculations to precision of float, don't rely on
543    // promotion to double
544    protected static boolean assertEquals(String message, float expected,
545            float actual, double error) {
546        boolean result = Float.isInfinite(expected)
547                ? expected == actual
548                : !(Math.abs(expected - actual) > error); // handles NaN
549        return handleAssert(result, message, String.valueOf(expected)
550                + (error == 0 ? "" : " (within " + error + ")"), String
551                .valueOf(actual));
552    }
553
554    protected static boolean assertEquals(String message, double expected,
555            double actual, double error) {
556        boolean result = Double.isInfinite(expected)
557                ? expected == actual
558                : !(Math.abs(expected - actual) > error); // handles NaN
559        return handleAssert(result, message, String.valueOf(expected)
560                + (error == 0 ? "" : " (within " + error + ")"), String
561                .valueOf(actual));
562    }
563
564    protected static <T> boolean assertEquals(String message, T[] expected, T[] actual) {
565        // Use toString on a List to get useful, readable messages
566        String expectedString = expected == null ? "null" : Arrays.asList(expected).toString();
567        String actualString = actual == null ? "null" : Arrays.asList(actual).toString();
568        return assertEquals(message, expectedString, actualString);
569    }
570
571    protected static boolean assertEquals(String message, Object expected,
572            Object actual) {
573        boolean result = expected == null ? actual == null : expected
574                .equals(actual);
575        return handleAssert(result, message, stringFor(expected),
576                stringFor(actual));
577    }
578
579    protected static boolean assertNotEquals(String message, Object expected,
580            Object actual) {
581        boolean result = !(expected == null ? actual == null : expected
582                .equals(actual));
583        return handleAssert(result, message, stringFor(expected),
584                stringFor(actual), "not equal to", true);
585    }
586
587    protected boolean assertSame(String message, Object expected, Object actual) {
588        return handleAssert(expected == actual, message, stringFor(expected),
589                stringFor(actual), "==", false);
590    }
591
592    protected static boolean assertNotSame(String message, Object expected,
593            Object actual) {
594        return handleAssert(expected != actual, message, stringFor(expected),
595                stringFor(actual), "!=", true);
596    }
597
598    protected static boolean assertNull(String message, Object actual) {
599        return handleAssert(actual == null, message, null, stringFor(actual));
600    }
601
602    protected static boolean assertNotNull(String message, Object actual) {
603        return handleAssert(actual != null, message, null, stringFor(actual),
604                "!=", true);
605    }
606
607    protected static void fail() {
608        fail("");
609    }
610
611    protected static void fail(String message) {
612        if (message == null) {
613            message = "";
614        }
615        if (!message.equals("")) {
616            message = ": " + message;
617        }
618        errln(sourceLocation() + message);
619    }
620
621    private static boolean handleAssert(boolean result, String message,
622            String expected, String actual) {
623        return handleAssert(result, message, expected, actual, null, false);
624    }
625
626    public static boolean handleAssert(boolean result, String message,
627            Object expected, Object actual, String relation, boolean flip) {
628        if (!result || isVerbose()) {
629            if (message == null) {
630                message = "";
631            }
632            if (!message.equals("")) {
633                message = ": " + message;
634            }
635            relation = relation == null ? ", got " : " " + relation + " ";
636            if (result) {
637                logln("OK " + message + ": "
638                        + (flip ? expected + relation + actual : expected));
639            } else {
640                // assert must assume errors are true errors and not just warnings
641                // so cannot warnln here
642                errln(  message
643                        + ": expected"
644                        + (flip ? relation + expected : " " + expected
645                                + (actual != null ? relation + actual : "")));
646            }
647        }
648        return result;
649    }
650
651    private static final String stringFor(Object obj) {
652        if (obj == null) {
653            return "null";
654        }
655        if (obj instanceof String) {
656            return "\"" + obj + '"';
657        }
658        return obj.getClass().getName() + "<" + obj + ">";
659    }
660
661    // Return the source code location of the caller located callDepth frames up the stack.
662    protected static String sourceLocation() {
663        // Walk up the stack to the first call site outside this file
664        for (StackTraceElement st : new Throwable().getStackTrace()) {
665            String source = st.getFileName();
666            if (source != null && !source.equals("TestFmwk.java") && !source.equals("AbstractTestLog.java")) {
667                String methodName = st.getMethodName();
668                if (methodName != null &&
669                       (methodName.startsWith("Test") || methodName.startsWith("test") || methodName.equals("main"))) {
670                    return "(" + source + ":" + st.getLineNumber() + ") ";
671                }
672            }
673        }
674        throw new InternalError();
675    }
676
677    protected static boolean checkDefaultPrivateConstructor(String fullyQualifiedClassName) throws Exception {
678        return checkDefaultPrivateConstructor(Class.forName(fullyQualifiedClassName));
679    }
680
681    protected static boolean checkDefaultPrivateConstructor(Class<?> classToBeTested) throws Exception {
682        Constructor<?> constructor = classToBeTested.getDeclaredConstructor();
683
684        // Check that the constructor is private.
685        boolean isPrivate = Modifier.isPrivate(constructor.getModifiers());
686
687        // Call the constructor for coverage.
688        constructor.setAccessible(true);
689        constructor.newInstance();
690
691        if (!isPrivate) {
692            errln("Default private constructor for class: " + classToBeTested.getName() + " is not private.");
693        }
694        return isPrivate;
695    }
696
697    /**
698     * Tests the toString method on a private or hard-to-reach class.  Assumes constructor of the class does not
699     * take any arguments.
700     * @param fullyQualifiedClassName
701     * @return The output of the toString method.
702     * @throws Exception
703     */
704    protected static String invokeToString(String fullyQualifiedClassName) throws Exception {
705        return invokeToString(fullyQualifiedClassName, new Class<?>[]{}, new Object[]{});
706    }
707
708    /**
709     * Tests the toString method on a private or hard-to-reach class.  Assumes constructor of the class does not
710     * take any arguments.
711     * @param classToBeTested
712     * @return The output of the toString method.
713     * @throws Exception
714     */
715    protected static String invokeToString(Class<?> classToBeTested) throws Exception {
716        return invokeToString(classToBeTested, new Class<?>[]{}, new Object[]{});
717    }
718
719    /**
720     * Tests the toString method on a private or hard-to-reach class.  Allows you to specify the argument types for
721     * the constructor.
722     * @param fullyQualifiedClassName
723     * @return The output of the toString method.
724     * @throws Exception
725     */
726    protected static String invokeToString(String fullyQualifiedClassName,
727            Class<?>[] constructorParamTypes, Object[] constructorParams) throws Exception {
728        return invokeToString(Class.forName(fullyQualifiedClassName), constructorParamTypes, constructorParams);
729    }
730
731    /**
732     * Tests the toString method on a private or hard-to-reach class.  Allows you to specify the argument types for
733     * the constructor.
734     * @param classToBeTested
735     * @return The output of the toString method.
736     * @throws Exception
737     */
738    protected static String invokeToString(Class<?> classToBeTested,
739            Class<?>[] constructorParamTypes, Object[] constructorParams) throws Exception {
740        Constructor<?> constructor = classToBeTested.getDeclaredConstructor(constructorParamTypes);
741        constructor.setAccessible(true);
742        Object obj = constructor.newInstance(constructorParams);
743        Method toStringMethod = classToBeTested.getDeclaredMethod("toString");
744        toStringMethod.setAccessible(true);
745        return (String) toStringMethod.invoke(obj);
746    }
747
748
749    // End JUnit-like assertions
750
751    // TODO (sgill): added to keep errors away
752    /* (non-Javadoc)
753     * @see com.ibm.icu.dev.test.TestLog#msg(java.lang.String, int, boolean, boolean)
754     */
755    //@Override
756    protected static void msg(String message, int level, boolean incCount, boolean newln) {
757        if (level == TestLog.WARN || level == TestLog.ERR) {
758            Assert.fail(message);
759        }
760        // TODO(stuartg): turned off - causing OOM running under ant
761//        while (level > 0) {
762//            System.out.print(" ");
763//            level--;
764//        }
765//        System.out.print(message);
766//        if (newln) {
767//            System.out.println();
768//        }
769    }
770
771}
772