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