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