1 /*
   2  * Copyright (c) 2018, 2026, 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 /*
  25  * @test
  26  * @bug 8192920 8204588 8246774 8248843 8268869 8235876 8328339 8335896 8344706
  27  *      8362237 8376534
  28  * @summary Test source launcher
  29  * @library /tools/lib
  30  * @modules jdk.compiler/com.sun.tools.javac.api
  31  *          jdk.compiler/com.sun.tools.javac.launcher
  32  *          jdk.compiler/com.sun.tools.javac.main
  33  *          java.base/jdk.internal.module
  34  * @build toolbox.JavaTask toolbox.JavacTask toolbox.TestRunner toolbox.ToolBox
  35  * @run main SourceLauncherTest
  36  * @ignore Verifier error
  37  */
  38 
  39 import java.lang.classfile.*;
  40 import java.lang.classfile.attribute.ModuleResolutionAttribute;
  41 import java.io.ByteArrayOutputStream;
  42 import java.io.File;
  43 import java.io.IOException;
  44 import java.io.OutputStream;
  45 import java.io.PrintStream;
  46 import java.io.PrintWriter;
  47 import java.io.StringWriter;
  48 import java.lang.reflect.InvocationTargetException;
  49 import java.nio.file.Files;
  50 import java.nio.file.Path;
  51 import java.nio.file.Paths;
  52 import java.util.ArrayList;
  53 import java.util.Collections;
  54 import java.util.List;
  55 import java.util.Properties;
  56 import java.util.regex.Pattern;
  57 
  58 import com.sun.tools.javac.launcher.SourceLauncher;
  59 import com.sun.tools.javac.launcher.Fault;
  60 
  61 import toolbox.JavaTask;
  62 import toolbox.JavacTask;
  63 import toolbox.Task;
  64 import toolbox.TestRunner;
  65 import toolbox.ToolBox;
  66 
  67 import static jdk.internal.module.ClassFileConstants.WARN_INCUBATING;
  68 
  69 public class SourceLauncherTest extends TestRunner {
  70     public static void main(String... args) throws Exception {
  71         SourceLauncherTest t = new SourceLauncherTest();
  72         t.runTests(m -> new Object[] { Paths.get(m.getName()) });
  73     }
  74 
  75     SourceLauncherTest() {
  76         super(System.err);
  77         tb = new ToolBox();
  78         System.err.println("version: " + thisVersion);
  79     }
  80 
  81     final ToolBox tb;
  82     private static final String thisVersion = System.getProperty("java.specification.version");
  83 
  84     /*
  85      * Positive tests.
  86      */
  87 
  88     @Test
  89     public void testHelloWorld(Path base) throws IOException {
  90         tb.writeJavaFiles(base,
  91             "import java.util.Arrays;\n" +
  92             "class HelloWorld {\n" +
  93             "    public static void main(String... args) {\n" +
  94             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
  95             "    }\n" +
  96             "}");
  97         testSuccess(base.resolve("HelloWorld.java"), "Hello World! [1, 2, 3]\n");
  98     }
  99 
 100     @Test
 101     public void testHelloWorldInPackage(Path base) throws IOException {
 102         tb.writeJavaFiles(base,
 103             "package hello;\n" +
 104             "import java.util.Arrays;\n" +
 105             "class World {\n" +
 106             "    public static void main(String... args) {\n" +
 107             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 108             "    }\n" +
 109             "}");
 110         testSuccess(base.resolve("hello").resolve("World.java"), "Hello World! [1, 2, 3]\n");
 111     }
 112 
 113     @Test
 114     public void testHelloWorldInPackageWithStaticImport(Path base) throws IOException {
 115         tb.writeJavaFiles(base,
 116                 """
 117                 package hello;
 118                 import static hello.Helper.*;
 119                 import java.util.Arrays;
 120                 class World {
 121                     public static void main(String... args) {
 122                         m(args);
 123                     }
 124                 }
 125                 class Helper {
 126                     static void m(String... args) {
 127                         System.out.println("Hello World! " + Arrays.toString(args));
 128                     }
 129                 }
 130                 """);
 131         testSuccess(base.resolve("hello").resolve("World.java"), "Hello World! [1, 2, 3]\n");
 132     }
 133 
 134     @Test
 135     public void testHelloWorldWithAux(Path base) throws IOException {
 136         tb.writeJavaFiles(base,
 137             "import java.util.Arrays;\n" +
 138             "class HelloWorld {\n" +
 139             "    public static void main(String... args) {\n" +
 140             "        Aux.write(args);\n" +
 141             "    }\n" +
 142             "}\n" +
 143             "class Aux {\n" +
 144             "    static void write(String... args) {\n" +
 145             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 146             "    }\n" +
 147             "}");
 148         testSuccess(base.resolve("HelloWorld.java"), "Hello World! [1, 2, 3]\n");
 149     }
 150 
 151     @Test
 152     public void testHelloWorldWithShebang(Path base) throws IOException {
 153         tb.writeJavaFiles(base,
 154             "#!/usr/bin/java --source " + thisVersion + "\n" +
 155             "import java.util.Arrays;\n" +
 156             "class HelloWorld {\n" +
 157             "    public static void main(String... args) {\n" +
 158             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 159             "    }\n" +
 160             "}");
 161         Files.copy(base.resolve("HelloWorld.java"), base.resolve("HelloWorld"));
 162         testSuccess(base.resolve("HelloWorld"), "Hello World! [1, 2, 3]\n");
 163     }
 164 
 165     @Test
 166     public void testNoAnnoProcessing(Path base) throws IOException {
 167         Path annoSrc = base.resolve("annoSrc");
 168         tb.writeJavaFiles(annoSrc,
 169             "import java.util.*;\n" +
 170             "import javax.annotation.processing.*;\n" +
 171             "import javax.lang.model.element.*;\n" +
 172             "@SupportedAnnotationTypes(\"*\")\n" +
 173             "public class AnnoProc extends AbstractProcessor {\n" +
 174             "    public boolean process(Set<? extends TypeElement> annos, RoundEnvironment rEnv) {\n" +
 175             "        throw new Error(\"Annotation processor should not be invoked\");\n" +
 176             "    }\n" +
 177             "}\n");
 178         Path annoClasses = Files.createDirectories(base.resolve("classes"));
 179         new JavacTask(tb)
 180                 .outdir(annoClasses)
 181                 .files(annoSrc.resolve("AnnoProc.java").toString())
 182                 .run();
 183         Path serviceFile = annoClasses.resolve("META-INF").resolve("services")
 184                 .resolve("javax.annotation.processing.Processor");
 185         tb.writeFile(serviceFile, "AnnoProc");
 186 
 187         Path mainSrc = base.resolve("mainSrc");
 188         tb.writeJavaFiles(mainSrc,
 189             "import java.util.Arrays;\n" +
 190             "class HelloWorld {\n" +
 191             "    public static void main(String... args) {\n" +
 192             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 193             "    }\n" +
 194             "}");
 195 
 196         List<String> javacArgs = List.of("-classpath", annoClasses.toString());
 197         List<String> classArgs = List.of("1", "2", "3");
 198         String expect = "Hello World! [1, 2, 3]\n";
 199         Result r = run(mainSrc.resolve("HelloWorld.java"), javacArgs, classArgs);
 200         checkEqual("stdout", r.stdOut, expect);
 201         checkEmpty("stderr", r.stdErr);
 202         checkNull("exception", r.exception);
 203     }
 204 
 205     @Test
 206     public void testEnablePreview(Path base) throws IOException {
 207         tb.writeJavaFiles(base,
 208             "import java.util.Arrays;\n" +
 209             "class HelloWorld {\n" +
 210             "    public static void main(String... args) {\n" +
 211             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 212             "    }\n" +
 213             "}");
 214 
 215         String log = new JavaTask(tb)
 216                 .vmOptions("--enable-preview", "--source", thisVersion)
 217                 .className(base.resolve("HelloWorld.java").toString())
 218                 .classArgs("1", "2", "3")
 219                 .run(Task.Expect.SUCCESS)
 220                 .getOutput(Task.OutputKind.STDOUT);
 221         checkEqual("stdout", log.trim(), "Hello World! [1, 2, 3]");
 222     }
 223 
 224     @Test
 225     public void testCodeSource(Path base) throws IOException {
 226         tb.writeJavaFiles(base,
 227             "import java.net.URL;\n" +
 228             "class ShowCodeSource {\n" +
 229             "    public static void main(String... args) {\n" +
 230             "        URL u = ShowCodeSource.class.getProtectionDomain().getCodeSource().getLocation();\n" +
 231             "        System.out.println(u);\n" +
 232             "    }\n" +
 233             "}");
 234 
 235         Path file = base.resolve("ShowCodeSource.java");
 236         String log = new JavaTask(tb)
 237                 .className(file.toString())
 238                 .run(Task.Expect.SUCCESS)
 239                 .getOutput(Task.OutputKind.STDOUT);
 240         checkEqual("stdout", log.trim(), file.toAbsolutePath().toUri().toURL().toString());
 241     }
 242 
 243     @Test
 244     public void testSystemProperty(Path base) throws IOException {
 245         tb.writeJavaFiles(base,
 246             "class ShowProperty {\n" +
 247             "    public static void main(String... args) {\n" +
 248             "        System.out.println(System.getProperty(\"jdk.launcher.sourcefile\"));\n" +
 249             "    }\n" +
 250             "}");
 251 
 252         Path file = base.resolve("ShowProperty.java");
 253         String log = new JavaTask(tb)
 254                 .className(file.toString())
 255                 .run(Task.Expect.SUCCESS)
 256                 .getOutput(Task.OutputKind.STDOUT);
 257         checkEqual("stdout", log.trim(), file.toAbsolutePath().toString());
 258     }
 259 
 260     @Test
 261     public void testThreadContextClassLoader(Path base) throws IOException {
 262         tb.writeJavaFiles(base, //language=java
 263                 """
 264                 class ThreadContextClassLoader {
 265                     public static void main(String... args) {
 266                         var expected = ThreadContextClassLoader.class.getClassLoader();
 267                         var actual = Thread.currentThread().getContextClassLoader();
 268                         System.out.println(expected == actual);
 269                     }
 270                 }
 271                 """);
 272 
 273         Path file = base.resolve("ThreadContextClassLoader.java");
 274         String log = new JavaTask(tb)
 275                 .className(file.toString())
 276                 .run(Task.Expect.SUCCESS)
 277                 .getOutput(Task.OutputKind.STDOUT);
 278         checkEqual("stdout", log.trim(), "true");
 279     }
 280 
 281     void testSuccess(Path file, String expect) throws IOException {
 282         Result r = run(file, Collections.emptyList(), List.of("1", "2", "3"));
 283         checkEqual("stdout", r.stdOut, expect);
 284         checkEmpty("stderr", r.stdErr);
 285         checkNull("exception", r.exception);
 286     }
 287 
 288 
 289     @Test
 290     public void testMainNoParams(Path base) throws IOException {
 291         tb.writeJavaFiles(base,
 292             "package hello;\n" +
 293             "import java.util.Arrays;\n" +
 294             "class World {\n" +
 295             "    public static void main(String... args) {\n" +
 296             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 297             "    }\n" +
 298             "}");
 299         testSuccess(base.resolve("hello").resolve("World.java"), "Hello World! [1, 2, 3]\n");
 300     }
 301 
 302     @Test
 303     public void testMainNotPublic(Path base) throws IOException {
 304         tb.writeJavaFiles(base,
 305             "package hello;\n" +
 306             "import java.util.Arrays;\n" +
 307             "class World {\n" +
 308             "    static void main(String... args) {\n" +
 309             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 310             "    }\n" +
 311             "}");
 312         testSuccess(base.resolve("hello").resolve("World.java"), "Hello World! [1, 2, 3]\n");
 313     }
 314 
 315     @Test
 316     public void testMainNotStatic(Path base) throws IOException {
 317         tb.writeJavaFiles(base,
 318             "package hello;\n" +
 319             "import java.util.Arrays;\n" +
 320             "class World {\n" +
 321             "    public void main(String... args) {\n" +
 322             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 323             "    }\n" +
 324             "}");
 325         testSuccess(base.resolve("hello").resolve("World.java"), "Hello World! [1, 2, 3]\n");
 326     }
 327 
 328     /*
 329      * Negative tests: such as cannot find or execute main method.
 330      */
 331 
 332     @Test
 333     public void testHelloWorldWithShebangJava(Path base) throws IOException {
 334         tb.writeJavaFiles(base,
 335             "#!/usr/bin/java --source " + thisVersion + "\n" +
 336             "import java.util.Arrays;\n" +
 337             "class HelloWorld {\n" +
 338             "    public static void main(String... args) {\n" +
 339             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 340             "    }\n" +
 341             "}");
 342         Path file = base.resolve("HelloWorld.java");
 343         testError(file,
 344             file + ":1: error: illegal character: '#'\n" +
 345             "#!/usr/bin/java --source " + thisVersion + "\n" +
 346             "^\n" +
 347             file + ":1: error: class, interface, annotation type, enum, record, method or field expected\n" +
 348             "#!/usr/bin/java --source " + thisVersion + "\n" +
 349             "  ^\n" +
 350             "2 errors\n",
 351             "error: compilation failed");
 352     }
 353 
 354     @Test
 355     public void testNoClass(Path base) throws IOException {
 356         var path = Files.createDirectories(base.resolve("p"));
 357         Path file = path.resolve("NoClass.java");
 358         Files.write(file, List.of("package p;"));
 359         testError(file, "", "error: no class declared in source file");
 360     }
 361 
 362     @Test
 363     public void testMismatchOfPathAndPackage(Path base) throws IOException {
 364         Files.createDirectories(base);
 365         Path file = base.resolve("MismatchOfPathAndPackage.java");
 366         Files.write(file, List.of("package p; class MismatchOfPathAndPackage {}"));
 367         testError(file, "", "error: end of path to source file does not match its package name p: " + file);
 368     }
 369 
 370     @Test
 371     public void testLoadClass(Path base) throws IOException {
 372         Path src1 = base.resolve("src1");
 373         Path file1 = src1.resolve("LoadClass.java");
 374         tb.writeJavaFiles(src1,
 375                 "class LoadClass {\n"
 376                 + "    public static void main(String... args) {\n"
 377                 + "        System.out.println(\"on classpath\");\n"
 378                 + "    };\n"
 379                 + "}\n");
 380         Path classes1 = Files.createDirectories(base.resolve("classes"));
 381         new JavacTask(tb)
 382                 .outdir(classes1)
 383                 .files(file1)
 384                 .run();
 385         String log1 = new JavaTask(tb)
 386                 .classpath(classes1.toString())
 387                 .className("LoadClass")
 388                 .run(Task.Expect.SUCCESS)
 389                 .getOutput(Task.OutputKind.STDOUT);
 390         checkEqual("stdout", log1.trim(),
 391                 "on classpath");
 392 
 393         Path src2 = base.resolve("src2");
 394         Path file2 = src2.resolve("LoadClass.java");
 395         tb.writeJavaFiles(src2,
 396                 "class LoadClass {\n"
 397                 + "    public static void main(String... args) {\n"
 398                 + "        System.out.println(\"in source file\");\n"
 399                 + "    };\n"
 400                 + "}\n");
 401         String log2 = new JavaTask(tb)
 402                 .classpath(classes1.toString())
 403                 .className(file2.toString())
 404                 .run(Task.Expect.SUCCESS)
 405                 .getOutput(Task.OutputKind.STDOUT);
 406         checkEqual("stdout", log2.trim(),
 407                 "in source file");
 408     }
 409 
 410     @Test
 411     public void testGetResource(Path base) throws IOException {
 412         Path src = base.resolve("src");
 413         Path file = src.resolve("GetResource.java");
 414         tb.writeJavaFiles(src,
 415                 "class GetResource {\n"
 416                 + "    public static void main(String... args) {\n"
 417                 + "        System.out.println(GetResource.class.getClassLoader().getResource(\"GetResource.class\"));\n"
 418                 + "    };\n"
 419                 + "}\n");
 420         Path classes = Files.createDirectories(base.resolve("classes"));
 421         new JavacTask(tb)
 422                 .outdir(classes)
 423                 .files(file)
 424                 .run();
 425 
 426         String log = new JavaTask(tb)
 427                 .classpath(classes.toString())
 428                 .className(file.toString())
 429                 .run(Task.Expect.SUCCESS)
 430                 .getOutput(Task.OutputKind.STDOUT);
 431         checkMatch("stdout", log.trim(),
 432                 Pattern.compile("sourcelauncher-memoryclassloader[0-9]+:GetResource.class"));
 433     }
 434 
 435     @Test
 436     public void testGetResources(Path base) throws IOException {
 437         Path src = base.resolve("src");
 438         Path file = src.resolve("GetResources.java");
 439         tb.writeJavaFiles(src,
 440                 "import java.io.*; import java.net.*; import java.util.*;\n"
 441                 + "class GetResources {\n"
 442                 + "    public static void main(String... args) throws IOException {\n"
 443                 + "        Enumeration<URL> e =\n"
 444                 + "            GetResources.class.getClassLoader().getResources(\"GetResources.class\");\n"
 445                 + "        while (e.hasMoreElements()) System.out.println(e.nextElement());\n"
 446                 + "    };\n"
 447                 + "}\n");
 448         Path classes = Files.createDirectories(base.resolve("classes"));
 449         new JavacTask(tb)
 450                 .outdir(classes)
 451                 .files(file)
 452                 .run();
 453 
 454         List<String> log = new JavaTask(tb)
 455                 .classpath(classes.toString())
 456                 .className(file.toString())
 457                 .run(Task.Expect.SUCCESS)
 458                 .getOutputLines(Task.OutputKind.STDOUT);
 459         checkMatch("stdout:0", log.get(0).trim(),
 460                 Pattern.compile("sourcelauncher-memoryclassloader[0-9]+:GetResources.class"));
 461         checkMatch("stdout:1", log.get(1).trim(),
 462                 Pattern.compile("file:/.*/testGetResources/classes/GetResources.class"));
 463     }
 464 
 465     @Test
 466     public void testSyntaxErr(Path base) throws IOException {
 467         tb.writeJavaFiles(base, "class SyntaxErr {");
 468         Path file = base.resolve("SyntaxErr.java");
 469         testError(file,
 470                 file + ":1: error: reached end of file while parsing\n" +
 471                 "class SyntaxErr {\n" +
 472                 "                 ^\n" +
 473                 "1 error\n",
 474                 "error: compilation failed");
 475     }
 476 
 477     @Test
 478     public void testNoSourceOnClassPath(Path base) throws IOException {
 479         Path extraSrc = base.resolve("extraSrc");
 480         tb.writeJavaFiles(extraSrc,
 481             "public class Extra {\n" +
 482             "    static final String MESSAGE = \"Hello World\";\n" +
 483             "}\n");
 484 
 485         Path mainSrc = base.resolve("mainSrc");
 486         tb.writeJavaFiles(mainSrc,
 487             "import java.util.Arrays;\n" +
 488             "class HelloWorld {\n" +
 489             "    public static void main(String... args) {\n" +
 490             "        System.out.println(Extra.MESSAGE + Arrays.toString(args));\n" +
 491             "    }\n" +
 492             "}");
 493 
 494         List<String> javacArgs = List.of("-classpath", extraSrc.toString());
 495         List<String> classArgs = List.of("1", "2", "3");
 496         String FS = File.separator;
 497         String expectStdErr =
 498             "testNoSourceOnClassPath" + FS + "mainSrc" + FS + "HelloWorld.java:4: error: cannot find symbol\n" +
 499             "        System.out.println(Extra.MESSAGE + Arrays.toString(args));\n" +
 500             "                           ^\n" +
 501             "  symbol:   variable Extra\n" +
 502             "  location: class HelloWorld\n" +
 503             "1 error\n";
 504         Result r = run(mainSrc.resolve("HelloWorld.java"), javacArgs, classArgs);
 505         checkEmpty("stdout", r.stdOut);
 506         checkEqual("stderr", r.stdErr, expectStdErr);
 507         checkFault("exception", r.exception, "error: compilation failed");
 508     }
 509 
 510     @Test
 511     public void testClassNotFound(Path base) throws IOException {
 512         Path src = base.resolve("src");
 513         Path file = src.resolve("ClassNotFound.java");
 514         tb.writeJavaFiles(src,
 515                 "class ClassNotFound {\n"
 516                 + "    public static void main(String... args) {\n"
 517                 + "        try {\n"
 518                 + "            Class.forName(\"NoSuchClass\");\n"
 519                 + "            System.out.println(\"no exception\");\n"
 520                 + "            System.exit(1);\n"
 521                 + "        } catch (ClassNotFoundException e) {\n"
 522                 + "            System.out.println(\"Expected exception thrown: \" + e);\n"
 523                 + "        }\n"
 524                 + "    };\n"
 525                 + "}\n");
 526         Path classes = Files.createDirectories(base.resolve("classes"));
 527         new JavacTask(tb)
 528                 .outdir(classes)
 529                 .files(file)
 530                 .run();
 531 
 532         String log = new JavaTask(tb)
 533                 .classpath(classes.toString())
 534                 .className(file.toString())
 535                 .run(Task.Expect.SUCCESS)
 536                 .getOutput(Task.OutputKind.STDOUT);
 537         checkEqual("stdout", log.trim(),
 538                 "Expected exception thrown: java.lang.ClassNotFoundException: NoSuchClass");
 539     }
 540 
 541     // For any source file that is invoked through the OS shebang mechanism, invalid shebang
 542     // lines will be caught and handled by the OS, before the launcher is even invoked.
 543     // However, if such a file is passed directly to the launcher, perhaps using the --source
 544     // option, a well-formed shebang line will be removed but a badly-formed one will be not be
 545     // removed and will cause compilation errors.
 546     @Test
 547     public void testBadShebang(Path base) throws IOException {
 548         tb.writeJavaFiles(base,
 549             "#/usr/bin/java --source " + thisVersion + "\n" +
 550             "import java.util.Arrays;\n" +
 551             "class HelloWorld {\n" +
 552             "    public static void main(String... args) {\n" +
 553             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 554             "    }\n" +
 555             "}");
 556         Path file = base.resolve("HelloWorld.java");
 557         testError(file,
 558             file + ":1: error: illegal character: '#'\n" +
 559             "#/usr/bin/java --source " + thisVersion + "\n" +
 560             "^\n" +
 561             file + ":1: error: class, interface, annotation type, enum, record, method or field expected\n" +
 562             "#/usr/bin/java --source " + thisVersion + "\n" +
 563             "  ^\n" +
 564             "2 errors\n",
 565             "error: compilation failed");
 566     }
 567 
 568     @Test
 569     public void testBadSourceOpt(Path base) throws IOException {
 570         Files.createDirectories(base);
 571         Path file = base.resolve("DummyClass.java");
 572         Files.write(file, List.of("class DummyClass { }"));
 573         Properties sysProps = System.getProperties();
 574         Properties p = new Properties(sysProps);
 575         p.setProperty("jdk.internal.javac.source", "<BAD>");
 576         System.setProperties(p);
 577         try {
 578             testError(file, "", "error: invalid value for --source option: <BAD>");
 579         } finally {
 580             System.setProperties(sysProps);
 581         }
 582     }
 583 
 584     @Test
 585     public void testEnablePreviewNoSource(Path base) throws IOException {
 586         tb.writeJavaFiles(base,
 587             "import java.util.Arrays;\n" +
 588             "class HelloWorld {\n" +
 589             "    public static void main(String... args) {\n" +
 590             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 591             "    }\n" +
 592             "}");
 593 
 594         List<String> log = new JavaTask(tb)
 595                 .vmOptions("--enable-preview")
 596                 .className(base.resolve("HelloWorld.java").toString())
 597                 .run(Task.Expect.SUCCESS)
 598                 .getOutputLines(Task.OutputKind.STDOUT);
 599         checkEqual("stdout", log, List.of("Hello World! []"));
 600     }
 601 
 602     @Test
 603     public void testNoMain(Path base) throws IOException {
 604         tb.writeJavaFiles(base, "class NoMain { }");
 605         testError(base.resolve("NoMain.java"), "",
 606                 "error: can't find main(String[]) or main() method in class: NoMain");
 607     }
 608 
 609     @Test
 610     public void testMainBadParams(Path base) throws IOException {
 611         tb.writeJavaFiles(base,
 612                 "class BadParams { public static void main(int n) { } }");
 613         testError(base.resolve("BadParams.java"), "",
 614                 "error: can't find main(String[]) or main() method in class: BadParams");
 615     }
 616 
 617     @Test
 618     public void testMainNotVoid(Path base) throws IOException {
 619         tb.writeJavaFiles(base,
 620                 "class NotVoid { public static int main(String... args) { return 0; } }");
 621         testError(base.resolve("NotVoid.java"), "",
 622                 "error: can't find main(String[]) or main() method in class: NotVoid");
 623     }
 624 
 625     @Test
 626     public void testClassInModule(Path base) throws IOException {
 627         tb.writeJavaFiles(base, "package java.net; class InModule { }");
 628         Path file = base.resolve("java").resolve("net").resolve("InModule.java");
 629         testError(file,
 630                 file + ":1: error: package exists in another module: java.base\n" +
 631                 "package java.net; class InModule { }\n" +
 632                 "^\n" +
 633                 "1 error\n",
 634                 "error: compilation failed");
 635     }
 636 
 637     @Test
 638     public void testNoRecompileWithSuggestions(Path base) throws IOException {
 639         tb.writeJavaFiles(base,
 640             "class NoRecompile {\n" +
 641             "    void use(String s) {}\n" +
 642             "    void test() {\n" +
 643             "        use(1);\n" +
 644             "    }\n" +
 645             "    <T> void test(T t, Object o) {\n" +
 646             "        T t1 = (T) o;\n" +
 647             "    }\n" +
 648             "    static class Generic<T> {\n" +
 649             "        T t;\n" +
 650             "        void raw(Generic raw) {\n" +
 651             "            raw.t = \"\";\n" +
 652             "        }\n" +
 653             "    }\n" +
 654             "    void deprecation() {\n" +
 655             "        Thread.currentThread().stop();\n" +
 656             "    }\n" +
 657             "    void preview(Object o) {\n" +
 658             "      if (o instanceof String s) {\n" +
 659             "          System.out.println(s);\n" +
 660             "      }\n" +
 661             "    }\n" +
 662             "}");
 663         Result r = run(base.resolve("NoRecompile.java"), Collections.emptyList(), Collections.emptyList());
 664         if (r.stdErr.contains("recompile with")) {
 665             error("Unexpected recompile suggestions in error output: " + r.stdErr);
 666         }
 667     }
 668 
 669     @Test
 670     public void testNoOptionsWarnings(Path base) throws IOException {
 671         tb.writeJavaFiles(base, "public class Main { public static void main(String... args) {}}");
 672         String log = new JavaTask(tb)
 673                 .vmOptions("--source", "21")
 674                 .className(base.resolve("Main.java").toString())
 675                 .run(Task.Expect.SUCCESS)
 676                 .getOutput(Task.OutputKind.STDERR);
 677 
 678         if (log.contains("warning: [options]")) {
 679             error("Unexpected options warning in error output: " + log);
 680         }
 681     }
 682 
 683     void testError(Path file, String expectStdErr, String expectFault) throws IOException {
 684         Result r = run(file, Collections.emptyList(), List.of("1", "2", "3"));
 685         checkEmpty("stdout", r.stdOut);
 686         checkEqual("stderr", r.stdErr, expectStdErr);
 687         checkFault("exception", r.exception, expectFault);
 688     }
 689 
 690     /*
 691      * Tests in which main throws an exception.
 692      */
 693     @Test
 694     public void testTargetException1(Path base) throws IOException {
 695         tb.writeJavaFiles(base,
 696             "import java.util.Arrays;\n" +
 697             "class Thrower {\n" +
 698             "    public static void main(String... args) {\n" +
 699             "        throwWhenZero(Integer.parseInt(args[0]));\n" +
 700             "    }\n" +
 701             "    static void throwWhenZero(int arg) {\n" +
 702             "        if (arg == 0) throw new Error(\"zero!\");\n" +
 703             "        throwWhenZero(arg - 1);\n" +
 704             "    }\n" +
 705             "}");
 706         Path file = base.resolve("Thrower.java");
 707         Result r = run(file, Collections.emptyList(), List.of("3"));
 708         checkEmpty("stdout", r.stdOut);
 709         checkEmpty("stderr", r.stdErr);
 710         checkTrace("exception", r.exception,
 711                 "java.lang.Error: zero!",
 712                 "at Thrower.throwWhenZero(Thrower.java:7)",
 713                 "at Thrower.throwWhenZero(Thrower.java:8)",
 714                 "at Thrower.throwWhenZero(Thrower.java:8)",
 715                 "at Thrower.throwWhenZero(Thrower.java:8)",
 716                 "at Thrower.main(Thrower.java:4)");
 717     }
 718 
 719     /*
 720      * Tests in which main throws a traceless exception.
 721      */
 722     @Test
 723     public void testTracelessTargetException(Path base) throws IOException {
 724         tb.writeJavaFiles(base, """
 725             class TestLauncherException extends RuntimeException {
 726                 TestLauncherException() {
 727                     super("No trace", null, true, false); // No writable trace
 728                 }
 729 
 730                 public static void main(String... args) {
 731                     throw new TestLauncherException();
 732                 }
 733             }
 734             """);
 735         Path file = base.resolve("TestLauncherException.java");
 736         SourceLauncherTest.Result r = run(file, List.of(), List.of("3"));
 737         checkEmpty("stdout", r.stdOut());
 738         checkEmpty("stderr", r.stdErr());
 739         checkTrace("exception", r.exception(), "TestLauncherException: No trace");
 740     }
 741 
 742     @Test
 743     public void testNoDuplicateIncubatorWarning(Path base) throws Exception {
 744         Path module = base.resolve("lib");
 745         Path moduleSrc = module.resolve("src");
 746         Path moduleClasses = module.resolve("classes");
 747         Files.createDirectories(moduleClasses);
 748         tb.cleanDirectory(moduleClasses);
 749         tb.writeJavaFiles(moduleSrc, "module test {}");
 750         new JavacTask(tb)
 751                 .outdir(moduleClasses)
 752                 .files(tb.findJavaFiles(moduleSrc))
 753                 .run()
 754                 .writeAll();
 755         markModuleAsIncubator(moduleClasses.resolve("module-info.class"));
 756         tb.writeJavaFiles(base, "public class Main { public static void main(String... args) {}}");
 757         String log = new JavaTask(tb)
 758                 .vmOptions("--module-path", moduleClasses.toString(),
 759                            "--add-modules", "test")
 760                 .className(base.resolve("Main.java").toString())
 761                 .run(Task.Expect.SUCCESS)
 762                 .writeAll()
 763                 .getOutput(Task.OutputKind.STDERR);
 764 
 765         int numberOfWarnings = log.split("WARNING").length - 1;
 766 
 767         if (log.contains("warning:") || numberOfWarnings != 1) {
 768             error("Unexpected warning in error output: " + log);
 769         }
 770 
 771         List<String> compileLog = new JavacTask(tb)
 772                 .options("--module-path", moduleClasses.toString(),
 773                          "--add-modules", "test",
 774                          "-XDrawDiagnostics",
 775                          "-XDsourceLauncher",
 776                          "-XDshould-stop.at=FLOW")
 777                 .files(base.resolve("Main.java").toString())
 778                 .run(Task.Expect.SUCCESS)
 779                 .writeAll()
 780                 .getOutputLines(Task.OutputKind.DIRECT);
 781 
 782         List<String> expectedOutput = List.of(
 783                 "- compiler.warn.incubating.modules: test",
 784                 "1 warning"
 785         );
 786 
 787         if (!expectedOutput.equals(compileLog)) {
 788             error("Unexpected options : " + compileLog);
 789         }
 790     }
 791         //where:
 792         private static void markModuleAsIncubator(Path moduleInfoFile) throws Exception {
 793             ClassModel cf = ClassFile.of().parse(moduleInfoFile);
 794             ModuleResolutionAttribute newAttr = ModuleResolutionAttribute.of(WARN_INCUBATING);
 795             byte[] newBytes = ClassFile.of().transformClass(cf,
 796                     ClassTransform.endHandler(classBuilder -> classBuilder.with(newAttr)));
 797             try (OutputStream out = Files.newOutputStream(moduleInfoFile)) {
 798                 out.write(newBytes);
 799             }
 800         }
 801 
 802     @Test
 803     public void testPrivateConstructor(Path base) throws IOException {
 804         tb.writeJavaFiles(base,
 805                 """
 806                 class PrivateConstructor {
 807                     private PrivateConstructor() {}
 808                     void main() {}
 809                 }
 810                 """);
 811         testError(base.resolve("PrivateConstructor.java"), "",
 812                 """
 813                 error: no non-private zero argument constructor found in class PrivateConstructor
 814                 remove private from existing constructor or define as:
 815                    public PrivateConstructor()""");
 816     }
 817 
 818     @Test
 819     public void testAbstractClassInstanceMain(Path base) throws IOException {
 820         tb.writeJavaFiles(base,
 821                           """
 822                           public abstract class AbstractMain {
 823                               void main(String[] args) {}
 824                           }
 825                           """);
 826         testError(base.resolve("AbstractMain.java"), "",
 827                 "error: abstract class: AbstractMain can not be instantiated");
 828     }
 829 
 830     @Test
 831     public void testWrongMainPrivate(Path base) throws IOException {
 832         tb.writeJavaFiles(base,
 833                           """
 834                           public class WrongMainPrivate {
 835                               private static void main(String[] args) {}
 836                               void main() {
 837                                   System.out.println("correct");
 838                               }
 839                           }
 840                           """);
 841         testSuccess(base.resolve("WrongMainPrivate.java"),
 842                     "correct\n");
 843     }
 844 
 845     @Test
 846     public void testWrongMainPrivateInstance(Path base) throws IOException {
 847         tb.writeJavaFiles(base,
 848                           """
 849                           public class WrongMainPrivate {
 850                               private void main(String[] args) {}
 851                               void main() {
 852                                   System.out.println("correct");
 853                               }
 854                           }
 855                           """);
 856         testSuccess(base.resolve("WrongMainPrivate.java"),
 857                     "correct\n");
 858     }
 859 
 860     @Test
 861     public void testWrongMainReturnType(Path base) throws IOException {
 862         tb.writeJavaFiles(base,
 863                           """
 864                           public class WrongMainReturnType {
 865                               public static int main(String[] args) {
 866                                   return -1;
 867                               }
 868                               void main() {
 869                                   System.out.println("correct");
 870                               }
 871                           }
 872                           """);
 873         testSuccess(base.resolve("WrongMainReturnType.java"),
 874                     "correct\n");
 875     }
 876 
 877     @Test
 878     public void testWrongMainReturnTypeInstance(Path base) throws IOException {
 879         tb.writeJavaFiles(base,
 880                           """
 881                           public class WrongMainReturnType {
 882                               public int main(String[] args) {
 883                                   return -1;
 884                               }
 885                               void main() {
 886                                   System.out.println("correct");
 887                               }
 888                           }
 889                           """);
 890         testSuccess(base.resolve("WrongMainReturnType.java"),
 891                     "correct\n");
 892     }
 893 
 894     @Test
 895     public void testInheritedMain(Path base) throws IOException {
 896         tb.writeJavaFiles(base,
 897             """
 898             class Sub extends Super {}
 899             """,
 900             """
 901             class Super {
 902                 void main() {
 903                     System.out.println(getClass().getName());
 904                 }
 905             }
 906             """);
 907         testSuccess(base.resolve("Sub.java"),
 908                     "Sub\n");
 909     }
 910 
 911     @Test
 912     public void testInheritedMainFromAbstract(Path base) throws IOException {
 913         tb.writeJavaFiles(base,
 914             """
 915             class Sub extends Super {}
 916             """,
 917             """
 918             abstract class Super {
 919                 void main() {
 920                     System.out.println(getClass().getName());
 921                 }
 922             }
 923             """);
 924         testSuccess(base.resolve("Sub.java"),
 925                     "Sub\n");
 926     }
 927 
 928     @Test
 929     public void testInheritedMainFromInterface(Path base) throws IOException {
 930         tb.writeJavaFiles(base,
 931             """
 932             public class Sub implements Super {}
 933             """,
 934             """
 935             public interface Super {
 936                 default void main() {
 937                     System.out.println(getClass().getName());
 938                 }
 939             }
 940             """);
 941         testSuccess(base.resolve("Sub.java"),
 942                     "Sub\n");
 943     }
 944 
 945     Result run(Path file, List<String> runtimeArgs, List<String> appArgs) {
 946         List<String> args = new ArrayList<>();
 947         args.add(file.toString());
 948         args.addAll(appArgs);
 949 
 950         PrintStream prev = System.out;
 951         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 952         try (PrintStream out = new PrintStream(baos, true)) {
 953             System.setOut(out);
 954             StringWriter sw = new StringWriter();
 955             try (PrintWriter err = new PrintWriter(sw, true)) {
 956                 SourceLauncher m = new SourceLauncher(err);
 957                 m.run(toArray(runtimeArgs), toArray(args));
 958                 return new Result(baos.toString(), sw.toString(), null);
 959             } catch (Throwable t) {
 960                 return new Result(baos.toString(), sw.toString(), t);
 961             }
 962         } finally {
 963             System.setOut(prev);
 964         }
 965     }
 966 
 967     void checkEqual(String name, String found, String expect) {
 968         expect = expect.replace("\n", tb.lineSeparator);
 969         out.println(name + ": " + found);
 970         if (!expect.equals(found)) {
 971             error("Unexpected output; expected: " + expect);
 972         }
 973     }
 974 
 975     void checkEqual(String name, List<String> found, List<String> expect) {
 976         out.println(name + ": " + found);
 977         tb.checkEqual(expect, found);
 978     }
 979 
 980     void checkMatch(String name, String found, Pattern expect) {
 981         out.println(name + ": " + found);
 982         if (!expect.matcher(found).matches()) {
 983             error("Unexpected output; expected match for: " + expect);
 984         }
 985     }
 986 
 987     void checkEmpty(String name, String found) {
 988         out.println(name + ": " + found);
 989         if (!found.isEmpty()) {
 990             error("Unexpected output; expected empty string");
 991         }
 992     }
 993 
 994     void checkNull(String name, Throwable found) {
 995         out.println(name + ": " + found);
 996         if (found != null) {
 997             error("Unexpected exception; expected null");
 998         }
 999     }
1000 
1001     void checkFault(String name, Throwable found, String expect) {
1002         out.println(name + ": " + found);
1003         if (found == null) {
1004             error("No exception thrown; expected Fault");
1005         } else {
1006             if (!(found instanceof Fault)) {
1007                 error("Unexpected exception; expected Fault");
1008             }
1009             String actual = found.getMessage();
1010             List<String> actualLines = actual.lines().toList();
1011             List<String> expectLines = expect.lines().toList();
1012             if (!(actualLines.equals(expectLines))) {
1013                 error("Unexpected detail message; expected: \n"
1014                       + expect.indent(2)
1015                       + "\nactual:\n"
1016                       + actual.indent(2));
1017             }
1018         }
1019     }
1020 
1021     void checkTrace(String name, Throwable found, String... expect) {
1022         if (!(found instanceof InvocationTargetException)) {
1023             error("Unexpected exception; expected InvocationTargetException");
1024             out.println("Found:");
1025             found.printStackTrace(out);
1026         }
1027         StringWriter sw = new StringWriter();
1028         try (PrintWriter pw = new PrintWriter(sw)) {
1029             ((InvocationTargetException) found).getTargetException().printStackTrace(pw);
1030         }
1031         String trace = sw.toString();
1032         out.println(name + ":\n" + trace);
1033         String[] traceLines = trace.trim().split("[\r\n]+\\s+");
1034         try {
1035             tb.checkEqual(List.of(traceLines), List.of(expect));
1036         } catch (Error e) {
1037             error(e.getMessage());
1038         }
1039     }
1040 
1041     String[] toArray(List<String> list) {
1042         return list.toArray(new String[list.size()]);
1043     }
1044 
1045     record Result(String stdOut, String stdErr, Throwable exception) {}
1046 }