From 106007cb89f1a78d51bada1dc29e134e50460fc8 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Tue, 4 Jun 2024 18:45:26 +0100 Subject: [PATCH] Implement the ydoc js bundle test (#10160) close #9929 Changelog: - update: customize Ydoc main hostname and port with environment variables - add: Ydoc initialization test --- build.sbt | 17 +- .../src/main/java/org/enso/ydoc/Main.java | 17 +- .../src/test/java/org/enso/ydoc/YdocTest.java | 156 ++++++++++++++++-- .../org/enso/ydoc/jsonrpc/JsonRpcRequest.java | 5 + .../enso/ydoc/jsonrpc/JsonRpcResponse.java | 16 ++ .../enso/ydoc/jsonrpc/model/ContentRoot.java | 10 ++ .../org/enso/ydoc/jsonrpc/model/FilePath.java | 6 + .../ydoc/jsonrpc/model/FileSystemObject.java | 8 + .../ydoc/jsonrpc/model/WriteCapability.java | 6 + .../jsonrpc/model/result/FileListResult.java | 6 + .../result/InitProtocolConnectionResult.java | 7 + .../ydoc/jsonrpc/model/result/Result.java | 3 + .../model/result/TextOpenFileResult.java | 6 + 13 files changed, 242 insertions(+), 21 deletions(-) create mode 100644 lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/JsonRpcRequest.java create mode 100644 lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/JsonRpcResponse.java create mode 100644 lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/ContentRoot.java create mode 100644 lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/FilePath.java create mode 100644 lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/FileSystemObject.java create mode 100644 lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/WriteCapability.java create mode 100644 lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/FileListResult.java create mode 100644 lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/InitProtocolConnectionResult.java create mode 100644 lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/Result.java create mode 100644 lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/TextOpenFileResult.java diff --git a/build.sbt b/build.sbt index 19d794495235..82f7df841a1f 100644 --- a/build.sbt +++ b/build.sbt @@ -1250,14 +1250,15 @@ lazy val `ydoc-server` = project (`profiling-utils` / Compile / productDirectories).value.head ), libraryDependencies ++= Seq( - "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided", - "org.graalvm.polyglot" % "inspect" % graalMavenPackagesVersion % "runtime", - "org.graalvm.polyglot" % "js" % graalMavenPackagesVersion % "runtime", - "org.slf4j" % "slf4j-api" % slf4jVersion, - "io.helidon.webclient" % "helidon-webclient-websocket" % helidonVersion, - "io.helidon.webserver" % "helidon-webserver-websocket" % helidonVersion, - "junit" % "junit" % junitVersion % Test, - "com.github.sbt" % "junit-interface" % junitIfVersion % Test + "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided", + "org.graalvm.polyglot" % "inspect" % graalMavenPackagesVersion % "runtime", + "org.graalvm.polyglot" % "js" % graalMavenPackagesVersion % "runtime", + "org.slf4j" % "slf4j-api" % slf4jVersion, + "io.helidon.webclient" % "helidon-webclient-websocket" % helidonVersion, + "io.helidon.webserver" % "helidon-webserver-websocket" % helidonVersion, + "junit" % "junit" % junitVersion % Test, + "com.github.sbt" % "junit-interface" % junitIfVersion % Test, + "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion % Test ) ) // `Compile/run` settings are necessary for the `run` task to work. diff --git a/lib/java/ydoc-server/src/main/java/org/enso/ydoc/Main.java b/lib/java/ydoc-server/src/main/java/org/enso/ydoc/Main.java index d4049aea33a2..6481afd25360 100644 --- a/lib/java/ydoc-server/src/main/java/org/enso/ydoc/Main.java +++ b/lib/java/ydoc-server/src/main/java/org/enso/ydoc/Main.java @@ -4,6 +4,9 @@ public class Main { + private static final String ENSO_YDOC_HOST = "ENSO_YDOC_HOST"; + private static final String ENSO_YDOC_PORT = "ENSO_YDOC_PORT"; + private static final Semaphore lock = new Semaphore(0); private Main() {} @@ -15,7 +18,19 @@ public static void main(String[] args) throws Exception { Sampling.init(); - try (var ydoc = Ydoc.builder().build()) { + var ydocHost = System.getenv(ENSO_YDOC_HOST); + var ydocPort = System.getenv(ENSO_YDOC_PORT); + + var builder = Ydoc.builder(); + if (ydocHost != null) { + builder.hostname(ydocHost); + } + if (ydocPort != null) { + var port = Integer.parseInt(ydocPort); + builder.port(port); + } + + try (var ydoc = builder.build()) { ydoc.start(); lock.acquire(); } diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/YdocTest.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/YdocTest.java index f2140526d0c2..0f55d564c023 100644 --- a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/YdocTest.java +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/YdocTest.java @@ -1,32 +1,50 @@ package org.enso.ydoc; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.helidon.common.buffers.BufferData; import io.helidon.webclient.websocket.WsClient; import io.helidon.webserver.WebServer; import io.helidon.webserver.websocket.WsRouting; import io.helidon.websocket.WsListener; import io.helidon.websocket.WsSession; +import java.util.List; +import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import org.enso.ydoc.jsonrpc.JsonRpcRequest; +import org.enso.ydoc.jsonrpc.JsonRpcResponse; +import org.enso.ydoc.jsonrpc.model.ContentRoot; +import org.enso.ydoc.jsonrpc.model.FilePath; +import org.enso.ydoc.jsonrpc.model.FileSystemObject; +import org.enso.ydoc.jsonrpc.model.WriteCapability; +import org.enso.ydoc.jsonrpc.model.result.FileListResult; +import org.enso.ydoc.jsonrpc.model.result.InitProtocolConnectionResult; +import org.enso.ydoc.jsonrpc.model.result.TextOpenFileResult; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class YdocTest { private static final int WEB_SERVER_PORT = 44556; - private static final String YDOC_URL = "ws://localhost:1234/project/index"; + private static final String YDOC_URL = "ws://localhost:1234/project/"; private static final String WEB_SERVER_URL = "ws://127.0.0.1:" + WEB_SERVER_PORT; + private static final Logger log = LoggerFactory.getLogger(YdocTest.class); + private Ydoc ydoc; private ExecutorService webServerExecutor; private WebServer ls; private static WebServer startWebSocketServer(ExecutorService executor) { - var routing = WsRouting.builder().endpoint("/", new WebServerWsListener()); + var routing = WsRouting.builder().endpoint("/", new LanguageServerConnection()); var ws = WebServer.builder().host("localhost").port(WEB_SERVER_PORT).addRouting(routing).build(); @@ -35,6 +53,10 @@ private static WebServer startWebSocketServer(ExecutorService executor) { return ws; } + private static String ydocUrl(String doc) { + return YDOC_URL + doc + "?ls=" + WEB_SERVER_URL; + } + @Before public void setup() { webServerExecutor = Executors.newSingleThreadExecutor(); @@ -45,39 +67,149 @@ public void setup() { @After public void tearDown() throws Exception { ls.stop(); - webServerExecutor.shutdownNow(); + webServerExecutor.shutdown(); + var stopped = webServerExecutor.awaitTermination(3, TimeUnit.SECONDS); + if (!stopped) { + var pending = webServerExecutor.shutdownNow(); + log.error("Executor pending [{}] tasks: [{}].", pending.size(), pending); + } ydoc.close(); } @Test - public void connect() throws Exception { + public void initialize() throws Exception { var queue = new LinkedBlockingQueue(); - var url = YDOC_URL + "?ls=" + WEB_SERVER_URL; ydoc.start(); var ws = WsClient.builder().build(); - ws.connect(url, new TestWsListener(queue)); + ws.connect(ydocUrl("index"), new DashboardConnection(queue)); - var msg = queue.take(); - Assert.assertArrayEquals(new byte[] {0, 0, 1, 0}, msg.readBytes()); + var ok1 = queue.take(); + Assert.assertTrue(ok1.debugDataHex(), BufferDataUtil.isOk(ok1)); + + var buffer = queue.take(); + var uuid = BufferDataUtil.readUUID(buffer); + WsClient.builder().build().connect(ydocUrl(uuid.toString()), new DashboardConnection(queue)); + + var ok2 = queue.take(); + Assert.assertTrue(ok2.debugDataHex(), BufferDataUtil.isOk(ok2)); + } + + private static final class BufferDataUtil { + + private static final int UUID_BYTES = 36; + private static final int SUFFIX_BYTES = 3; + + private static boolean isOk(BufferData data) { + return data.readInt16() == 0; + } + + private static UUID readUUID(BufferData data) { + try { + data.skip(data.available() - UUID_BYTES - SUFFIX_BYTES); + var uuidString = data.readString(UUID_BYTES); + return UUID.fromString(uuidString); + } catch (Exception e) { + log.error("Failed to read UUID of\n{}", data.debugDataHex()); + throw e; + } + } } - private static final class TestWsListener implements WsListener { + private static final class DashboardConnection implements WsListener { + + private static final Logger log = LoggerFactory.getLogger(DashboardConnection.class); private final BlockingQueue messages; - TestWsListener(BlockingQueue messages) { + private DashboardConnection(BlockingQueue messages) { this.messages = messages; } @Override public void onMessage(WsSession session, BufferData buffer, boolean last) { + log.debug("Got message\n{}", buffer.debugDataHex()); + messages.add(buffer); } + + @Override + public void onMessage(WsSession session, String text, boolean last) { + log.error("Got unexpected message [{}].", text); + } } - private static final class WebServerWsListener implements WsListener { - WebServerWsListener() {} + private static final class LanguageServerConnection implements WsListener { + + private static final String METHOD_INIT_PROTOCOL_CONNECTION = "session/initProtocolConnection"; + private static final String METHOD_CAPABILITY_ACQUIRE = "capability/acquire"; + private static final String METHOD_FILE_LIST = "file/list"; + private static final String METHOD_TEXT_OPEN_FILE = "text/openFile"; + + private static final UUID PROJECT_ROOT_ID = new UUID(0, 1); + + private static final Logger log = LoggerFactory.getLogger(LanguageServerConnection.class); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private LanguageServerConnection() {} + + @Override + public void onMessage(WsSession session, String text, boolean last) { + log.debug("Got message [{}].", text); + + try { + var request = objectMapper.readValue(text, JsonRpcRequest.class); + + JsonRpcResponse jsonRpcResponse = null; + + switch (request.method()) { + case METHOD_INIT_PROTOCOL_CONNECTION -> { + var contentRoots = + List.of( + new ContentRoot("Project", PROJECT_ROOT_ID), + new ContentRoot("Home", new UUID(0, 2)), + new ContentRoot("FileSystemRoot", new UUID(0, 3), "/")); + var initProtocolConnectionResult = + new InitProtocolConnectionResult("0.0.0-dev", "0.0.0-dev", contentRoots); + jsonRpcResponse = new JsonRpcResponse(request.id(), initProtocolConnectionResult); + } + case METHOD_CAPABILITY_ACQUIRE -> jsonRpcResponse = JsonRpcResponse.ok(request.id()); + case METHOD_FILE_LIST -> { + var paths = + List.of( + FileSystemObject.file( + "Main.enso", new FilePath(PROJECT_ROOT_ID, List.of("src")))); + var fileListResult = new FileListResult(paths); + jsonRpcResponse = new JsonRpcResponse(request.id(), fileListResult); + } + case METHOD_TEXT_OPEN_FILE -> { + var options = + new WriteCapability.Options( + new FilePath(PROJECT_ROOT_ID, List.of("src", "Main.enso"))); + var writeCapability = new WriteCapability("text/canEdit", options); + var textOpenFileResult = + new TextOpenFileResult( + writeCapability, + "main =", + "e5aeae8609bd90f94941d4227e6ec1e0f069d3318fb7bd93ffe4d391"); + jsonRpcResponse = new JsonRpcResponse(request.id(), textOpenFileResult); + } + } + + if (jsonRpcResponse != null) { + var response = objectMapper.writeValueAsString(jsonRpcResponse); + + log.debug("Sending [{}].", response); + session.send(response, true); + } else { + log.error("Unknown request."); + } + } catch (JsonProcessingException e) { + log.error("Failed to parse JSON.", e); + Assert.fail(e.getMessage()); + } + } } } diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/JsonRpcRequest.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/JsonRpcRequest.java new file mode 100644 index 000000000000..c28722cfda6d --- /dev/null +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/JsonRpcRequest.java @@ -0,0 +1,5 @@ +package org.enso.ydoc.jsonrpc; + +import com.fasterxml.jackson.databind.JsonNode; + +public record JsonRpcRequest(String jsonrpc, String id, String method, JsonNode params) {} diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/JsonRpcResponse.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/JsonRpcResponse.java new file mode 100644 index 000000000000..f446e2fc4c56 --- /dev/null +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/JsonRpcResponse.java @@ -0,0 +1,16 @@ +package org.enso.ydoc.jsonrpc; + +import org.enso.ydoc.jsonrpc.model.result.Result; + +public record JsonRpcResponse(String jsonrpc, String id, Result result) { + + private static final String JSONRPC_VERSION_2_0 = "2.0"; + + public JsonRpcResponse(String id, Result result) { + this(JSONRPC_VERSION_2_0, id, result); + } + + public static JsonRpcResponse ok(String id) { + return new JsonRpcResponse(id, null); + } +} diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/ContentRoot.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/ContentRoot.java new file mode 100644 index 000000000000..74fc9d13efc8 --- /dev/null +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/ContentRoot.java @@ -0,0 +1,10 @@ +package org.enso.ydoc.jsonrpc.model; + +import java.util.UUID; + +public record ContentRoot(String type, UUID id, String path) { + + public ContentRoot(String type, UUID id) { + this(type, id, null); + } +} diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/FilePath.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/FilePath.java new file mode 100644 index 000000000000..b1bff5849d8c --- /dev/null +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/FilePath.java @@ -0,0 +1,6 @@ +package org.enso.ydoc.jsonrpc.model; + +import java.util.List; +import java.util.UUID; + +public record FilePath(UUID rootId, List segments) {} diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/FileSystemObject.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/FileSystemObject.java new file mode 100644 index 000000000000..4fdfbe2a6a8f --- /dev/null +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/FileSystemObject.java @@ -0,0 +1,8 @@ +package org.enso.ydoc.jsonrpc.model; + +public record FileSystemObject(String type, String name, FilePath path) { + + public static FileSystemObject file(String name, FilePath path) { + return new FileSystemObject("File", name, path); + } +} diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/WriteCapability.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/WriteCapability.java new file mode 100644 index 000000000000..abce6fef3a45 --- /dev/null +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/WriteCapability.java @@ -0,0 +1,6 @@ +package org.enso.ydoc.jsonrpc.model; + +public record WriteCapability(String method, Options registerOptions) { + + public record Options(FilePath path) {} +} diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/FileListResult.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/FileListResult.java new file mode 100644 index 000000000000..92e8da00dd69 --- /dev/null +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/FileListResult.java @@ -0,0 +1,6 @@ +package org.enso.ydoc.jsonrpc.model.result; + +import java.util.List; +import org.enso.ydoc.jsonrpc.model.FileSystemObject; + +public record FileListResult(List paths) implements Result {} diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/InitProtocolConnectionResult.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/InitProtocolConnectionResult.java new file mode 100644 index 000000000000..3a0dad0d39a1 --- /dev/null +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/InitProtocolConnectionResult.java @@ -0,0 +1,7 @@ +package org.enso.ydoc.jsonrpc.model.result; + +import java.util.List; +import org.enso.ydoc.jsonrpc.model.ContentRoot; + +public record InitProtocolConnectionResult( + String ensoVersion, String currentEdition, List contentRoots) implements Result {} diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/Result.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/Result.java new file mode 100644 index 000000000000..f94dd6f49306 --- /dev/null +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/Result.java @@ -0,0 +1,3 @@ +package org.enso.ydoc.jsonrpc.model.result; + +public interface Result {} diff --git a/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/TextOpenFileResult.java b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/TextOpenFileResult.java new file mode 100644 index 000000000000..88c2317672d5 --- /dev/null +++ b/lib/java/ydoc-server/src/test/java/org/enso/ydoc/jsonrpc/model/result/TextOpenFileResult.java @@ -0,0 +1,6 @@ +package org.enso.ydoc.jsonrpc.model.result; + +import org.enso.ydoc.jsonrpc.model.WriteCapability; + +public record TextOpenFileResult( + WriteCapability writeCapability, String content, String currentVersion) implements Result {}