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    static final String ICU_TRAC_URL = "http://bugs.icu-project.org/trac/ticket/";
121    static final String CLDR_TRAC_URL = "http://unicode.org/cldr/trac/ticket/";
122    static final String CLDR_TICKET_PREFIX = "cldrbug:";
123
124    /**
125     * Log the known issue.
126     * This method returns true unless -prop:logKnownIssue=no is specified
127     * in the argument list.
128     *
129     * @param ticket A ticket number string. For an ICU ticket, use numeric characters only,
130     * such as "10245". For a CLDR ticket, use prefix "cldrbug:" followed by ticket number,
131     * such as "cldrbug:5013".
132     * @param comment Additional comment, or null
133     * @return true unless -prop:logKnownIssue=no is specified in the test command line argument.
134     */
135    protected static boolean logKnownIssue(String ticket, String comment) {
136        if (!getBooleanProperty("logKnownIssue", true)) {
137            return false;
138        }
139
140        StringBuffer descBuf = new StringBuffer();
141        // TODO(junit) : what to do about this?
142        //getParams().stack.appendPath(descBuf);
143        if (comment != null && comment.length() > 0) {
144            descBuf.append(" (" + comment + ")");
145        }
146        String description = descBuf.toString();
147
148        String ticketLink = "Unknown Ticket";
149        if (ticket != null && ticket.length() > 0) {
150            boolean isCldr = false;
151            ticket = ticket.toLowerCase(Locale.ENGLISH);
152            if (ticket.startsWith(CLDR_TICKET_PREFIX)) {
153                isCldr = true;
154                ticket = ticket.substring(CLDR_TICKET_PREFIX.length());
155            }
156            ticketLink = (isCldr ? CLDR_TRAC_URL : ICU_TRAC_URL) + ticket;
157        }
158
159        if (getParams().knownIssues == null) {
160            getParams().knownIssues = new TreeMap<String, List<String>>();
161        }
162        List<String> lines = getParams().knownIssues.get(ticketLink);
163        if (lines == null) {
164            lines = new ArrayList<String>();
165            getParams().knownIssues.put(ticketLink, lines);
166        }
167        if (!lines.contains(description)) {
168            lines.add(description);
169        }
170
171        return true;
172    }
173
174    protected static String getProperty(String key) {
175        return getParams().getProperty(key);
176    }
177
178    protected static boolean getBooleanProperty(String key) {
179        return getParams().getBooleanProperty(key);
180    }
181
182    protected static boolean getBooleanProperty(String key, boolean defVal) {
183        return getParams().getBooleanProperty(key, defVal);
184    }
185
186    protected static int getIntProperty(String key, int defVal) {
187        return getParams().getIntProperty(key, defVal);
188    }
189
190    protected static int getIntProperty(String key, int defVal, int maxVal) {
191        return getParams().getIntProperty(key, defVal, maxVal);
192    }
193
194    protected static TimeZone safeGetTimeZone(String id) {
195        TimeZone tz = TimeZone.getTimeZone(id);
196        if (tz == null) {
197            // should never happen
198            errln("FAIL: TimeZone.getTimeZone(" + id + ") => null");
199        }
200        if (!tz.getID().equals(id)) {
201            warnln("FAIL: TimeZone.getTimeZone(" + id + ") => " + tz.getID());
202        }
203        return tz;
204    }
205
206
207    // Utility Methods
208
209    protected static String hex(char[] s){
210        StringBuffer result = new StringBuffer();
211        for (int i = 0; i < s.length; ++i) {
212            if (i != 0) result.append(',');
213            result.append(hex(s[i]));
214        }
215        return result.toString();
216    }
217
218    protected static String hex(byte[] s){
219        StringBuffer result = new StringBuffer();
220        for (int i = 0; i < s.length; ++i) {
221            if (i != 0) result.append(',');
222            result.append(hex(s[i]));
223        }
224        return result.toString();
225    }
226
227    protected static String hex(char ch) {
228        StringBuffer result = new StringBuffer();
229        String foo = Integer.toString(ch, 16).toUpperCase();
230        for (int i = foo.length(); i < 4; ++i) {
231            result.append('0');
232        }
233        return result + foo;
234    }
235
236    protected static String hex(int ch) {
237        StringBuffer result = new StringBuffer();
238        String foo = Integer.toString(ch, 16).toUpperCase();
239        for (int i = foo.length(); i < 4; ++i) {
240            result.append('0');
241        }
242        return result + foo;
243    }
244
245    protected static String hex(CharSequence s) {
246        StringBuilder result = new StringBuilder();
247        for (int i = 0; i < s.length(); ++i) {
248            if (i != 0)
249                result.append(',');
250            result.append(hex(s.charAt(i)));
251        }
252        return result.toString();
253    }
254
255    protected static String prettify(CharSequence s) {
256        StringBuilder result = new StringBuilder();
257        int ch;
258        for (int i = 0; i < s.length(); i += Character.charCount(ch)) {
259            ch = Character.codePointAt(s, i);
260            if (ch > 0xfffff) {
261                result.append("\\U00");
262                result.append(hex(ch));
263            } else if (ch > 0xffff) {
264                result.append("\\U000");
265                result.append(hex(ch));
266            } else if (ch < 0x20 || 0x7e < ch) {
267                result.append("\\u");
268                result.append(hex(ch));
269            } else {
270                result.append((char) ch);
271            }
272
273        }
274        return result.toString();
275    }
276
277    private static java.util.GregorianCalendar cal;
278
279    /**
280     * Return a Date given a year, month, and day of month. This is similar to
281     * new Date(y-1900, m, d). It uses the default time zone at the time this
282     * method is first called.
283     *
284     * @param year
285     *            use 2000 for 2000, unlike new Date()
286     * @param month
287     *            use Calendar.JANUARY etc.
288     * @param dom
289     *            day of month, 1-based
290     * @return a Date object for the given y/m/d
291     */
292    protected static synchronized java.util.Date getDate(int year, int month,
293            int dom) {
294        if (cal == null) {
295            cal = new java.util.GregorianCalendar();
296        }
297        cal.clear();
298        cal.set(year, month, dom);
299        return cal.getTime();
300    }
301
302    private static class TestParams {
303
304        private int inclusion;
305        private long seed;
306        private int loggingLevel;
307
308        private String policyFileName;
309        private SecurityManager testSecurityManager;
310        private SecurityManager originalSecurityManager;
311
312        private Map<String, List<String>> knownIssues;
313
314        private Properties props;
315
316
317        private TestParams() {
318        }
319
320        static TestParams create() {
321            TestParams params = new TestParams();
322            Properties props = System.getProperties();
323            params.parseProperties(props);
324            return params;
325        }
326
327        private void parseProperties(Properties props) {
328            this.props = props;
329
330            inclusion = getIntProperty(EXHAUSTIVENESS, DEFAULT_EXHAUSTIVENESS, MAX_EXHAUSTIVENESS);
331            seed = getLongProperty(SEED, System.currentTimeMillis());
332            loggingLevel = getIntProperty(LOGGING_LEVEL, DEFAULT_LOGGING_LEVEL, MAX_LOGGING_LEVEL);
333
334            policyFileName = getProperty(SECURITY_POLICY);
335            if (policyFileName != null) {
336                String originalPolicyFileName = System.getProperty("java.security.policy");
337                originalSecurityManager = System.getSecurityManager();
338                System.setProperty("java.security.policy", policyFileName);
339                Policy.getPolicy().refresh();
340                testSecurityManager = new SecurityManager();
341                System.setProperty("java.security.policy", originalPolicyFileName==null ? "" : originalPolicyFileName);
342            }
343        }
344
345        public String getProperty(String key) {
346            String val = null;
347            if (key != null && key.length() > 0) {
348                val = props.getProperty(key);
349            }
350            return val;
351        }
352
353        public boolean getBooleanProperty(String key) {
354            return getBooleanProperty(key, false);
355        }
356
357        public boolean getBooleanProperty(String key, boolean defVal) {
358            String s = getProperty(key);
359            if (s == null) {
360                return defVal;
361            }
362            if (s.equalsIgnoreCase("yes") || s.equalsIgnoreCase("true") || s.equals("1")) {
363                return true;
364            }
365            return false;
366        }
367
368        public int getIntProperty(String key, int defVal) {
369            return getIntProperty(key, defVal, -1);
370        }
371
372        public int getIntProperty(String key, int defVal, int maxVal) {
373            String s = getProperty(key);
374            if (s == null) {
375                return defVal;
376            }
377            return (maxVal == -1) ? Integer.valueOf(s) : Math.max(Integer.valueOf(s), maxVal);
378        }
379
380        public long getLongProperty(String key, long defVal) {
381            String s = getProperty(key);
382            if (s == null) {
383                return defVal;
384            }
385            return Long.valueOf(s);
386        }
387
388        public int getInclusion() {
389            return inclusion;
390        }
391
392        public long getSeed() {
393            return seed;
394        }
395
396        public int getLoggingLevel() {
397            return loggingLevel;
398        }
399    }
400
401    /**
402     * Check the given array to see that all the strings in the expected array
403     * are present.
404     *
405     * @param msg
406     *            string message, for log output
407     * @param array
408     *            array of strings to check
409     * @param expected
410     *            array of strings we expect to see, or null
411     * @return the length of 'array', or -1 on error
412     */
413    protected static int checkArray(String msg, String array[], String expected[]) {
414        int explen = (expected != null) ? expected.length : 0;
415        if (!(explen >= 0 && explen < 31)) { // [sic] 31 not 32
416            errln("Internal error");
417            return -1;
418        }
419        int i = 0;
420        StringBuffer buf = new StringBuffer();
421        int seenMask = 0;
422        for (; i < array.length; ++i) {
423            String s = array[i];
424            if (i != 0)
425                buf.append(", ");
426            buf.append(s);
427            // check expected list
428            for (int j = 0, bit = 1; j < explen; ++j, bit <<= 1) {
429                if ((seenMask & bit) == 0) {
430                    if (s.equals(expected[j])) {
431                        seenMask |= bit;
432                        logln("Ok: \"" + s + "\" seen");
433                    }
434                }
435            }
436        }
437        logln(msg + " = [" + buf + "] (" + i + ")");
438        // did we see all expected strings?
439        if (((1 << explen) - 1) != seenMask) {
440            for (int j = 0, bit = 1; j < expected.length; ++j, bit <<= 1) {
441                if ((seenMask & bit) == 0) {
442                    errln("\"" + expected[j] + "\" not seen");
443                }
444            }
445        }
446        return array.length;
447    }
448
449    /**
450     * Check the given array to see that all the locales in the expected array
451     * are present.
452     *
453     * @param msg
454     *            string message, for log output
455     * @param array
456     *            array of locales to check
457     * @param expected
458     *            array of locales names we expect to see, or null
459     * @return the length of 'array'
460     */
461    protected static int checkArray(String msg, Locale array[], String expected[]) {
462        String strs[] = new String[array.length];
463        for (int i = 0; i < array.length; ++i) {
464            strs[i] = array[i].toString();
465        }
466        return checkArray(msg, strs, expected);
467    }
468
469    /**
470     * Check the given array to see that all the locales in the expected array
471     * are present.
472     *
473     * @param msg
474     *            string message, for log output
475     * @param array
476     *            array of locales to check
477     * @param expected
478     *            array of locales names we expect to see, or null
479     * @return the length of 'array'
480     */
481    protected static int checkArray(String msg, ULocale array[], String expected[]) {
482        String strs[] = new String[array.length];
483        for (int i = 0; i < array.length; ++i) {
484            strs[i] = array[i].toString();
485        }
486        return checkArray(msg, strs, expected);
487    }
488
489    // JUnit-like assertions.
490
491    protected static boolean assertTrue(String message, boolean condition) {
492        return handleAssert(condition, message, "true", null);
493    }
494
495    protected static boolean assertFalse(String message, boolean condition) {
496        return handleAssert(!condition, message, "false", null);
497    }
498
499    protected static boolean assertEquals(String message, boolean expected,
500            boolean actual) {
501        return handleAssert(expected == actual, message, String
502                .valueOf(expected), String.valueOf(actual));
503    }
504
505    protected static boolean assertEquals(String message, long expected, long actual) {
506        return handleAssert(expected == actual, message, String
507                .valueOf(expected), String.valueOf(actual));
508    }
509
510    // do NaN and range calculations to precision of float, don't rely on
511    // promotion to double
512    protected static boolean assertEquals(String message, float expected,
513            float actual, double error) {
514        boolean result = Float.isInfinite(expected)
515                ? expected == actual
516                : !(Math.abs(expected - actual) > error); // handles NaN
517        return handleAssert(result, message, String.valueOf(expected)
518                + (error == 0 ? "" : " (within " + error + ")"), String
519                .valueOf(actual));
520    }
521
522    protected static boolean assertEquals(String message, double expected,
523            double actual, double error) {
524        boolean result = Double.isInfinite(expected)
525                ? expected == actual
526                : !(Math.abs(expected - actual) > error); // handles NaN
527        return handleAssert(result, message, String.valueOf(expected)
528                + (error == 0 ? "" : " (within " + error + ")"), String
529                .valueOf(actual));
530    }
531
532    protected static <T> boolean assertEquals(String message, T[] expected, T[] actual) {
533        // Use toString on a List to get useful, readable messages
534        String expectedString = expected == null ? "null" : Arrays.asList(expected).toString();
535        String actualString = actual == null ? "null" : Arrays.asList(actual).toString();
536        return assertEquals(message, expectedString, actualString);
537    }
538
539    protected static boolean assertEquals(String message, Object expected,
540            Object actual) {
541        boolean result = expected == null ? actual == null : expected
542                .equals(actual);
543        return handleAssert(result, message, stringFor(expected),
544                stringFor(actual));
545    }
546
547    protected static boolean assertNotEquals(String message, Object expected,
548            Object actual) {
549        boolean result = !(expected == null ? actual == null : expected
550                .equals(actual));
551        return handleAssert(result, message, stringFor(expected),
552                stringFor(actual), "not equal to", true);
553    }
554
555    protected boolean assertSame(String message, Object expected, Object actual) {
556        return handleAssert(expected == actual, message, stringFor(expected),
557                stringFor(actual), "==", false);
558    }
559
560    protected static boolean assertNotSame(String message, Object expected,
561            Object actual) {
562        return handleAssert(expected != actual, message, stringFor(expected),
563                stringFor(actual), "!=", true);
564    }
565
566    protected static boolean assertNull(String message, Object actual) {
567        return handleAssert(actual == null, message, null, stringFor(actual));
568    }
569
570    protected static boolean assertNotNull(String message, Object actual) {
571        return handleAssert(actual != null, message, null, stringFor(actual),
572                "!=", true);
573    }
574
575    protected static void fail() {
576        fail("");
577    }
578
579    protected static void fail(String message) {
580        if (message == null) {
581            message = "";
582        }
583        if (!message.equals("")) {
584            message = ": " + message;
585        }
586        errln(sourceLocation() + message);
587    }
588
589    private static boolean handleAssert(boolean result, String message,
590            String expected, String actual) {
591        return handleAssert(result, message, expected, actual, null, false);
592    }
593
594    public static boolean handleAssert(boolean result, String message,
595            Object expected, Object actual, String relation, boolean flip) {
596        if (!result || isVerbose()) {
597            if (message == null) {
598                message = "";
599            }
600            if (!message.equals("")) {
601                message = ": " + message;
602            }
603            relation = relation == null ? ", got " : " " + relation + " ";
604            if (result) {
605                logln("OK " + message + ": "
606                        + (flip ? expected + relation + actual : expected));
607            } else {
608                // assert must assume errors are true errors and not just warnings
609                // so cannot warnln here
610                errln(  message
611                        + ": expected"
612                        + (flip ? relation + expected : " " + expected
613                                + (actual != null ? relation + actual : "")));
614            }
615        }
616        return result;
617    }
618
619    private static final String stringFor(Object obj) {
620        if (obj == null) {
621            return "null";
622        }
623        if (obj instanceof String) {
624            return "\"" + obj + '"';
625        }
626        return obj.getClass().getName() + "<" + obj + ">";
627    }
628
629    // Return the source code location of the caller located callDepth frames up the stack.
630    protected static String sourceLocation() {
631        // Walk up the stack to the first call site outside this file
632        for (StackTraceElement st : new Throwable().getStackTrace()) {
633            String source = st.getFileName();
634            if (source != null && !source.equals("TestFmwk.java") && !source.equals("AbstractTestLog.java")) {
635                String methodName = st.getMethodName();
636                if (methodName != null &&
637                       (methodName.startsWith("Test") || methodName.startsWith("test") || methodName.equals("main"))) {
638                    return "(" + source + ":" + st.getLineNumber() + ") ";
639                }
640            }
641        }
642        throw new InternalError();
643    }
644
645    protected static boolean checkDefaultPrivateConstructor(String fullyQualifiedClassName) throws Exception {
646        return checkDefaultPrivateConstructor(Class.forName(fullyQualifiedClassName));
647    }
648
649    protected static boolean checkDefaultPrivateConstructor(Class<?> classToBeTested) throws Exception {
650        Constructor<?> constructor = classToBeTested.getDeclaredConstructor();
651
652        // Check that the constructor is private.
653        boolean isPrivate = Modifier.isPrivate(constructor.getModifiers());
654
655        // Call the constructor for coverage.
656        constructor.setAccessible(true);
657        constructor.newInstance();
658
659        if (!isPrivate) {
660            errln("Default private constructor for class: " + classToBeTested.getName() + " is not private.");
661        }
662        return isPrivate;
663    }
664
665    /**
666     * Tests the toString method on a private or hard-to-reach class.  Assumes constructor of the class does not
667     * take any arguments.
668     * @param fullyQualifiedClassName
669     * @return The output of the toString method.
670     * @throws Exception
671     */
672    protected static String invokeToString(String fullyQualifiedClassName) throws Exception {
673        return invokeToString(fullyQualifiedClassName, new Class<?>[]{}, new Object[]{});
674    }
675
676    /**
677     * Tests the toString method on a private or hard-to-reach class.  Assumes constructor of the class does not
678     * take any arguments.
679     * @param classToBeTested
680     * @return The output of the toString method.
681     * @throws Exception
682     */
683    protected static String invokeToString(Class<?> classToBeTested) throws Exception {
684        return invokeToString(classToBeTested, new Class<?>[]{}, new Object[]{});
685    }
686
687    /**
688     * Tests the toString method on a private or hard-to-reach class.  Allows you to specify the argument types for
689     * the constructor.
690     * @param fullyQualifiedClassName
691     * @return The output of the toString method.
692     * @throws Exception
693     */
694    protected static String invokeToString(String fullyQualifiedClassName,
695            Class<?>[] constructorParamTypes, Object[] constructorParams) throws Exception {
696        return invokeToString(Class.forName(fullyQualifiedClassName), constructorParamTypes, constructorParams);
697    }
698
699    /**
700     * Tests the toString method on a private or hard-to-reach class.  Allows you to specify the argument types for
701     * the constructor.
702     * @param classToBeTested
703     * @return The output of the toString method.
704     * @throws Exception
705     */
706    protected static String invokeToString(Class<?> classToBeTested,
707            Class<?>[] constructorParamTypes, Object[] constructorParams) throws Exception {
708        Constructor<?> constructor = classToBeTested.getDeclaredConstructor(constructorParamTypes);
709        constructor.setAccessible(true);
710        Object obj = constructor.newInstance(constructorParams);
711        Method toStringMethod = classToBeTested.getDeclaredMethod("toString");
712        toStringMethod.setAccessible(true);
713        return (String) toStringMethod.invoke(obj);
714    }
715
716
717    // End JUnit-like assertions
718
719    // TODO (sgill): added to keep errors away
720    /* (non-Javadoc)
721     * @see com.ibm.icu.dev.test.TestLog#msg(java.lang.String, int, boolean, boolean)
722     */
723    //@Override
724    protected static void msg(String message, int level, boolean incCount, boolean newln) {
725        if (level == TestLog.WARN || level == TestLog.ERR) {
726            Assert.fail(message);
727        }
728        // TODO(stuartg): turned off - causing OOM running under ant
729//        while (level > 0) {
730//            System.out.print(" ");
731//            level--;
732//        }
733//        System.out.print(message);
734//        if (newln) {
735//            System.out.println();
736//        }
737    }
738
739}
740