Skip to content

Commit

Permalink
Implement the ydoc js bundle test (#10160)
Browse files Browse the repository at this point in the history
close #9929

Changelog:
- update: customize Ydoc main hostname and port with environment variables
- add: Ydoc initialization test
  • Loading branch information
4e6 authored Jun 4, 2024
1 parent 7c35781 commit 106007c
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 21 deletions.
17 changes: 9 additions & 8 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 16 additions & 1 deletion lib/java/ydoc-server/src/main/java/org/enso/ydoc/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
Expand All @@ -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();
}
Expand Down
156 changes: 144 additions & 12 deletions lib/java/ydoc-server/src/test/java/org/enso/ydoc/YdocTest.java
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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();
Expand All @@ -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<BufferData>();
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<BufferData> messages;

TestWsListener(BlockingQueue<BufferData> messages) {
private DashboardConnection(BlockingQueue<BufferData> 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());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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) {}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.enso.ydoc.jsonrpc.model;

import java.util.List;
import java.util.UUID;

public record FilePath(UUID rootId, List<String> segments) {}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.enso.ydoc.jsonrpc.model;

public record WriteCapability(String method, Options registerOptions) {

public record Options(FilePath path) {}
}
Original file line number Diff line number Diff line change
@@ -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<FileSystemObject> paths) implements Result {}
Original file line number Diff line number Diff line change
@@ -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<ContentRoot> contentRoots) implements Result {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.enso.ydoc.jsonrpc.model.result;

public interface Result {}
Original file line number Diff line number Diff line change
@@ -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 {}

0 comments on commit 106007c

Please sign in to comment.