Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the ydoc js bundle test #10160

Merged
merged 6 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding test only dependency is OK.

)
)
// `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 YDOC_HOST = "YDOC_HOST";
private static final String YDOC_PORT = "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(YDOC_HOST);
var ydocPort = System.getenv(YDOC_PORT);
4e6 marked this conversation as resolved.
Show resolved Hide resolved

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(36);
4e6 marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a mock server, mocking language server protocol. The real Ydoc code is connecting to it and we are verifying it connects and exchanges few messages, rigth?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct


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 {}
Loading