1 /*
   2  * Copyright (c) 2017, 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.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 /**
  27  * CheckRecordMembers
  28  *
  29  * @test
  30  * @summary check that the accessors, equals, hashCode and toString methods
  31  *          work as expected
  32  * @library /tools/javac/lib
  33  * @modules jdk.compiler/com.sun.tools.javac.file
  34  *          jdk.compiler/com.sun.tools.javac.api
  35  *          jdk.compiler/com.sun.tools.javac.util
  36  * @build combo.ComboTestHelper
  37 
  38  * @run main CheckRecordMembers
  39  */
  40 
  41 import java.lang.reflect.AccessibleObject;
  42 import java.lang.reflect.Constructor;
  43 import java.lang.reflect.Field;
  44 import java.lang.reflect.InvocationTargetException;
  45 import java.lang.reflect.Method;
  46 import java.net.URL;
  47 import java.net.URLClassLoader;
  48 import java.util.Arrays;
  49 import java.util.List;
  50 import java.util.Map;
  51 import java.util.Objects;
  52 import javax.tools.JavaFileObject;
  53 
  54 import com.sun.tools.javac.file.PathFileObject;
  55 import combo.ComboTask;
  56 
  57 public class CheckRecordMembers extends combo.ComboInstance<CheckRecordMembers> {
  58 
  59     enum FieldTypeKind implements combo.ComboParameter {
  60         BYTE("byte", byte.class),
  61         SHORT("short", short.class),
  62         CHAR("char", char.class),
  63         INT("int", int.class),
  64         LONG("long", long.class),
  65         FLOAT("float", float.class),
  66         DOUBLE("double", double.class),
  67         BOOLEAN("boolean", boolean.class),
  68         OBJECT("Object", Object.class),
  69         STRING("String", String.class);
  70 
  71         String retTypeStr;
  72         Class<?> clazz;
  73 
  74         FieldTypeKind(String retTypeStr, Class<?> clazz) {
  75             this.retTypeStr = retTypeStr;
  76             this.clazz = clazz;
  77         }
  78 
  79         public String expand(String optParameter) {
  80             return retTypeStr;
  81         }
  82     }
  83 
  84     static final Map<FieldTypeKind, List<Object>> dataValues
  85             = Map.ofEntries(
  86             Map.entry(FieldTypeKind.BYTE, List.of(Byte.MIN_VALUE, (byte) -4, (byte) -1, (byte) 0, (byte) 1, (byte) 4, Byte.MAX_VALUE)),
  87             Map.entry(FieldTypeKind.SHORT, List.of(Short.MIN_VALUE, (short) -4, (short) -1, (short) 0, (short) 1, (short) 4, Short.MAX_VALUE)),
  88             Map.entry(FieldTypeKind.CHAR, List.of(Character.MIN_VALUE, 'a', 'A', 'z', (char) 0, Character.MAX_VALUE)),
  89             Map.entry(FieldTypeKind.INT, List.of(Integer.MIN_VALUE, (int) -4, (int) -1, (int) 0, (int) 1, (int) 4, Integer.MAX_VALUE)),
  90             Map.entry(FieldTypeKind.LONG, List.of(Long.MIN_VALUE, (long) -4, (long) -1, (long) 0, (long) 1, (long) 4, Long.MAX_VALUE)),
  91             Map.entry(FieldTypeKind.FLOAT, List.of(Float.MIN_VALUE, Float.NaN, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, 0.0f, -0.0f, 1.0f, -1.0f, 2.0f, -2.0f, Float.MAX_VALUE)),
  92             Map.entry(FieldTypeKind.DOUBLE, List.of(Double.MIN_VALUE, Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, 0.0d, -0.0d, 1.0d, -1.0d, 2.0d, -2.0d, Double.MAX_VALUE)),
  93             Map.entry(FieldTypeKind.BOOLEAN, List.of(true, false)),
  94             Map.entry(FieldTypeKind.OBJECT, Arrays.asList(null, 3, "foo", new String[] {"a"})),
  95             Map.entry(FieldTypeKind.STRING, Arrays.asList(null, "", "foo", "bar"))
  96     );
  97 
  98     static final String sourceTemplate =
  99             "record Data(#{FT[0]} f0, #{FT[1]} f1) { }";
 100 
 101     public static void main(String... args) throws Exception {
 102         new combo.ComboTestHelper<CheckRecordMembers>()
 103                 .withArrayDimension("FT", (x, t, index) -> {
 104                     x.fieldType[index] = t;
 105                 }, 2, FieldTypeKind.values())
 106                 .run(CheckRecordMembers::new);
 107     }
 108 
 109     FieldTypeKind[] fieldType = new FieldTypeKind[2];
 110 
 111     @Override
 112     public void doWork() throws Throwable {
 113         newCompilationTask()
 114                 .withSourceFromTemplate(sourceTemplate)
 115                 .generate(this::check);
 116     }
 117 
 118     void check(ComboTask.Result<Iterable<? extends JavaFileObject>> result) {
 119         List<Object> f0s = dataValues.get(fieldType[0]);
 120         List<Object> f1s = dataValues.get(fieldType[1]);
 121 
 122         if (result.hasErrors() || result.hasWarnings())
 123             fail("Compilation errors not expected: " + result.compilationInfo());
 124 
 125         Iterable<? extends PathFileObject> pfoIt = (Iterable<? extends PathFileObject>) result.get();
 126         PathFileObject pfo = pfoIt.iterator().next();
 127         Class<?> clazz;
 128         Constructor<?> ctor;
 129         Method getterF0, getterF1, hashCodeMethod, equalsMethod, toStringMethod;
 130         Field fieldF0, fieldF1;
 131 
 132         try {
 133             URL[] urls = new URL[] {pfo.getPath().getParent().toUri().toURL()};
 134             ClassLoader cl = new URLClassLoader(urls);
 135             clazz = cl.loadClass("Data");
 136 
 137             ctor = clazz.getConstructor(fieldType[0].clazz, fieldType[1].clazz);
 138             getterF0 = clazz.getMethod("f0");
 139             getterF1 = clazz.getMethod("f1");
 140             fieldF0 = clazz.getDeclaredField("f0");
 141             fieldF1 = clazz.getDeclaredField("f1");
 142             equalsMethod = clazz.getMethod("equals", Object.class);
 143             hashCodeMethod = clazz.getMethod("hashCode");
 144             toStringMethod = clazz.getMethod("toString");
 145 
 146             if (getterF0.getReturnType() != fieldType[0].clazz
 147                 || getterF1.getReturnType() != fieldType[1].clazz
 148                 || fieldF0.getType() != fieldType[0].clazz
 149                 || fieldF1.getType() != fieldType[1].clazz)
 150                 fail("Unexpected field or getter type: " + result.compilationInfo());
 151 
 152             for (AccessibleObject o : List.of(ctor, getterF0, getterF1, equalsMethod, hashCodeMethod, toStringMethod)) {
 153                 // @@@ Why do we need this?
 154                 o.setAccessible(true);
 155             }
 156 
 157             for (Object f0 : f0s) {
 158                 for (Object f1 : f1s) {
 159                     // Create object
 160                     Object datum = ctor.newInstance(f0, f1);
 161 
 162                     // Test getters
 163                     Object actualF0 = getterF0.invoke(datum);
 164                     Object actualF1 = getterF1.invoke(datum);
 165                     if (!Objects.equals(f0, actualF0) || !Objects.equals(f1, actualF1))
 166                         fail(String.format("Getters don't report back right values for %s %s/%s, %s %s/%s",
 167                                            fieldType[0].clazz, f0, actualF0,
 168                                            fieldType[1].clazz, f1, actualF1));
 169 
 170                     int hashCode = (int) hashCodeMethod.invoke(datum);
 171                     int expectedHash = Objects.hash(f0, f1);
 172                     // @@@ fail
 173                     if (hashCode != expectedHash) {
 174                         System.err.println(String.format("Hashcode not as expected: expected=%d, actual=%d",
 175                                            expectedHash, hashCode));
 176                     }
 177 
 178                     String toString = (String) toStringMethod.invoke(datum);
 179                     String expectedToString = String.format("Data[f0=%s, f1=%s]", f0, f1);
 180                     if (!toString.equals(expectedToString)) {
 181                         fail(String.format("ToString not as expected: expected=%s, actual=%s",
 182                                            expectedToString, toString));
 183                     }
 184 
 185                     // Test equals
 186                     for (Object f2 : f0s) {
 187                         for (Object f3 : f1s) {
 188                             Object other = ctor.newInstance(f2, f3);
 189                             boolean isEqual = (boolean) equalsMethod.invoke(datum, other);
 190                             boolean isEqualReverse = (boolean) equalsMethod.invoke(other, datum);
 191                             boolean f0f2Equal = Objects.equals(f0, f2);
 192                             boolean f1f3Equal = Objects.equals(f1, f3);
 193                             if (fieldType[0] == FieldTypeKind.FLOAT) {
 194                                 f0f2Equal = Float.compare((float)f0, (float)f2) == 0;
 195                             } else if (fieldType[0] == FieldTypeKind.DOUBLE) {
 196                                 f0f2Equal = Double.compare((double)f0, (double)f2) == 0;
 197                             }
 198                             if (fieldType[1] == FieldTypeKind.FLOAT) {
 199                                 f1f3Equal = Float.compare((float)f1, (float)f3) == 0;
 200                             } else if (fieldType[1] == FieldTypeKind.DOUBLE) {
 201                                 f1f3Equal = Double.compare((double)f1, (double)f3) == 0;
 202                             }
 203                             boolean shouldEqual = f0f2Equal && f1f3Equal;
 204                             // @@@ fail
 205                             if (shouldEqual != isEqual)
 206                                 System.err.println(String.format("Equals not as expected: %s %s/%s, %s %s/%s",
 207                                                    fieldType[0].clazz, f0, f2,
 208                                                    fieldType[1].clazz, f1, f3));
 209                             if (isEqualReverse != isEqual)
 210                                 fail(String.format("Equals not symmetric: %s %s/%s, %s %s/%s",
 211                                                    fieldType[0].clazz, f0, f2,
 212                                                    fieldType[1].clazz, f1, f3));
 213 
 214                         }
 215                     }
 216                 }
 217             }
 218         } catch (Throwable e) {
 219             throw new AssertionError(e);
 220         }
 221     }
 222 }