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 package runtime.valhalla.valuetypes;
  26 
  27 import javax.tools.JavaFileObject;
  28 
  29 import jdk.test.lib.combo.ComboInstance;
  30 import jdk.test.lib.combo.ComboParameter;
  31 import jdk.test.lib.combo.ComboTask.Result;
  32 import jdk.test.lib.combo.ComboTestHelper;
  33 import jdk.test.lib.combo.ComboTestHelper.ArrayDimensionKind;
  34 import jdk.experimental.value.ValueType;
  35 
  36 import java.io.File;
  37 import java.io.FileInputStream;
  38 import java.lang.invoke.MethodHandle;
  39 import java.lang.invoke.MethodHandles;
  40 import java.lang.invoke.MethodType;
  41 import static java.lang.invoke.MethodType.methodType;
  42 import java.lang.reflect.Field;
  43 import java.lang.reflect.Modifier;
  44 import java.net.URL;
  45 import java.net.URLClassLoader;
  46 import java.util.stream.Stream;
  47 
  48 /**
  49  * Test combinations of value type field layouts.
  50  *
  51  * Testing all permutations of all 8 primitive types and a reference type is
  52  * prohibitive both in terms of resource usage on testing infrastructure, and
  53  * development time. Sanity or "check-in" level testing should be in the order
  54  * of seconds in terms of wall-clock execution time.
  55  *
  56  * ### Combinations vs Permutations
  57  *
  58  * For a given number of fields, or "set of cardinality 'K'" ("arity" in code
  59  * here), of a set of "N" types ("BasicType"), the number of test cases can be
  60  * expressed as:
  61  *
  62  * Combinations: "(N + K - 1) ! / K ! (N - 1)!", for K=4, N=9: test cases =  496
  63  * Permutations: "N ^ K",                        for K=4, N=9: test cases = 6561
  64  *
  65  * Given the knowledge that the VM always reorders field declarations to suit
  66  * the given hardware, order of fields doesn't actually matter. I.e. for
  67  * N={int, long}, useful test cases are:
  68  *
  69  *  Test-0: {int , int}
  70  *  Test-1: {int , long}
  71  *  Test-2: {long, long}
  72  *
  73  * Where as {long, int} is unnecessary given "Test-1".
  74  *
  75  * # TLDR; Combinations give considerable savings.
  76  *
  77  * ### Maintain the ability to repoduce single test case
  78  *
  79  * Given the large number of test cases, ensure this class is always capable of
  80  * reporting the specific test case when something goes wrong, and allow running
  81  * of that single test case to enable efficent debugging.
  82  *
  83  * Note: upon crash the generated test class should be present in $CWD/Test.class
  84  */
  85 public class MVTCombo extends ComboInstance<MVTCombo> {
  86 
  87     // Set of fields types to test
  88     enum BasicType implements ComboParameter {
  89         BOOLEAN(Boolean.TYPE),
  90         BYTE(Byte.TYPE),
  91         CHAR(Character.TYPE),
  92         SHORT(Short.TYPE),
  93         FLOAT(Float.TYPE),
  94         DOUBLE(Double.TYPE),
  95         INT(Integer.TYPE),
  96         LONG(Long.TYPE),
  97         STRING(String.class);
  98 
  99         // Reduced set of 'N' types for large number of fields ('K')
 100         public static final BasicType[] REDUCED_SET = new BasicType[] {
 101             INT,    // Single slot
 102             DOUBLE, // Double slot FP
 103             STRING  // Reference
 104         };
 105 
 106         Class<?> clazz;
 107 
 108         BasicType(Class<?> clazz) {
 109             this.clazz = clazz;
 110         }
 111 
 112         @Override
 113         public String expand(String optParameter) {
 114             if (optParameter == null) {
 115                 return clazz.getName();
 116             } else if (optParameter.startsWith("FROM_INT_")) {
 117                 String varName = optParameter.substring(9);
 118                 switch (this) {
 119                 case BOOLEAN:
 120                     return varName + " == 0 ? false : true";
 121                 case STRING:
 122                     return "String.valueOf(" + varName + ")";
 123                 default:
 124                     return "(" + clazz.getName() + ")" + varName;
 125                 }
 126             } else if (optParameter.startsWith("EQUALS_")) {
 127                 String varName = "f_" + optParameter.substring(7);
 128                 switch (this) {
 129                 case STRING:
 130                     return "this." + varName + ".equals(that." + varName + ")";
 131                 default:
 132                     return "this." + varName + " == that." + varName;
 133                 }
 134             } else throw new IllegalStateException("optParameter=" + optParameter);
 135         }
 136     }
 137 
 138     // Nof fields to test
 139     static class Arity implements ComboParameter {
 140 
 141         int arity;
 142         int maxArity;
 143 
 144         Arity(int arity, int maxArity) {
 145             this.arity = arity;
 146             this.maxArity = maxArity;
 147         }
 148 
 149         @Override
 150         public String expand(String optParameter) {
 151             for (Snippet s : Snippet.values()) {
 152                 if (s.name().equals(optParameter)) {
 153                     return s.expand(arity);
 154                 }
 155             }
 156             throw new IllegalStateException("Cannot get here!");
 157         }
 158 
 159         public String toString() { return "Arity " + arity + "/" + maxArity; }
 160 
 161         // Produce 1..K arity
 162         public static Arity[] values(int maxArity) {
 163             Arity[] vals = new Arity[maxArity];
 164             for (int i = 0; i < maxArity; i++) {
 165                 vals[i] = new Arity(i + 1, maxArity);
 166             }
 167             return vals;
 168         }
 169     }
 170 
 171     enum Snippet {
 172         FIELD_DECL("public final #{TYPE[#IDX]} f_#IDX;", "\n    "),
 173         FIELD_ASSIGN("this.f_#IDX = f_#IDX;", "\n        "),
 174         FIELD_EQUALS("if (!(#{TYPE[#IDX].EQUALS_#IDX})) return false;", "\n        "),
 175         CONSTR_FORMALS("#{TYPE[#IDX]} f_#IDX", ","),
 176         CONSTR_ACTUALS("f_#IDX", ","),
 177         CONSTR_ACTUALS_INDEXED("#{TYPE[#IDX].FROM_INT_INDEX}", ",");
 178 
 179         String snippetStr;
 180         String sep;
 181 
 182         Snippet(String snippetStr, String sep) {
 183             this.snippetStr = snippetStr;
 184             this.sep = sep;
 185         }
 186 
 187         String expand(int arity) {
 188             StringBuilder buf = new StringBuilder();
 189             String tempSep = "";
 190             for (int i = 0 ; i < arity ; i++) {
 191                 buf.append(tempSep);
 192                 buf.append(snippetStr.replaceAll("#IDX", String.valueOf(i)));
 193                 tempSep = sep;
 194             }
 195             return buf.toString();
 196         }
 197     }
 198 
 199     public static final String VCC_TEMPLATE =
 200         "@jvm.internal.value.ValueCapableClass\n" +
 201         "public final class Test {\n\n" +
 202         "    // Declare fields...\n" +
 203         "    #{ARITY.FIELD_DECL}\n" +
 204         "    // Private Constructor...\n" +
 205         "    private Test(#{ARITY.CONSTR_FORMALS}) {\n" +
 206         "        #{ARITY.FIELD_ASSIGN}\n" +
 207         "    }\n" +
 208         "    public boolean equals(Object o) {\n" +
 209         "        Test that = (Test) o;\n" +
 210         "        #{ARITY.FIELD_EQUALS}\n" +
 211         "        return true;\n" +
 212         "    }\n" +
 213         "    // Public factory method\n" +
 214         "    public static Test create(#{ARITY.CONSTR_FORMALS}) {\n" +
 215         "        return new Test(#{ARITY.CONSTR_ACTUALS});\n" +
 216         "    }\n" +
 217         "    // Public indexed test case factory method\n" +
 218         "    public static Test createIndexed(int INDEX) {\n" +
 219         "        return new Test(#{ARITY.CONSTR_ACTUALS_INDEXED});\n" +
 220         "    }\n" +
 221         "}\n";
 222 
 223     public static void runTests(boolean reduceTypes, int nofFields, int specificTestCase) throws Exception {
 224         ComboTestHelper<MVTCombo> test = new ComboTestHelper<MVTCombo>()
 225             .withDimension("ARITY", (x, expr) -> x.setArity(expr), Arity.values(nofFields))
 226             .withArrayDimension("TYPE",
 227                                 (x, t, idx) -> x.basicTypes[idx] = t,
 228                                 nofFields,
 229                                 ArrayDimensionKind.COMBINATIONS,
 230                                 reduceTypes ? BasicType.REDUCED_SET : BasicType.values());
 231         if (specificTestCase == -1) {
 232             test.withFilter(MVTCombo::redundantFilter);
 233         } else {
 234             test.withFilter((x)->test.info().getComboCount() == specificTestCase);
 235         }
 236         test.run(MVTCombo::new);
 237     }
 238 
 239     public static final String ARG_REDUCE_TYPES = "-reducetypes";
 240 
 241     // main "-reducetypes <nofFields> <specific-test-number>"
 242     public static void main(String... args) throws Exception {
 243         // Default args
 244         boolean reduceTypes  = false;
 245         int nofFields        = 4;
 246         int specificTestCase = -1;
 247 
 248         // Parse
 249         int argIndex = 0;
 250         if (args.length > argIndex && (args[argIndex].equals(ARG_REDUCE_TYPES)))  {
 251             reduceTypes = true;
 252             argIndex++;
 253         }
 254         if (args.length > argIndex) {
 255             nofFields = Integer.parseInt(args[argIndex]);
 256             argIndex++;
 257         }
 258         if (args.length > argIndex) {
 259             specificTestCase = Integer.parseInt(args[argIndex]);
 260             argIndex++;
 261         }
 262 
 263         runTests(reduceTypes, nofFields, specificTestCase);
 264     }
 265 
 266     Arity arity;
 267     BasicType[] basicTypes;
 268 
 269     public String toString() {
 270         String s = "MVTCombo " + arity + " types: ";
 271         for (int i = 0 ; i < basicTypes.length; i++) {
 272             s += " " + basicTypes[i];
 273         }
 274         return s;
 275     }
 276 
 277     void setArity(Arity arity) {
 278         this.arity = arity;
 279         // Even if we are testing 1..K fields, combo needs K fields
 280         this.basicTypes = new BasicType[arity.maxArity];
 281     }
 282 
 283     /*
 284        The way the 'combo' package works, it produces combinations or permutations
 285        for each dimension, so for arity we don't care for basicTypes[arity...maxArity]
 286      */
 287     boolean redundantFilter() {
 288         BasicType lastArityType = basicTypes[arity.arity - 1];
 289         for (int i = arity.arity ; i < arity.maxArity ; i++) {
 290             if (basicTypes[i] != lastArityType) {
 291                 return false;
 292             }
 293         }
 294         return true;
 295     }
 296 
 297     @Override
 298     public void doWork() throws Throwable {
 299         Result<Iterable<? extends JavaFileObject>> result = newCompilationTask()
 300                 .withSourceFromTemplate(VCC_TEMPLATE)
 301                 .generate();
 302         //System.out.println("COMP: " + result.compilationInfo()); // Print the generated source
 303         if (result.hasErrors()) {
 304             fail("ERROR " + result.compilationInfo());
 305         }
 306         JavaFileObject jfo = result.get().iterator().next(); // Blindly assume one
 307         String url = jfo.toUri().toURL().toString();
 308         url = url.substring(0, url.length() - jfo.getName().length());
 309         Class<?> clazz = new URLClassLoader(new URL[] { new URL(url) }).loadClass("Test");
 310         try {
 311             doTestMvtClasses(clazz);
 312             doTestSingleInstance(clazz);
 313             doTestArray(clazz);
 314         } catch (Throwable ex) {
 315             throw new AssertionError("ERROR: " + result.compilationInfo(), ex);
 316         }
 317     }
 318 
 319     protected void doTestMvtClasses(Class<?> testSubject) throws Throwable {
 320         if (!ValueType.classHasValueType(testSubject)) {
 321             throw new IllegalArgumentException("Not a VCC: " + testSubject);
 322         }
 323         ValueType<?> vt = ValueType.forClass(testSubject);
 324         Class<?> boxClass    = vt.boxClass();
 325         Class<?> sourceClass = vt.sourceClass();
 326         Class<?> vtClass     = vt.valueClass();
 327         Class<?> arrayClass  = vt.arrayValueClass();
 328         Class<?> mArrayClass = vt.arrayValueClass(4);
 329         if (boxClass != testSubject) {
 330             throw new RuntimeException("Box class != VCC");
 331         }
 332         if (sourceClass != testSubject) {
 333             throw new RuntimeException("Source class != VCC");
 334         }
 335         if (vt.toString() == null) {
 336             throw new RuntimeException("No toString() return");
 337         }
 338     }
 339 
 340     protected void doTestSingleInstance(Class<?> testSubject) throws Throwable {
 341         ValueType<?> vt = ValueType.forClass(testSubject);
 342         Object obj = MethodHandles.filterReturnValue(vt.defaultValueConstant(), vt.box()).invoke();
 343         obj = MethodHandles.filterReturnValue(vt.unbox(), vt.box()).invoke(obj);
 344         int hashCode = (int) MethodHandles.filterReturnValue(vt.defaultValueConstant(), vt.substitutabilityHashCode()).invoke();
 345 
 346         //test(default(), default())
 347         MethodHandle test0 = MethodHandles.collectArguments(vt.substitutabilityTest(), 0, vt.defaultValueConstant());
 348         boolean isEqual = (boolean) MethodHandles.collectArguments(test0, 0, vt.defaultValueConstant()).invoke();
 349         if (!isEqual) {
 350             throw new RuntimeException("test(default(), default()) failed");
 351         }
 352     }
 353 
 354     protected void doTestArray(Class<?> testSubject) throws Throwable {
 355         ValueType<?> vt = ValueType.forClass(testSubject);
 356         MethodHandle arrayGetter = vt.arrayGetter();
 357         MethodHandle arraySetter = vt.arraySetter();
 358         MethodHandle unbox = vt.unbox();
 359         MethodHandle box = vt.box();
 360         int testArrayLen = 7;
 361         Object array = vt.newArray().invoke(testArrayLen);
 362         for (int i = 0; i < testArrayLen; i++) {
 363             MethodHandle equalsDefault0 = MethodHandles.collectArguments(vt.substitutabilityTest(), 0, vt.defaultValueConstant());
 364             boolean isEqual = (boolean) MethodHandles.collectArguments(equalsDefault0, 0, arrayGetter).invoke(array, i);
 365             if (!isEqual) {
 366                 System.out.println("PROBLEM:");
 367                 printFieldValues(MethodHandles.filterReturnValue(vt.defaultValueConstant(), box));
 368                 System.out.println("VERSUS value from array:");
 369                 printFieldValues(MethodHandles.filterReturnValue(arrayGetter, box).invoke(array, i));
 370                 throw new IllegalStateException("Failed equality test for class: " + vt.sourceClass().getName()  + " at index: " + i);
 371             }
 372         }
 373 
 374         // populate the last element with some values...
 375         int testIndex = testArrayLen - 1;
 376         /*
 377            Do the following in MHs...
 378 
 379           Object testObj = Test.createIndexed(testIndex);
 380           array[testIndex] = unbox(testObj);
 381           if (!testObj.equals(array[testIndex])) throw...
 382         */
 383         MethodHandle createIndexed = MethodHandles.privateLookupIn(testSubject, mhLookup)
 384             .findStatic(testSubject, "createIndexed", methodType(testSubject, Integer.TYPE));
 385         Object testObj = createIndexed.invoke(testIndex);
 386         arraySetter.invoke(array, testIndex, testObj);
 387         Object testElem = MethodHandles.filterReturnValue(arrayGetter, box).invoke(array, testIndex);
 388         if (!testObj.equals(testElem)) {
 389             System.out.println("PROBLEM:");
 390             printFieldValues(testObj);
 391             System.out.println("VERSUS:");
 392             printFieldValues(testElem);
 393             throw new RuntimeException("Inequality after value array store and load");
 394         }
 395     }
 396 
 397     // Some general helper methods...
 398     public static void printFieldValues(Object obj) throws IllegalAccessException {
 399         Class<?> clazz = obj.getClass();
 400         System.out.println("Object: " +  obj + " class: " + clazz.getName());
 401         Field[] fields = reflectPublicFinalInstanceFields(clazz);
 402         for (Field f : fields) {
 403             System.out.printf("\t%s %s = %s\n", f.getType().getName(), f.getName(), f.get(obj));
 404         }
 405     }
 406 
 407     public static Field[] reflectPublicFinalInstanceFields(Class<?> clazz) {
 408         return reflectInstanceFields(clazz, Modifier.PUBLIC | Modifier.FINAL);
 409     }
 410 
 411     public static Field[] reflectInstanceFields(Class<?> clazz, int mask) {
 412         return Stream.of(clazz.getDeclaredFields())
 413             .filter(f -> (f.getModifiers() & (mask)) == mask)
 414             .toArray(Field[]::new);
 415     }
 416 
 417     static final MethodHandles.Lookup mhLookup = MethodHandles.lookup();
 418 }