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 }