From aa205509820b526bf21dfedebfd798ded0d763e7 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 24 Oct 2021 01:19:02 +0200 Subject: test(web): websockets fixes and tests --- .../refinery/language/web/ServerLauncher.java | 2 + .../xtext/server/push/PushWebDocumentAccess.java | 4 +- .../language/web/ProblemWebInjectorProvider.java | 46 ++++++ .../ProblemWebSocketServletIntegrationTest.java | 172 +++++++++++++++++++++ .../xtext/servlet/ProblemWebInjectorProvider.java | 47 ------ .../web/xtext/servlet/TransactionExecutorTest.java | 23 ++- 6 files changed, 243 insertions(+), 51 deletions(-) create mode 100644 language-web/src/test/java/tools/refinery/language/web/ProblemWebInjectorProvider.java create mode 100644 language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java delete mode 100644 language-web/src/test/java/tools/refinery/language/web/xtext/servlet/ProblemWebInjectorProvider.java (limited to 'language-web/src') diff --git a/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java index a71d8e93..cde7278f 100644 --- a/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java +++ b/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java @@ -18,6 +18,7 @@ import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,6 +74,7 @@ public class ServerLauncher { allowedOriginsString); } handler.addServlet(problemServletHolder, "/xtext-service/*"); + JettyWebSocketServletContainerInitializer.configure(handler, null); } private void addDefaultServlet(ServletContextHandler handler) { diff --git a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java index ff4bb035..b3666a86 100644 --- a/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java +++ b/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java @@ -58,10 +58,10 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess { protected String getPrecomputedServiceName(AbstractCachedService service) { if (service instanceof ValidationService) { - return "validation"; + return "validate"; } if (service instanceof HighlightingService) { - return "highlighting"; + return "highlight"; } throw new IllegalArgumentException("Unknown precomputed service: " + service); } diff --git a/language-web/src/test/java/tools/refinery/language/web/ProblemWebInjectorProvider.java b/language-web/src/test/java/tools/refinery/language/web/ProblemWebInjectorProvider.java new file mode 100644 index 00000000..2db4590e --- /dev/null +++ b/language-web/src/test/java/tools/refinery/language/web/ProblemWebInjectorProvider.java @@ -0,0 +1,46 @@ +package tools.refinery.language.web; + +import org.eclipse.xtext.ide.ExecutorServiceProvider; +import org.eclipse.xtext.util.DisposableRegistry; +import org.eclipse.xtext.util.Modules2; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +import tools.refinery.language.ide.ProblemIdeModule; +import tools.refinery.language.tests.ProblemInjectorProvider; +import tools.refinery.language.web.xtext.servlet.AwaitTerminationExecutorServiceProvider; + +public class ProblemWebInjectorProvider extends ProblemInjectorProvider { + + protected Injector internalCreateInjector() { + return new ProblemWebSetup() { + @Override + public Injector createInjector() { + return Guice.createInjector( + Modules2.mixin(createRuntimeModule(), new ProblemIdeModule(), createWebModule())); + } + }.createInjectorAndDoEMFRegistration(); + } + + protected ProblemWebModule createWebModule() { + // Await termination of the executor service to avoid race conditions between + // the tasks in the service and the {@link + // org.eclipse.xtext.testing.extensions.InjectionExtension}. + return new ProblemWebModule() { + @SuppressWarnings("unused") + public Class bindExecutorServiceProvider() { + return AwaitTerminationExecutorServiceProvider.class; + } + }; + } + + @Override + public void restoreRegistry() { + // Also make sure to dispose any IDisposable instances (that may depend on the + // global state) created by Xtext before restoring the global state. + var disposableRegistry = getInjector().getInstance(DisposableRegistry.class); + disposableRegistry.dispose(); + super.restoreRegistry(); + } +} diff --git a/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java b/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java new file mode 100644 index 00000000..60581b5c --- /dev/null +++ b/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java @@ -0,0 +1,172 @@ +package tools.refinery.language.web; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.StatusCode; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; +import org.eclipse.xtext.testing.GlobalRegistries; +import org.eclipse.xtext.testing.GlobalRegistries.GlobalStateMemento; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; + +class ProblemWebSocketServletIntegrationTest { + private static int SERVER_PORT = 28080; + + private static long TIMEOUT_MILLIS = Duration.ofSeconds(1).toMillis(); + + private GlobalStateMemento stateBeforeInjectorCreation; + + private Server server; + + private WebSocketClient client; + + @BeforeEach + void startServer() throws Exception { + stateBeforeInjectorCreation = GlobalRegistries.makeCopyOfGlobalState(); + server = new Server(new InetSocketAddress(SERVER_PORT)); + var handler = new ServletContextHandler(); + handler.addServlet(ProblemWebSocketServlet.class, "/xtext-service/*"); + JettyWebSocketServletContainerInitializer.configure(handler, null); + server.setHandler(handler); + server.start(); + client = new WebSocketClient(); + client.start(); + } + + @AfterEach + void stopServer() throws Exception { + client.stop(); + server.stop(); + stateBeforeInjectorCreation.restoreGlobalState(); + } + + @Test + void updateTest() throws IOException { + var clientSocket = new UpdateTestClient(); + var upgradeRequest = new ClientUpgradeRequest(); + upgradeRequest.setSubProtocols(XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1); + var sessionFuture = client.connect(clientSocket, URI.create("ws://localhost:" + SERVER_PORT + "/xtext-service"), + upgradeRequest); + var session = sessionFuture.join(); + assertThat(session.getUpgradeResponse().getAcceptedSubProtocol(), + equalTo(XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1)); + clientSocket.waitForTestResult(); + } + + @WebSocket + public static class UpdateTestClient { + private boolean finished = false; + + private Object lock = new Object(); + + private Throwable error; + + private int closeStatusCode; + + private String closeReason; + + private List responses = new ArrayList<>(); + + @OnWebSocketConnect + public void onConnect(Session session) { + try { + session.getRemote().sendString( + "{\"id\":\"foo\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"fullText\":\"class Person.\n\"}}"); + } catch (IOException e) { + finishedWithError(e); + } + } + + @OnWebSocketClose + public void onClose(int statusCode, String reason) { + closeStatusCode = statusCode; + closeReason = reason; + testFinished(); + } + + @OnWebSocketError + public void onError(Throwable error) { + finishedWithError(error); + } + + @OnWebSocketMessage + public void onMessage(Session session, String message) { + try { + responses.add(message); + switch (responses.size()) { + case 3 -> session.getRemote().sendString( + "{\"id\":\"bar\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"requiredStateId\":\"-80000000\",\"deltaText\":\"class Car.\n\",\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}"); + case 5 -> session.close(); + } + } catch (IOException e) { + finishedWithError(e); + } + } + + private void finishedWithError(Throwable t) { + error = t; + testFinished(); + } + + private void testFinished() { + synchronized (lock) { + finished = true; + lock.notify(); + } + } + + public void waitForTestResult() { + synchronized (lock) { + if (!finished) { + try { + lock.wait(TIMEOUT_MILLIS); + } catch (InterruptedException e) { + fail("Unexpected InterruptedException", e); + } + } + } + if (!finished) { + fail("Test still not finished after timeout"); + } + if (error != null) { + fail("Unexpected exception in websocket thread", error); + } + if (closeStatusCode != StatusCode.NORMAL) { + fail("Abnormal close status " + closeStatusCode + ": " + closeReason); + } + assertThat(responses, hasSize(5)); + assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}")); + assertThat(responses.get(1), startsWith( + "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\",\"push\":{\"regions\":[")); + assertThat(responses.get(2), equalTo( + "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"validate\",\"push\":{\"issues\":[]}}")); + assertThat(responses.get(3), equalTo("{\"id\":\"bar\",\"response\":{\"stateId\":\"-7fffffff\"}}")); + assertThat(responses.get(4), startsWith( + "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\",\"push\":{\"regions\":[")); + } + } +} diff --git a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/ProblemWebInjectorProvider.java b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/ProblemWebInjectorProvider.java deleted file mode 100644 index a6d97c8b..00000000 --- a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/ProblemWebInjectorProvider.java +++ /dev/null @@ -1,47 +0,0 @@ -package tools.refinery.language.web.xtext.servlet; - -import org.eclipse.xtext.ide.ExecutorServiceProvider; -import org.eclipse.xtext.util.DisposableRegistry; -import org.eclipse.xtext.util.Modules2; - -import com.google.inject.Guice; -import com.google.inject.Injector; - -import tools.refinery.language.ide.ProblemIdeModule; -import tools.refinery.language.tests.ProblemInjectorProvider; -import tools.refinery.language.web.ProblemWebModule; -import tools.refinery.language.web.ProblemWebSetup; - -public class ProblemWebInjectorProvider extends ProblemInjectorProvider { - - protected Injector internalCreateInjector() { - return new ProblemWebSetup() { - @Override - public Injector createInjector() { - return Guice.createInjector( - Modules2.mixin(createRuntimeModule(), new ProblemIdeModule(), createWebModule())); - } - }.createInjectorAndDoEMFRegistration(); - } - - protected ProblemWebModule createWebModule() { - // Await termination of the executor service to avoid race conditions between - // the tasks in the service and the {@link - // org.eclipse.xtext.testing.extensions.InjectionExtension}. - return new ProblemWebModule() { - @SuppressWarnings("unused") - public Class bindExecutorServiceProvider() { - return AwaitTerminationExecutorServiceProvider.class; - } - }; - } - - @Override - public void restoreRegistry() { - // Also make sure to dispose any IDisposable instances (that may depend on the - // global state) created by Xtext before restoring the global state. - var disposableRegistry = getInjector().getInstance(DisposableRegistry.class); - disposableRegistry.dispose(); - super.restoreRegistry(); - } -} diff --git a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java index 7f7c3e43..2d3f45d6 100644 --- a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java +++ b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.verify; import java.util.List; import java.util.Map; +import org.eclipse.emf.common.util.URI; import org.eclipse.xtext.resource.IResourceServiceProvider; import org.eclipse.xtext.testing.InjectWith; import org.eclipse.xtext.testing.extensions.InjectionExtension; @@ -25,6 +26,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import com.google.inject.Inject; +import tools.refinery.language.web.ProblemWebInjectorProvider; import tools.refinery.language.web.xtext.server.ResponseHandler; import tools.refinery.language.web.xtext.server.ResponseHandlerException; import tools.refinery.language.web.xtext.server.TransactionExecutor; @@ -38,6 +40,8 @@ import tools.refinery.language.web.xtext.server.message.XtextWebResponse; class TransactionExecutorTest { private static final String RESOURCE_NAME = "test.problem"; + private static final String PROBLEM_CONTENT_TYPE = "application/x-tools.refinery.problem"; + private static final String TEST_PROBLEM = """ class Person { Person[0..*] friend opposite friend @@ -95,6 +99,21 @@ class TransactionExecutorTest { assertHighlightingResponse(newStateId, captor.getAllValues().get(1)); } + @Test + void fullTextWithoutResourceTest() throws ResponseHandlerException { + var resourceServiceProvider = resourceServiceProviderRegistry + .getResourceServiceProvider(URI.createFileURI(RESOURCE_NAME)); + resourceServiceProviderRegistry.getContentTypeToFactoryMap().put(PROBLEM_CONTENT_TYPE, resourceServiceProvider); + var responseHandler = sendRequestAndWaitForAllResponses(new XtextWebRequest("foo", + Map.of("contentType", PROBLEM_CONTENT_TYPE, "fullText", TEST_PROBLEM, "serviceType", "validate"))); + + var captor = newCaptor(); + verify(responseHandler).onResponse(captor.capture()); + var response = captor.getValue(); + assertThat(response, hasProperty("id", equalTo("foo"))); + assertThat(response, hasProperty("responseData", instanceOf(ValidationResult.class))); + } + private ArgumentCaptor newCaptor() { return ArgumentCaptor.forClass(XtextWebResponse.class); } @@ -132,14 +151,14 @@ class TransactionExecutorTest { private void assertHighlightingResponse(String stateId, XtextWebResponse highlightingResponse) { assertThat(highlightingResponse, hasProperty("resourceId", equalTo(RESOURCE_NAME))); assertThat(highlightingResponse, hasProperty("stateId", equalTo(stateId))); - assertThat(highlightingResponse, hasProperty("service", equalTo("highlighting"))); + assertThat(highlightingResponse, hasProperty("service", equalTo("highlight"))); assertThat(highlightingResponse, hasProperty("pushData", instanceOf(HighlightingResult.class))); } private void assertValidationResponse(String stateId, XtextWebResponse validationResponse) { assertThat(validationResponse, hasProperty("resourceId", equalTo(RESOURCE_NAME))); assertThat(validationResponse, hasProperty("stateId", equalTo(stateId))); - assertThat(validationResponse, hasProperty("service", equalTo("validation"))); + assertThat(validationResponse, hasProperty("service", equalTo("validate"))); assertThat(validationResponse, hasProperty("pushData", instanceOf(ValidationResult.class))); } } -- cgit v1.2.3-54-g00ecf