View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.math4.legacy.core.jdkmath;
18  
19  import java.lang.reflect.InvocationTargetException;
20  import java.lang.reflect.Method;
21  import java.lang.reflect.Modifier;
22  import java.lang.reflect.Type;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.List;
26  
27  import org.apache.commons.numbers.core.Precision;
28  import org.junit.Assert;
29  import org.junit.Test;
30  import org.junit.runner.RunWith;
31  import org.junit.runners.Parameterized;
32  import org.junit.runners.Parameterized.Parameters;
33  
34  // Unit test should be moved to module "commons-math-core".
35  // [Currently, it can't be because it depends on "legacy" classes.]
36  import org.apache.commons.math4.core.jdkmath.AccurateMath;
37  
38  /**
39   * Test to compare AccurateMath results against StrictMath results for boundary values.
40   * <p>
41   * Running all tests independently: <br>
42   * {@code mvn test -Dtest=AccurateMathStrictComparisonTest}<br>
43   * or just run tests against a single method (e.g. scalb):<br>
44   * {@code mvn test -Dtest=AccurateMathStrictComparisonTest -DargLine="-DtestMethod=scalb"}
45   */
46  @SuppressWarnings("boxing")
47  @RunWith(Parameterized.class)
48  public class AccurateMathStrictComparisonTest {
49  
50      // Values which often need special handling
51      private static final Double[] DOUBLE_SPECIAL_VALUES = {
52          -0.0, +0.0,                                         // 1,2
53          Double.NaN,                                         // 3
54          Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, // 4,5
55          -Double.MAX_VALUE, Double.MAX_VALUE,                // 6,7
56          // decreasing order of absolute value to help catch first failure
57          -Precision.EPSILON, Precision.EPSILON,              // 8,9
58          -Precision.SAFE_MIN, Precision.SAFE_MIN,            // 10,11
59          -Double.MIN_VALUE, Double.MIN_VALUE,                // 12,13
60      };
61  
62      private static final Float[] FLOAT_SPECIAL_VALUES = {
63          -0.0f, +0.0f,                                       // 1,2
64          Float.NaN,                                          // 3
65          Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY,   // 4,5
66          Float.MIN_VALUE, Float.MAX_VALUE,                   // 6,7
67          -Float.MIN_VALUE, -Float.MAX_VALUE,                 // 8,9
68      };
69  
70      private static final Object[] LONG_SPECIAL_VALUES = {
71          -1, 0, 1,                                           // 1,2,3
72          Long.MIN_VALUE, Long.MAX_VALUE,                     // 4,5
73      };
74  
75      private static final Object[] INT_SPECIAL_VALUES = {
76          -1, 0, 1,                                           // 1,2,3
77          Integer.MIN_VALUE, Integer.MAX_VALUE,               // 4,5
78      };
79  
80      private final Method mathMethod;
81      private final Method fastMethod;
82      private final Type[] types;
83      private final Object[][] valueArrays;
84  
85      public AccurateMathStrictComparisonTest(Method m, Method f, Type[] types, Object[][] data) throws Exception {
86          this.mathMethod = m;
87          this.fastMethod = f;
88          this.types = types;
89          this.valueArrays = data;
90      }
91  
92      @Test
93      public void test1() throws Exception {
94          setupMethodCall(mathMethod, fastMethod, types, valueArrays);
95      }
96  
97      private static boolean isNumber(Double d) {
98          return !(d.isInfinite() || d.isNaN());
99      }
100 
101     private static boolean isNumber(Float f) {
102         return !(f.isInfinite() || f.isNaN());
103     }
104 
105     private static void reportFailedResults(Method mathMethod, Object[] params, Object expected, Object actual, int[] entries) {
106         final String methodName = mathMethod.getName();
107         String format = null;
108         long actL = 0;
109         long expL = 0;
110         if (expected instanceof Double) {
111             Double exp = (Double) expected;
112             Double act = (Double) actual;
113             if (isNumber(exp) && isNumber(act) && exp != 0) { // show difference as hex
114                 actL = Double.doubleToLongBits(act);
115                 expL = Double.doubleToLongBits(exp);
116                 if (Math.abs(actL - expL) == 1) {
117                     // Not 100% sure off-by-one errors are allowed everywhere, so only allow for these methods
118                     if (methodName.equals("toRadians") || methodName.equals("atan2")) {
119                         return;
120                     }
121                 }
122                 format = "%016x";
123             }
124         } else if (expected instanceof Float) {
125             Float exp = (Float) expected;
126             Float act = (Float) actual;
127             if (isNumber(exp) && isNumber(act) && exp != 0) { // show difference as hex
128                 actL = Float.floatToIntBits(act);
129                 expL = Float.floatToIntBits(exp);
130                 format = "%08x";
131             }
132         }
133         StringBuilder sb = new StringBuilder();
134         sb.append(mathMethod.getReturnType().getSimpleName());
135         sb.append(" ");
136         sb.append(methodName);
137         sb.append("(");
138         String sep = "";
139         for (Object o : params) {
140             sb.append(sep);
141             sb.append(o);
142             sep = ", ";
143         }
144         sb.append(") expected ");
145         if (format != null) {
146             sb.append(String.format(format, expL));
147         } else {
148             sb.append(expected);
149         }
150         sb.append(" actual ");
151         if (format != null) {
152             sb.append(String.format(format, actL));
153         } else {
154             sb.append(actual);
155         }
156         sb.append(" entries ");
157         sb.append(Arrays.toString(entries));
158         String message = sb.toString();
159         final boolean fatal = true;
160         if (fatal) {
161             Assert.fail(message);
162         } else {
163             // CHECKSTYLE: stop Regexp
164             System.out.println(message);
165             // CHECKSTYLE: resume Regexp
166         }
167     }
168 
169     private static void callMethods(Method mathMethod, Method fastMethod,
170             Object[] params, int[] entries) throws IllegalAccessException {
171         try {
172             Object expected;
173             try {
174                 expected = mathMethod.invoke(mathMethod, params);
175             } catch (InvocationTargetException ite) {
176                 expected = ite.getCause();
177             }
178             Object actual;
179             try {
180                 actual = fastMethod.invoke(mathMethod, params);
181             } catch (InvocationTargetException ite) {
182                 actual = ite.getCause();
183             }
184             if (expected instanceof ArithmeticException) {
185                 Assert.assertTrue(actual instanceof ArithmeticException);
186             } else if (!expected.equals(actual)) {
187                 reportFailedResults(mathMethod, params, expected, actual, entries);
188             }
189         } catch (IllegalArgumentException e) {
190             Assert.fail(mathMethod + " " + e);
191         }
192     }
193 
194     private static void setupMethodCall(Method mathMethod, Method fastMethod,
195             Type[] types, Object[][] valueArrays) throws Exception {
196         Object[] params = new Object[types.length];
197         int entry1 = 0;
198         int[] entries = new int[types.length];
199         for (Object d : valueArrays[0]) {
200             entry1++;
201             params[0] = d;
202             entries[0] = entry1;
203             if (params.length > 1) {
204                 int entry2 = 0;
205                 for (Object d1 : valueArrays[1]) {
206                     entry2++;
207                     params[1] = d1;
208                     entries[1] = entry2;
209                     callMethods(mathMethod, fastMethod, params, entries);
210                 }
211             } else {
212                 callMethods(mathMethod, fastMethod, params, entries);
213             }
214         }
215     }
216 
217     @Parameters
218     public static List<Object[]> data() throws Exception {
219         // CHECKSTYLE: stop Regexp
220         String singleMethod = System.getProperty("testMethod");
221         List<Object[]> list = new ArrayList<>();
222         for (Method mathMethod : StrictMath.class.getDeclaredMethods()) {
223             method:
224             if (Modifier.isPublic(mathMethod.getModifiers())) { // Only test public methods
225                 Type[] types = mathMethod.getGenericParameterTypes();
226                 if (types.length >= 1) { // Only check methods with at least one parameter
227                     try {
228                         // Get the corresponding AccurateMath method
229                         Method fastMethod = AccurateMath.class.getDeclaredMethod(mathMethod.getName(), (Class[]) types);
230                         if (Modifier.isPublic(fastMethod.getModifiers())) { // It must be public too
231                             if (singleMethod != null && !fastMethod.getName().equals(singleMethod)) {
232                                 break method;
233                             }
234                             Object[][] values = new Object[types.length][];
235                             int index = 0;
236                             for (Type t : types) {
237                                 if (t.equals(double.class)) {
238                                     values[index] = DOUBLE_SPECIAL_VALUES;
239                                 } else if (t.equals(float.class)) {
240                                     values[index] = FLOAT_SPECIAL_VALUES;
241                                 } else if (t.equals(long.class)) {
242                                     values[index] = LONG_SPECIAL_VALUES;
243                                 } else if (t.equals(int.class)) {
244                                     values[index] = INT_SPECIAL_VALUES;
245                                 } else {
246                                     System.out.println("Cannot handle class " + t + " for " + mathMethod);
247                                     break method;
248                                 }
249                                 index++;
250                             }
251 //                          System.out.println(fastMethod);
252                             /*
253                              * The current implementation runs each method as a separate test.
254                              * Could be amended to run each value as a separate test
255                              */
256                             list.add(new Object[]{mathMethod, fastMethod, types, values});
257 //                            setupMethodCall(mathMethod, fastMethod, params, data);
258                         } else {
259                             System.out
260                                 .println("Cannot find public AccurateMath method corresponding to: " + mathMethod);
261                         }
262                     } catch (NoSuchMethodException e) {
263                         System.out.println("Cannot find AccurateMath method corresponding to: " + mathMethod);
264                     }
265                 }
266             }
267         }
268         return list;
269         // CHECKSTYLE: resume Regexp
270     }
271 }