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 
main(String... args)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 
SourceMode()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
testSimpleJava()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
testSimple()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
testExecSimple()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
testSimpleJavaAtFile()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
testSimpleAtFile()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
testClasspath()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
testAddExports()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
testClassNamedJava()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
testSourceNoArg()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
testSourceJarConflict()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
testSourceModuleConflict()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
testTerminalOptionInShebang()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
testTerminalOptionInShebangAtFile()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
testMainClassInShebang()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 
starting(String label)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 
show(TestResult tr)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 
getLauncherDebugEnv()389     private Map<String,String> getLauncherDebugEnv() {
390         return Map.of("_JAVA_LAUNCHER_DEBUG", "1");
391     }
392 
getSimpleFile(String name, boolean shebang)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 
createFile(Path file, List<String> lines)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 
setExecutable(Path file)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 
error(TestResult tr, String message)428     private void error(TestResult tr, String message) {
429         show(tr);
430         throw new RuntimeException(message);
431     }
432 }
433