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