1 /*
   2  * Copyright (c) 2018, 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
  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  * @build toolbox.JavaTask toolbox.JavacTask toolbox.TestRunner toolbox.ToolBox
  33  * @run main SourceLauncherTest
  34  */
  35 
  36 import java.io.ByteArrayOutputStream;
  37 import java.io.File;
  38 import java.io.IOException;
  39 import java.io.PrintStream;
  40 import java.io.PrintWriter;
  41 import java.io.StringWriter;
  42 import java.lang.reflect.InvocationTargetException;
  43 import java.nio.file.Files;
  44 import java.nio.file.Path;
  45 import java.nio.file.Paths;
  46 import java.util.ArrayList;
  47 import java.util.Collections;
  48 import java.util.List;
  49 import java.util.Properties;
  50 import java.util.regex.Pattern;
  51 
  52 import com.sun.tools.javac.launcher.Main;
  53 
  54 import toolbox.JavaTask;
  55 import toolbox.JavacTask;
  56 import toolbox.Task;
  57 import toolbox.TestRunner;
  58 import toolbox.TestRunner;
  59 import toolbox.ToolBox;
  60 
  61 public class SourceLauncherTest extends TestRunner {
  62     public static void main(String... args) throws Exception {
  63         SourceLauncherTest t = new SourceLauncherTest();
  64         t.runTests(m -> new Object[] { Paths.get(m.getName()) });
  65     }
  66 
  67     SourceLauncherTest() {
  68         super(System.err);
  69         tb = new ToolBox();
  70         System.err.println("version: " + thisVersion);
  71     }
  72 
  73     private final ToolBox tb;
  74     private static final String thisVersion = System.getProperty("java.specification.version");
  75 
  76     /*
  77      * Positive tests.
  78      */
  79 
  80     @Test
  81     public void testHelloWorld(Path base) throws IOException {
  82         tb.writeJavaFiles(base,
  83             "import java.util.Arrays;\n" +
  84             "class HelloWorld {\n" +
  85             "    public static void main(String... args) {\n" +
  86             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
  87             "    }\n" +
  88             "}");
  89         testSuccess(base.resolve("HelloWorld.java"), "Hello World! [1, 2, 3]\n");
  90     }
  91 
  92     @Test
  93     public void testHelloWorldInPackage(Path base) throws IOException {
  94         tb.writeJavaFiles(base,
  95             "package hello;\n" +
  96             "import java.util.Arrays;\n" +
  97             "class World {\n" +
  98             "    public static void main(String... args) {\n" +
  99             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 100             "    }\n" +
 101             "}");
 102         testSuccess(base.resolve("hello").resolve("World.java"), "Hello World! [1, 2, 3]\n");
 103     }
 104 
 105     @Test
 106     public void testHelloWorldWithAux(Path base) throws IOException {
 107         tb.writeJavaFiles(base,
 108             "import java.util.Arrays;\n" +
 109             "class HelloWorld {\n" +
 110             "    public static void main(String... args) {\n" +
 111             "        Aux.write(args);\n" +
 112             "    }\n" +
 113             "}\n" +
 114             "class Aux {\n" +
 115             "    static void write(String... args) {\n" +
 116             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 117             "    }\n" +
 118             "}");
 119         testSuccess(base.resolve("HelloWorld.java"), "Hello World! [1, 2, 3]\n");
 120     }
 121 
 122     @Test
 123     public void testHelloWorldWithShebang(Path base) throws IOException {
 124         tb.writeJavaFiles(base,
 125             "#!/usr/bin/java --source " + thisVersion + "\n" +
 126             "import java.util.Arrays;\n" +
 127             "class HelloWorld {\n" +
 128             "    public static void main(String... args) {\n" +
 129             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 130             "    }\n" +
 131             "}");
 132         Files.copy(base.resolve("HelloWorld.java"), base.resolve("HelloWorld"));
 133         testSuccess(base.resolve("HelloWorld"), "Hello World! [1, 2, 3]\n");
 134     }
 135 
 136     @Test
 137     public void testNoAnnoProcessing(Path base) throws IOException {
 138         Path annoSrc = base.resolve("annoSrc");
 139         tb.writeJavaFiles(annoSrc,
 140             "import java.util.*;\n" +
 141             "import javax.annotation.processing.*;\n" +
 142             "import javax.lang.model.element.*;\n" +
 143             "@SupportedAnnotationTypes(\"*\")\n" +
 144             "public class AnnoProc extends AbstractProcessor {\n" +
 145             "    public boolean process(Set<? extends TypeElement> annos, RoundEnvironment rEnv) {\n" +
 146             "        throw new Error(\"Annotation processor should not be invoked\");\n" +
 147             "    }\n" +
 148             "}\n");
 149         Path annoClasses = Files.createDirectories(base.resolve("classes"));
 150         new JavacTask(tb)
 151                 .outdir(annoClasses)
 152                 .files(annoSrc.resolve("AnnoProc.java").toString())
 153                 .run();
 154         Path serviceFile = annoClasses.resolve("META-INF").resolve("services")
 155                 .resolve("javax.annotation.processing.Processor");
 156         tb.writeFile(serviceFile, "AnnoProc");
 157 
 158         Path mainSrc = base.resolve("mainSrc");
 159         tb.writeJavaFiles(mainSrc,
 160             "import java.util.Arrays;\n" +
 161             "class HelloWorld {\n" +
 162             "    public static void main(String... args) {\n" +
 163             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 164             "    }\n" +
 165             "}");
 166 
 167         List<String> javacArgs = List.of("-classpath", annoClasses.toString());
 168         List<String> classArgs = List.of("1", "2", "3");
 169         String expect = "Hello World! [1, 2, 3]\n";
 170         Result r = run(mainSrc.resolve("HelloWorld.java"), javacArgs, classArgs);
 171         checkEqual("stdout", r.stdOut, expect);
 172         checkEmpty("stderr", r.stdErr);
 173         checkNull("exception", r.exception);
 174     }
 175 
 176     @Test
 177     public void testEnablePreview(Path base) throws IOException {
 178         tb.writeJavaFiles(base,
 179             "import java.util.Arrays;\n" +
 180             "class HelloWorld {\n" +
 181             "    public static void main(String... args) {\n" +
 182             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 183             "    }\n" +
 184             "}");
 185 
 186         String log = new JavaTask(tb)
 187                 .vmOptions("--enable-preview", "--source", thisVersion)
 188                 .className(base.resolve("HelloWorld.java").toString())
 189                 .classArgs("1", "2", "3")
 190                 .run(Task.Expect.SUCCESS)
 191                 .getOutput(Task.OutputKind.STDOUT);
 192         checkEqual("stdout", log.trim(), "Hello World! [1, 2, 3]");
 193     }
 194 
 195     @Test
 196     public void testCodeSource(Path base) throws IOException {
 197         tb.writeJavaFiles(base,
 198             "import java.net.URL;\n" +
 199             "class ShowCodeSource {\n" +
 200             "    public static void main(String... args) {\n" +
 201             "        URL u = ShowCodeSource.class.getProtectionDomain().getCodeSource().getLocation();\n" +
 202             "        System.out.println(u);\n" +
 203             "    }\n" +
 204             "}");
 205 
 206         Path file = base.resolve("ShowCodeSource.java");
 207         String log = new JavaTask(tb)
 208                 .className(file.toString())
 209                 .run(Task.Expect.SUCCESS)
 210                 .getOutput(Task.OutputKind.STDOUT);
 211         checkEqual("stdout", log.trim(), file.toAbsolutePath().toUri().toURL().toString());
 212     }
 213 
 214     @Test
 215     public void testPermissions(Path base) throws IOException {
 216         Path policyFile = base.resolve("test.policy");
 217         Path sourceFile = base.resolve("TestPermissions.java");
 218 
 219         tb.writeFile(policyFile,
 220             "grant codeBase \"jrt:/jdk.compiler\" {\n" +
 221             "    permission java.security.AllPermission;\n" +
 222             "};\n" +
 223             "grant codeBase \"" + sourceFile.toUri().toURL() + "\" {\n" +
 224             "    permission java.util.PropertyPermission \"user.dir\", \"read\";\n" +
 225             "};\n");
 226 
 227         tb.writeJavaFiles(base,
 228             "import java.net.URL;\n" +
 229             "class TestPermissions {\n" +
 230             "    public static void main(String... args) {\n" +
 231             "        System.out.println(\"user.dir=\" + System.getProperty(\"user.dir\"));\n" +
 232             "        try {\n" +
 233             "            System.setProperty(\"user.dir\", \"\");\n" +
 234             "            System.out.println(\"no exception\");\n" +
 235             "            System.exit(1);\n" +
 236             "        } catch (SecurityException e) {\n" +
 237             "            System.out.println(\"exception: \" + e);\n" +
 238             "        }\n" +
 239             "    }\n" +
 240             "}");
 241 
 242         String log = new JavaTask(tb)
 243                 .vmOptions("-Djava.security.manager", "-Djava.security.policy=" + policyFile)
 244                 .className(sourceFile.toString())
 245                 .run(Task.Expect.SUCCESS)
 246                 .getOutput(Task.OutputKind.STDOUT);
 247         checkEqual("stdout", log.trim(),
 248                 "user.dir=" + System.getProperty("user.dir") + "\n" +
 249                 "exception: java.security.AccessControlException: " +
 250                     "access denied (\"java.util.PropertyPermission\" \"user.dir\" \"write\")");
 251     }
 252 
 253     public void testSystemProperty(Path base) throws IOException {
 254         tb.writeJavaFiles(base,
 255             "class ShowProperty {\n" +
 256             "    public static void main(String... args) {\n" +
 257             "        System.out.println(System.getProperty(\"jdk.launcher.sourcefile\"));\n" +
 258             "    }\n" +
 259             "}");
 260 
 261         Path file = base.resolve("ShowProperty.java");
 262         String log = new JavaTask(tb)
 263                 .className(file.toString())
 264                 .run(Task.Expect.SUCCESS)
 265                 .getOutput(Task.OutputKind.STDOUT);
 266         checkEqual("stdout", log.trim(), file.toAbsolutePath().toString());
 267     }
 268 
 269     void testSuccess(Path file, String expect) throws IOException {
 270         Result r = run(file, Collections.emptyList(), List.of("1", "2", "3"));
 271         checkEqual("stdout", r.stdOut, expect);
 272         checkEmpty("stderr", r.stdErr);
 273         checkNull("exception", r.exception);
 274     }
 275 
 276     /*
 277      * Negative tests: such as cannot find or execute main method.
 278      */
 279 
 280     @Test
 281     public void testHelloWorldWithShebangJava(Path base) throws IOException {
 282         tb.writeJavaFiles(base,
 283             "#!/usr/bin/java --source " + thisVersion + "\n" +
 284             "import java.util.Arrays;\n" +
 285             "class HelloWorld {\n" +
 286             "    public static void main(String... args) {\n" +
 287             "        System.out.println(\"Hello World! \" + Arrays.toString(args));\n" +
 288             "    }\n" +
 289             "}");
 290         Path file = base.resolve("HelloWorld.java");
 291         testError(file,
 292             file + ":1: error: illegal character: '#'\n" +
 293             "#!/usr/bin/java --source " + thisVersion + "\n" +
 294             "^\n" +
 295             file + ":1: error: class, interface, or enum expected\n" +
 296             "#!/usr/bin/java --source " + thisVersion + "\n" +
 297             "  ^\n" +
 298             "2 errors\n",
 299             "error: compilation failed");
 300     }
 301 
 302     @Test
 303     public void testNoClass(Path base) throws IOException {
 304         Files.createDirectories(base);
 305         Path file = base.resolve("NoClass.java");
 306         Files.write(file, List.of("package p;"));
 307         testError(file, "", "error: no class declared in source 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 auxSrc = base.resolve("auxSrc");
 420         tb.writeJavaFiles(auxSrc,
 421             "public class Aux {\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(Aux.MESSAGE + Arrays.toString(args));\n" +
 431             "    }\n" +
 432             "}");
 433 
 434         List<String> javacArgs = List.of("-classpath", auxSrc.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(Aux.MESSAGE + Arrays.toString(args));\n" +
 440             "                           ^\n" +
 441             "  symbol:   variable Aux\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, or enum 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         String log = new JavaTask(tb)
 535                 .vmOptions("--enable-preview")
 536                 .className(base.resolve("HelloWorld.java").toString())
 537                 .run(Task.Expect.FAIL)
 538                 .getOutput(Task.OutputKind.STDERR);
 539         checkEqual("stderr", log.trim(),
 540                 "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
 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
 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: 'main' method is not declared 'public static'");
 564     }
 565 
 566     @Test
 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: 'main' method is not declared 'public static'");
 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: 'main' method is not declared with a return type of 'void'");
 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     void testError(Path file, String expectStdErr, String expectFault) throws IOException {
 595         Result r = run(file, Collections.emptyList(), List.of("1", "2", "3"));
 596         checkEmpty("stdout", r.stdOut);
 597         checkEqual("stderr", r.stdErr, expectStdErr);
 598         checkFault("exception", r.exception, expectFault);
 599     }
 600 
 601     /*
 602      * Tests in which main throws an exception.
 603      */
 604     @Test
 605     public void testTargetException1(Path base) throws IOException {
 606         tb.writeJavaFiles(base,
 607             "import java.util.Arrays;\n" +
 608             "class Thrower {\n" +
 609             "    public static void main(String... args) {\n" +
 610             "        throwWhenZero(Integer.parseInt(args[0]));\n" +
 611             "    }\n" +
 612             "    static void throwWhenZero(int arg) {\n" +
 613             "        if (arg == 0) throw new Error(\"zero!\");\n" +
 614             "        throwWhenZero(arg - 1);\n" +
 615             "    }\n" +
 616             "}");
 617         Path file = base.resolve("Thrower.java");
 618         Result r = run(file, Collections.emptyList(), List.of("3"));
 619         checkEmpty("stdout", r.stdOut);
 620         checkEmpty("stderr", r.stdErr);
 621         checkTrace("exception", r.exception,
 622                 "java.lang.Error: zero!",
 623                 "at Thrower.throwWhenZero(Thrower.java:7)",
 624                 "at Thrower.throwWhenZero(Thrower.java:8)",
 625                 "at Thrower.throwWhenZero(Thrower.java:8)",
 626                 "at Thrower.throwWhenZero(Thrower.java:8)",
 627                 "at Thrower.main(Thrower.java:4)");
 628     }
 629 
 630     Result run(Path file, List<String> runtimeArgs, List<String> appArgs) {
 631         List<String> args = new ArrayList<>();
 632         args.add(file.toString());
 633         args.addAll(appArgs);
 634 
 635         PrintStream prev = System.out;
 636         ByteArrayOutputStream baos = new ByteArrayOutputStream();
 637         try (PrintStream out = new PrintStream(baos, true)) {
 638             System.setOut(out);
 639             StringWriter sw = new StringWriter();
 640             try (PrintWriter err = new PrintWriter(sw, true)) {
 641                 Main m = new Main(err);
 642                 m.run(toArray(runtimeArgs), toArray(args));
 643                 return new Result(baos.toString(), sw.toString(), null);
 644             } catch (Throwable t) {
 645                 return new Result(baos.toString(), sw.toString(), t);
 646             }
 647         } finally {
 648             System.setOut(prev);
 649         }
 650     }
 651 
 652     void checkEqual(String name, String found, String expect) {
 653         expect = expect.replace("\n", tb.lineSeparator);
 654         out.println(name + ": " + found);
 655         if (!expect.equals(found)) {
 656             error("Unexpected output; expected: " + expect);
 657         }
 658     }
 659 
 660     void checkMatch(String name, String found, Pattern expect) {
 661         out.println(name + ": " + found);
 662         if (!expect.matcher(found).matches()) {
 663             error("Unexpected output; expected match for: " + expect);
 664         }
 665     }
 666 
 667     void checkEmpty(String name, String found) {
 668         out.println(name + ": " + found);
 669         if (!found.isEmpty()) {
 670             error("Unexpected output; expected empty string");
 671         }
 672     }
 673 
 674     void checkNull(String name, Throwable found) {
 675         out.println(name + ": " + found);
 676         if (found != null) {
 677             error("Unexpected exception; expected null");
 678         }
 679     }
 680 
 681     void checkFault(String name, Throwable found, String expect) {
 682         expect = expect.replace("\n", tb.lineSeparator);
 683         out.println(name + ": " + found);
 684         if (found == null) {
 685             error("No exception thrown; expected Main.Fault");
 686         } else {
 687             if (!(found instanceof Main.Fault)) {
 688                 error("Unexpected exception; expected Main.Fault");
 689             }
 690             if (!(found.getMessage().equals(expect))) {
 691                 error("Unexpected detail message; expected: " + expect);
 692             }
 693         }
 694     }
 695 
 696     void checkTrace(String name, Throwable found, String... expect) {
 697         if (!(found instanceof InvocationTargetException)) {
 698             error("Unexpected exception; expected InvocationTargetException");
 699             out.println("Found:");
 700             found.printStackTrace(out);
 701         }
 702         StringWriter sw = new StringWriter();
 703         try (PrintWriter pw = new PrintWriter(sw)) {
 704             ((InvocationTargetException) found).getTargetException().printStackTrace(pw);
 705         }
 706         String trace = sw.toString();
 707         out.println(name + ":\n" + trace);
 708         String[] traceLines = trace.trim().split("[\r\n]+\\s+");
 709         try {
 710             tb.checkEqual(List.of(traceLines), List.of(expect));
 711         } catch (Error e) {
 712             error(e.getMessage());
 713         }
 714     }
 715 
 716     String[] toArray(List<String> list) {
 717         return list.toArray(new String[list.size()]);
 718     }
 719 
 720     class Result {
 721         private final String stdOut;
 722         private final String stdErr;
 723         private final Throwable exception;
 724 
 725         Result(String stdOut, String stdErr, Throwable exception) {
 726             this.stdOut = stdOut;
 727             this.stdErr = stdErr;
 728             this.exception = exception;
 729         }
 730     }
 731 }