1 /*
  2  * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /*
 25  * @test
 26  * @bug 8192920 8204588 8246774 8248843 8268869 8235876 8328339 8335896
 27  * @summary Test source launcher
 28  * @library /tools/lib
 29  * @modules jdk.compiler/com.sun.tools.javac.api
 30  *          jdk.compiler/com.sun.tools.javac.launcher
 31  *          jdk.compiler/com.sun.tools.javac.main
 32  *          java.base/jdk.internal.module
 33  * @build toolbox.JavaTask toolbox.JavacTask toolbox.TestRunner toolbox.ToolBox
 34  * @run main SourceLauncherTest
 35  * @ignore Verifier error
 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     private 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      * Negative tests: such as cannot find or execute main method.
289      */
290 
291     @Test
292     public void testHelloWorldWithShebangJava(Path base) throws IOException {
293         tb.writeJavaFiles(base,
294             "#!/usr/bin/java --source " + thisVersion + "\n" +
295             "import java.util.Arrays;\n" +
296             "class HelloWorld {\n" +
297             "    public static void main(String... args) {\n" +
298             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
299             "    }\n" +
300             "}");
301         Path file = base.resolve("HelloWorld.java");
302         testError(file,
303             file + ":1: error: illegal character: '#'\n" +
304             "#!/usr/bin/java --source " + thisVersion + "\n" +
305             "^\n" +
306             file + ":1: error: class, interface, enum, or record expected\n" +
307             "#!/usr/bin/java --source " + thisVersion + "\n" +
308             "  ^\n" +
309             "2 errors\n",
310             "error: compilation failed");
311     }
312 
313     @Test
314     public void testNoClass(Path base) throws IOException {
315         var path = Files.createDirectories(base.resolve("p"));
316         Path file = path.resolve("NoClass.java");
317         Files.write(file, List.of("package p;"));
318         testError(file, "", "error: no class declared in source file");
319     }
320 
321     @Test
322     public void testMismatchOfPathAndPackage(Path base) throws IOException {
323         Files.createDirectories(base);
324         Path file = base.resolve("MismatchOfPathAndPackage.java");
325         Files.write(file, List.of("package p; class MismatchOfPathAndPackage {}"));
326         testError(file, "", "error: end of path to source file does not match its package name p: " + file);
327     }
328 
329     @Test
330     public void testLoadClass(Path base) throws IOException {
331         Path src1 = base.resolve("src1");
332         Path file1 = src1.resolve("LoadClass.java");
333         tb.writeJavaFiles(src1,
334                 "class LoadClass {\n"
335                 + "    public static void main(String... args) {\n"
336                 + "        System.out.println(\"on classpath\");\n"
337                 + "    };\n"
338                 + "}\n");
339         Path classes1 = Files.createDirectories(base.resolve("classes"));
340         new JavacTask(tb)
341                 .outdir(classes1)
342                 .files(file1)
343                 .run();
344         String log1 = new JavaTask(tb)
345                 .classpath(classes1.toString())
346                 .className("LoadClass")
347                 .run(Task.Expect.SUCCESS)
348                 .getOutput(Task.OutputKind.STDOUT);
349         checkEqual("stdout", log1.trim(),
350                 "on classpath");
351 
352         Path src2 = base.resolve("src2");
353         Path file2 = src2.resolve("LoadClass.java");
354         tb.writeJavaFiles(src2,
355                 "class LoadClass {\n"
356                 + "    public static void main(String... args) {\n"
357                 + "        System.out.println(\"in source file\");\n"
358                 + "    };\n"
359                 + "}\n");
360         String log2 = new JavaTask(tb)
361                 .classpath(classes1.toString())
362                 .className(file2.toString())
363                 .run(Task.Expect.SUCCESS)
364                 .getOutput(Task.OutputKind.STDOUT);
365         checkEqual("stdout", log2.trim(),
366                 "in source file");
367     }
368 
369     @Test
370     public void testGetResource(Path base) throws IOException {
371         Path src = base.resolve("src");
372         Path file = src.resolve("GetResource.java");
373         tb.writeJavaFiles(src,
374                 "class GetResource {\n"
375                 + "    public static void main(String... args) {\n"
376                 + "        System.out.println(GetResource.class.getClassLoader().getResource(\"GetResource.class\"));\n"
377                 + "    };\n"
378                 + "}\n");
379         Path classes = Files.createDirectories(base.resolve("classes"));
380         new JavacTask(tb)
381                 .outdir(classes)
382                 .files(file)
383                 .run();
384 
385         String log = new JavaTask(tb)
386                 .classpath(classes.toString())
387                 .className(file.toString())
388                 .run(Task.Expect.SUCCESS)
389                 .getOutput(Task.OutputKind.STDOUT);
390         checkMatch("stdout", log.trim(),
391                 Pattern.compile("sourcelauncher-memoryclassloader[0-9]+:GetResource.class"));
392     }
393 
394     @Test
395     public void testGetResources(Path base) throws IOException {
396         Path src = base.resolve("src");
397         Path file = src.resolve("GetResources.java");
398         tb.writeJavaFiles(src,
399                 "import java.io.*; import java.net.*; import java.util.*;\n"
400                 + "class GetResources {\n"
401                 + "    public static void main(String... args) throws IOException {\n"
402                 + "        Enumeration<URL> e =\n"
403                 + "            GetResources.class.getClassLoader().getResources(\"GetResources.class\");\n"
404                 + "        while (e.hasMoreElements()) System.out.println(e.nextElement());\n"
405                 + "    };\n"
406                 + "}\n");
407         Path classes = Files.createDirectories(base.resolve("classes"));
408         new JavacTask(tb)
409                 .outdir(classes)
410                 .files(file)
411                 .run();
412 
413         List<String> log = new JavaTask(tb)
414                 .classpath(classes.toString())
415                 .className(file.toString())
416                 .run(Task.Expect.SUCCESS)
417                 .getOutputLines(Task.OutputKind.STDOUT);
418         checkMatch("stdout:0", log.get(0).trim(),
419                 Pattern.compile("sourcelauncher-memoryclassloader[0-9]+:GetResources.class"));
420         checkMatch("stdout:1", log.get(1).trim(),
421                 Pattern.compile("file:/.*/testGetResources/classes/GetResources.class"));
422     }
423 
424     @Test
425     public void testSyntaxErr(Path base) throws IOException {
426         tb.writeJavaFiles(base, "class SyntaxErr {");
427         Path file = base.resolve("SyntaxErr.java");
428         testError(file,
429                 file + ":1: error: reached end of file while parsing\n" +
430                 "class SyntaxErr {\n" +
431                 "                 ^\n" +
432                 "1 error\n",
433                 "error: compilation failed");
434     }
435 
436     @Test
437     public void testNoSourceOnClassPath(Path base) throws IOException {
438         Path extraSrc = base.resolve("extraSrc");
439         tb.writeJavaFiles(extraSrc,
440             "public class Extra {\n" +
441             "    static final String MESSAGE = \"Hello World\";\n" +
442             "}\n");
443 
444         Path mainSrc = base.resolve("mainSrc");
445         tb.writeJavaFiles(mainSrc,
446             "import java.util.Arrays;\n" +
447             "class HelloWorld {\n" +
448             "    public static void main(String... args) {\n" +
449             "        System.out.println(Extra.MESSAGE + Arrays.toString(args));\n" +
450             "    }\n" +
451             "}");
452 
453         List<String> javacArgs = List.of("-classpath", extraSrc.toString());
454         List<String> classArgs = List.of("1", "2", "3");
455         String FS = File.separator;
456         String expectStdErr =
457             "testNoSourceOnClassPath" + FS + "mainSrc" + FS + "HelloWorld.java:4: error: cannot find symbol\n" +
458             "        System.out.println(Extra.MESSAGE + Arrays.toString(args));\n" +
459             "                           ^\n" +
460             "  symbol:   variable Extra\n" +
461             "  location: class HelloWorld\n" +
462             "1 error\n";
463         Result r = run(mainSrc.resolve("HelloWorld.java"), javacArgs, classArgs);
464         checkEmpty("stdout", r.stdOut);
465         checkEqual("stderr", r.stdErr, expectStdErr);
466         checkFault("exception", r.exception, "error: compilation failed");
467     }
468 
469     @Test
470     public void testClassNotFound(Path base) throws IOException {
471         Path src = base.resolve("src");
472         Path file = src.resolve("ClassNotFound.java");
473         tb.writeJavaFiles(src,
474                 "class ClassNotFound {\n"
475                 + "    public static void main(String... args) {\n"
476                 + "        try {\n"
477                 + "            Class.forName(\"NoSuchClass\");\n"
478                 + "            System.out.println(\"no exception\");\n"
479                 + "            System.exit(1);\n"
480                 + "        } catch (ClassNotFoundException e) {\n"
481                 + "            System.out.println(\"Expected exception thrown: \" + e);\n"
482                 + "        }\n"
483                 + "    };\n"
484                 + "}\n");
485         Path classes = Files.createDirectories(base.resolve("classes"));
486         new JavacTask(tb)
487                 .outdir(classes)
488                 .files(file)
489                 .run();
490 
491         String log = new JavaTask(tb)
492                 .classpath(classes.toString())
493                 .className(file.toString())
494                 .run(Task.Expect.SUCCESS)
495                 .getOutput(Task.OutputKind.STDOUT);
496         checkEqual("stdout", log.trim(),
497                 "Expected exception thrown: java.lang.ClassNotFoundException: NoSuchClass");
498     }
499 
500     // For any source file that is invoked through the OS shebang mechanism, invalid shebang
501     // lines will be caught and handled by the OS, before the launcher is even invoked.
502     // However, if such a file is passed directly to the launcher, perhaps using the --source
503     // option, a well-formed shebang line will be removed but a badly-formed one will be not be
504     // removed and will cause compilation errors.
505     @Test
506     public void testBadShebang(Path base) throws IOException {
507         tb.writeJavaFiles(base,
508             "#/usr/bin/java --source " + thisVersion + "\n" +
509             "import java.util.Arrays;\n" +
510             "class HelloWorld {\n" +
511             "    public static void main(String... args) {\n" +
512             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
513             "    }\n" +
514             "}");
515         Path file = base.resolve("HelloWorld.java");
516         testError(file,
517             file + ":1: error: illegal character: '#'\n" +
518             "#/usr/bin/java --source " + thisVersion + "\n" +
519             "^\n" +
520             file + ":1: error: class, interface, enum, or record expected\n" +
521             "#/usr/bin/java --source " + thisVersion + "\n" +
522             "  ^\n" +
523             "2 errors\n",
524             "error: compilation failed");
525     }
526 
527     @Test
528     public void testBadSourceOpt(Path base) throws IOException {
529         Files.createDirectories(base);
530         Path file = base.resolve("DummyClass.java");
531         Files.write(file, List.of("class DummyClass { }"));
532         Properties sysProps = System.getProperties();
533         Properties p = new Properties(sysProps);
534         p.setProperty("jdk.internal.javac.source", "<BAD>");
535         System.setProperties(p);
536         try {
537             testError(file, "", "error: invalid value for --source option: <BAD>");
538         } finally {
539             System.setProperties(sysProps);
540         }
541     }
542 
543     @Test
544     public void testEnablePreviewNoSource(Path base) throws IOException {
545         tb.writeJavaFiles(base,
546             "import java.util.Arrays;\n" +
547             "class HelloWorld {\n" +
548             "    public static void main(String... args) {\n" +
549             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
550             "    }\n" +
551             "}");
552 
553         List<String> log = new JavaTask(tb)
554                 .vmOptions("--enable-preview")
555                 .className(base.resolve("HelloWorld.java").toString())
556                 .run(Task.Expect.SUCCESS)
557                 .getOutputLines(Task.OutputKind.STDOUT);
558         checkEqual("stdout", log, List.of("Hello World! []"));
559     }
560 
561     @Test
562     public void testNoMain(Path base) throws IOException {
563         tb.writeJavaFiles(base, "class NoMain { }");
564         testError(base.resolve("NoMain.java"), "",
565                 "error: can't find main(String[]) method in class: NoMain");
566     }
567 
568     @Test
569     public void testMainBadParams(Path base) throws IOException {
570         tb.writeJavaFiles(base,
571                 "class BadParams { public static void main() { } }");
572         testError(base.resolve("BadParams.java"), "",
573                 "error: can't find main(String[]) method in class: BadParams");
574     }
575 
576     @Test
577     public void testMainNotPublic(Path base) throws IOException {
578         tb.writeJavaFiles(base,
579                 "class NotPublic { static void main(String... args) { } }");
580         testError(base.resolve("NotPublic.java"), "",
581                 "error: can't find main(String[]) method in class: NotPublic");
582     }
583 
584     @Test
585     public void testMainNotStatic(Path base) throws IOException {
586         tb.writeJavaFiles(base,
587                 "class NotStatic { public void main(String... args) { } }");
588         testError(base.resolve("NotStatic.java"), "",
589                 "error: can't find main(String[]) method in class: NotStatic");
590     }
591 
592     @Test
593     public void testMainNotVoid(Path base) throws IOException {
594         tb.writeJavaFiles(base,
595                 "class NotVoid { public static int main(String... args) { return 0; } }");
596         testError(base.resolve("NotVoid.java"), "",
597                 "error: can't find main(String[]) method in class: NotVoid");
598     }
599 
600     @Test
601     public void testClassInModule(Path base) throws IOException {
602         tb.writeJavaFiles(base, "package java.net; class InModule { }");
603         Path file = base.resolve("java").resolve("net").resolve("InModule.java");
604         testError(file,
605                 file + ":1: error: package exists in another module: java.base\n" +
606                 "package java.net; class InModule { }\n" +
607                 "^\n" +
608                 "1 error\n",
609                 "error: compilation failed");
610     }
611 
612     @Test
613     public void testNoRecompileWithSuggestions(Path base) throws IOException {
614         tb.writeJavaFiles(base,
615             "class NoRecompile {\n" +
616             "    void use(String s) {}\n" +
617             "    void test() {\n" +
618             "        use(1);\n" +
619             "    }\n" +
620             "    <T> void test(T t, Object o) {\n" +
621             "        T t1 = (T) o;\n" +
622             "    }\n" +
623             "    static class Generic<T> {\n" +
624             "        T t;\n" +
625             "        void raw(Generic raw) {\n" +
626             "            raw.t = \"\";\n" +
627             "        }\n" +
628             "    }\n" +
629             "    void deprecation() {\n" +
630             "        Thread.currentThread().stop();\n" +
631             "    }\n" +
632             "    void preview(Object o) {\n" +
633             "      if (o instanceof String s) {\n" +
634             "          System.out.println(s);\n" +
635             "      }\n" +
636             "    }\n" +
637             "}");
638         Result r = run(base.resolve("NoRecompile.java"), Collections.emptyList(), Collections.emptyList());
639         if (r.stdErr.contains("recompile with")) {
640             error("Unexpected recompile suggestions in error output: " + r.stdErr);
641         }
642     }
643 
644     @Test
645     public void testNoOptionsWarnings(Path base) throws IOException {
646         tb.writeJavaFiles(base, "public class Main { public static void main(String... args) {}}");
647         String log = new JavaTask(tb)
648                 .vmOptions("--source", "21")
649                 .className(base.resolve("Main.java").toString())
650                 .run(Task.Expect.SUCCESS)
651                 .getOutput(Task.OutputKind.STDERR);
652 
653         if (log.contains("warning: [options]")) {
654             error("Unexpected options warning in error output: " + log);
655         }
656     }
657 
658     void testError(Path file, String expectStdErr, String expectFault) throws IOException {
659         Result r = run(file, Collections.emptyList(), List.of("1", "2", "3"));
660         checkEmpty("stdout", r.stdOut);
661         checkEqual("stderr", r.stdErr, expectStdErr);
662         checkFault("exception", r.exception, expectFault);
663     }
664 
665     /*
666      * Tests in which main throws an exception.
667      */
668     @Test
669     public void testTargetException1(Path base) throws IOException {
670         tb.writeJavaFiles(base,
671             "import java.util.Arrays;\n" +
672             "class Thrower {\n" +
673             "    public static void main(String... args) {\n" +
674             "        throwWhenZero(Integer.parseInt(args[0]));\n" +
675             "    }\n" +
676             "    static void throwWhenZero(int arg) {\n" +
677             "        if (arg == 0) throw new Error(\"zero!\");\n" +
678             "        throwWhenZero(arg - 1);\n" +
679             "    }\n" +
680             "}");
681         Path file = base.resolve("Thrower.java");
682         Result r = run(file, Collections.emptyList(), List.of("3"));
683         checkEmpty("stdout", r.stdOut);
684         checkEmpty("stderr", r.stdErr);
685         checkTrace("exception", r.exception,
686                 "java.lang.Error: zero!",
687                 "at Thrower.throwWhenZero(Thrower.java:7)",
688                 "at Thrower.throwWhenZero(Thrower.java:8)",
689                 "at Thrower.throwWhenZero(Thrower.java:8)",
690                 "at Thrower.throwWhenZero(Thrower.java:8)",
691                 "at Thrower.main(Thrower.java:4)");
692     }
693 
694     @Test
695     public void testNoDuplicateIncubatorWarning(Path base) throws Exception {
696         Path module = base.resolve("lib");
697         Path moduleSrc = module.resolve("src");
698         Path moduleClasses = module.resolve("classes");
699         Files.createDirectories(moduleClasses);
700         tb.cleanDirectory(moduleClasses);
701         tb.writeJavaFiles(moduleSrc, "module test {}");
702         new JavacTask(tb)
703                 .outdir(moduleClasses)
704                 .files(tb.findJavaFiles(moduleSrc))
705                 .run()
706                 .writeAll();
707         markModuleAsIncubator(moduleClasses.resolve("module-info.class"));
708         tb.writeJavaFiles(base, "public class Main { public static void main(String... args) {}}");
709         String log = new JavaTask(tb)
710                 .vmOptions("--module-path", moduleClasses.toString(),
711                            "--add-modules", "test")
712                 .className(base.resolve("Main.java").toString())
713                 .run(Task.Expect.SUCCESS)
714                 .writeAll()
715                 .getOutput(Task.OutputKind.STDERR);
716 
717         int numberOfWarnings = log.split("WARNING").length - 1;
718 
719         if (log.contains("warning:") || numberOfWarnings != 1) {
720             error("Unexpected warning in error output: " + log);
721         }
722 
723         List<String> compileLog = new JavacTask(tb)
724                 .options("--module-path", moduleClasses.toString(),
725                          "--add-modules", "test",
726                          "-XDrawDiagnostics",
727                          "-XDsourceLauncher",
728                          "-XDshould-stop.at=FLOW")
729                 .files(base.resolve("Main.java").toString())
730                 .run(Task.Expect.SUCCESS)
731                 .writeAll()
732                 .getOutputLines(Task.OutputKind.DIRECT);
733 
734         List<String> expectedOutput = List.of(
735                 "- compiler.warn.incubating.modules: test",
736                 "1 warning"
737         );
738 
739         if (!expectedOutput.equals(compileLog)) {
740             error("Unexpected options : " + compileLog);
741         }
742     }
743         //where:
744         private static void markModuleAsIncubator(Path moduleInfoFile) throws Exception {
745             ClassModel cf = ClassFile.of().parse(moduleInfoFile);
746             ModuleResolutionAttribute newAttr = ModuleResolutionAttribute.of(WARN_INCUBATING);
747             byte[] newBytes = ClassFile.of().transformClass(cf,
748                     ClassTransform.endHandler(classBuilder -> classBuilder.with(newAttr)));
749             try (OutputStream out = Files.newOutputStream(moduleInfoFile)) {
750                 out.write(newBytes);
751             }
752         }
753 
754     Result run(Path file, List<String> runtimeArgs, List<String> appArgs) {
755         List<String> args = new ArrayList<>();
756         args.add(file.toString());
757         args.addAll(appArgs);
758 
759         PrintStream prev = System.out;
760         ByteArrayOutputStream baos = new ByteArrayOutputStream();
761         try (PrintStream out = new PrintStream(baos, true)) {
762             System.setOut(out);
763             StringWriter sw = new StringWriter();
764             try (PrintWriter err = new PrintWriter(sw, true)) {
765                 SourceLauncher m = new SourceLauncher(err);
766                 m.run(toArray(runtimeArgs), toArray(args));
767                 return new Result(baos.toString(), sw.toString(), null);
768             } catch (Throwable t) {
769                 return new Result(baos.toString(), sw.toString(), t);
770             }
771         } finally {
772             System.setOut(prev);
773         }
774     }
775 
776     void checkEqual(String name, String found, String expect) {
777         expect = expect.replace("\n", tb.lineSeparator);
778         out.println(name + ": " + found);
779         if (!expect.equals(found)) {
780             error("Unexpected output; expected: " + expect);
781         }
782     }
783 
784     void checkContains(String name, String found, String expect) {
785         expect = expect.replace("\n", tb.lineSeparator);
786         out.println(name + ": " + found);
787         if (!found.contains(expect)) {
788             error("Expected output not found: " + expect);
789         }
790     }
791 
792     void checkEqual(String name, List<String> found, List<String> expect) {
793         out.println(name + ": " + found);
794         tb.checkEqual(expect, found);
795     }
796 
797     void checkMatch(String name, String found, Pattern expect) {
798         out.println(name + ": " + found);
799         if (!expect.matcher(found).matches()) {
800             error("Unexpected output; expected match for: " + expect);
801         }
802     }
803 
804     void checkEmpty(String name, String found) {
805         out.println(name + ": " + found);
806         if (!found.isEmpty()) {
807             error("Unexpected output; expected empty string");
808         }
809     }
810 
811     void checkNull(String name, Throwable found) {
812         out.println(name + ": " + found);
813         if (found != null) {
814             error("Unexpected exception; expected null");
815         }
816     }
817 
818     void checkFault(String name, Throwable found, String expect) {
819         expect = expect.replace("\n", tb.lineSeparator);
820         out.println(name + ": " + found);
821         if (found == null) {
822             error("No exception thrown; expected Fault");
823         } else {
824             if (!(found instanceof Fault)) {
825                 error("Unexpected exception; expected Fault");
826             }
827             if (!(found.getMessage().equals(expect))) {
828                 error("Unexpected detail message; expected: " + expect);
829             }
830         }
831     }
832 
833     void checkTrace(String name, Throwable found, String... expect) {
834         if (!(found instanceof InvocationTargetException)) {
835             error("Unexpected exception; expected InvocationTargetException");
836             out.println("Found:");
837             found.printStackTrace(out);
838         }
839         StringWriter sw = new StringWriter();
840         try (PrintWriter pw = new PrintWriter(sw)) {
841             ((InvocationTargetException) found).getTargetException().printStackTrace(pw);
842         }
843         String trace = sw.toString();
844         out.println(name + ":\n" + trace);
845         String[] traceLines = trace.trim().split("[\r\n]+\\s+");
846         try {
847             tb.checkEqual(List.of(traceLines), List.of(expect));
848         } catch (Error e) {
849             error(e.getMessage());
850         }
851     }
852 
853     String[] toArray(List<String> list) {
854         return list.toArray(new String[list.size()]);
855     }
856 
857     record Result(String stdOut, String stdErr, Throwable exception) {}
858 }