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