1 /*
2  * Copyright (c) 2015, 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 8144095 8164825 8169818 8153402 8165405 8177079 8178013 8167554 8166232
27  * @summary Test Command Completion
28  * @modules jdk.compiler/com.sun.tools.javac.api
29  *          jdk.compiler/com.sun.tools.javac.main
30  *          jdk.jdeps/com.sun.tools.javap
31  *          jdk.jshell/jdk.internal.jshell.tool
32  * @library /tools/lib
33  * @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask
34  * @build ReplToolTesting TestingInputStream Compiler
35  * @run testng CommandCompletionTest
36  */
37 
38 import java.io.IOException;
39 import java.nio.file.FileSystems;
40 import java.nio.file.Files;
41 import java.nio.file.Path;
42 import java.nio.file.Paths;
43 import java.util.Arrays;
44 import java.util.Collections;
45 import java.util.List;
46 import java.util.Locale;
47 import java.util.function.Predicate;
48 import java.util.stream.Collectors;
49 import java.util.stream.Stream;
50 import java.util.stream.StreamSupport;
51 
52 import org.testng.annotations.Test;
53 import jdk.internal.jshell.tool.JShellTool;
54 import jdk.internal.jshell.tool.JShellToolBuilder;
55 import jdk.jshell.SourceCodeAnalysis.Suggestion;
56 import static org.testng.Assert.assertEquals;
57 import static org.testng.Assert.assertTrue;
58 import static org.testng.Assert.fail;
59 
60 public class CommandCompletionTest extends ReplToolTesting {
61 
62 
63     private JShellTool repl;
64 
65     @Override
testRawRun(Locale locale, String[] args)66     protected void testRawRun(Locale locale, String[] args) {
67         repl = ((JShellToolBuilder) builder(locale))
68                 .rawTool();
69         try {
70             repl.start(args);
71         } catch (Exception ex) {
72             fail("Repl tool died with exception", ex);
73         }
74     }
75 
assertCompletion(boolean after, String code, boolean isSmart, String... expected)76     public void assertCompletion(boolean after, String code, boolean isSmart, String... expected) {
77         if (!after) {
78             setCommandInput("\n");
79         } else {
80             assertCompletion(code, isSmart, expected);
81         }
82     }
83 
assertCompletion(String code, boolean isSmart, String... expected)84     public void assertCompletion(String code, boolean isSmart, String... expected) {
85         List<String> completions = computeCompletions(code, isSmart);
86         assertEquals(completions, Arrays.asList(expected), "Command: " + code + ", output: " +
87                 completions.toString());
88     }
89 
computeCompletions(String code, boolean isSmart)90     private List<String> computeCompletions(String code, boolean isSmart) {
91         int cursor =  code.indexOf('|');
92         code = code.replace("|", "");
93         assertTrue(cursor > -1, "'|' not found: " + code);
94         List<Suggestion> completions =
95                 repl.commandCompletionSuggestions(code, cursor, new int[] {-1}); //XXX: ignoring anchor for now
96         return completions.stream()
97                           .filter(s -> isSmart == s.matchesType())
98                           .map(s -> s.continuation())
99                           .distinct()
100                           .collect(Collectors.toList());
101     }
102 
103     @Test
testCommand()104     public void testCommand() {
105         testNoStartUp(
106                 a -> assertCompletion(a, "/deb|", false),
107                 a -> assertCompletion(a, "/re|", false, "/reload ", "/reset "),
108                 a -> assertCompletion(a, "/h|", false, "/help ", "/history ")
109         );
110     }
111 
112     @Test
testList()113     public void testList() {
114         test(false, new String[] {"--no-startup"},
115                 a -> assertCompletion(a, "/l|", false, "/list "),
116                 a -> assertCompletion(a, "/list |", false, "-all", "-history", "-start "),
117                 a -> assertCompletion(a, "/list -h|", false, "-history"),
118                 a -> assertCompletion(a, "/list q|", false),
119                 a -> assertVariable(a, "int", "xray"),
120                 a -> assertCompletion(a, "/list |", false, "-all", "-history", "-start ", "1 ", "xray "),
121                 a -> assertCompletion(a, "/list x|", false, "xray "),
122                 a -> assertCompletion(a, "/list xray |", false)
123         );
124     }
125 
126     @Test
testHistory()127     public void testHistory() {
128         test(false, new String[] {"--no-startup"},
129                 a -> assertCompletion(a, "/hi|", false, "/history "),
130                 a -> assertCompletion(a, "/history |", false, "-all")
131         );
132     }
133 
134     @Test
testDrop()135     public void testDrop() {
136         test(false, new String[] {"--no-startup"},
137                 a -> assertCompletion(a, "/d|", false, "/drop "),
138                 a -> assertClass(a, "class cTest {}", "class", "cTest"),
139                 a -> assertMethod(a, "int mTest() { return 0; }", "()I", "mTest"),
140                 a -> assertVariable(a, "int", "fTest"),
141                 a -> assertCompletion(a, "/drop |", false, "1 ", "2 ", "3 ", "cTest ", "fTest ", "mTest "),
142                 a -> assertCompletion(a, "/drop f|", false, "fTest ")
143         );
144     }
145 
146     @Test
testEdit()147     public void testEdit() {
148         test(false, new String[]{"--no-startup"},
149                 a -> assertCompletion(a, "/e|", false, "/edit ", "/env ", "/exit "),
150                 a -> assertCompletion(a, "/ed|", false, "/edit "),
151                 a -> assertClass(a, "class cTest {}", "class", "cTest"),
152                 a -> assertMethod(a, "int mTest() { return 0; }", "()I", "mTest"),
153                 a -> assertVariable(a, "int", "fTest"),
154                 a -> assertCompletion(a, "/edit |", false,
155                         "-all" , "-start " , "1 ", "2 ", "3 ", "cTest ", "fTest ", "mTest "),
156                 a -> assertCompletion(a, "/edit cTest |", false,
157                         "2 ", "3 ", "fTest ", "mTest "),
158                 a -> assertCompletion(a, "/edit 1 fTest |", false,
159                         "2 ", "mTest "),
160                 a -> assertCompletion(a, "/edit f|", false, "fTest "),
161                 a -> assertCompletion(a, "/edit mTest f|", false, "fTest ")
162         );
163     }
164 
165     @Test
testHelp()166     public void testHelp() {
167         testNoStartUp(
168                 a -> assertCompletion(a, "/help |", false,
169                 "/! ", "/-<n> ", "/<id> ", "/? ", "/drop ",
170                 "/edit ", "/env ", "/exit ",
171                 "/help ", "/history ", "/imports ",
172                 "/list ", "/methods ", "/open ", "/reload ", "/reset ",
173                 "/save ", "/set ", "/types ", "/vars ", "context ",
174                 "id ", "intro ", "keys ", "rerun ", "shortcuts "),
175                 a -> assertCompletion(a, "/? |", false,
176                 "/! ", "/-<n> ", "/<id> ", "/? ", "/drop ",
177                 "/edit ", "/env ", "/exit ",
178                 "/help ", "/history ", "/imports ",
179                 "/list ", "/methods ", "/open ", "/reload ", "/reset ",
180                 "/save ", "/set ", "/types ", "/vars ", "context ",
181                 "id ", "intro ", "keys ", "rerun ", "shortcuts "),
182                 a -> assertCompletion(a, "/help /s|", false,
183                 "/save ", "/set "),
184                 a -> assertCompletion(a, "/help /set |", false,
185                 "editor", "feedback", "format", "mode", "prompt", "start", "truncation"),
186                 a -> assertCompletion(a, "/help set |", false,
187                 "editor", "feedback", "format", "mode", "prompt", "start", "truncation"),
188                 a -> assertCompletion(a, "/help /edit |", false),
189                 a -> assertCompletion(a, "/help dr|", false,
190                 "drop ")
191         );
192     }
193 
194     @Test
testReload()195     public void testReload() {
196         String[] ropts = new String[] { "-add-exports ", "-add-modules ",
197             "-class-path ", "-module-path ", "-quiet ", "-restore " };
198         String[] dropts = new String[] { "--add-exports ", "--add-modules ",
199             "--class-path ", "--module-path ", "--quiet ", "--restore " };
200         testNoStartUp(
201                 a -> assertCompletion(a, "/reloa |", false, ropts),
202                 a -> assertCompletion(a, "/relo               |", false, ropts),
203                 a -> assertCompletion(a, "/reload -|", false, ropts),
204                 a -> assertCompletion(a, "/reload --|", false, dropts),
205                 a -> assertCompletion(a, "/reload -restore |", false, ropts),
206                 a -> assertCompletion(a, "/reload -restore --|", false, dropts),
207                 a -> assertCompletion(a, "/reload -rest|", false, "-restore "),
208                 a -> assertCompletion(a, "/reload --r|", false, "--restore "),
209                 a -> assertCompletion(a, "/reload -q|", false, "-quiet "),
210                 a -> assertCompletion(a, "/reload -add|", false, "-add-exports ", "-add-modules "),
211                 a -> assertCompletion(a, "/reload -class-path . -quiet |", false, ropts)
212         );
213     }
214 
215     @Test
testEnv()216     public void testEnv() {
217         String[] ropts = new String[] { "-add-exports ", "-add-modules ",
218             "-class-path ", "-module-path " };
219         String[] dropts = new String[] { "--add-exports ", "--add-modules ",
220             "--class-path ", "--module-path " };
221         testNoStartUp(
222                 a -> assertCompletion(a, "/env |", false, ropts),
223                 a -> assertCompletion(a, "/env -|", false, ropts),
224                 a -> assertCompletion(a, "/env --|", false, dropts),
225                 a -> assertCompletion(a, "/env --a|", false, "--add-exports ", "--add-modules "),
226                 a -> assertCompletion(a, "/env -add-|", false, "-add-exports ", "-add-modules "),
227                 a -> assertCompletion(a, "/env -class-path . |", false, ropts),
228                 a -> assertCompletion(a, "/env -class-path . --|", false, dropts)
229         );
230     }
231 
232     @Test
testReset()233     public void testReset() {
234         String[] ropts = new String[] { "-add-exports ", "-add-modules ",
235             "-class-path ", "-module-path " };
236         String[] dropts = new String[] { "--add-exports ", "--add-modules ",
237             "--class-path ", "--module-path " };
238         testNoStartUp(
239                 a -> assertCompletion(a, "/reset    |", false, ropts),
240                 a -> assertCompletion(a, "/res -m|", false, "-module-path "),
241                 a -> assertCompletion(a, "/res -module-|", false, "-module-path "),
242                 a -> assertCompletion(a, "/res --m|", false, "--module-path "),
243                 a -> assertCompletion(a, "/res --module-|", false, "--module-path "),
244                 a -> assertCompletion(a, "/reset -add|", false, "-add-exports ", "-add-modules "),
245                 a -> assertCompletion(a, "/rese -class-path . |", false, ropts),
246                 a -> assertCompletion(a, "/rese -class-path . --|", false, dropts)
247         );
248     }
249 
250     @Test
testVarsMethodsTypes()251     public void testVarsMethodsTypes() {
252         testNoStartUp(
253                 a -> assertCompletion(a, "/v|", false, "/vars "),
254                 a -> assertCompletion(a, "/m|", false, "/methods "),
255                 a -> assertCompletion(a, "/t|", false, "/types "),
256                 a -> assertClass(a, "class cTest {}", "class", "cTest"),
257                 a -> assertMethod(a, "int mTest() { return 0; }", "()I", "mTest"),
258                 a -> assertVariable(a, "int", "fTest"),
259                 a -> assertCompletion(a, "/vars |", false, "-all", "-start ", "3 ", "fTest "),
260                 a -> assertCompletion(a, "/meth |", false, "-all", "-start ", "2 ", "mTest "),
261                 a -> assertCompletion(a, "/typ |", false, "-all", "-start ", "1 ", "cTest "),
262                 a -> assertCompletion(a, "/var f|", false, "fTest ")
263         );
264     }
265 
266     @Test
testOpen()267     public void testOpen() throws IOException {
268         Compiler compiler = new Compiler();
269         testNoStartUp(
270                 a -> assertCompletion(a, "/o|", false, "/open ")
271         );
272         List<String> p1 = listFiles(Paths.get(""));
273         getRootDirectories().forEach(s -> p1.add(s.toString()));
274         Collections.sort(p1);
275         testNoStartUp(
276                 a -> assertCompletion(a, "/open |", false, p1.toArray(new String[p1.size()]))
277         );
278         Path classDir = compiler.getClassDir();
279         List<String> p2 = listFiles(classDir);
280         testNoStartUp(
281                 a -> assertCompletion(a, "/open " + classDir + "/|", false, p2.toArray(new String[p2.size()]))
282         );
283     }
284 
285     @Test
testSave()286     public void testSave() throws IOException {
287         Compiler compiler = new Compiler();
288         testNoStartUp(
289                 a -> assertCompletion(a, "/s|", false, "/save ", "/set ")
290         );
291         List<String> p1 = listFiles(Paths.get(""));
292         Collections.addAll(p1, "-all ", "-history ", "-start ");
293         getRootDirectories().forEach(s -> p1.add(s.toString()));
294         Collections.sort(p1);
295         testNoStartUp(
296                 a -> assertCompletion(a, "/save |", false, p1.toArray(new String[p1.size()]))
297         );
298         Path classDir = compiler.getClassDir();
299         List<String> p2 = listFiles(classDir);
300         testNoStartUp(
301                 a -> assertCompletion(a, "/save " + classDir + "/|",
302                 false, p2.toArray(new String[p2.size()])),
303                 a -> assertCompletion(a, "/save -all " + classDir + "/|",
304                 false, p2.toArray(new String[p2.size()]))
305         );
306     }
307 
308     @Test
testClassPath()309     public void testClassPath() throws IOException {
310         Compiler compiler = new Compiler();
311         Path outDir = compiler.getPath("testClasspathCompletion");
312         Files.createDirectories(outDir);
313         Files.createDirectories(outDir.resolve("dir"));
314         createIfNeeded(outDir.resolve("test.jar"));
315         createIfNeeded(outDir.resolve("test.zip"));
316         compiler.compile(outDir, "package pkg; public class A { public String toString() { return \"A\"; } }");
317         String jarName = "test.jar";
318         compiler.jar(outDir, jarName, "pkg/A.class");
319         compiler.getPath(outDir).resolve(jarName);
320         List<String> paths = listFiles(outDir, CLASSPATH_FILTER);
321         String[] pathArray = paths.toArray(new String[paths.size()]);
322         testNoStartUp(
323                 a -> assertCompletion(a, "/env -class-path " + outDir + "/|", false, pathArray),
324                 a -> assertCompletion(a, "/env --class-path " + outDir + "/|", false, pathArray),
325                 a -> assertCompletion(a, "/env -clas    " + outDir + "/|", false, pathArray),
326                 a -> assertCompletion(a, "/env --class-p    " + outDir + "/|", false, pathArray),
327                 a -> assertCompletion(a, "/env --module-path . --class-p    " + outDir + "/|", false, pathArray)
328         );
329     }
330 
331     @Test
testUserHome()332     public void testUserHome() throws IOException {
333         List<String> completions;
334         Path home = Paths.get(System.getProperty("user.home"));
335         try (Stream<Path> content = Files.list(home)) {
336             completions = content.filter(CLASSPATH_FILTER)
337                                  .map(file -> file.getFileName().toString() + (Files.isDirectory(file) ? "/" : ""))
338                                  .sorted()
339                                  .collect(Collectors.toList());
340         }
341         testNoStartUp(
342                 a -> assertCompletion(a, "/env --class-path ~/|", false, completions.toArray(new String[completions.size()]))
343         );
344     }
345 
346     @Test
testSet()347     public void testSet() throws IOException {
348         List<String> p1 = listFiles(Paths.get(""));
349         getRootDirectories().forEach(s -> p1.add(s.toString()));
350         Collections.sort(p1);
351 
352         String[] modes = {"concise ", "normal ", "silent ", "verbose "};
353         String[] options = {"-command", "-delete", "-quiet"};
354         String[] modesWithOptions = Stream.concat(Arrays.stream(options), Arrays.stream(modes)).sorted().toArray(String[]::new);
355         test(false, new String[] {"--no-startup"},
356                 a -> assertCompletion(a, "/se|", false, "/set "),
357                 a -> assertCompletion(a, "/set |", false, "editor ", "feedback ", "format ", "mode ", "prompt ", "start ", "truncation "),
358 
359                 // /set editor
360                 a -> assertCompletion(a, "/set e|", false, "editor "),
361                 a -> assertCompletion(a, "/set editor |", false, p1.toArray(new String[p1.size()])),
362 
363                 // /set feedback
364                 a -> assertCompletion(a, "/set fe|", false, "feedback "),
365                 a -> assertCompletion(a, "/set fe |", false, modes),
366 
367                 // /set format
368                 a -> assertCompletion(a, "/set fo|", false, "format "),
369                 a -> assertCompletion(a, "/set fo |", false, modes),
370 
371                 // /set mode
372                 a -> assertCompletion(a, "/set mo|", false, "mode "),
373                 a -> assertCompletion(a, "/set mo |", false),
374                 a -> assertCompletion(a, "/set mo newmode |", false, modesWithOptions),
375                 a -> assertCompletion(a, "/set mo newmode -|", false, options),
376                 a -> assertCompletion(a, "/set mo newmode -command |", false),
377                 a -> assertCompletion(a, "/set mo newmode normal |", false, options),
378 
379                 // /set prompt
380                 a -> assertCompletion(a, "/set pro|", false, "prompt "),
381                 a -> assertCompletion(a, "/set pro |", false, modes),
382 
383                 // /set start
384                 a -> assertCompletion(a, "/set st|", false, "start "),
385                 a -> assertCompletion(a, "/set st |", false, p1.toArray(new String[p1.size()])),
386 
387                 // /set truncation
388                 a -> assertCompletion(a, "/set tr|", false, "truncation "),
389                 a -> assertCompletion(a, "/set tr |", false, modes)
390         );
391     }
392 
createIfNeeded(Path file)393     private void createIfNeeded(Path file) throws IOException {
394         if (!Files.exists(file))
395             Files.createFile(file);
396     }
listFiles(Path path)397     private List<String> listFiles(Path path) throws IOException {
398         return listFiles(path, ACCEPT_ALL);
399     }
400 
listFiles(Path path, Predicate<? super Path> filter)401     private List<String> listFiles(Path path, Predicate<? super Path> filter) throws IOException {
402         try (Stream<Path> stream = Files.list(path)) {
403             return stream.filter(filter)
404                          .map(p -> p.getFileName().toString() + (Files.isDirectory(p) ? "/" : ""))
405                          .sorted()
406                          .collect(Collectors.toList());
407         }
408     }
409 
410     private static final Predicate<? super Path> ACCEPT_ALL =
411             (file) -> !file.endsWith(".") && !file.endsWith("..");
412 
413     private static final Predicate<? super Path> CLASSPATH_FILTER =
414             (file) -> ACCEPT_ALL.test(file) &&
415                     (Files.isDirectory(file) ||
416                      file.getFileName().toString().endsWith(".jar") ||
417                      file.getFileName().toString().endsWith(".zip"));
418 
getRootDirectories()419     private static Iterable<? extends Path> getRootDirectories() {
420         return StreamSupport.stream(FileSystems.getDefault()
421                                                .getRootDirectories()
422                                                .spliterator(),
423                                     false)
424                             .filter(p -> Files.exists(p))
425                             .collect(Collectors.toList());
426     }
427 }
428