1 /*
  2  * Copyright (c) 2018, 2023, 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
 27  * @summary Test source launcher
 28  * @library /tools/lib
 29  * @enablePreview
 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.classfile.impl
 34  *          java.base/jdk.internal.module
 35  * @build toolbox.JavaTask toolbox.JavacTask toolbox.TestRunner toolbox.ToolBox
 36  * @run main SourceLauncherTest
 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.HashMap;
 55 import java.util.Map;
 56 import java.util.List;
 57 import java.util.Properties;
 58 import java.util.regex.Pattern;
 59 import java.util.stream.Collectors;
 60 
 61 import com.sun.tools.javac.launcher.SourceLauncher;
 62 import com.sun.tools.javac.launcher.Fault;
 63 
 64 import toolbox.JavaTask;
 65 import toolbox.JavacTask;
 66 import toolbox.Task;
 67 import toolbox.TestRunner;
 68 import toolbox.ToolBox;
 69 
 70 import static jdk.internal.module.ClassFileConstants.WARN_INCUBATING;
 71 
 72 public class SourceLauncherTest extends TestRunner {
 73     public static void main(String... args) throws Exception {
 74         SourceLauncherTest t = new SourceLauncherTest();
 75         t.runTests(m -> new Object[] { Paths.get(m.getName()) });
 76     }
 77 
 78     SourceLauncherTest() {
 79         super(System.err);
 80         tb = new ToolBox();
 81         System.err.println("version: " + thisVersion);
 82     }
 83 
 84     private final ToolBox tb;
 85     private static final String thisVersion = System.getProperty("java.specification.version");
 86 
 87     /*
 88      * Positive tests.
 89      */
 90 
 91     @Test
 92     public void testHelloWorld(Path base) throws IOException {
 93         tb.writeJavaFiles(base,
 94             "import java.util.Arrays;\n" +
 95             "class HelloWorld {\n" +
 96             "    public static void main(String... args) {\n" +
 97             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 98             "    }\n" +
 99             "}");
100         testSuccess(base.resolve("HelloWorld.java"), "Hello World! [1, 2, 3]\n");
101     }
102 
103     @Test
104     public void testHelloWorldInPackage(Path base) throws IOException {
105         tb.writeJavaFiles(base,
106             "package hello;\n" +
107             "import java.util.Arrays;\n" +
108             "class World {\n" +
109             "    public static void main(String... args) {\n" +
110             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
111             "    }\n" +
112             "}");
113         testSuccess(base.resolve("hello").resolve("World.java"), "Hello World! [1, 2, 3]\n");
114     }
115 
116     @Test
117     public void testHelloWorldWithAux(Path base) throws IOException {
118         tb.writeJavaFiles(base,
119             "import java.util.Arrays;\n" +
120             "class HelloWorld {\n" +
121             "    public static void main(String... args) {\n" +
122             "        Aux.write(args);\n" +
123             "    }\n" +
124             "}\n" +
125             "class Aux {\n" +
126             "    static void write(String... args) {\n" +
127             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
128             "    }\n" +
129             "}");
130         testSuccess(base.resolve("HelloWorld.java"), "Hello World! [1, 2, 3]\n");
131     }
132 
133     @Test
134     public void testHelloWorldWithShebang(Path base) throws IOException {
135         tb.writeJavaFiles(base,
136             "#!/usr/bin/java --source " + thisVersion + "\n" +
137             "import java.util.Arrays;\n" +
138             "class HelloWorld {\n" +
139             "    public static void main(String... args) {\n" +
140             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
141             "    }\n" +
142             "}");
143         Files.copy(base.resolve("HelloWorld.java"), base.resolve("HelloWorld"));
144         testSuccess(base.resolve("HelloWorld"), "Hello World! [1, 2, 3]\n");
145     }
146 
147     @Test
148     public void testNoAnnoProcessing(Path base) throws IOException {
149         Path annoSrc = base.resolve("annoSrc");
150         tb.writeJavaFiles(annoSrc,
151             "import java.util.*;\n" +
152             "import javax.annotation.processing.*;\n" +
153             "import javax.lang.model.element.*;\n" +
154             "@SupportedAnnotationTypes(\"*\")\n" +
155             "public class AnnoProc extends AbstractProcessor {\n" +
156             "    public boolean process(Set<? extends TypeElement> annos, RoundEnvironment rEnv) {\n" +
157             "        throw new Error(\"Annotation processor should not be invoked\");\n" +
158             "    }\n" +
159             "}\n");
160         Path annoClasses = Files.createDirectories(base.resolve("classes"));
161         new JavacTask(tb)
162                 .outdir(annoClasses)
163                 .files(annoSrc.resolve("AnnoProc.java").toString())
164                 .run();
165         Path serviceFile = annoClasses.resolve("META-INF").resolve("services")
166                 .resolve("javax.annotation.processing.Processor");
167         tb.writeFile(serviceFile, "AnnoProc");
168 
169         Path mainSrc = base.resolve("mainSrc");
170         tb.writeJavaFiles(mainSrc,
171             "import java.util.Arrays;\n" +
172             "class HelloWorld {\n" +
173             "    public static void main(String... args) {\n" +
174             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
175             "    }\n" +
176             "}");
177 
178         List<String> javacArgs = List.of("-classpath", annoClasses.toString());
179         List<String> classArgs = List.of("1", "2", "3");
180         String expect = "Hello World! [1, 2, 3]\n";
181         Result r = run(mainSrc.resolve("HelloWorld.java"), javacArgs, classArgs);
182         checkEqual("stdout", r.stdOut, expect);
183         checkEmpty("stderr", r.stdErr);
184         checkNull("exception", r.exception);
185     }
186 
187     @Test
188     public void testEnablePreview(Path base) throws IOException {
189         tb.writeJavaFiles(base,
190             "import java.util.Arrays;\n" +
191             "class HelloWorld {\n" +
192             "    public static void main(String... args) {\n" +
193             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
194             "    }\n" +
195             "}");
196 
197         String log = new JavaTask(tb)
198                 .vmOptions("--enable-preview", "--source", thisVersion)
199                 .className(base.resolve("HelloWorld.java").toString())
200                 .classArgs("1", "2", "3")
201                 .run(Task.Expect.SUCCESS)
202                 .getOutput(Task.OutputKind.STDOUT);
203         checkEqual("stdout", log.trim(), "Hello World! [1, 2, 3]");
204     }
205 
206     @Test
207     public void testCodeSource(Path base) throws IOException {
208         tb.writeJavaFiles(base,
209             "import java.net.URL;\n" +
210             "class ShowCodeSource {\n" +
211             "    public static void main(String... args) {\n" +
212             "        URL u = ShowCodeSource.class.getProtectionDomain().getCodeSource().getLocation();\n" +
213             "        System.out.println(u);\n" +
214             "    }\n" +
215             "}");
216 
217         Path file = base.resolve("ShowCodeSource.java");
218         String log = new JavaTask(tb)
219                 .className(file.toString())
220                 .run(Task.Expect.SUCCESS)
221                 .getOutput(Task.OutputKind.STDOUT);
222         checkEqual("stdout", log.trim(), file.toAbsolutePath().toUri().toURL().toString());
223     }
224 
225     @Test
226     public void testSecurityManager(Path base) throws IOException {
227         Path sourceFile = base.resolve("HelloWorld.java");
228         tb.writeJavaFiles(base,
229                 "class HelloWorld {\n" +
230                         "    public static void main(String... args) {\n" +
231                         "        System.out.println(\"Hello World!\");\n" +
232                         "    }\n" +
233                         "}");
234 
235         String log = new JavaTask(tb)
236                 .vmOptions("-Djava.security.manager=default")
237                 .className(sourceFile.toString())
238                 .run(Task.Expect.FAIL)
239                 .getOutput(Task.OutputKind.STDERR);
240         checkContains("stderr", log,
241                 "error: cannot use source-code launcher with a security manager enabled");
242     }
243 
244     @Test
245     public void testSystemProperty(Path base) throws IOException {
246         tb.writeJavaFiles(base,
247             "class ShowProperty {\n" +
248             "    public static void main(String... args) {\n" +
249             "        System.out.println(System.getProperty(\"jdk.launcher.sourcefile\"));\n" +
250             "    }\n" +
251             "}");
252 
253         Path file = base.resolve("ShowProperty.java");
254         String log = new JavaTask(tb)
255                 .className(file.toString())
256                 .run(Task.Expect.SUCCESS)
257                 .getOutput(Task.OutputKind.STDOUT);
258         checkEqual("stdout", log.trim(), file.toAbsolutePath().toString());
259     }
260 
261     void testSuccess(Path file, String expect) throws IOException {
262         Result r = run(file, Collections.emptyList(), List.of("1", "2", "3"));
263         checkEqual("stdout", r.stdOut, expect);
264         checkEmpty("stderr", r.stdErr);
265         checkNull("exception", r.exception);
266     }
267 
268     /*
269      * Negative tests: such as cannot find or execute main method.
270      */
271 
272     @Test
273     public void testHelloWorldWithShebangJava(Path base) throws IOException {
274         tb.writeJavaFiles(base,
275             "#!/usr/bin/java --source " + thisVersion + "\n" +
276             "import java.util.Arrays;\n" +
277             "class HelloWorld {\n" +
278             "    public static void main(String... args) {\n" +
279             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
280             "    }\n" +
281             "}");
282         Path file = base.resolve("HelloWorld.java");
283         testError(file,
284             file + ":1: error: illegal character: '#'\n" +
285             "#!/usr/bin/java --source " + thisVersion + "\n" +
286             "^\n" +
287             file + ":1: error: class, interface, enum, or record expected\n" +
288             "#!/usr/bin/java --source " + thisVersion + "\n" +
289             "  ^\n" +
290             "2 errors\n",
291             "error: compilation failed");
292     }
293 
294     @Test
295     public void testNoClass(Path base) throws IOException {
296         var path = Files.createDirectories(base.resolve("p"));
297         Path file = path.resolve("NoClass.java");
298         Files.write(file, List.of("package p;"));
299         testError(file, "", "error: no class declared in source file");
300     }
301 
302     @Test
303     public void testMismatchOfPathAndPackage(Path base) throws IOException {
304         Files.createDirectories(base);
305         Path file = base.resolve("MismatchOfPathAndPackage.java");
306         Files.write(file, List.of("package p;"));
307         testError(file, "", "error: end of path to source file does not match its package name p: " + file);
308     }
309 
310     @Test
311     public void testLoadClass(Path base) throws IOException {
312         Path src1 = base.resolve("src1");
313         Path file1 = src1.resolve("LoadClass.java");
314         tb.writeJavaFiles(src1,
315                 "class LoadClass {\n"
316                 + "    public static void main(String... args) {\n"
317                 + "        System.out.println(\"on classpath\");\n"
318                 + "    };\n"
319                 + "}\n");
320         Path classes1 = Files.createDirectories(base.resolve("classes"));
321         new JavacTask(tb)
322                 .outdir(classes1)
323                 .files(file1)
324                 .run();
325         String log1 = new JavaTask(tb)
326                 .classpath(classes1.toString())
327                 .className("LoadClass")
328                 .run(Task.Expect.SUCCESS)
329                 .getOutput(Task.OutputKind.STDOUT);
330         checkEqual("stdout", log1.trim(),
331                 "on classpath");
332 
333         Path src2 = base.resolve("src2");
334         Path file2 = src2.resolve("LoadClass.java");
335         tb.writeJavaFiles(src2,
336                 "class LoadClass {\n"
337                 + "    public static void main(String... args) {\n"
338                 + "        System.out.println(\"in source file\");\n"
339                 + "    };\n"
340                 + "}\n");
341         String log2 = new JavaTask(tb)
342                 .classpath(classes1.toString())
343                 .className(file2.toString())
344                 .run(Task.Expect.SUCCESS)
345                 .getOutput(Task.OutputKind.STDOUT);
346         checkEqual("stdout", log2.trim(),
347                 "in source file");
348     }
349 
350     @Test
351     public void testGetResource(Path base) throws IOException {
352         Path src = base.resolve("src");
353         Path file = src.resolve("GetResource.java");
354         tb.writeJavaFiles(src,
355                 "class GetResource {\n"
356                 + "    public static void main(String... args) {\n"
357                 + "        System.out.println(GetResource.class.getClassLoader().getResource(\"GetResource.class\"));\n"
358                 + "    };\n"
359                 + "}\n");
360         Path classes = Files.createDirectories(base.resolve("classes"));
361         new JavacTask(tb)
362                 .outdir(classes)
363                 .files(file)
364                 .run();
365 
366         String log = new JavaTask(tb)
367                 .classpath(classes.toString())
368                 .className(file.toString())
369                 .run(Task.Expect.SUCCESS)
370                 .getOutput(Task.OutputKind.STDOUT);
371         checkMatch("stdout", log.trim(),
372                 Pattern.compile("sourcelauncher-memoryclassloader[0-9]+:GetResource.class"));
373     }
374 
375     @Test
376     public void testGetResources(Path base) throws IOException {
377         Path src = base.resolve("src");
378         Path file = src.resolve("GetResources.java");
379         tb.writeJavaFiles(src,
380                 "import java.io.*; import java.net.*; import java.util.*;\n"
381                 + "class GetResources {\n"
382                 + "    public static void main(String... args) throws IOException {\n"
383                 + "        Enumeration<URL> e =\n"
384                 + "            GetResources.class.getClassLoader().getResources(\"GetResources.class\");\n"
385                 + "        while (e.hasMoreElements()) System.out.println(e.nextElement());\n"
386                 + "    };\n"
387                 + "}\n");
388         Path classes = Files.createDirectories(base.resolve("classes"));
389         new JavacTask(tb)
390                 .outdir(classes)
391                 .files(file)
392                 .run();
393 
394         List<String> log = new JavaTask(tb)
395                 .classpath(classes.toString())
396                 .className(file.toString())
397                 .run(Task.Expect.SUCCESS)
398                 .getOutputLines(Task.OutputKind.STDOUT);
399         checkMatch("stdout:0", log.get(0).trim(),
400                 Pattern.compile("sourcelauncher-memoryclassloader[0-9]+:GetResources.class"));
401         checkMatch("stdout:1", log.get(1).trim(),
402                 Pattern.compile("file:/.*/testGetResources/classes/GetResources.class"));
403     }
404 
405     @Test
406     public void testSyntaxErr(Path base) throws IOException {
407         tb.writeJavaFiles(base, "class SyntaxErr {");
408         Path file = base.resolve("SyntaxErr.java");
409         testError(file,
410                 file + ":1: error: reached end of file while parsing\n" +
411                 "class SyntaxErr {\n" +
412                 "                 ^\n" +
413                 "1 error\n",
414                 "error: compilation failed");
415     }
416 
417     @Test
418     public void testNoSourceOnClassPath(Path base) throws IOException {
419         Path extraSrc = base.resolve("extraSrc");
420         tb.writeJavaFiles(extraSrc,
421             "public class Extra {\n" +
422             "    static final String MESSAGE = \"Hello World\";\n" +
423             "}\n");
424 
425         Path mainSrc = base.resolve("mainSrc");
426         tb.writeJavaFiles(mainSrc,
427             "import java.util.Arrays;\n" +
428             "class HelloWorld {\n" +
429             "    public static void main(String... args) {\n" +
430             "        System.out.println(Extra.MESSAGE + Arrays.toString(args));\n" +
431             "    }\n" +
432             "}");
433 
434         List<String> javacArgs = List.of("-classpath", extraSrc.toString());
435         List<String> classArgs = List.of("1", "2", "3");
436         String FS = File.separator;
437         String expectStdErr =
438             "testNoSourceOnClassPath" + FS + "mainSrc" + FS + "HelloWorld.java:4: error: cannot find symbol\n" +
439             "        System.out.println(Extra.MESSAGE + Arrays.toString(args));\n" +
440             "                           ^\n" +
441             "  symbol:   variable Extra\n" +
442             "  location: class HelloWorld\n" +
443             "1 error\n";
444         Result r = run(mainSrc.resolve("HelloWorld.java"), javacArgs, classArgs);
445         checkEmpty("stdout", r.stdOut);
446         checkEqual("stderr", r.stdErr, expectStdErr);
447         checkFault("exception", r.exception, "error: compilation failed");
448     }
449 
450     @Test
451     public void testClassNotFound(Path base) throws IOException {
452         Path src = base.resolve("src");
453         Path file = src.resolve("ClassNotFound.java");
454         tb.writeJavaFiles(src,
455                 "class ClassNotFound {\n"
456                 + "    public static void main(String... args) {\n"
457                 + "        try {\n"
458                 + "            Class.forName(\"NoSuchClass\");\n"
459                 + "            System.out.println(\"no exception\");\n"
460                 + "            System.exit(1);\n"
461                 + "        } catch (ClassNotFoundException e) {\n"
462                 + "            System.out.println(\"Expected exception thrown: \" + e);\n"
463                 + "        }\n"
464                 + "    };\n"
465                 + "}\n");
466         Path classes = Files.createDirectories(base.resolve("classes"));
467         new JavacTask(tb)
468                 .outdir(classes)
469                 .files(file)
470                 .run();
471 
472         String log = new JavaTask(tb)
473                 .classpath(classes.toString())
474                 .className(file.toString())
475                 .run(Task.Expect.SUCCESS)
476                 .getOutput(Task.OutputKind.STDOUT);
477         checkEqual("stdout", log.trim(),
478                 "Expected exception thrown: java.lang.ClassNotFoundException: NoSuchClass");
479     }
480 
481     // For any source file that is invoked through the OS shebang mechanism, invalid shebang
482     // lines will be caught and handled by the OS, before the launcher is even invoked.
483     // However, if such a file is passed directly to the launcher, perhaps using the --source
484     // option, a well-formed shebang line will be removed but a badly-formed one will be not be
485     // removed and will cause compilation errors.
486     @Test
487     public void testBadShebang(Path base) throws IOException {
488         tb.writeJavaFiles(base,
489             "#/usr/bin/java --source " + thisVersion + "\n" +
490             "import java.util.Arrays;\n" +
491             "class HelloWorld {\n" +
492             "    public static void main(String... args) {\n" +
493             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
494             "    }\n" +
495             "}");
496         Path file = base.resolve("HelloWorld.java");
497         testError(file,
498             file + ":1: error: illegal character: '#'\n" +
499             "#/usr/bin/java --source " + thisVersion + "\n" +
500             "^\n" +
501             file + ":1: error: class, interface, enum, or record expected\n" +
502             "#/usr/bin/java --source " + thisVersion + "\n" +
503             "  ^\n" +
504             "2 errors\n",
505             "error: compilation failed");
506     }
507 
508     @Test
509     public void testBadSourceOpt(Path base) throws IOException {
510         Files.createDirectories(base);
511         Path file = base.resolve("DummyClass.java");
512         Files.write(file, List.of("class DummyClass { }"));
513         Properties sysProps = System.getProperties();
514         Properties p = new Properties(sysProps);
515         p.setProperty("jdk.internal.javac.source", "<BAD>");
516         System.setProperties(p);
517         try {
518             testError(file, "", "error: invalid value for --source option: <BAD>");
519         } finally {
520             System.setProperties(sysProps);
521         }
522     }
523 
524     @Test
525     public void testEnablePreviewNoSource(Path base) throws IOException {
526         tb.writeJavaFiles(base,
527             "import java.util.Arrays;\n" +
528             "class HelloWorld {\n" +
529             "    public static void main(String... args) {\n" +
530             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
531             "    }\n" +
532             "}");
533 
534         List<String> log = new JavaTask(tb)
535                 .vmOptions("--enable-preview")
536                 .className(base.resolve("HelloWorld.java").toString())
537                 .run(Task.Expect.FAIL)
538                 .getOutputLines(Task.OutputKind.STDERR);
539         log = log.stream().filter(s->!s.matches("^Picked up .*JAVA.*OPTIONS:.*")).collect(Collectors.toList());
540         checkEqual("stderr", log, List.of("error: --enable-preview must be used with --source"));
541     }
542 
543     @Test
544     public void testNoMain(Path base) throws IOException {
545         tb.writeJavaFiles(base, "class NoMain { }");
546         testError(base.resolve("NoMain.java"), "",
547                 "error: can't find main(String[]) method in class: NoMain");
548     }
549 
550     //@Test temporary disabled as enabled preview allows no-param main
551     public void testMainBadParams(Path base) throws IOException {
552         tb.writeJavaFiles(base,
553                 "class BadParams { public static void main() { } }");
554         testError(base.resolve("BadParams.java"), "",
555                 "error: can't find main(String[]) method in class: BadParams");
556     }
557 
558     //@Test temporary disabled as enabled preview allows non-public main
559     public void testMainNotPublic(Path base) throws IOException {
560         tb.writeJavaFiles(base,
561                 "class NotPublic { static void main(String... args) { } }");
562         testError(base.resolve("NotPublic.java"), "",
563                 "error: can't find main(String[]) method in class: NotPublic");
564     }
565 
566     //@Test temporary disabled as enabled preview allows non-static main
567     public void testMainNotStatic(Path base) throws IOException {
568         tb.writeJavaFiles(base,
569                 "class NotStatic { public void main(String... args) { } }");
570         testError(base.resolve("NotStatic.java"), "",
571                 "error: can't find main(String[]) method in class: NotStatic");
572     }
573 
574     @Test
575     public void testMainNotVoid(Path base) throws IOException {
576         tb.writeJavaFiles(base,
577                 "class NotVoid { public static int main(String... args) { return 0; } }");
578         testError(base.resolve("NotVoid.java"), "",
579                 "error: can't find main(String[]) method in class: NotVoid");
580     }
581 
582     @Test
583     public void testClassInModule(Path base) throws IOException {
584         tb.writeJavaFiles(base, "package java.net; class InModule { }");
585         Path file = base.resolve("java").resolve("net").resolve("InModule.java");
586         testError(file,
587                 file + ":1: error: package exists in another module: java.base\n" +
588                 "package java.net; class InModule { }\n" +
589                 "^\n" +
590                 "1 error\n",
591                 "error: compilation failed");
592     }
593 
594     @Test
595     public void testNoRecompileWithSuggestions(Path base) throws IOException {
596         tb.writeJavaFiles(base,
597             "class NoRecompile {\n" +
598             "    void use(String s) {}\n" +
599             "    void test() {\n" +
600             "        use(1);\n" +
601             "    }\n" +
602             "    <T> void test(T t, Object o) {\n" +
603             "        T t1 = (T) o;\n" +
604             "    }\n" +
605             "    static class Generic<T> {\n" +
606             "        T t;\n" +
607             "        void raw(Generic raw) {\n" +
608             "            raw.t = \"\";\n" +
609             "        }\n" +
610             "    }\n" +
611             "    void deprecation() {\n" +
612             "        Thread.currentThread().stop();\n" +
613             "    }\n" +
614             "    void preview(Object o) {\n" +
615             "      if (o instanceof String s) {\n" +
616             "          System.out.println(s);\n" +
617             "      }\n" +
618             "    }\n" +
619             "}");
620         Result r = run(base.resolve("NoRecompile.java"), Collections.emptyList(), Collections.emptyList());
621         if (r.stdErr.contains("recompile with")) {
622             error("Unexpected recompile suggestions in error output: " + r.stdErr);
623         }
624     }
625 
626     @Test
627     public void testNoOptionsWarnings(Path base) throws IOException {
628         tb.writeJavaFiles(base, "public class Main { public static void main(String... args) {}}");
629         String log = new JavaTask(tb)
630                 .vmOptions("--source", "21")
631                 .className(base.resolve("Main.java").toString())
632                 .run(Task.Expect.SUCCESS)
633                 .getOutput(Task.OutputKind.STDERR);
634 
635         if (log.contains("warning: [options]")) {
636             error("Unexpected options warning in error output: " + log);
637         }
638     }
639 
640     void testError(Path file, String expectStdErr, String expectFault) throws IOException {
641         Result r = run(file, Collections.emptyList(), List.of("1", "2", "3"));
642         checkEmpty("stdout", r.stdOut);
643         checkEqual("stderr", r.stdErr, expectStdErr);
644         checkFault("exception", r.exception, expectFault);
645     }
646 
647     /*
648      * Tests in which main throws an exception.
649      */
650     @Test
651     public void testTargetException1(Path base) throws IOException {
652         tb.writeJavaFiles(base,
653             "import java.util.Arrays;\n" +
654             "class Thrower {\n" +
655             "    public static void main(String... args) {\n" +
656             "        throwWhenZero(Integer.parseInt(args[0]));\n" +
657             "    }\n" +
658             "    static void throwWhenZero(int arg) {\n" +
659             "        if (arg == 0) throw new Error(\"zero!\");\n" +
660             "        throwWhenZero(arg - 1);\n" +
661             "    }\n" +
662             "}");
663         Path file = base.resolve("Thrower.java");
664         Result r = run(file, Collections.emptyList(), List.of("3"));
665         checkEmpty("stdout", r.stdOut);
666         checkEmpty("stderr", r.stdErr);
667         checkTrace("exception", r.exception,
668                 "java.lang.Error: zero!",
669                 "at Thrower.throwWhenZero(Thrower.java:7)",
670                 "at Thrower.throwWhenZero(Thrower.java:8)",
671                 "at Thrower.throwWhenZero(Thrower.java:8)",
672                 "at Thrower.throwWhenZero(Thrower.java:8)",
673                 "at Thrower.main(Thrower.java:4)");
674     }
675 
676     @Test
677     public void testNoDuplicateIncubatorWarning(Path base) throws Exception {
678         Path module = base.resolve("lib");
679         Path moduleSrc = module.resolve("src");
680         Path moduleClasses = module.resolve("classes");
681         Files.createDirectories(moduleClasses);
682         tb.cleanDirectory(moduleClasses);
683         tb.writeJavaFiles(moduleSrc, "module test {}");
684         new JavacTask(tb)
685                 .outdir(moduleClasses)
686                 .files(tb.findJavaFiles(moduleSrc))
687                 .run()
688                 .writeAll();
689         markModuleAsIncubator(moduleClasses.resolve("module-info.class"));
690         tb.writeJavaFiles(base, "public class Main { public static void main(String... args) {}}");
691         String log = new JavaTask(tb)
692                 .vmOptions("--module-path", moduleClasses.toString(),
693                            "--add-modules", "test")
694                 .className(base.resolve("Main.java").toString())
695                 .run(Task.Expect.SUCCESS)
696                 .writeAll()
697                 .getOutput(Task.OutputKind.STDERR);
698 
699         int numberOfWarnings = log.split("WARNING").length - 1;
700 
701         if (log.contains("warning:") || numberOfWarnings != 1) {
702             error("Unexpected warning in error output: " + log);
703         }
704 
705         List<String> compileLog = new JavacTask(tb)
706                 .options("--module-path", moduleClasses.toString(),
707                          "--add-modules", "test",
708                          "-XDrawDiagnostics",
709                          "-XDsourceLauncher",
710                          "-XDshould-stop.at=FLOW")
711                 .files(base.resolve("Main.java").toString())
712                 .run(Task.Expect.SUCCESS)
713                 .writeAll()
714                 .getOutputLines(Task.OutputKind.DIRECT);
715 
716         List<String> expectedOutput = List.of(
717                 "- compiler.warn.incubating.modules: test",
718                 "1 warning"
719         );
720 
721         if (!expectedOutput.equals(compileLog)) {
722             error("Unexpected options : " + compileLog);
723         }
724     }
725         //where:
726         private static void markModuleAsIncubator(Path moduleInfoFile) throws Exception {
727             ClassModel cf = ClassFile.of().parse(moduleInfoFile);
728             ModuleResolutionAttribute newAttr = ModuleResolutionAttribute.of(WARN_INCUBATING);
729             byte[] newBytes = ClassFile.of().transform(cf, ClassTransform.dropping(ce -> ce instanceof Attributes)
730                     .andThen(ClassTransform.endHandler(classBuilder -> classBuilder.with(newAttr))));
731             try (OutputStream out = Files.newOutputStream(moduleInfoFile)) {
732                 out.write(newBytes);
733             }
734         }
735 
736     Result run(Path file, List<String> runtimeArgs, List<String> appArgs) {
737         List<String> args = new ArrayList<>();
738         args.add(file.toString());
739         args.addAll(appArgs);
740 
741         PrintStream prev = System.out;
742         ByteArrayOutputStream baos = new ByteArrayOutputStream();
743         try (PrintStream out = new PrintStream(baos, true)) {
744             System.setOut(out);
745             StringWriter sw = new StringWriter();
746             try (PrintWriter err = new PrintWriter(sw, true)) {
747                 SourceLauncher m = new SourceLauncher(err);
748                 m.run(toArray(runtimeArgs), toArray(args));
749                 return new Result(baos.toString(), sw.toString(), null);
750             } catch (Throwable t) {
751                 return new Result(baos.toString(), sw.toString(), t);
752             }
753         } finally {
754             System.setOut(prev);
755         }
756     }
757 
758     void checkEqual(String name, String found, String expect) {
759         expect = expect.replace("\n", tb.lineSeparator);
760         out.println(name + ": " + found);
761         if (!expect.equals(found)) {
762             error("Unexpected output; expected: " + expect);
763         }
764     }
765 
766     void checkContains(String name, String found, String expect) {
767         expect = expect.replace("\n", tb.lineSeparator);
768         out.println(name + ": " + found);
769         if (!found.contains(expect)) {
770             error("Expected output not found: " + expect);
771         }
772     }
773 
774     void checkEqual(String name, List<String> found, List<String> expect) {
775         out.println(name + ": " + found);
776         tb.checkEqual(expect, found);
777     }
778 
779     void checkMatch(String name, String found, Pattern expect) {
780         out.println(name + ": " + found);
781         if (!expect.matcher(found).matches()) {
782             error("Unexpected output; expected match for: " + expect);
783         }
784     }
785 
786     void checkEmpty(String name, String found) {
787         out.println(name + ": " + found);
788         if (!found.isEmpty()) {
789             error("Unexpected output; expected empty string");
790         }
791     }
792 
793     void checkNull(String name, Throwable found) {
794         out.println(name + ": " + found);
795         if (found != null) {
796             error("Unexpected exception; expected null");
797         }
798     }
799 
800     void checkFault(String name, Throwable found, String expect) {
801         expect = expect.replace("\n", tb.lineSeparator);
802         out.println(name + ": " + found);
803         if (found == null) {
804             error("No exception thrown; expected Fault");
805         } else {
806             if (!(found instanceof Fault)) {
807                 error("Unexpected exception; expected Fault");
808             }
809             if (!(found.getMessage().equals(expect))) {
810                 error("Unexpected detail message; expected: " + expect);
811             }
812         }
813     }
814 
815     void checkTrace(String name, Throwable found, String... expect) {
816         if (!(found instanceof InvocationTargetException)) {
817             error("Unexpected exception; expected InvocationTargetException");
818             out.println("Found:");
819             found.printStackTrace(out);
820         }
821         StringWriter sw = new StringWriter();
822         try (PrintWriter pw = new PrintWriter(sw)) {
823             ((InvocationTargetException) found).getTargetException().printStackTrace(pw);
824         }
825         String trace = sw.toString();
826         out.println(name + ":\n" + trace);
827         String[] traceLines = trace.trim().split("[\r\n]+\\s+");
828         try {
829             tb.checkEqual(List.of(traceLines), List.of(expect));
830         } catch (Error e) {
831             error(e.getMessage());
832         }
833     }
834 
835     String[] toArray(List<String> list) {
836         return list.toArray(new String[list.size()]);
837     }
838 
839     record Result(String stdOut, String stdErr, Throwable exception) {}
840 }