1 /*
2  * Copyright (c) 2015, 2017, 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  * @summary Testing external editor.
27  * @bug 8143955 8080843 8163816 8143006 8169828 8171130 8162989 8210808
28  * @modules jdk.jshell/jdk.internal.jshell.tool
29  * @build ReplToolTesting CustomEditor EditorTestBase
30  * @run testng ExternalEditorTest
31  * @key intermittent
32  */
33 
34 import java.io.BufferedWriter;
35 import java.io.DataInputStream;
36 import java.io.DataOutputStream;
37 import java.io.IOException;
38 import java.io.UncheckedIOException;
39 import java.net.ServerSocket;
40 import java.net.Socket;
41 import java.net.SocketTimeoutException;
42 import java.nio.charset.StandardCharsets;
43 import java.nio.file.Files;
44 import java.nio.file.Path;
45 import java.nio.file.Paths;
46 import java.util.concurrent.ExecutionException;
47 import java.util.concurrent.Future;
48 import java.util.function.Consumer;
49 
50 import org.testng.annotations.AfterClass;
51 import org.testng.annotations.BeforeClass;
52 import org.testng.annotations.Test;
53 
54 import static org.testng.Assert.assertEquals;
55 import static org.testng.Assert.assertFalse;
56 import static org.testng.Assert.assertTrue;
57 import static org.testng.Assert.fail;
58 
59 public class ExternalEditorTest extends EditorTestBase {
60 
61     private static Path executionScript;
62     private static ServerSocket listener;
63 
64     private DataInputStream inputStream;
65     private DataOutputStream outputStream;
66 
67     @Override
writeSource(String s)68     public void writeSource(String s) {
69         try {
70             outputStream.writeInt(CustomEditor.SOURCE_CODE);
71             byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
72             outputStream.writeInt(bytes.length);
73             outputStream.write(bytes);
74         } catch (IOException e) {
75             throw new UncheckedIOException(e);
76         }
77     }
78 
79     @Override
getSource()80     public String getSource() {
81         return readString(CustomEditor.GET_SOURCE_CODE);
82     }
83 
sendCode(int code)84     private void sendCode(int code) {
85         try {
86             outputStream.writeInt(code);
87         } catch (IOException e) {
88             throw new UncheckedIOException(e);
89         }
90     }
91 
92     @Override
accept()93     public void accept() {
94         sendCode(CustomEditor.ACCEPT_CODE);
95     }
96 
97     @Override
exit()98     public void exit() {
99         sendCode(CustomEditor.EXIT_CODE);
100         inputStream = null;
101         outputStream = null;
102     }
103 
104     @Override
cancel()105     public void cancel() {
106         sendCode(CustomEditor.CANCEL_CODE);
107     }
108 
getFilename()109     protected String getFilename() {
110         return readString(CustomEditor.GET_FILENAME);
111     }
112 
readString(int code)113     private String readString(int code) {
114         try {
115             outputStream.writeInt(code);
116             int length = inputStream.readInt();
117             byte[] bytes = new byte[length];
118             inputStream.readFully(bytes);
119             return new String(bytes, StandardCharsets.UTF_8);
120         } catch (IOException e) {
121             throw new UncheckedIOException(e);
122         }
123     }
124 
125     @Override
testEditor(boolean defaultStartup, String[] args, ReplTest... tests)126     public void testEditor(boolean defaultStartup, String[] args, ReplTest... tests) {
127         ReplTest[] t = new ReplTest[tests.length + 1];
128         t[0] = a -> assertCommandCheckOutput(a, "/set editor " + executionScript,
129                 assertStartsWith("|  Editor set to: " + executionScript));
130         System.arraycopy(tests, 0, t, 1, tests.length);
131         super.testEditor(defaultStartup, args, t);
132     }
133 
134     @Test
testStatementSemicolonAddition()135     public void testStatementSemicolonAddition() {
136         testEditor(
137                 a -> assertCommand(a, "if (true) {}", ""),
138                 a -> assertCommand(a, "if (true) {} else {}", ""),
139                 a -> assertCommand(a, "Object o", "o ==> null"),
140                 a -> assertCommand(a, "if (true) o = new Object() { int x; }", ""),
141                 a -> assertCommand(a, "if (true) o = new Object() { int y; }", ""),
142                 a -> assertCommand(a, "System.err.flush()", ""), // test still ; for expression statement
143                 a -> assertEditOutput(a, "/ed", "", () -> {
144                     assertEquals(getSource(),
145                             "if (true) {}\n" +
146                             "if (true) {} else {}\n" +
147                             "Object o;\n" +
148                             "if (true) o = new Object() { int x; };\n" +
149                             "if (true) o = new Object() { int y; };\n" +
150                             "System.err.flush();\n");
151                     exit();
152                 })
153         );
154     }
155 
156     @Test
testTempFileDeleted()157     public void testTempFileDeleted() {
158         String[] fna = new String[1];
159         testEditor(
160                 a -> assertVariable(a, "int", "a", "0", "0"),
161                 a -> assertEditOutput(a, "/ed 1", "a ==> 10", () -> {
162                     fna[0] = getFilename();
163                     assertTrue(Files.exists(Paths.get(fna[0])), "Test set-up failed: " + fna[0]);
164                     writeSource("\n\n\nint a = 10;\n\n\n");
165                     exit();
166                 }),
167                a -> assertCommand(a, "if (true) {} else {}", "")
168         );
169         assertFalse(Files.exists(Paths.get(fna[0])), "File not deleted: " + fna[0]);
170     }
171 
isWindows()172     private static boolean isWindows() {
173         return System.getProperty("os.name").startsWith("Windows");
174     }
175 
176     @BeforeClass
setUpExternalEditorTest()177     public static void setUpExternalEditorTest() throws IOException {
178         listener = new ServerSocket(0);
179         listener.setSoTimeout(30000);
180         int localPort = listener.getLocalPort();
181 
182         executionScript = Paths.get(isWindows() ? "editor.bat" : "editor.sh").toAbsolutePath();
183         Path java = Paths.get(System.getProperty("java.home")).resolve("bin").resolve("java");
184         try (BufferedWriter writer = Files.newBufferedWriter(executionScript)) {
185             if(!isWindows()) {
186                 writer.append(java.toString()).append(" ")
187                         .append(" -cp ").append(System.getProperty("java.class.path"))
188                         .append(" CustomEditor ").append(Integer.toString(localPort)).append(" $@");
189                 executionScript.toFile().setExecutable(true);
190             } else {
191                 writer.append(java.toString()).append(" ")
192                         .append(" -cp ").append(System.getProperty("java.class.path"))
193                         .append(" CustomEditor ").append(Integer.toString(localPort)).append(" %*");
194             }
195         }
196     }
197 
198     private Future<?> task;
199     @Override
assertEdit(boolean after, String cmd, Consumer<String> checkInput, Consumer<String> checkOutput, Action action)200     void assertEdit(boolean after, String cmd,
201                            Consumer<String> checkInput, Consumer<String> checkOutput, Action action) {
202         if (!after) {
203             setCommandInput(cmd + "\n");
204             task = getExecutor().submit(() -> {
205                 try (Socket socket = listener.accept()) {
206                     inputStream = new DataInputStream(socket.getInputStream());
207                     outputStream = new DataOutputStream(socket.getOutputStream());
208                     checkInput.accept(getSource());
209                     action.accept();
210                 } catch (SocketTimeoutException e) {
211                     fail("Socket timeout exception.\n Output: " + getCommandOutput() +
212                             "\n, error: " + getCommandErrorOutput());
213                 } catch (Throwable e) {
214                     shutdownEditor();
215                     if (e instanceof AssertionError) {
216                         throw (AssertionError) e;
217                     }
218                     throw new RuntimeException(e);
219                 }
220             });
221         } else {
222             try {
223                 task.get();
224                 checkOutput.accept(getCommandOutput());
225             } catch (ExecutionException e) {
226                 if (e.getCause() instanceof AssertionError) {
227                     throw (AssertionError) e.getCause();
228                 }
229                 throw new RuntimeException(e);
230             } catch (Exception e) {
231                 throw new RuntimeException(e);
232             }
233         }
234     }
235 
236     @Override
shutdownEditor()237     public void shutdownEditor() {
238         if (outputStream != null) {
239             exit();
240         }
241     }
242 
243     @Test
setUnknownEditor()244     public void setUnknownEditor() {
245         test(
246                 a -> assertCommand(a, "/set editor UNKNOWN", "|  Editor set to: UNKNOWN"),
247                 a -> assertCommand(a, "int a;", null),
248                 a -> assertCommandOutputStartsWith(a, "/ed 1",
249                         "|  Edit Error:")
250         );
251     }
252 
253     @Test(enabled = false) // TODO 8159229
testRemoveTempFile()254     public void testRemoveTempFile() {
255         test(new String[]{"--no-startup"},
256                 a -> assertCommandCheckOutput(a, "/set editor " + executionScript,
257                         assertStartsWith("|  Editor set to: " + executionScript)),
258                 a -> assertVariable(a, "int", "a", "0", "0"),
259                 a -> assertEditOutput(a, "/ed 1", assertStartsWith("|  Edit Error: Failure in read edit file:"), () -> {
260                     sendCode(CustomEditor.REMOVE_CODE);
261                     exit();
262                 }),
263                 a -> assertCommandCheckOutput(a, "/vars", assertVariables())
264         );
265     }
266 
267     @AfterClass
shutdown()268     public static void shutdown() throws IOException {
269         executorShutdown();
270         if (listener != null) {
271             listener.close();
272         }
273     }
274 }
275