1 /*
   2  * Copyright (c) 2017, 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 mode
  28  * @modules jdk.compiler jdk.jlink
  29  * @run main SourceMode
  30  */
  31 
  32 
  33 import java.io.IOException;
  34 import java.io.PrintStream;
  35 import java.nio.file.Files;
  36 import java.nio.file.Path;
  37 import java.nio.file.Paths;
  38 import java.nio.file.attribute.PosixFilePermission;
  39 import java.util.ArrayList;
  40 import java.util.Arrays;
  41 import java.util.HashMap;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Set;
  45 import java.util.spi.ToolProvider;
  46 
  47 public class SourceMode extends TestHelper {
  48 
  49     public static void main(String... args) throws Exception {
  50         new SourceMode().run(args);
  51     }
  52 
  53     // To reduce the chance of creating shebang lines that are too long,
  54     // use a shorter path for a java command if the standard path is too long.
  55     private final Path shebangJavaCmd;
  56 
  57     // Whether or not to automatically skip the shebang tests
  58     private final boolean skipShebangTest;
  59 
  60     private final PrintStream log;
  61 
  62     private static final String thisVersion = System.getProperty("java.specification.version");
  63 
  64     SourceMode() throws Exception {
  65         log = System.err;
  66 
  67         if (isWindows) {
  68             // Skip shebang tests on Windows, because that requires Cygwin.
  69             skipShebangTest = true;
  70             shebangJavaCmd = null;
  71         } else {
  72             // Try to ensure the path to the Java launcher is reasonably short,
  73             // to work around the mostly undocumented limit of 120 characters
  74             // for a shebang line.
  75             // The value of 120 is the typical kernel compile-time buffer limit.
  76             // The following limit of 80 allows room for arguments to be placed
  77             // after the path to the launcher on the shebang line.
  78             Path cmd = Paths.get(javaCmd);
  79             if (cmd.toString().length() < 80) {
  80                 shebangJavaCmd = cmd;
  81             } else {
  82                 // Create a small image in the current directory, such that
  83                 // the path for the launcher is just "tmpJDK/bin/java".
  84                 Path tmpJDK = Paths.get("tmpJDK");
  85                 ToolProvider jlink = ToolProvider.findFirst("jlink")
  86                     .orElseThrow(() -> new Exception("cannot find jlink"));
  87                 jlink.run(System.out, System.err,
  88                     "--add-modules", "jdk.compiler,jdk.zipfs", "--output", tmpJDK.toString());
  89                 shebangJavaCmd = tmpJDK.resolve("bin").resolve("java");
  90             }
  91             log.println("Using java command: " + shebangJavaCmd);
  92             skipShebangTest = false;
  93         }
  94     }
  95 
  96     // java Simple.java 1 2 3
  97     @Test
  98     void testSimpleJava() throws IOException {
  99         starting("testSimpleJava");
 100         Path file = getSimpleFile("Simple.java", false);
 101         TestResult tr = doExec(javaCmd, file.toString(), "1", "2", "3");
 102         if (!tr.isOK())
 103             error(tr, "Bad exit code: " + tr.exitValue);
 104         if (!tr.contains("[1, 2, 3]"))
 105             error(tr, "Expected output not found");
 106         show(tr);
 107     }
 108 
 109     // java --source N simple 1 2 3
 110     @Test
 111     void testSimple() throws IOException {
 112         starting("testSimple");
 113         Path file = getSimpleFile("simple", false);
 114         TestResult tr = doExec(javaCmd, "--source", thisVersion, file.toString(), "1", "2", "3");
 115         if (!tr.isOK())
 116             error(tr, "Bad exit code: " + tr.exitValue);
 117         if (!tr.contains("[1, 2, 3]"))
 118             error(tr, "Expected output not found");
 119         show(tr);
 120     }
 121 
 122     // execSimple 1 2 3
 123     @Test
 124     void testExecSimple() throws IOException {
 125         starting("testExecSimple");
 126         if (skipShebangTest) {
 127             log.println("SKIPPED");
 128             return;
 129         }
 130         Path file = setExecutable(getSimpleFile("execSimple", true));
 131         TestResult tr = doExec(file.toAbsolutePath().toString(), "1", "2", "3");
 132         if (!tr.isOK())
 133             error(tr, "Bad exit code: " + tr.exitValue);
 134         if (!tr.contains("[1, 2, 3]"))
 135             error(tr, "Expected output not found");
 136         show(tr);
 137     }
 138 
 139     // java @simpleJava.at  (contains Simple.java 1 2 3)
 140     @Test
 141     void testSimpleJavaAtFile() throws IOException {
 142         starting("testSimpleJavaAtFile");
 143         Path file = getSimpleFile("Simple.java", false);
 144         Path atFile = Paths.get("simpleJava.at");
 145         createFile(atFile, List.of(file + " 1 2 3"));
 146         TestResult tr = doExec(javaCmd, "@" + atFile);
 147         if (!tr.isOK())
 148             error(tr, "Bad exit code: " + tr.exitValue);
 149         if (!tr.contains("[1, 2, 3]"))
 150             error(tr, "Expected output not found");
 151         show(tr);
 152     }
 153 
 154     // java @simple.at  (contains --source N simple 1 2 3)
 155     @Test
 156     void testSimpleAtFile() throws IOException {
 157         starting("testSimpleAtFile");
 158         Path file = getSimpleFile("simple", false);
 159         Path atFile = Paths.get("simple.at");
 160         createFile(atFile, List.of("--source " + thisVersion + " " + file + " 1 2 3"));
 161         TestResult tr = doExec(javaCmd, "@" + atFile);
 162         if (!tr.isOK())
 163             error(tr, "Bad exit code: " + tr.exitValue);
 164         if (!tr.contains("[1, 2, 3]"))
 165             error(tr, "Expected output not found");
 166         show(tr);
 167     }
 168 
 169     // java -cp classes Main.java 1 2 3
 170     @Test
 171     void testClasspath() throws IOException {
 172         starting("testClasspath");
 173         Path base = Files.createDirectories(Paths.get("testClasspath"));
 174         Path otherJava = base.resolve("Other.java");
 175         createFile(otherJava, List.of(
 176             "public class Other {",
 177             "  public static String join(String[] args) {",
 178             "    return String.join(\"-\", args);",
 179             "  }",
 180             "}"
 181         ));
 182         Path classes = Files.createDirectories(base.resolve("classes"));
 183         Path mainJava = base.resolve("Main.java");
 184         createFile(mainJava, List.of(
 185             "class Main {",
 186             "  public static void main(String[] args) {",
 187             "    System.out.println(Other.join(args));",
 188             "  }}"
 189         ));
 190         compile("-d", classes.toString(), otherJava.toString());
 191         TestResult tr = doExec(javaCmd, "-cp", classes.toString(),
 192                 mainJava.toString(), "1", "2", "3");
 193         if (!tr.isOK())
 194             error(tr, "Bad exit code: " + tr.exitValue);
 195         if (!tr.contains("1-2-3"))
 196             error(tr, "Expected output not found");
 197         show(tr);
 198     }
 199 
 200     // java --add-exports=... Export.java --help
 201     @Test
 202     void testAddExports() throws IOException {
 203         starting("testAddExports");
 204         Path exportJava = Paths.get("Export.java");
 205         createFile(exportJava, List.of(
 206             "public class Export {",
 207             "  public static void main(String[] args) {",
 208             "    new com.sun.tools.javac.main.Main(\"demo\").compile(args);",
 209             "  }",
 210             "}"
 211         ));
 212         // verify access fails without --add-exports
 213         TestResult tr1 = doExec(javaCmd, exportJava.toString(), "--help");
 214         if (tr1.isOK())
 215             error(tr1, "Compilation succeeded unexpectedly");
 216         show(tr1);
 217         // verify access succeeds with --add-exports
 218         TestResult tr2 = doExec(javaCmd,
 219             "--add-exports", "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED",
 220             exportJava.toString(), "--help");
 221         if (!tr2.isOK())
 222             error(tr2, "Bad exit code: " + tr2.exitValue);
 223         if (!(tr2.contains("demo") && tr2.contains("Usage")))
 224             error(tr2, "Expected output not found");
 225         show(tr2);
 226     }
 227 
 228     // java -cp ... HelloWorld.java  (for a class "java" in package "HelloWorld")
 229     @Test
 230     void testClassNamedJava() throws IOException {
 231         starting("testClassNamedJava");
 232         Path base = Files.createDirectories(Paths.get("testClassNamedJava"));
 233         Path src = Files.createDirectories(base.resolve("src"));
 234         Path srcfile = src.resolve("java.java");
 235         createFile(srcfile, List.of(
 236                 "package HelloWorld;",
 237                 "class java {",
 238                 "    public static void main(String... args) {",
 239                 "        System.out.println(HelloWorld.java.class.getName());",
 240                 "    }",
 241                 "}"
 242         ));
 243         Path classes = base.resolve("classes");
 244         compile("-d", classes.toString(), srcfile.toString());
 245         TestResult tr =
 246             doExec(javaCmd, "-cp", classes.toString(), "HelloWorld.java");
 247         if (!tr.isOK())
 248             error(tr, "Command failed");
 249         if (!tr.contains("HelloWorld.java"))
 250             error(tr, "Expected output not found");
 251         show(tr);
 252     }
 253 
 254     // java --source
 255     @Test
 256     void testSourceNoArg() throws IOException {
 257         starting("testSourceNoArg");
 258         TestResult tr = doExec(javaCmd, "--source");
 259         if (tr.isOK())
 260             error(tr, "Command succeeded unexpectedly");
 261         if (!tr.contains("--source requires source version"))
 262             error(tr, "Expected output not found");
 263         show(tr);
 264     }
 265 
 266     // java --source N -jar simple.jar
 267     @Test
 268     void testSourceJarConflict() throws IOException {
 269         starting("testSourceJarConflict");
 270         Path base = Files.createDirectories(Paths.get("testSourceJarConflict"));
 271         Path file = getSimpleFile("Simple.java", false);
 272         Path classes = Files.createDirectories(base.resolve("classes"));
 273         compile("-d", classes.toString(), file.toString());
 274         Path simpleJar = base.resolve("simple.jar");
 275         createJar("cf", simpleJar.toString(), "-C", classes.toString(), ".");
 276         TestResult tr =
 277             doExec(javaCmd, "--source", thisVersion, "-jar", simpleJar.toString());
 278         if (tr.isOK())
 279             error(tr, "Command succeeded unexpectedly");
 280         if (!tr.contains("Option -jar is not allowed with --source"))
 281             error(tr, "Expected output not found");
 282         show(tr);
 283     }
 284 
 285     // java --source N -m jdk.compiler
 286     @Test
 287     void testSourceModuleConflict() throws IOException {
 288         starting("testSourceModuleConflict");
 289         TestResult tr = doExec(javaCmd, "--source", thisVersion, "-m", "jdk.compiler");
 290         if (tr.isOK())
 291             error(tr, "Command succeeded unexpectedly");
 292         if (!tr.contains("Option -m is not allowed with --source"))
 293             error(tr, "Expected output not found");
 294         show(tr);
 295     }
 296 
 297     // #!.../java --source N -version
 298     @Test
 299     void testTerminalOptionInShebang() throws IOException {
 300         starting("testTerminalOptionInShebang");
 301         if (skipShebangTest || isAIX || isMacOSX || isSolaris) {
 302             // On MacOSX, we cannot distinguish between terminal options on the
 303             // shebang line and those on the command line.
 304             // On Solaris, all options after the first on the shebang line are
 305             // ignored. Similar on AIX.
 306             log.println("SKIPPED");
 307             return;
 308         }
 309         Path base = Files.createDirectories(
 310             Paths.get("testTerminalOptionInShebang"));
 311         Path bad = base.resolve("bad");
 312         createFile(bad, List.of(
 313             "#!" + shebangJavaCmd + " --source " + thisVersion + " -version"));
 314         setExecutable(bad);
 315         TestResult tr = doExec(bad.toString());
 316         if (!tr.contains("Option -version is not allowed in this context"))
 317             error(tr, "Expected output not found");
 318         show(tr);
 319     }
 320 
 321     // #!.../java --source N @bad.at  (contains -version)
 322     @Test
 323     void testTerminalOptionInShebangAtFile() throws IOException {
 324         starting("testTerminalOptionInShebangAtFile");
 325         if (skipShebangTest || isAIX || isMacOSX || isSolaris) {
 326             // On MacOSX, we cannot distinguish between terminal options in a
 327             // shebang @-file and those on the command line.
 328             // On Solaris, all options after the first on the shebang line are
 329             // ignored. Similar on AIX.
 330             log.println("SKIPPED");
 331             return;
 332         }
 333         // Use a short directory name, to avoid line length limitations
 334         Path base = Files.createDirectories(Paths.get("testBadAtFile"));
 335         Path bad_at = base.resolve("bad.at");
 336         createFile(bad_at, List.of("-version"));
 337         Path bad = base.resolve("bad");
 338         createFile(bad, List.of(
 339             "#!" + shebangJavaCmd + " --source " + thisVersion + " @" + bad_at));
 340         setExecutable(bad);
 341         TestResult tr = doExec(bad.toString());
 342         if (!tr.contains("Option -version in @testBadAtFile/bad.at is "
 343                 + "not allowed in this context"))
 344             error(tr, "Expected output not found");
 345         show(tr);
 346     }
 347 
 348     // #!.../java --source N HelloWorld
 349     @Test
 350     void testMainClassInShebang() throws IOException {
 351         starting("testMainClassInShebang");
 352         if (skipShebangTest || isAIX || isMacOSX || isSolaris) {
 353             // On MacOSX, we cannot distinguish between a main class on the
 354             // shebang line and one on the command line.
 355             // On Solaris, all options after the first on the shebang line are
 356             // ignored. Similar on AIX.
 357             log.println("SKIPPED");
 358             return;
 359         }
 360         Path base = Files.createDirectories(Paths.get("testMainClassInShebang"));
 361         Path bad = base.resolve("bad");
 362         createFile(bad, List.of(
 363             "#!" + shebangJavaCmd + " --source " + thisVersion + " HelloWorld"));
 364         setExecutable(bad);
 365         TestResult tr = doExec(bad.toString());
 366         if (!tr.contains("Cannot specify main class in this context"))
 367             error(tr, "Expected output not found");
 368         show(tr);
 369     }
 370 
 371     //--------------------------------------------------------------------------
 372 
 373     private void starting(String label) {
 374         System.out.println();
 375         System.out.println("*** Starting: " + label + " (stdout)");
 376 
 377         System.err.println();
 378         System.err.println("*** Starting: " + label + " (stderr)");
 379     }
 380 
 381     private void show(TestResult tr) {
 382         log.println("*** Test Output:");
 383         for (String line: tr.testOutput) {
 384             log.println(line);
 385         }
 386         log.println("*** End Of Test Output:");
 387     }
 388 
 389     private Map<String,String> getLauncherDebugEnv() {
 390         return Map.of("_JAVA_LAUNCHER_DEBUG", "1");
 391     }
 392 
 393     private Path getSimpleFile(String name, boolean shebang) throws IOException {
 394         Path file = Paths.get(name);
 395         if (!Files.exists(file)) {
 396             createFile(file, List.of(
 397                 (shebang ? "#!" + shebangJavaCmd + " --source=" + thisVersion: ""),
 398                 "public class Simple {",
 399                 "  public static void main(String[] args) {",
 400                 "    System.out.println(java.util.Arrays.toString(args));",
 401                 "  }}"));
 402         }
 403         return file;
 404     }
 405 
 406     private void createFile(Path file, List<String> lines) throws IOException {
 407         lines.stream()
 408             .filter(line -> line.length() > 128)
 409             .forEach(line -> {
 410                     log.println("*** Warning: long line ("
 411                                         + line.length()
 412                                         + " chars) in file " + file);
 413                     log.println("*** " + line);
 414                 });
 415         log.println("*** File: " + file);
 416         lines.stream().forEach(log::println);
 417         log.println("*** End Of File");
 418         createFile(file.toFile(), lines);
 419     }
 420 
 421     private Path setExecutable(Path file) throws IOException {
 422         Set<PosixFilePermission> perms = Files.getPosixFilePermissions(file);
 423         perms.add(PosixFilePermission.OWNER_EXECUTE);
 424         Files.setPosixFilePermissions(file, perms);
 425         return file;
 426     }
 427 
 428     private void error(TestResult tr, String message) {
 429         show(tr);
 430         throw new RuntimeException(message);
 431     }
 432 }