1 /*
  2  * Copyright (c) 1998, 2023, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 import java.io.IOException;
 25 import java.io.PrintWriter;
 26 import java.lang.reflect.InvocationTargetException;
 27 import java.lang.reflect.Method;
 28 import java.lang.reflect.Modifier;
 29 import java.util.ArrayList;
 30 import java.util.Comparator;
 31 import java.util.Map;
 32 import java.util.LinkedHashMap;
 33 import java.util.List;
 34 
 35 /**
 36  * IntlTest is a base class for tests that can be run conveniently from
 37  * the command line as well as under the Java test harness.
 38  * <p>
 39  * Sub-classes implement a set of public void methods named "Test*" or
 40  * "test*" with no arguments. Each of these methods performs some
 41  * test. Test methods should indicate errors by calling either err() or
 42  * errln().  This will increment the errorCount field and may optionally
 43  * print a message to the log.  Debugging information may also be added to
 44  * the log via the log and logln methods.  These methods will add their
 45  * arguments to the log only if the test is being run in verbose mode.
 46  */
 47 public abstract class IntlTest {
 48 
 49     //------------------------------------------------------------------------
 50     // Everything below here is boilerplate code that makes it possible
 51     // to add a new test by simply adding a method to an existing class.
 52     //------------------------------------------------------------------------
 53     protected IntlTest() {
 54         Class<? extends IntlTest> testClass = getClass();
 55         testName = testClass.getCanonicalName();
 56         // Populate testMethods with all the test methods.
 57         Method[] methods = testClass.getDeclaredMethods();
 58         for (Method method : methods) {
 59             if (Modifier.isPublic(method.getModifiers())
 60                 && method.getReturnType() == void.class
 61                 && method.getParameterCount() == 0) {
 62                 String name = method.getName();
 63                 if (name.length() > 4) {
 64                     if (name.startsWith("Test") || name.startsWith("test")) {
 65                         testMethods.put(name, method);
 66                     }
 67                 }
 68             }
 69         }
 70     }
 71 
 72     protected void run(String[] args) throws Exception {
 73         // Set up the log and reference streams.  We use PrintWriters in order to
 74         // take advantage of character conversion.  The JavaEsc converter will
 75         // convert Unicode outside the ASCII range to Java's \\uxxxx notation.
 76         log = new PrintWriter(System.out, true);
 77         List<Method> testsToRun = configureTestsAndArgs(args);
 78         System.out.println(testName + " {");
 79         indentLevel++;
 80 
 81         // Run the list of tests given in the test arguments
 82         for (Method testMethod : testsToRun) {
 83             int oldCount = errorCount;
 84             String testName = testMethod.getName();
 85             writeTestName(testName);
 86             try {
 87                 testMethod.invoke(this);
 88             } catch (IllegalAccessException e) {
 89                 errln("Can't access test method " + testName);
 90             } catch (InvocationTargetException e) {
 91                 // Log exception first, that way if -nothrow is
 92                 // not an arg, the original exception is still logged
 93                 logExc(e);
 94                 errln(String.format("$$$ Uncaught exception thrown in %s," +
 95                         " see above for cause", testName));
 96             }
 97             writeTestResult(errorCount - oldCount);
 98         }
 99         indentLevel--;
100         if (prompt) {
101             System.out.println("Hit RETURN to exit...");
102             try {
103                 System.in.read();
104             } catch (IOException e) {
105                 System.out.println("Exception: " + e.toString() + e.getMessage());
106             }
107         }
108         if (exitCode) {
109             System.exit(errorCount);
110         }
111         if (errorCount > 0) {
112             throw new RuntimeException(String.format(
113                     "$$$ %s FAILED with %s failures%n", testName, errorCount));
114         } else {
115             log.println(String.format("\t$$$ %s PASSED%n", testName));
116         }
117     }
118 
119     private List<Method> configureTestsAndArgs(String[] args) {
120         // Parse the test arguments. They can be either the flag
121         // "-verbose" or names of test methods. Create a list of
122         // tests to be run.
123         List<Method> testsToRun = new ArrayList<>(args.length);
124         for (String arg : args) {
125             switch (arg) {
126                 case "-verbose" -> verbose = true;
127                 case "-prompt" -> prompt = true;
128                 case "-nothrow" -> nothrow = true;
129                 case "-exitcode" -> exitCode = true;
130                 default -> {
131                     Method m = testMethods.get(arg);
132                     if (m == null) {
133                         System.out.println("Method " + arg + ": not found");
134                         usage();
135                         return testsToRun;
136                     }
137                     testsToRun.add(m);
138                 }
139             }
140         }
141         // If no test method names were given explicitly, run them all.
142         if (testsToRun.isEmpty()) {
143             testsToRun.addAll(testMethods.values());
144         }
145         // Arbitrarily sort the tests, so that they are run in the same order every time
146         testsToRun.sort(Comparator.comparing(Method::getName));
147         return testsToRun;
148     }
149 
150     /**
151      * Adds the given message to the log if we are in verbose mode.
152      */
153     protected void log(String message) {
154         logImpl(message, false);
155     }
156 
157     protected void logln(String message) {
158         logImpl(message, true);
159     }
160 
161     protected void logln() {
162         logImpl(null, true);
163     }
164 
165     private void logImpl(String message, boolean newline) {
166         if (verbose) {
167             if (message != null) {
168                 indent(indentLevel + 1);
169                 log.print(message);
170             }
171             if (newline) {
172                 log.println();
173             }
174         }
175     }
176 
177     private void logExc(InvocationTargetException ite) {
178         indent(indentLevel);
179         ite.getTargetException().printStackTrace(this.log);
180     }
181 
182     protected void err(String message) {
183         errImpl(message, false);
184     }
185 
186     protected void errln(String message) {
187         errImpl(message, true);
188     }
189 
190     private void errImpl(String message, boolean newline) {
191         errorCount++;
192         indent(indentLevel + 1);
193         log.print(message);
194         if (newline) {
195             log.println();
196         }
197         log.flush();
198 
199         if (!nothrow) {
200             throw new RuntimeException(message);
201         }
202     }
203 
204     protected int getErrorCount() {
205         return errorCount;
206     }
207 
208     protected void writeTestName(String testName) {
209         indent(indentLevel);
210         log.print(testName);
211         log.flush();
212         needLineFeed = true;
213     }
214 
215     protected void writeTestResult(int count) {
216         if (!needLineFeed) {
217             indent(indentLevel);
218             log.print("}");
219         }
220         needLineFeed = false;
221 
222         if (count != 0) {
223             log.println(" FAILED");
224         } else {
225             log.println(" Passed");
226         }
227     }
228 
229     private void indent(int distance) {
230         if (needLineFeed) {
231             log.println(" {");
232             needLineFeed = false;
233         }
234         log.print(SPACES.substring(0, distance * 2));
235     }
236 
237     /**
238      * Print a usage message for this test class.
239      */
240     void usage() {
241         System.out.println(getClass().getName() +
242                             ": [-verbose] [-nothrow] [-exitcode] [-prompt] [test names]");
243 
244         System.out.println("  Available test names:");
245         for (String methodName : testMethods.keySet()) {
246             System.out.println("\t" + methodName);
247         }
248     }
249     private final String testName;
250     private boolean     prompt;
251     private boolean     nothrow;
252     protected boolean   verbose;
253     private boolean     exitCode;
254     private PrintWriter log;
255     private int         indentLevel;
256     private boolean     needLineFeed;
257     private int         errorCount;
258 
259     private final Map<String, Method> testMethods = new LinkedHashMap<>();
260 
261     private static final String SPACES = "                                          ";
262 }