/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.commons.math4.util; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.commons.math4.exception.MathArithmeticException; import org.apache.commons.math4.util.FastMath; import org.apache.commons.numbers.core.Precision; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; /** * Test to compare FastMath results against StrictMath results for boundary values. * <p> * Running all tests independently: <br> * {@code mvn test -Dtest=FastMathStrictComparisonTest}<br> * or just run tests against a single method (e.g. scalb):<br> * {@code mvn test -Dtest=FastMathStrictComparisonTest -DargLine="-DtestMethod=scalb"} */ @SuppressWarnings("boxing") @RunWith(Parameterized.class) public class FastMathStrictComparisonTest { // Values which often need special handling private static final Double[] DOUBLE_SPECIAL_VALUES = { -0.0, +0.0, // 1,2 Double.NaN, // 3 Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, // 4,5 -Double.MAX_VALUE, Double.MAX_VALUE, // 6,7 // decreasing order of absolute value to help catch first failure -Precision.EPSILON, Precision.EPSILON, // 8,9 -Precision.SAFE_MIN, Precision.SAFE_MIN, // 10,11 -Double.MIN_VALUE, Double.MIN_VALUE, // 12,13 }; private static final Float [] FLOAT_SPECIAL_VALUES = { -0.0f, +0.0f, // 1,2 Float.NaN, // 3 Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, // 4,5 Float.MIN_VALUE, Float.MAX_VALUE, // 6,7 -Float.MIN_VALUE, -Float.MAX_VALUE, // 8,9 }; private static final Object [] LONG_SPECIAL_VALUES = { -1,0,1, // 1,2,3 Long.MIN_VALUE, Long.MAX_VALUE, // 4,5 }; private static final Object[] INT_SPECIAL_VALUES = { -1,0,1, // 1,2,3 Integer.MIN_VALUE, Integer.MAX_VALUE, // 4,5 }; private final Method mathMethod; private final Method fastMethod; private final Type[] types; private final Object[][] valueArrays; public FastMathStrictComparisonTest(Method m, Method f, Type[] types, Object[][] data) throws Exception{ this.mathMethod=m; this.fastMethod=f; this.types=types; this.valueArrays=data; } @Test public void test1() throws Exception{ setupMethodCall(mathMethod, fastMethod, types, valueArrays); } private static boolean isNumber(Double d) { return !(d.isInfinite() || d.isNaN()); } private static boolean isNumber(Float f) { return !(f.isInfinite() || f.isNaN()); } private static void reportFailedResults(Method mathMethod, Object[] params, Object expected, Object actual, int[] entries){ final String methodName = mathMethod.getName(); String format = null; long actL=0; long expL=0; if (expected instanceof Double) { Double exp = (Double) expected; Double act = (Double) actual; if (isNumber(exp) && isNumber(act) && exp != 0) { // show difference as hex actL = Double.doubleToLongBits(act); expL = Double.doubleToLongBits(exp); if (Math.abs(actL-expL)==1) { // Not 100% sure off-by-one errors are allowed everywhere, so only allow for these methods if (methodName.equals("toRadians") || methodName.equals("atan2")) { return; } } format = "%016x"; } } else if (expected instanceof Float ){ Float exp = (Float) expected; Float act = (Float) actual; if (isNumber(exp) && isNumber(act) && exp != 0) { // show difference as hex actL = Float.floatToIntBits(act); expL = Float.floatToIntBits(exp); format = "%08x"; } } StringBuilder sb = new StringBuilder(); sb.append(mathMethod.getReturnType().getSimpleName()); sb.append(" "); sb.append(methodName); sb.append("("); String sep = ""; for(Object o : params){ sb.append(sep); sb.append(o); sep=", "; } sb.append(") expected "); if (format != null){ sb.append(String.format(format, expL)); } else { sb.append(expected); } sb.append(" actual "); if (format != null){ sb.append(String.format(format, actL)); } else { sb.append(actual); } sb.append(" entries "); sb.append(Arrays.toString(entries)); String message = sb.toString(); final boolean fatal = true; if (fatal) { Assert.fail(message); } else { System.out.println(message); } } private static void callMethods(Method mathMethod, Method fastMethod, Object[] params, int[] entries) throws IllegalAccessException { try { Object expected; try { expected = mathMethod.invoke(mathMethod, params); } catch (InvocationTargetException ite) { expected = ite.getCause(); } Object actual; try { actual = fastMethod.invoke(mathMethod, params); } catch (InvocationTargetException ite) { actual = ite.getCause(); } if (expected instanceof ArithmeticException) { Assert.assertEquals(MathArithmeticException.class, actual.getClass()); } else if (!expected.equals(actual)) { reportFailedResults(mathMethod, params, expected, actual, entries); } } catch (IllegalArgumentException e) { Assert.fail(mathMethod+" "+e); } } private static void setupMethodCall(Method mathMethod, Method fastMethod, Type[] types, Object[][] valueArrays) throws Exception { Object[] params = new Object[types.length]; int entry1 = 0; int[] entries = new int[types.length]; for(Object d : valueArrays[0]) { entry1++; params[0] = d; entries[0] = entry1; if (params.length > 1){ int entry2 = 0; for(Object d1 : valueArrays[1]) { entry2++; params[1] = d1; entries[1] = entry2; callMethods(mathMethod, fastMethod, params, entries); } } else { callMethods(mathMethod, fastMethod, params, entries); } } } @Parameters public static List<Object[]> data() throws Exception { String singleMethod = System.getProperty("testMethod"); List<Object[]> list = new ArrayList<>(); for(Method mathMethod : StrictMath.class.getDeclaredMethods()) { method: if (Modifier.isPublic(mathMethod.getModifiers())){// Only test public methods Type []types = mathMethod.getGenericParameterTypes(); if (types.length >=1) { // Only check methods with at least one parameter try { // Get the corresponding FastMath method Method fastMethod = FastMath.class.getDeclaredMethod(mathMethod.getName(), (Class[]) types); if (Modifier.isPublic(fastMethod.getModifiers())) { // It must be public too if (singleMethod != null && !fastMethod.getName().equals(singleMethod)) { break method; } Object [][] values = new Object[types.length][]; int index = 0; for(Type t : types) { if (t.equals(double.class)){ values[index]=DOUBLE_SPECIAL_VALUES; } else if (t.equals(float.class)) { values[index]=FLOAT_SPECIAL_VALUES; } else if (t.equals(long.class)) { values[index]=LONG_SPECIAL_VALUES; } else if (t.equals(int.class)) { values[index]=INT_SPECIAL_VALUES; } else { System.out.println("Cannot handle class "+t+" for "+mathMethod); break method; } index++; } // System.out.println(fastMethod); /* * The current implementation runs each method as a separate test. * Could be amended to run each value as a separate test */ list.add(new Object[]{mathMethod, fastMethod, types, values}); // setupMethodCall(mathMethod, fastMethod, params, data); } else { System.out.println("Cannot find public FastMath method corresponding to: "+mathMethod); } } catch (NoSuchMethodException e) { System.out.println("Cannot find FastMath method corresponding to: "+mathMethod); } } } } return list; } }