From e90f0cd525c619b1d0e21bfcd7466a99853ee710 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 24 Oct 2021 14:34:39 +0200 Subject: test(web): more websocket integration tests --- .../refinery/language/web/ServerLauncher.java | 2 +- .../language/web/ProblemWebInjectorProvider.java | 46 ---- .../ProblemWebSocketServletIntegrationTest.java | 232 ++++++++++++--------- .../AwaitTerminationExecutorServiceProvider.java | 42 ++++ .../web/tests/ProblemWebInjectorProvider.java | 47 +++++ .../web/tests/RestartableCachedThreadPool.java | 109 ++++++++++ .../web/tests/WebSocketIntegrationTestClient.java | 98 +++++++++ .../AwaitTerminationExecutorServiceProvider.java | 42 ---- .../xtext/servlet/RestartableCachedThreadPool.java | 109 ---------- .../web/xtext/servlet/TransactionExecutorTest.java | 3 +- 10 files changed, 431 insertions(+), 299 deletions(-) delete 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/tests/AwaitTerminationExecutorServiceProvider.java create mode 100644 language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java create mode 100644 language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java create mode 100644 language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java delete mode 100644 language-web/src/test/java/tools/refinery/language/web/xtext/servlet/AwaitTerminationExecutorServiceProvider.java delete mode 100644 language-web/src/test/java/tools/refinery/language/web/xtext/servlet/RestartableCachedThreadPool.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 cde7278f..ffd903d0 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 @@ -73,7 +73,7 @@ public class ServerLauncher { problemServletHolder.setInitParameter(XtextWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM, allowedOriginsString); } - handler.addServlet(problemServletHolder, "/xtext-service/*"); + handler.addServlet(problemServletHolder, "/xtext-service"); JettyWebSocketServletContainerInitializer.configure(handler, null); } 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 deleted file mode 100644 index 2db4590e..00000000 --- a/language-web/src/test/java/tools/refinery/language/web/ProblemWebInjectorProvider.java +++ /dev/null @@ -1,46 +0,0 @@ -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 index 60581b5c..5ccd155f 100644 --- a/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java +++ b/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java @@ -3,25 +3,25 @@ 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.instanceOf; import static org.hamcrest.Matchers.startsWith; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; 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 java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; 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.api.exceptions.UpgradeException; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.WebSocketClient; import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; @@ -30,13 +30,17 @@ 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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import tools.refinery.language.web.tests.WebSocketIntegrationTestClient; +import tools.refinery.language.web.xtext.servlet.XtextStatusCode; 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 static String SERVLET_URI = "/xtext-service"; private GlobalStateMemento stateBeforeInjectorCreation; @@ -45,128 +49,156 @@ class ProblemWebSocketServletIntegrationTest { private WebSocketClient client; @BeforeEach - void startServer() throws Exception { + void beforeEach() 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 { + void afterEach() throws Exception { client.stop(); - server.stop(); + client = null; + if (server != null) { + server.stop(); + server = null; + } stateBeforeInjectorCreation.restoreGlobalState(); + stateBeforeInjectorCreation = null; } @Test - void updateTest() throws IOException { + void updateTest() { + startServer(null); 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(); + var session = connect(clientSocket, null, XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1); assertThat(session.getUpgradeResponse().getAcceptedSubProtocol(), equalTo(XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1)); clientSocket.waitForTestResult(); + assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); + var responses = clientSocket.getResponses(); + 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\":[")); } @WebSocket - public static class UpdateTestClient { - private boolean finished = false; - - private Object lock = new Object(); + public static class UpdateTestClient extends WebSocketIntegrationTestClient { + @Override + protected void arrange(Session session, int responsesReceived) throws IOException { + switch (responsesReceived) { + case 0 -> session.getRemote().sendString( + "{\"id\":\"foo\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"fullText\":\"class Person.\n\"}}"); + 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(); + } + } + } - private Throwable error; + @Test + void badSubProtocolTest() { + startServer(null); + var clientSocket = new CloseImmediatelyTestClient(); + var session = connect(clientSocket, null, ""); + assertThat(session.getUpgradeResponse().getAcceptedSubProtocol(), equalTo(null)); + clientSocket.waitForTestResult(); + assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); + } - private int closeStatusCode; + @WebSocket + public static class CloseImmediatelyTestClient extends WebSocketIntegrationTestClient { + @Override + protected void arrange(Session session, int responsesReceived) throws IOException { + session.close(); + } + } - private String closeReason; + @Test + void subProtocolNegotiationTest() { + startServer(null); + var clientSocket = new CloseImmediatelyTestClient(); + var session = connect(clientSocket, null, "", XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1); + assertThat(session.getUpgradeResponse().getAcceptedSubProtocol(), + equalTo(XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1)); + clientSocket.waitForTestResult(); + assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); + } - private List responses = new ArrayList<>(); + @Test + void invalidJsonTest() { + startServer(null); + var clientSocket = new InvalidJsonTestClient(); + connect(clientSocket, null, XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1); + clientSocket.waitForTestResult(); + assertThat(clientSocket.getCloseStatusCode(), equalTo(XtextStatusCode.INVALID_JSON)); + } - @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); - } + @WebSocket + public static class InvalidJsonTestClient extends WebSocketIntegrationTestClient { + @Override + protected void arrange(Session session, int responsesReceived) throws IOException { + session.getRemote().sendString(""); } + } - @OnWebSocketClose - public void onClose(int statusCode, String reason) { - closeStatusCode = statusCode; - closeReason = reason; - testFinished(); - } + @ParameterizedTest(name = "Origin: {0}") + @ValueSource(strings = { "https://refinery.example", "https://refinery.example:443", "HTTPS://REFINERY.EXAMPLE" }) + void validOriginTest(String origin) { + startServer("https://refinery.example;https://refinery.example:443"); + var clientSocket = new CloseImmediatelyTestClient(); + connect(clientSocket, origin, XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1); + clientSocket.waitForTestResult(); + assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); + } - @OnWebSocketError - public void onError(Throwable error) { - finishedWithError(error); - } + @Test + void invalidOriginTest() { + startServer("https://refinery.example;https://refinery.example:443"); + var clientSocket = new CloseImmediatelyTestClient(); + var exception = assertThrows(CompletionException.class, + () -> connect(clientSocket, "https://invalid.example", XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1)); + var innerException = exception.getCause(); + assertThat(innerException, instanceOf(UpgradeException.class)); + assertThat(((UpgradeException) innerException).getResponseStatusCode(), equalTo(HttpStatus.FORBIDDEN_403)); + } - @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 startServer(String allowedOrigins) { + server = new Server(new InetSocketAddress(SERVER_PORT)); + var handler = new ServletContextHandler(); + var holder = new ServletHolder(ProblemWebSocketServlet.class); + if (allowedOrigins != null) { + holder.setInitParameter(ProblemWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM, allowedOrigins); } - - private void finishedWithError(Throwable t) { - error = t; - testFinished(); + handler.addServlet(holder, SERVLET_URI); + JettyWebSocketServletContainerInitializer.configure(handler, null); + server.setHandler(handler); + try { + server.start(); + } catch (Exception e) { + throw new RuntimeException("Failed to start websocket server"); } + } - private void testFinished() { - synchronized (lock) { - finished = true; - lock.notify(); - } + private Session connect(Object webSocketClient, String origin, String... subProtocols) { + var upgradeRequest = new ClientUpgradeRequest(); + if (origin != null) { + upgradeRequest.setHeader(HttpHeader.ORIGIN.name(), origin); } - - 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\":[")); + upgradeRequest.setSubProtocols(subProtocols); + CompletableFuture sessionFuture; + try { + sessionFuture = client.connect(webSocketClient, URI.create("ws://localhost:" + SERVER_PORT + SERVLET_URI), + upgradeRequest); + } catch (IOException e) { + throw new AssertionError("Unexpected exception while connection to websocket", e); } + return sessionFuture.join(); } } diff --git a/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java b/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java new file mode 100644 index 00000000..b70d0ed5 --- /dev/null +++ b/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java @@ -0,0 +1,42 @@ +package tools.refinery.language.web.tests; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; + +import org.eclipse.xtext.ide.ExecutorServiceProvider; + +import com.google.inject.Singleton; + +@Singleton +public class AwaitTerminationExecutorServiceProvider extends ExecutorServiceProvider { + private List servicesToShutDown = new ArrayList<>(); + + @Override + protected ExecutorService createInstance(String key) { + var instance = new RestartableCachedThreadPool(); + synchronized (servicesToShutDown) { + servicesToShutDown.add(instance); + } + return instance; + } + + public void waitForAllTasksToFinish() { + synchronized (servicesToShutDown) { + for (var executorService : servicesToShutDown) { + executorService.waitForAllTasksToFinish(); + } + } + } + + @Override + public void dispose() { + super.dispose(); + synchronized (servicesToShutDown) { + for (var executorService : servicesToShutDown) { + executorService.waitForTermination(); + } + servicesToShutDown.clear(); + } + } +} diff --git a/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java b/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java new file mode 100644 index 00000000..43c12faa --- /dev/null +++ b/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java @@ -0,0 +1,47 @@ +package tools.refinery.language.web.tests; + +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/tests/RestartableCachedThreadPool.java b/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java new file mode 100644 index 00000000..1468273d --- /dev/null +++ b/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java @@ -0,0 +1,109 @@ +package tools.refinery.language.web.tests; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RestartableCachedThreadPool implements ExecutorService { + private static final Logger LOG = LoggerFactory.getLogger(RestartableCachedThreadPool.class); + + private ExecutorService delegate; + + public RestartableCachedThreadPool() { + delegate = createExecutorService(); + } + + public void waitForAllTasksToFinish() { + delegate.shutdown(); + waitForTermination(); + delegate = createExecutorService(); + } + + public void waitForTermination() { + try { + delegate.awaitTermination(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LOG.warn("Interrupted while waiting for delegate executor to stop", e); + } + } + + protected ExecutorService createExecutorService() { + return Executors.newCachedThreadPool(); + } + + @Override + public boolean awaitTermination(long arg0, TimeUnit arg1) throws InterruptedException { + return delegate.awaitTermination(arg0, arg1); + } + + @Override + public void execute(Runnable arg0) { + delegate.execute(arg0); + } + + @Override + public List> invokeAll(Collection> arg0, long arg1, TimeUnit arg2) + throws InterruptedException { + return delegate.invokeAll(arg0, arg1, arg2); + } + + @Override + public List> invokeAll(Collection> arg0) throws InterruptedException { + return delegate.invokeAll(arg0); + } + + @Override + public T invokeAny(Collection> arg0, long arg1, TimeUnit arg2) + throws InterruptedException, ExecutionException, TimeoutException { + return delegate.invokeAny(arg0, arg1, arg2); + } + + @Override + public T invokeAny(Collection> arg0) throws InterruptedException, ExecutionException { + return delegate.invokeAny(arg0); + } + + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + @Override + public void shutdown() { + delegate.shutdown(); + } + + @Override + public List shutdownNow() { + return delegate.shutdownNow(); + } + + @Override + public Future submit(Callable arg0) { + return delegate.submit(arg0); + } + + @Override + public Future submit(Runnable arg0, T arg1) { + return delegate.submit(arg0, arg1); + } + + @Override + public Future submit(Runnable arg0) { + return delegate.submit(arg0); + } +} diff --git a/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java b/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java new file mode 100644 index 00000000..49464d27 --- /dev/null +++ b/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java @@ -0,0 +1,98 @@ +package tools.refinery.language.web.tests; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jetty.websocket.api.Session; +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; + +public abstract class WebSocketIntegrationTestClient { + private static long TIMEOUT_MILLIS = Duration.ofSeconds(1).toMillis(); + + private boolean finished = false; + + private Object lock = new Object(); + + private Throwable error; + + private int closeStatusCode; + + private List responses = new ArrayList<>(); + + public int getCloseStatusCode() { + return closeStatusCode; + } + + public List getResponses() { + return responses; + } + + @OnWebSocketConnect + public void onConnect(Session session) { + arrangeAndCatchErrors(session); + } + + private void arrangeAndCatchErrors(Session session) { + try { + arrange(session, responses.size()); + } catch (Exception e) { + finishedWithError(e); + } + } + + protected abstract void arrange(Session session, int responsesReceived) throws IOException; + + @OnWebSocketClose + public void onClose(int statusCode, String reason) { + closeStatusCode = statusCode; + testFinished(); + } + + @OnWebSocketError + public void onError(Throwable error) { + finishedWithError(error); + } + + @OnWebSocketMessage + public void onMessage(Session session, String message) { + responses.add(message); + arrangeAndCatchErrors(session); + } + + 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); + } + } +} diff --git a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/AwaitTerminationExecutorServiceProvider.java b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/AwaitTerminationExecutorServiceProvider.java deleted file mode 100644 index 25bcec37..00000000 --- a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/AwaitTerminationExecutorServiceProvider.java +++ /dev/null @@ -1,42 +0,0 @@ -package tools.refinery.language.web.xtext.servlet; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; - -import org.eclipse.xtext.ide.ExecutorServiceProvider; - -import com.google.inject.Singleton; - -@Singleton -public class AwaitTerminationExecutorServiceProvider extends ExecutorServiceProvider { - private List servicesToShutDown = new ArrayList<>(); - - @Override - protected ExecutorService createInstance(String key) { - var instance = new RestartableCachedThreadPool(); - synchronized (servicesToShutDown) { - servicesToShutDown.add(instance); - } - return instance; - } - - public void waitForAllTasksToFinish() { - synchronized (servicesToShutDown) { - for (var executorService : servicesToShutDown) { - executorService.waitForAllTasksToFinish(); - } - } - } - - @Override - public void dispose() { - super.dispose(); - synchronized (servicesToShutDown) { - for (var executorService : servicesToShutDown) { - executorService.waitForTermination(); - } - servicesToShutDown.clear(); - } - } -} diff --git a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/RestartableCachedThreadPool.java b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/RestartableCachedThreadPool.java deleted file mode 100644 index 02ef38e2..00000000 --- a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/RestartableCachedThreadPool.java +++ /dev/null @@ -1,109 +0,0 @@ -package tools.refinery.language.web.xtext.servlet; - -import java.util.Collection; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class RestartableCachedThreadPool implements ExecutorService { - private static final Logger LOG = LoggerFactory.getLogger(RestartableCachedThreadPool.class); - - private ExecutorService delegate; - - public RestartableCachedThreadPool() { - delegate = createExecutorService(); - } - - public void waitForAllTasksToFinish() { - delegate.shutdown(); - waitForTermination(); - delegate = createExecutorService(); - } - - public void waitForTermination() { - try { - delegate.awaitTermination(1, TimeUnit.SECONDS); - } catch (InterruptedException e) { - LOG.warn("Interrupted while waiting for delegate executor to stop", e); - } - } - - protected ExecutorService createExecutorService() { - return Executors.newCachedThreadPool(); - } - - @Override - public boolean awaitTermination(long arg0, TimeUnit arg1) throws InterruptedException { - return delegate.awaitTermination(arg0, arg1); - } - - @Override - public void execute(Runnable arg0) { - delegate.execute(arg0); - } - - @Override - public List> invokeAll(Collection> arg0, long arg1, TimeUnit arg2) - throws InterruptedException { - return delegate.invokeAll(arg0, arg1, arg2); - } - - @Override - public List> invokeAll(Collection> arg0) throws InterruptedException { - return delegate.invokeAll(arg0); - } - - @Override - public T invokeAny(Collection> arg0, long arg1, TimeUnit arg2) - throws InterruptedException, ExecutionException, TimeoutException { - return delegate.invokeAny(arg0, arg1, arg2); - } - - @Override - public T invokeAny(Collection> arg0) throws InterruptedException, ExecutionException { - return delegate.invokeAny(arg0); - } - - @Override - public boolean isShutdown() { - return delegate.isShutdown(); - } - - @Override - public boolean isTerminated() { - return delegate.isTerminated(); - } - - @Override - public void shutdown() { - delegate.shutdown(); - } - - @Override - public List shutdownNow() { - return delegate.shutdownNow(); - } - - @Override - public Future submit(Callable arg0) { - return delegate.submit(arg0); - } - - @Override - public Future submit(Runnable arg0, T arg1) { - return delegate.submit(arg0, arg1); - } - - @Override - public Future submit(Runnable arg0) { - return delegate.submit(arg0); - } -} 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 2d3f45d6..0892954b 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 @@ -26,7 +26,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import com.google.inject.Inject; -import tools.refinery.language.web.ProblemWebInjectorProvider; +import tools.refinery.language.web.tests.AwaitTerminationExecutorServiceProvider; +import tools.refinery.language.web.tests.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; -- cgit v1.2.3-54-g00ecf