1 /*
   2  * Copyright (c) 2024, 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.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import combo.ComboInstance;
  25 import combo.ComboParameter;
  26 import combo.ComboTask;
  27 import combo.ComboTestHelper;
  28 
  29 import java.io.ByteArrayInputStream;
  30 import java.io.ByteArrayOutputStream;
  31 import java.io.EOFException;
  32 import java.io.IOException;
  33 import java.io.InputStream;
  34 import java.io.InvalidClassException;
  35 import java.io.NotSerializableException;
  36 import java.io.ObjectInputStream;
  37 import java.io.ObjectOutputStream;
  38 import java.io.ObjectStreamClass;
  39 import java.io.OptionalDataException;
  40 import java.io.Reader;
  41 import java.io.UncheckedIOException;
  42 import java.net.URL;
  43 import java.net.URLClassLoader;
  44 import java.nio.file.Files;
  45 import java.nio.file.Path;
  46 import java.util.Arrays;
  47 import java.util.HashMap;
  48 import java.util.HashSet;
  49 import java.util.List;
  50 import java.util.Locale;
  51 import java.util.Map;
  52 import java.util.Optional;
  53 import java.util.Set;
  54 import java.util.concurrent.ConcurrentHashMap;
  55 import java.util.function.BiFunction;
  56 import java.util.function.Predicate;
  57 import java.util.stream.Collectors;
  58 import java.util.stream.Stream;
  59 
  60 import javax.tools.Diagnostic;
  61 import javax.tools.JavaFileObject;
  62 
  63 import static jdk.test.lib.Asserts.assertEquals;
  64 import static jdk.test.lib.Asserts.assertTrue;
  65 import static jdk.test.lib.Asserts.assertFalse;
  66 
  67 import jdk.test.lib.hexdump.HexPrinter;
  68 import jdk.test.lib.hexdump.ObjectStreamPrinter;
  69 
  70 /*
  71  * @test
  72  * @summary Deserialization Combo tests
  73  * @library /test/langtools/tools/javac/lib /test/lib .
  74  * @modules jdk.compiler/com.sun.tools.javac.api
  75  *          jdk.compiler/com.sun.tools.javac.code
  76  *          jdk.compiler/com.sun.tools.javac.comp
  77  *          jdk.compiler/com.sun.tools.javac.file
  78  *          jdk.compiler/com.sun.tools.javac.main
  79  *          jdk.compiler/com.sun.tools.javac.tree
  80  *          jdk.compiler/com.sun.tools.javac.util
  81  * @build combo.ComboTestHelper SerializedObjectCombo
  82  * @run main/othervm --enable-preview  SerializedObjectCombo
  83  */
  84 
  85 
  86 public final class SerializedObjectCombo extends ComboInstance<SerializedObjectCombo> {
  87     private static final Map<Path, URLClassLoader> LOADER_FOR_PATH = new ConcurrentHashMap<>();
  88     private static final ParamSet KIND_SET = new ParamSet("KIND",
  89             SerializationKind.values());
  90     private static final ParamSet FIELD_SET = new ParamSet("FIELD",
  91             2, ArgumentValue.BASIC_VALUES);
  92     private static final ParamSet CLASSACCESS_SET = new ParamSet("CLASSACCESS",
  93             new ClassAccessKind[]{ClassAccessKind.PUBLIC});
  94     private static final ParamSet SPECIAL_WRITE_METHODS_SET = new ParamSet("SPECIAL_WRITE_METHODS",
  95             WriteObjectFragments.values());
  96     private static final ParamSet SPECIAL_READ_METHODS_SET = new ParamSet("SPECIAL_READ_METHODS",
  97             ReadObjectFragments.values());
  98     private static final ParamSet EXTERNALIZABLE_METHODS_SET = new ParamSet("EXTERNALIZABLE_METHODS",
  99             ExternalizableMethodFragments.values());
 100     private static final ParamSet OBJECT_CONSTRUCTOR_SET = new ParamSet("OBJECT_CONSTRUCTOR",
 101             ObjectConstructorFragment.ANNOTATED_OBJECT_CONSTRUCTOR_FRAGMENT, ObjectConstructorFragment.NONE);
 102     private static final ParamSet VALUE_SET = new ParamSet("VALUE",
 103             ValueKind.values());
 104     private static final ParamSet TESTNAME_EXTENDS_SET = new ParamSet("TESTNAME_EXTENDS",
 105                     TestNameExtendsFragments.NONE, TestNameExtendsFragments.TESTNAME_EXTENDS_FRAGMENT);
 106     private static final ParamSet TOP_ABSTRACT_SET = new ParamSet("TOP_FRAGMENTS",
 107             TopFragments.values());
 108     /**
 109      * The base template to generate all test classes.
 110      * Each substitutable fragment is defined by an Enum of the alternatives.
 111      * Giving each a name and an array of ComboParameters with the expansion value.
 112      */
 113     private static final String TEST_SOURCE_TEMPLATE = """
 114             import java.io.*;
 115             import java.util.*;
 116             import jdk.internal.value.DeserializeConstructor;
 117             import jdk.internal.MigratedValueClass;
 118 
 119             #{TOP_FRAGMENTS}
 120 
 121             @MigratedValueClass
 122             #{CLASSACCESS} #{VALUE} class #{TESTNAME} #{TESTNAME_EXTENDS} #{KIND.IMPLEMENTS} {
 123                 #{FIELD[0]} f1;
 124                 #{FIELD[1]} f2;
 125                 #{FIELD_ADDITIONS}
 126                 #{CLASSACCESS} #{TESTNAME}() {
 127                     f1 = #{FIELD[0].RANDOM};
 128                     f2 = #{FIELD[1].RANDOM};
 129                     #{FIELD_CONSTRUCTOR_ADDITIONS}
 130                 }
 131             #{OBJECT_CONSTRUCTOR}
 132                 @Override public boolean equals(Object obj) {
 133                     if (obj instanceof #{TESTNAME} other) {
 134                         if (#{FIELD[0]}.class.isPrimitive()) {
 135                             if (f1 != other.f1) return false;
 136                         } else {
 137                             if (!Objects.equals(f1, other.f1)) return false;
 138                         }
 139                         if (#{FIELD[1]}.class.isPrimitive()) {
 140                             if (f2 != other.f2) return false;
 141                         } else {
 142                             if (!Objects.equals(f2, other.f2)) return false;
 143                         }
 144                         return true;
 145                     }
 146                     return false;
 147                 }
 148                 @Override public String toString() {
 149                     return "f1: " + String.valueOf(f1) +
 150                             ", f2: " + String.valueOf(f2)
 151                             #{FIELD_TOSTRING_ADDITIONS};
 152                 }
 153             #{KIND.SPECIAL_METHODS}
 154                 private static final long serialVersionUID = 1L;
 155             }
 156             """;
 157 
 158     // The unique number to qualify interface names, unique across multiple runs
 159     private static int uniqueId = 0;
 160     // Compilation errors prevent execution; set/cleared by checkCompile
 161     private ComboTask.Result<?> compilationResult = null;
 162     // The current set of parameters for the file being compiled and tested
 163     private final Set<ComboParameter> currParams = new HashSet<>();
 164 
 165     private static List<String> focusKeys = null;
 166 
 167     private enum CommandOption {
 168         SHOW_SOURCE("--show-source", "show source files"),
 169         VERBOSE("--verbose", "show extra information"),
 170         SHOW_SERIAL_STREAM("--show-serial", "show and format the serialized stream"),
 171         EVERYTHING("--everything", "run all tests"),
 172         TRACE("--trace", "set TRACE system property of ObjectInputStream (temp)"),
 173         MAX_COMBOS("--max-combo", "maximum number of values for each parameter", CommandOption::parseInt),
 174         NO_PRE_FILTER("--no-pre-filter", "disable pre-filter checks"),
 175         SELFTEST("--self-test", "run some self tests and exit"),
 176         ;
 177         private final String option;
 178         private final String usage;
 179         private final BiFunction<CommandOption, String, Boolean> parseArg;
 180         private Optional<Object> value;
 181         CommandOption(String option, String usage, BiFunction<CommandOption, String, Boolean> parseArg) {
 182             this.option = option;
 183             this.usage = usage;
 184             this.parseArg = parseArg;
 185             this.value = Optional.empty();
 186         }
 187         CommandOption(String option, String usage) {
 188             this(option, usage, null);
 189         }
 190 
 191         /**
 192          * Evaluate and parse an array of command line args
 193          * @param args array of strings
 194          * @return true if parsing succeeded
 195          */
 196         static boolean parseOptions(String[] args) {
 197             boolean unknownArg = false;
 198             for (int i = 0; i < args.length; i++) {
 199                 String arg = args[i];
 200                 Optional<CommandOption> knownOpt = Arrays.stream(CommandOption.values())
 201                         .filter(o -> o.option.equals(arg))
 202                         .findFirst();
 203                 if (knownOpt.isEmpty()) { // Not a recognized option
 204                     if (arg.startsWith("-")) {
 205                         System.err.println("Unrecognized option: " + arg);
 206                         unknownArg = true;
 207                     } else {
 208                         // Take the remaining non-option args as selectors of keys to be run
 209                         String[] keys = Arrays.copyOfRange(args, i, args.length);
 210                         focusKeys = List.of(keys);
 211                     }
 212                 } else {
 213                     CommandOption option = knownOpt.get();
 214                     if (option.parseArg == null) {
 215                         option.setValue(true);
 216                     } else {
 217                         i++;
 218                         if (i >= args.length || args[i].startsWith("--")) {
 219                             System.err.println("Missing argument for " + option.option);
 220                             continue;
 221                         }
 222                         option.parseArg.apply(option, args[i]);
 223                     }
 224                 }
 225             }
 226             return !unknownArg;
 227         }
 228         static void showUsage() {
 229             System.out.println("""
 230                 Usage:
 231                 """);
 232             Arrays.stream(CommandOption.values()).forEach(o -> System.out.printf("  %-15s: %s\n", o.option, o.usage));
 233         }
 234         boolean present() {
 235             return value != null && value.isPresent();
 236         }
 237         void setValue(Object o) {
 238             value = Optional.ofNullable(o);
 239         }
 240         private static boolean parseInt(CommandOption option, String arg) {
 241             try {
 242                 int count = Integer.parseInt(arg);
 243                 option.setValue(count);
 244             } catch (NumberFormatException nfe) {
 245                 System.out.println("--max-combo argument not a number: " + arg);
 246             }
 247             return true;
 248         }
 249         // Get the int value from the option, defaulting if not valid or present
 250         private int getInt(int otherMax) {
 251             Object obj = value == null ? otherMax : value.orElseGet(() -> otherMax);
 252             return (obj instanceof Integer i) ? i : otherMax;
 253         }
 254     }
 255 
 256     private static URLClassLoader getLoaderFor(Path path) {
 257         return LOADER_FOR_PATH.computeIfAbsent(path,
 258                 p -> {
 259                     try {
 260                         // new URLClassLoader for path
 261                         Files.createDirectories(p);
 262                         URL[] urls = {p.toUri().toURL()};
 263                         return new URLClassLoader(p.toString(), urls, null);
 264                     } catch (IOException ioe) {
 265                         throw new UncheckedIOException(ioe);
 266                     }
 267                 });
 268     }
 269 
 270     // Map an array of strings to an array of ComboParameter.Constants.
 271     @SuppressWarnings("unchecked")
 272     private static ComboParameter.Constant<String>[] paramsForStrings(String... strings) {
 273         return Arrays.stream(strings)
 274                 .map(ComboParameter.Constant::new).toArray(ComboParameter.Constant[]::new);
 275     }
 276 
 277     /**
 278      * Main to generate combinations and run the tests.
 279      *
 280      * @param args may contain "--verbose" to show source of every file
 281      * @throws Exception In case of failure
 282      */
 283     public static void main(String... args) throws Exception {
 284         if (!CommandOption.parseOptions(args)) {
 285             CommandOption.showUsage();
 286             System.exit(1);
 287         }
 288 
 289         Arrays.stream(CommandOption.values())
 290                 .filter(o -> o.present())
 291                 .forEach( o1 -> System.out.printf("   %15s: %s\n", o1.option, o1.value
 292                 ));
 293 
 294         if (CommandOption.SELFTEST.present()) {
 295             selftest();
 296             return;
 297         }
 298 
 299         // Sets of all possible ComboParameters (substitutions)
 300         Set<ParamSet> allParams = Set.of(
 301                 VALUE_SET,
 302                 KIND_SET,
 303                 TOP_ABSTRACT_SET,
 304                 OBJECT_CONSTRUCTOR_SET,
 305                 TESTNAME_EXTENDS_SET,
 306                 CLASSACCESS_SET,
 307                 SPECIAL_READ_METHODS_SET,
 308                 SPECIAL_WRITE_METHODS_SET,
 309                 EXTERNALIZABLE_METHODS_SET,
 310                 FIELD_SET
 311         );
 312 
 313         // Test variations of all code shapes
 314         var helper = new ComboTestHelper<SerializedObjectCombo>();
 315         int maxCombos = CommandOption.MAX_COMBOS.getInt(2);
 316 
 317         Set<ParamSet> subSet = CommandOption.EVERYTHING.present() ? allParams
 318                 : computeSubset(allParams, focusKeys, maxCombos);
 319         withDimensions(helper, subSet);
 320         if (CommandOption.VERBOSE.present()) {
 321             System.out.println("Keys; maximum combinations: " + maxCombos);
 322             subSet.stream()
 323                     .sorted((p, q) -> String.CASE_INSENSITIVE_ORDER.compare(p.key(), q.key()))
 324                     .forEach(p -> System.out.println("    " + p.key + ": " + Arrays.toString(p.params)));
 325         }
 326         helper.withFilter(SerializedObjectCombo::filter)
 327                 .withFailMode(ComboTestHelper.FailMode.FAIL_FAST)
 328                 .run(SerializedObjectCombo::new);
 329     }
 330 
 331     private static void withDimensions(ComboTestHelper<SerializedObjectCombo> helper, Set<ParamSet> subSet) {
 332         subSet.forEach(p -> {
 333             if (p.count() == 1)
 334                 helper.withDimension(p.key(), SerializedObjectCombo::saveParameter, p.params());
 335             else
 336                 helper.withArrayDimension(p.key(), SerializedObjectCombo::saveParameter, p.count(), p.params());
 337         });
 338     }
 339 
 340     // Return a subset of ParamSets with the non-focused ParamSet's truncated to a max number of values
 341     private static Set<ParamSet> computeSubset(Set<ParamSet> allParams, List<String> focusKeys, int maxKeys) {
 342         if (focusKeys == null || focusKeys.isEmpty())
 343             return allParams;
 344         Set<ParamSet> r = allParams.stream().map(p ->
 345                         (focusKeys.contains(p.key())) ? p
 346                                 : new ParamSet(p.key, p.count(), Arrays.copyOfRange(p.params(), 0, Math.min(p.params().length, maxKeys))))
 347                 .collect(Collectors.toUnmodifiableSet());
 348         return r;
 349     }
 350 
 351     /**
 352      * Print the source files to System out
 353      *
 354      * @param task the compilation task
 355      */
 356     static void showSources(ComboTask task) {
 357         task.getSources()
 358                 .forEach(fo -> {
 359                     System.out.println("Source: " + fo.getName());
 360                     System.out.println(getSource(fo));
 361                 });
 362     }
 363 
 364     /**
 365      * Return the contents of the source file
 366      *
 367      * @param fo a file object
 368      * @return the contents of the source file
 369      */
 370     static String getSource(JavaFileObject fo) {
 371         try (Reader reader = fo.openReader(true)) {
 372             char[] buf = new char[100000];
 373             var len = reader.read(buf);
 374             return new String(buf, 0, len);
 375         } catch (IOException ioe) {
 376             return "IOException: " + fo.getName() + ", ex: " + ioe.getMessage();
 377         }
 378     }
 379 
 380     /**
 381      * Dump the serial stream.
 382      *
 383      * @param bytes the bytes of the stream
 384      */
 385     private static void showSerialStream(byte[] bytes) {
 386         HexPrinter.simple().dest(System.out).formatter(ObjectStreamPrinter.formatter()).format(bytes);
 387     }
 388 
 389     /**
 390      * Serialize an object into byte array.
 391      */
 392     private static byte[] serialize(Object obj) throws IOException {
 393         ByteArrayOutputStream bs = new ByteArrayOutputStream();
 394         try (ObjectOutputStream out = new ObjectOutputStream(bs)) {
 395             out.writeObject(obj);
 396         }
 397         return bs.toByteArray();
 398     }
 399 
 400     /**
 401      * Deserialize an object from byte array using the requested classloader.
 402      */
 403     private static Object deserialize(byte[] ba, ClassLoader loader) throws IOException, ClassNotFoundException {
 404         try (ObjectInputStream in = new LoaderObjectInputStream(new ByteArrayInputStream(ba), loader)) {
 405             return in.readObject();
 406         }
 407     }
 408 
 409 
 410     @Override
 411     public int id() {
 412         return ++uniqueId;
 413     }
 414 
 415     private void fail(String msg, Throwable thrown) {
 416         super.fail(msg);
 417         thrown.printStackTrace(System.out);
 418     }
 419 
 420     /**
 421      * Save a parameter.
 422      *
 423      * @param param a ComboParameter
 424      */
 425     private void saveParameter(ComboParameter param) {
 426         saveParameter(param, 0);
 427     }
 428 
 429     /**
 430      * Save an indexed parameter.
 431      *
 432      * @param param a ComboParameter
 433      * @param index unused
 434      */
 435     private void saveParameter(ComboParameter param, int index) {
 436         currParams.add(param);
 437     }
 438 
 439     /**
 440      * Filter out needless tests (mostly with more variations of arguments than needed).
 441      * Usually, these are compile time failures, or code shapes that cannot succeed.
 442      *
 443      * @return true to run the test, false if not
 444      */
 445     boolean filter() {
 446         if (!CommandOption.NO_PRE_FILTER.present()) {
 447             for (CodeShape shape : CodeShape.values()) {
 448                 if (shape.test(currParams)) {
 449                     if (CommandOption.VERBOSE.present()) {
 450                         System.out.println("IGNORING: " + shape);
 451                     }
 452                     return false;
 453                 }
 454             }
 455         }
 456         if (CommandOption.VERBOSE.present()) {
 457             System.out.println("TESTING: ");
 458             showParams();
 459         }
 460         return true;
 461     }
 462 
 463     /**
 464      * Generate the source files from the parameters and test a single combination.
 465      * Two versions are compiled into different directories and separate class loaders.
 466      * They differ only with the addition of a field to the generated class.
 467      * Then each class is serialized and deserialized by the other class,
 468      * testing simple evolution in the process.
 469      *
 470      * @throws IOException catch all IOException
 471      */
 472     @Override
 473     public void doWork() throws IOException {
 474         String cp = System.getProperty("test.classes");
 475         String className = "Class_" + this.id();
 476 
 477         // new URLClassLoader for path
 478         final Path firstPath = Path.of(cp, "1st");
 479         URLClassLoader firstLoader = getLoaderFor(firstPath);
 480         final Path secondPath = Path.of(cp, "2nd");
 481         URLClassLoader secondLoader = getLoaderFor(secondPath);
 482 
 483         // Create a map of additional constants that are resolved without the combo overhead.
 484         final Map<String, ComboParameter.Constant<String>> params = new HashMap<>();
 485         params.put("TESTNAME", new ComboParameter.Constant<>(className));
 486         params.put("SPECIAL_METHODS_SERIALIZABLE", new ComboParameter.Constant<>("#{SPECIAL_READ_METHODS} #{SPECIAL_WRITE_METHODS}"));
 487         params.put("SPECIAL_METHODS_EXTERNALIZABLE", new ComboParameter.Constant<>("#{EXTERNALIZABLE_METHODS}"));
 488         params.put("FIELD_ADDITIONS", new ComboParameter.Constant<>(""));
 489         params.put("FIELD_CONSTRUCTOR_ADDITIONS", new ComboParameter.Constant<>(""));
 490         params.put("FIELD_TOSTRING_ADDITIONS", new ComboParameter.Constant<>(""));
 491 
 492         final ComboTask firstTask = generateAndCompile(firstPath, className, params);
 493 
 494         if (firstTask == null) {
 495             return; // Skip execution, errors already reported
 496         }
 497 
 498         if (CommandOption.EVERYTHING.present()) {
 499             params.put("FIELD_ADDITIONS", new ComboParameter.Constant<>("int fExtra;"));
 500             params.put("FIELD_CONSTRUCTOR_ADDITIONS", new ComboParameter.Constant<>("this.fExtra = 99;"));
 501             params.put("FIELD_TOSTRING_ADDITIONS", new ComboParameter.Constant<>("+ \", fExtra: String.valueOf(fExtra)\""));
 502             final ComboTask secondTask = generateAndCompile(secondPath, className, params);
 503             if (secondTask == null) {
 504                 return; // Skip execution, errors already reported
 505             }
 506 
 507             doTestWork(className, firstTask, firstLoader, secondLoader);
 508             doTestWork(className, secondTask, secondLoader, firstLoader);
 509         } else {
 510             doTestWork(className, firstTask, firstLoader, firstLoader);
 511         }
 512     }
 513 
 514     /**
 515      * Test that two versions of the class can be serialized using one version and deserialized
 516      * by the other version.
 517      * The two classes have the same name and have been compiled into different classloaders.
 518      * The original and result objects are compared using .equals if there is only 1 classloader.
 519      * If the classloaders are different the `toString()` output for each object is compared loosely.
 520      * (One must be the prefix of the other)
 521      *
 522      * @param className    the class name
 523      * @param task         the task context (for source and parameters to report failures)
 524      * @param firstLoader  the first classloader
 525      * @param secondLoader the second classloader
 526      */
 527     private void doTestWork(String className, ComboTask task, ClassLoader firstLoader, ClassLoader secondLoader) {
 528         byte[] bytes = null;
 529         try {
 530             Class<?> tc = Class.forName(className, true, firstLoader);
 531             Object testObj = tc.getDeclaredConstructor().newInstance();
 532             bytes = serialize(testObj);
 533             if (CommandOption.VERBOSE.present()) {
 534                 System.out.println("Testing: " + task.getSources());
 535                 if (CommandOption.SHOW_SOURCE.present()) {
 536                     showParams();
 537                     showSources(task);
 538                 }
 539                 if (CommandOption.SHOW_SERIAL_STREAM.present()) {
 540                     showSerialStream(bytes);
 541                 }
 542             }
 543 
 544             if (CodeShape.BAD_SO_CONSTRUCTOR.test(currParams)) {
 545                 // should have thrown ICE due to mismatch between value class and missing constructor
 546                 System.out.println(CodeShape.BAD_SO_CONSTRUCTOR.explain(currParams));
 547                 fail(CodeShape.BAD_SO_CONSTRUCTOR.explain(currParams));
 548             }
 549 
 550             Object actual = deserialize(bytes, secondLoader);
 551             if (testObj.getClass().getClassLoader().equals(actual.getClass().getClassLoader())) {
 552                 assertEquals(testObj, actual, "Round-trip comparison fail using .equals");
 553             } else {
 554                 // The instances are from different classloaders and can't be compared directly
 555                 final String s1 = testObj.toString();
 556                 final String s2 = actual.toString();
 557                 assertTrue(s1.startsWith(s2) || s2.startsWith(s1),
 558                         "Round-trip comparison fail using toString(): s1: " + s1 + ", s2: " + s2);
 559             }
 560         } catch (InvalidClassException ice) {
 561             for (CodeShape shape : CodeShape.values()){
 562                 if (ice.equals(shape.exception)) {
 563                     if (shape.test(currParams)) {
 564                         if (CommandOption.VERBOSE.present()) {
 565                             System.out.println("OK: " + shape.explain(currParams));
 566                         } else {
 567                             // unexpected ICE
 568                             ice.printStackTrace(System.out);
 569                             showParams();
 570                             showSources(task);
 571                             if (bytes != null)
 572                                 showSerialStream(bytes);
 573                             fail(ice.getMessage());
 574                         }
 575                     }
 576                 }
 577             }
 578         } catch (EOFException | OptionalDataException eof) {
 579             // Ignore if conditions of the source invite EOF
 580             if (0 == CodeShape.shapesThrowing(EOFException.class).peek(s -> {
 581                 // Ignore: Serialized Object to reads custom data but none written
 582                 if (CommandOption.VERBOSE.present()) {
 583                     System.out.println("OK: " + s.explain(currParams));
 584                 }
 585             }).count()) {
 586                 eof.printStackTrace(System.out);
 587                 showParams();
 588                 showSources(task);
 589                 showSerialStream(bytes);
 590                 fail(eof.getMessage(), eof);
 591             }
 592         } catch (ClassFormatError cfe) {
 593             System.out.println(cfe.toString());
 594         } catch (NotSerializableException nse) {
 595             if (CodeShape.BAD_EXT_VALUE.test(currParams)) {
 596                 // Expected Value class that is Externalizable w/o writeReplace
 597             } else {
 598                 // unexpected NSE
 599                 nse.printStackTrace(System.out);
 600                 showParams();
 601                 showSources(task);
 602                 fail(nse.getMessage(), nse);
 603             }
 604         } catch (Throwable ex) {
 605             ex.printStackTrace(System.out);
 606             showParams();
 607             showSources(task);
 608             fail(ex.getMessage());
 609         }
 610     }
 611 
 612     // Side effect of error is compilationResult.hasErrors() > 0
 613     private ComboTask generateAndCompile(Path path, String className, Map<String, ComboParameter.Constant<String>> params) {
 614         ComboTask task = newCompilationTask()
 615                 .withSourceFromTemplate(className,
 616                         TEST_SOURCE_TEMPLATE,
 617                         r -> params.computeIfAbsent(r, s -> new ComboParameter.Constant<>("UNKNOWN_" + s)))
 618                 .withOption("-d")
 619                 .withOption(path.toString())
 620                 .withOption("--enable-preview")
 621                 .withOption("--add-modules")
 622                 .withOption("java.base")
 623                 .withOption("--add-exports")
 624                 .withOption("java.base/jdk.internal=ALL-UNNAMED")
 625                 .withOption("--add-exports")
 626                 .withOption("java.base/jdk.internal.value=ALL-UNNAMED")
 627                 .withOption("--source")
 628                 .withOption(Integer.toString(Runtime.version().feature()));
 629         ;
 630         task.generate(this::checkCompile);
 631         if (compilationResult.hasErrors()) {
 632             boolean match = false;
 633             for (CodeShape shape : CodeShape.values()){
 634                 if (CompileException.class.equals(shape.exception)) {
 635                     if (shape.test(currParams)) {
 636                         // shape matches known error
 637                         if (!uniqueParams.contains(shape))  {
 638                             System.out.println("// Unique: " + shape);
 639                             uniqueParams.add(shape);
 640                         }
 641                         match = true;
 642                     }
 643                 }
 644             }
 645             if (match)
 646                 return null;
 647             // Unexpected compilation error
 648             showDiags(compilationResult);
 649             showSources(task);
 650             showParams();
 651             fail("Compilation failure");
 652         }
 653         return task;
 654     }
 655 
 656     private static Set<CodeShape> uniqueParams = new HashSet<>();
 657 
 658     private String paramToString(ComboParameter param) {
 659         String name = param.getClass().getName();
 660         return name.substring(name.indexOf('$') + 1) + "::" +
 661                 param + ": " + truncate(param.expand(null), 60);
 662     }
 663 
 664     private void showParams() {
 665         currParams.stream()
 666                 .sorted((p, q) -> String.CASE_INSENSITIVE_ORDER.compare(paramToString(p), paramToString(q)))
 667                 .forEach(p -> System.out.println("    " + paramToString(p)));
 668     }
 669 
 670     private void showParams(ComboParameter... params) {
 671         for (ComboParameter param : params) {
 672             System.out.println(">>> " + paramToString(param) + ", present: "
 673                     + currParams.contains(param));
 674         }
 675     }
 676 
 677     private static String truncate(String s, int maxLen) {
 678         int nl = s.indexOf("\n");
 679         if (nl >= 0)
 680             maxLen = nl;
 681         if (maxLen < s.length()) {
 682             return s.substring(0, maxLen).concat("...");
 683         } else {
 684             return s;
 685         }
 686     }
 687 
 688     /**
 689      * Report any compilation errors.
 690      *
 691      * @param res the result
 692      */
 693     void checkCompile(ComboTask.Result<?> res) {
 694         compilationResult = res;
 695     }
 696 
 697     void showDiags(ComboTask.Result<?> res) {
 698         res.diagnosticsForKind(Diagnostic.Kind.ERROR).forEach(SerializedObjectCombo::showDiag);
 699         res.diagnosticsForKind(Diagnostic.Kind.WARNING).forEach(SerializedObjectCombo::showDiag);
 700     }
 701 
 702     static void showDiag(Diagnostic<? extends JavaFileObject> diag) {
 703         System.out.println(diag.getKind() + ": " + diag.getMessage(Locale.ROOT));
 704         System.out.println("File: " + diag.getSource() +
 705                 " line: " + diag.getLineNumber() + ", col: " + diag.getColumnNumber());
 706     }
 707 
 708     private static class CodeShapePredicateOp<T> implements Predicate<T> {
 709         private final Predicate<T> first;
 710         private final Predicate<T> other;
 711         private final String op;
 712 
 713         CodeShapePredicateOp(Predicate<T> first, Predicate<T> other, String op) {
 714             if ("OR" != op && "AND" != op && "NOT" != op)
 715                 throw new IllegalArgumentException("unknown op: " + op);
 716             this.first = first;
 717             this.other = other;
 718             this.op = op;
 719         }
 720 
 721         @Override
 722         public boolean test(T comboParameters) {
 723             return switch (op) {
 724                 case "NOT" -> !first.test(comboParameters);
 725                 case "OR" -> first.test(comboParameters) || other.test(comboParameters);
 726                 case "AND" -> first.test(comboParameters) && other.test(comboParameters);
 727                 default -> throw new IllegalArgumentException("unknown op: " + op);
 728             };
 729         }
 730         @Override
 731         public Predicate<T> and(Predicate<? super T> other) {
 732             return new CodeShapePredicateOp(this, other,"AND");
 733         }
 734 
 735 
 736         @Override
 737         public Predicate<T>  negate() {
 738             return new CodeShapePredicateOp(this, null,"NOT");
 739         }
 740 
 741         @Override
 742         public Predicate<T> or(Predicate<? super T> other) {
 743             return new CodeShapePredicateOp(this, other,"OR");
 744         }
 745         public String toString() {
 746             return switch (op) {
 747                 case "NOT" -> op + " " + first;
 748                 case "OR" -> "(" + first + " " + op + " " + other + ")";
 749                 case "AND" -> "(" + first + " " + op + " " + other + ")";
 750                 default -> throw new IllegalArgumentException("unknown op: " + op);
 751             };
 752         }
 753     }
 754 
 755     interface CodeShapePredicate extends Predicate<Set<ComboParameter>> {
 756         @Override
 757         default boolean test(Set<ComboParameter> comboParameters) {
 758             return comboParameters.contains(this);
 759         }
 760 
 761         @Override
 762         default Predicate<Set<ComboParameter>> and(Predicate<? super Set<ComboParameter>> other) {
 763             return new CodeShapePredicateOp(this, other,"AND");
 764         }
 765 
 766 
 767         @Override
 768         default Predicate<Set<ComboParameter>>  negate() {
 769             return new CodeShapePredicateOp(this, null,"NOT");
 770         }
 771 
 772         @Override
 773         default Predicate<Set<ComboParameter>> or(Predicate<? super Set<ComboParameter>> other) {
 774             return new CodeShapePredicateOp(this, other,"OR");
 775         }
 776     }
 777 
 778     /**
 779      * A set of code shapes that are interesting, usually indicating an error
 780      * compile time, or runtime based on the shape of the code and the dependencies between
 781      * the code fragments.
 782      * The descriptive text may be easier to understand than the boolean expression of the fragments.
 783      * They can also be to filter out test cases that would not succeed.
 784      * Or can be used after a successful deserialization to check
 785      * if an exception should have been thrown.
 786      */
 787     private enum CodeShape implements Predicate<Set<ComboParameter>> {
 788         BAD_SO_CONSTRUCTOR("Value class does not have a constructor annotated with DeserializeConstructor",
 789                 InvalidClassException.class,
 790                 ValueKind.VALUE,
 791                 ObjectConstructorFragment.ANNOTATED_OBJECT_CONSTRUCTOR_FRAGMENT.negate()
 792                 ),
 793         BAD_EXT_VALUE("Externalizable can not be a value class",
 794                 CompileException.class,
 795                 SerializationKind.EXTERNALIZABLE,
 796                 ValueKind.VALUE),
 797         BAD_EXT_METHODS("Externalizable methods but not Externalizable",
 798                 CompileException.class,
 799                 ExternalizableMethodFragments.EXTERNALIZABLE_METHODS,
 800                 SerializationKind.EXTERNALIZABLE.negate()),
 801         BAD_EXT_NO_METHODS("Externalizable but no implementation of readExternal or writeExternal",
 802                 CompileException.class,
 803                 SerializationKind.EXTERNALIZABLE,
 804                 ExternalizableMethodFragments.EXTERNALIZABLE_METHODS.negate()),
 805         BAD_VALUE_NON_ABSTRACT_SUPER("Can't inherit from non-abstract super or abstract super with fields",
 806                 CompileException.class,
 807                 ValueKind.VALUE,
 808                 TestNameExtendsFragments.TESTNAME_EXTENDS_FRAGMENT,
 809                 TopFragments.ABSTRACT_NO_FIELDS.negate()),
 810         BAD_MISSING_SUPER("Extends TOP_ without TOP_ superclass",
 811                 CompileException.class,
 812                 TestNameExtendsFragments.TESTNAME_EXTENDS_FRAGMENT,
 813                 TopFragments.NONE),
 814         BAD_READ_CUSTOM_METHODS("Custom read fragment but no custom write fragment",
 815                 EOFException.class,
 816                 ReadObjectFragments.READ_OBJECT_FIELDS_CUSTOM_FRAGMENT
 817                         .or(ReadObjectFragments.READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT),
 818                 WriteObjectFragments.WRITE_OBJECT_FIELDS_CUSTOM_FRAGMENT
 819                         .or(WriteObjectFragments.WRITE_OBJECT_DEFAULT_CUSTOM_FRAGMENT).negate()
 820                 ),
 821         BAD_RW_CUSTOM_METHODS("Custom write fragment but no custom read fragment",
 822                 null,
 823                 WriteObjectFragments.WRITE_OBJECT_FIELDS_CUSTOM_FRAGMENT
 824                         .or(WriteObjectFragments.WRITE_OBJECT_DEFAULT_CUSTOM_FRAGMENT),
 825                 ReadObjectFragments.READ_OBJECT_FIELDS_CUSTOM_FRAGMENT
 826                         .or(ReadObjectFragments.READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT).negate()),
 827         BAD_VALUE_READOBJECT_METHODS("readObjectXXX(OIS) methods incompatible with Value class",
 828                 CompileException.class,
 829                 ReadObjectFragments.READ_OBJECT_FIELDS_FRAGMENT
 830                         .or(ReadObjectFragments.READ_OBJECT_DEFAULT_FRAGMENT)
 831                         .or(ReadObjectFragments.READ_OBJECT_FIELDS_CUSTOM_FRAGMENT)
 832                         .or(ReadObjectFragments.READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT),
 833                 ValueKind.VALUE),
 834         ;
 835 
 836         private final String description;
 837         private final Class<? extends Exception> exception;
 838         private final List<Predicate<Set<ComboParameter>>> predicates;
 839         CodeShape(String desc, Class<? extends Exception> exception, Predicate<Set<ComboParameter>>... predicates) {
 840             this.description = desc;
 841             this.exception = exception;
 842             this.predicates = List.of(predicates);
 843         }
 844 
 845         // Return a stream of CodeShapes throwing the exception
 846         static Stream<CodeShape> shapesThrowing(Class<?> exception) {
 847             return Arrays.stream(values()).filter(s -> exception.equals(s.exception));
 848 
 849         }
 850 
 851         /**
 852          * {@return true if all of the predicates are true in the set of ComboParameters}
 853          * @param comboParameters a set of ComboParameters
 854          */
 855         @Override
 856         public boolean test(Set<ComboParameter> comboParameters) {
 857             for (Predicate<Set<ComboParameter>> p : predicates) {
 858                 if (!p.test(comboParameters))
 859                     return false;
 860             }
 861             return true;
 862         }
 863 
 864         /**
 865          * {@return a string describing the predicate in relation to a set of parameters}
 866          * @param comboParameters a set of active ComboParameters.
 867          */
 868         public String explain(Set<ComboParameter> comboParameters) {
 869             StringBuffer sbTrue = new StringBuffer();
 870             StringBuffer sbFalse = new StringBuffer();
 871             for (Predicate<Set<ComboParameter>> p : predicates) {
 872                 ((p.test(comboParameters)) ? sbTrue : sbFalse)
 873                         .append(p).append(", ");
 874             }
 875             return description + "\n" +"Missing: " + sbFalse + "\nTrue: " + sbTrue;
 876         }
 877         public String toString() {
 878             return super.toString() + "::" + description + ", params: " + predicates;
 879         }
 880     }
 881 
 882     /**
 883      * TopAbstract Fragments
 884      */
 885     enum TopFragments implements ComboParameter, CodeShapePredicate {
 886         NONE(""),
 887         ABSTRACT_NO_FIELDS("""
 888                 @MigratedValueClass
 889                 abstract #{VALUE} class TOP_#{TESTNAME} implements Serializable {
 890                     #{CLASSACCESS} TOP_#{TESTNAME}() {}
 891                 }
 892                 """),
 893         ABSTRACT_ONE_FIELD("""
 894                 @MigratedValueClass
 895                 abstract #{VALUE} class TOP_#{TESTNAME} implements Serializable {
 896                     private int t1;
 897                     #{CLASSACCESS} TOP_#{TESTNAME}() {
 898                         t1 = 1;
 899                     }
 900                 }
 901                 """),
 902         NO_FIELDS("""
 903                 @MigratedValueClass
 904                 #{VALUE} class TOP_#{TESTNAME} implements Serializable {
 905                     #{CLASSACCESS} TOP_#{TESTNAME}() {}
 906                 }
 907                 """),
 908         ONE_FIELD("""
 909                 @MigratedValueClass
 910                 #{VALUE} class TOP_#{TESTNAME} implements Serializable {
 911                     private int t1;
 912                     #{CLASSACCESS} TOP_#{TESTNAME}() {
 913                         t1 = 1;
 914                     }
 915                 }
 916                 """),
 917         ;
 918 
 919         private final String template;
 920 
 921         TopFragments(String template) {
 922             this.template = template;
 923         }
 924 
 925         @Override
 926         public String expand(String optParameter) {
 927             return template;
 928         }
 929     }
 930 
 931     /**
 932      * TopAbstract Fragments
 933      */
 934     enum TestNameExtendsFragments implements ComboParameter, CodeShapePredicate {
 935         NONE(""),
 936         TESTNAME_EXTENDS_FRAGMENT("extends TOP_#{TESTNAME}"),
 937         ;
 938 
 939         private final String template;
 940 
 941         TestNameExtendsFragments(String template) {
 942             this.template = template;
 943         }
 944 
 945         @Override
 946         public String expand(String optParameter) {
 947             return template;
 948         }
 949     }
 950 
 951     /**
 952      * SerializedObjectCustom Fragments
 953      */
 954     enum SerializedObjectCustomFragments implements ComboParameter, CodeShapePredicate {
 955         NONE(""),
 956         ;
 957 
 958         private final String template;
 959 
 960         SerializedObjectCustomFragments(String template) {
 961             this.template = template;
 962         }
 963 
 964         @Override
 965         public String expand(String optParameter) {
 966             return template;
 967         }
 968     }
 969 
 970     /**
 971      * ExternalizableMethod Fragments
 972      */
 973     enum ExternalizableMethodFragments implements ComboParameter, CodeShapePredicate {
 974         NONE(""),
 975         EXTERNALIZABLE_METHODS("""
 976                     public void writeExternal(ObjectOutput oos) throws IOException {
 977                         oos.write#{FIELD[0].READFIELD}(f1);
 978                         oos.write#{FIELD[1].READFIELD}(f2);
 979                     }
 980 
 981                     public void readExternal(ObjectInput ois) throws IOException, ClassNotFoundException {
 982                         f1 = (#{FIELD[0]})ois.read#{FIELD[0].READFIELD}();
 983                         f2 = (#{FIELD[1]})ois.read#{FIELD[1].READFIELD}();
 984                     }
 985                 """),
 986         ;
 987 
 988         private final String template;
 989 
 990         ExternalizableMethodFragments(String template) {
 991             this.template = template;
 992         }
 993 
 994         @Override
 995         public String expand(String optParameter) {
 996             return template;
 997         }
 998     }
 999 
1000     /**
1001      * ObjectConstructorFragment Fragments
1002      */
1003     enum ObjectConstructorFragment implements ComboParameter, CodeShapePredicate {
1004         NONE(""),
1005         ANNOTATED_OBJECT_CONSTRUCTOR_FRAGMENT("""
1006                     @DeserializeConstructor
1007                     #{CLASSACCESS} #{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2) {
1008                         this.f1 = f1;
1009                         this.f2 = f2;
1010                         #{FIELD_CONSTRUCTOR_ADDITIONS}
1011                     }
1012 
1013                     @DeserializeConstructor
1014                     #{CLASSACCESS} #{TESTNAME}(#{FIELD[0]} f1, #{FIELD[1]} f2, int fExtra) {
1015                         this.f1 = f1;
1016                         this.f2 = f2;
1017                         #{FIELD_CONSTRUCTOR_ADDITIONS}
1018                     }
1019                 """),
1020 
1021         ;
1022 
1023         private final String template;
1024 
1025         ObjectConstructorFragment(String template) {
1026             this.template = template;
1027         }
1028 
1029         @Override
1030         public String expand(String optParameter) {
1031             return template;
1032         }
1033     }
1034 
1035     /**
1036      * WriteObject templates
1037      */
1038     enum WriteObjectFragments implements ComboParameter, CodeShapePredicate {
1039         NONE(""),
1040         WRITE_OBJECT_DEFAULT_FRAGMENT("""
1041                     private void writeObject(ObjectOutputStream oos) throws IOException {
1042                         oos.defaultWriteObject();
1043                     }
1044                 """),
1045         WRITE_OBJECT_FIELDS_FRAGMENT("""
1046                     private void writeObject(ObjectOutputStream oos) throws IOException {
1047                         ObjectOutputStream.PutField fields = oos.putFields();
1048                         fields.put("f1", f1);
1049                         fields.put("f2", f2);
1050                         oos.writeFields();
1051                     }
1052                 """),
1053         WRITE_OBJECT_DEFAULT_CUSTOM_FRAGMENT("""
1054                     private void writeObject(ObjectOutputStream oos) throws IOException {
1055                         oos.defaultWriteObject();
1056                         // Write custom data
1057                         oos.write#{FIELD[0].READFIELD}(#{FIELD[0].DEFAULT});
1058                         oos.write#{FIELD[1].READFIELD}(#{FIELD[1].DEFAULT});
1059                     }
1060                 """),
1061         WRITE_OBJECT_FIELDS_CUSTOM_FRAGMENT("""
1062                     private void writeObject(ObjectOutputStream oos) throws IOException {
1063                         ObjectOutputStream.PutField fields = oos.putFields();
1064                         fields.put("f1", f1);
1065                         fields.put("f2", f2);
1066                         oos.writeFields();
1067                         // Write custom data
1068                         oos.write#{FIELD[0].READFIELD}(#{FIELD[0].DEFAULT});
1069                         oos.write#{FIELD[1].READFIELD}(#{FIELD[1].DEFAULT});
1070                     }
1071                 """),
1072         ;
1073 
1074         private final String template;
1075 
1076         WriteObjectFragments(String template) {
1077             this.template = template;
1078         }
1079 
1080         @Override
1081         public String expand(String optParameter) {
1082             return template;
1083         }
1084     }
1085 
1086     /**
1087      * ReadObject templates
1088      */
1089     enum ReadObjectFragments implements ComboParameter, CodeShapePredicate {
1090         NONE(""),
1091         READ_OBJECT_DEFAULT_FRAGMENT("""
1092                     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
1093                         ois.defaultReadObject();
1094                     }
1095                 """),
1096         READ_OBJECT_FIELDS_FRAGMENT("""
1097                     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
1098                         ObjectInputStream.GetField fields = ois.readFields();
1099                         this.f1 = (#{FIELD[0]})fields.get("f1", #{FIELD[0].DEFAULT});
1100                         this.f2 = (#{FIELD[1]})fields.get("f2", #{FIELD[1].DEFAULT});
1101                     }
1102                  """),
1103         READ_OBJECT_DEFAULT_CUSTOM_FRAGMENT("""
1104                     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
1105                         ois.defaultReadObject();
1106                         // Read custom data
1107                         #{FIELD[0]} d1 = (#{FIELD[0]})ois.read#{FIELD[0].READFIELD}();
1108                         #{FIELD[1]} d2 = (#{FIELD[1]})ois.read#{FIELD[1].READFIELD}();
1109                         assert Objects.equals(#{FIELD[0].DEFAULT}, d1) : "reading custom data1, actual: " + d1;
1110                         assert Objects.equals(#{FIELD[1].DEFAULT}, d2) : "reading custom data2, actual: " + d2;
1111                     }
1112                 """),
1113         READ_OBJECT_FIELDS_CUSTOM_FRAGMENT("""
1114                     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
1115                         ObjectInputStream.GetField fields = ois.readFields();
1116                         this.f1 = (#{FIELD[0]})fields.get("f1", #{FIELD[0].DEFAULT});
1117                         this.f2 = (#{FIELD[1]})fields.get("f2", #{FIELD[1].DEFAULT});
1118                         // Read custom data
1119                         #{FIELD[0]} d1 = (#{FIELD[0]})ois.read#{FIELD[0].READFIELD}();
1120                         #{FIELD[1]} d2 = (#{FIELD[1]})ois.read#{FIELD[1].READFIELD}();
1121                         assert Objects.equals(#{FIELD[0].DEFAULT}, d1) : "reading custom data1, actual: " + d1;
1122                         assert Objects.equals(#{FIELD[1].DEFAULT}, d2) : "reading custom data2, actual: " + d2;
1123                     }
1124                  """),
1125         ;
1126 
1127         private final String template;
1128 
1129         ReadObjectFragments(String template) {
1130             this.template = template;
1131         }
1132 
1133         @Override
1134         public String expand(String optParameter) {
1135             return template;
1136         }
1137     }
1138 
1139     /**
1140      * Value and Identity kinds.
1141      */
1142     enum ValueKind implements ComboParameter, CodeShapePredicate {
1143         VALUE("value"),
1144         IDENTITY(""),
1145         ;
1146 
1147         private final String template;
1148 
1149         ValueKind(String template) {
1150             this.template = template;
1151         }
1152 
1153         @Override
1154         public String expand(String optParameter) {
1155             return template;
1156         }
1157     }
1158 
1159     enum SerializationKind implements ComboParameter, CodeShapePredicate {
1160         SERIALIZABLE("SER", "implements Serializable"),
1161         EXTERNALIZABLE("EXT", "implements Externalizable"),
1162         ;
1163 
1164         private final String key;
1165         private final String declaration;
1166 
1167         SerializationKind(String key, String declaration) {
1168             this.key = key;
1169             this.declaration = declaration;
1170         }
1171 
1172         public String expand(String optParameter) {
1173             return switch (optParameter) {
1174                 case null -> key;
1175                 case "IMPLEMENTS" -> declaration;
1176                 default ->
1177                         "#{" + optParameter + "_" + this + "}";   // everything ELSE turn into requested key with suffix
1178             };
1179         }
1180     }
1181 
1182     /**
1183      * Class Access kinds.
1184      */
1185     enum ClassAccessKind implements ComboParameter, CodeShapePredicate {
1186         PUBLIC("public"),
1187         PACKAGE(""),
1188         ;
1189 
1190         private final String classAccessTemplate;
1191 
1192         ClassAccessKind(String classAccessTemplate) {
1193             this.classAccessTemplate = classAccessTemplate;
1194         }
1195 
1196         @Override
1197         public String expand(String optParameter) {
1198             return classAccessTemplate;
1199         }
1200     }
1201 
1202     /**
1203      * Type of arguments to insert in method signatures
1204      */
1205     enum ArgumentValue implements ComboParameter, CodeShapePredicate {
1206         BOOLEAN("boolean", true),
1207         BYTE("byte", (byte) 127),
1208         CHAR("char", 'Z'),
1209         SHORT("short", (short) 0x7fff),
1210         INT("int", 0x7fffffff),
1211         LONG("long", 0x7fffffffffffffffL),
1212         FLOAT("float", 1.0F),
1213         DOUBLE("double", 1.0d),
1214         STRING("String", "xyz");
1215 
1216         static final ArgumentValue[] BASIC_VALUES = {INT, STRING};
1217 
1218         private final String argumentsValueTemplate;
1219         private final Object value;
1220 
1221         ArgumentValue(String argumentsValueTemplate, Object value) {
1222             this.argumentsValueTemplate = argumentsValueTemplate;
1223             this.value = value;
1224         }
1225 
1226         @Override
1227         public String expand(String optParameter) {
1228             return switch (optParameter) {
1229                 case null -> argumentsValueTemplate;
1230                 case "TITLECASE" -> Character.toTitleCase(argumentsValueTemplate.charAt(0)) +
1231                         argumentsValueTemplate.substring(1);
1232                 case "DEFAULT" -> switch (this) {
1233                     case BOOLEAN -> "false";
1234                     case BYTE -> "(byte)-1";
1235                     case CHAR -> "'" + "!" + "'";
1236                     case SHORT -> "(short)-1";
1237                     case INT -> "-1";
1238                     case LONG -> "-1L";
1239                     case FLOAT -> "-1.0f";
1240                     case DOUBLE -> "-1.0d";
1241                     case STRING -> '"' + "n/a" + '"';
1242                 };
1243                 case "READFIELD" -> switch (this) {
1244                     case BOOLEAN -> "Boolean";
1245                     case BYTE -> "Byte";
1246                     case CHAR -> "Char";
1247                     case SHORT -> "Short";
1248                     case INT -> "Int";
1249                     case LONG -> "Long";
1250                     case FLOAT -> "Float";
1251                     case DOUBLE -> "Double";
1252                     case STRING -> "Object";
1253                 };
1254                 case "RANDOM" -> switch (this) {  // or can be Random
1255                     case BOOLEAN -> Boolean.toString(!(boolean) value);
1256                     case BYTE -> "(byte)" + value + 1;
1257                     case CHAR -> "'" + value + "'";
1258                     case SHORT -> "(short)" + value + 1;
1259                     case INT -> "-2";
1260                     case LONG -> "-2L";
1261                     case FLOAT -> (1.0f + (float) value) + "f";
1262                     case DOUBLE -> (1.0d + (float) value) + "d";
1263                     case STRING -> "\"" + value + "!\"";
1264                 };
1265                 default -> switch (this) {
1266                     case BOOLEAN -> value.toString();
1267                     case BYTE -> "(byte)" + value;
1268                     case CHAR -> "'" + value + "'";
1269                     case SHORT -> "(short)" + value;
1270                     case INT -> "-1";
1271                     case LONG -> "-1L";
1272                     case FLOAT -> value + "f";
1273                     case DOUBLE -> value + "d";
1274                     case STRING -> '"' + (String) value + '"';
1275                 };
1276             };
1277         }
1278     }
1279 
1280     /**
1281      * Set of Parameters to fill in template.
1282      *
1283      * @param key    the key
1284      * @param params the ComboParameters (one or more)
1285      */
1286     record ParamSet(String key, int count, ComboParameter... params) {
1287         /**
1288          * Set of parameter strings for fill in template.
1289          * The strings are mapped to CompboParameter.Constants.
1290          *
1291          * @param key     the key
1292          * @param strings varargs strings
1293          */
1294         ParamSet(String key, String... strings) {
1295             this(key, 1, paramsForStrings(strings));
1296         }
1297 
1298         /**
1299          * Set of parameter strings for fill in template.
1300          * The strings are mapped to CompboParameter.Constants.
1301          *
1302          * @param key     the key
1303          * @param strings varargs strings
1304          */
1305         ParamSet(String key, int count, String... strings) {
1306             this(key, count, paramsForStrings(strings));
1307         }
1308 
1309         /**
1310          * Set of parameters for fill in template.
1311          * The strings are mapped to CompboParameter.Constants.
1312          *
1313          * @param key    the key
1314          * @param params varargs strings
1315          */
1316         ParamSet(String key, ComboParameter... params) {
1317             this(key, 1, params);
1318         }
1319     }
1320 
1321     /**
1322      * Marks conditions that should match compile time errors
1323      */
1324     static class CompileException extends RuntimeException {
1325         CompileException(String msg) {
1326             super(msg);
1327         }
1328     }
1329 
1330     /**
1331      * Custom ObjectInputStream to be resolve classes from a specific class loader.
1332      */
1333     private static class LoaderObjectInputStream extends ObjectInputStream {
1334         private final ClassLoader loader;
1335 
1336         public LoaderObjectInputStream(InputStream in, ClassLoader loader) throws IOException {
1337             super(in);
1338             this.loader = loader;
1339         }
1340 
1341         /**
1342          * Override resolveClass to be resolve classes from the specified loader.
1343          *
1344          * @param desc an instance of class {@code ObjectStreamClass}
1345          * @return the class
1346          * @throws ClassNotFoundException if the class is not found
1347          */
1348         @Override
1349         protected Class<?> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException {
1350             String name = desc.getName();
1351             try {
1352                 return Class.forName(name, false, loader);
1353             } catch (ClassNotFoundException ex) {
1354                 Class<?> cl = Class.forPrimitiveName(name);
1355                 if (cl != null) {
1356                     return cl;
1357                 } else {
1358                     throw ex;
1359                 }
1360             }
1361         }
1362     }
1363 
1364     private abstract class MyCompilationTask extends ComboInstance {
1365 
1366     }
1367     private static void selftest() {
1368         Set<ComboParameter> params = Set.of(ValueKind.VALUE, SerializationKind.EXTERNALIZABLE);
1369         assertTrue(ValueKind.VALUE.test(params), "VALUE");
1370         assertTrue(SerializationKind.EXTERNALIZABLE.test(params), "SerializationKind.EXTERNALIZABLE");
1371         assertFalse(CodeShape.BAD_EXT_VALUE.test(params));
1372     }
1373 }