diff options
Diffstat (limited to 'subprojects/language-web/src/test')
6 files changed, 665 insertions, 0 deletions
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java new file mode 100644 index 00000000..a26ce040 --- /dev/null +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java | |||
@@ -0,0 +1,204 @@ | |||
1 | package tools.refinery.language.web; | ||
2 | |||
3 | import static org.hamcrest.MatcherAssert.assertThat; | ||
4 | import static org.hamcrest.Matchers.equalTo; | ||
5 | import static org.hamcrest.Matchers.hasSize; | ||
6 | import static org.hamcrest.Matchers.instanceOf; | ||
7 | import static org.hamcrest.Matchers.startsWith; | ||
8 | import static org.junit.jupiter.api.Assertions.assertThrows; | ||
9 | |||
10 | import java.io.IOException; | ||
11 | import java.net.InetSocketAddress; | ||
12 | import java.net.URI; | ||
13 | import java.util.concurrent.CompletableFuture; | ||
14 | import java.util.concurrent.CompletionException; | ||
15 | |||
16 | import org.eclipse.jetty.http.HttpHeader; | ||
17 | import org.eclipse.jetty.http.HttpStatus; | ||
18 | import org.eclipse.jetty.server.Server; | ||
19 | import org.eclipse.jetty.servlet.ServletContextHandler; | ||
20 | import org.eclipse.jetty.servlet.ServletHolder; | ||
21 | import org.eclipse.jetty.websocket.api.Session; | ||
22 | import org.eclipse.jetty.websocket.api.StatusCode; | ||
23 | import org.eclipse.jetty.websocket.api.annotations.WebSocket; | ||
24 | import org.eclipse.jetty.websocket.api.exceptions.UpgradeException; | ||
25 | import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; | ||
26 | import org.eclipse.jetty.websocket.client.WebSocketClient; | ||
27 | import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; | ||
28 | import org.eclipse.xtext.testing.GlobalRegistries; | ||
29 | import org.eclipse.xtext.testing.GlobalRegistries.GlobalStateMemento; | ||
30 | import org.junit.jupiter.api.AfterEach; | ||
31 | import org.junit.jupiter.api.BeforeEach; | ||
32 | import org.junit.jupiter.api.Test; | ||
33 | import org.junit.jupiter.params.ParameterizedTest; | ||
34 | import org.junit.jupiter.params.provider.ValueSource; | ||
35 | |||
36 | import tools.refinery.language.web.tests.WebSocketIntegrationTestClient; | ||
37 | import tools.refinery.language.web.xtext.servlet.XtextStatusCode; | ||
38 | import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; | ||
39 | |||
40 | class ProblemWebSocketServletIntegrationTest { | ||
41 | private static int SERVER_PORT = 28080; | ||
42 | |||
43 | private static String SERVLET_URI = "/xtext-service"; | ||
44 | |||
45 | private GlobalStateMemento stateBeforeInjectorCreation; | ||
46 | |||
47 | private Server server; | ||
48 | |||
49 | private WebSocketClient client; | ||
50 | |||
51 | @BeforeEach | ||
52 | void beforeEach() throws Exception { | ||
53 | stateBeforeInjectorCreation = GlobalRegistries.makeCopyOfGlobalState(); | ||
54 | client = new WebSocketClient(); | ||
55 | client.start(); | ||
56 | } | ||
57 | |||
58 | @AfterEach | ||
59 | void afterEach() throws Exception { | ||
60 | client.stop(); | ||
61 | client = null; | ||
62 | if (server != null) { | ||
63 | server.stop(); | ||
64 | server = null; | ||
65 | } | ||
66 | stateBeforeInjectorCreation.restoreGlobalState(); | ||
67 | stateBeforeInjectorCreation = null; | ||
68 | } | ||
69 | |||
70 | @Test | ||
71 | void updateTest() { | ||
72 | startServer(null); | ||
73 | var clientSocket = new UpdateTestClient(); | ||
74 | var session = connect(clientSocket, null, XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1); | ||
75 | assertThat(session.getUpgradeResponse().getAcceptedSubProtocol(), | ||
76 | equalTo(XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1)); | ||
77 | clientSocket.waitForTestResult(); | ||
78 | assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); | ||
79 | var responses = clientSocket.getResponses(); | ||
80 | assertThat(responses, hasSize(5)); | ||
81 | assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}")); | ||
82 | assertThat(responses.get(1), startsWith( | ||
83 | "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\",\"push\":{\"regions\":[")); | ||
84 | assertThat(responses.get(2), equalTo( | ||
85 | "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"validate\",\"push\":{\"issues\":[]}}")); | ||
86 | assertThat(responses.get(3), equalTo("{\"id\":\"bar\",\"response\":{\"stateId\":\"-7fffffff\"}}")); | ||
87 | assertThat(responses.get(4), startsWith( | ||
88 | "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\",\"push\":{\"regions\":[")); | ||
89 | } | ||
90 | |||
91 | @WebSocket | ||
92 | public static class UpdateTestClient extends WebSocketIntegrationTestClient { | ||
93 | @Override | ||
94 | protected void arrange(Session session, int responsesReceived) throws IOException { | ||
95 | switch (responsesReceived) { | ||
96 | case 0 -> session.getRemote().sendString( | ||
97 | "{\"id\":\"foo\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"fullText\":\"class Person.\n\"}}"); | ||
98 | case 3 -> session.getRemote().sendString( | ||
99 | "{\"id\":\"bar\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"requiredStateId\":\"-80000000\",\"deltaText\":\"indiv q.\nnode(q).\n\",\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}"); | ||
100 | case 5 -> session.close(); | ||
101 | } | ||
102 | } | ||
103 | } | ||
104 | |||
105 | @Test | ||
106 | void badSubProtocolTest() { | ||
107 | startServer(null); | ||
108 | var clientSocket = new CloseImmediatelyTestClient(); | ||
109 | var session = connect(clientSocket, null, "<invalid sub-protocol>"); | ||
110 | assertThat(session.getUpgradeResponse().getAcceptedSubProtocol(), equalTo(null)); | ||
111 | clientSocket.waitForTestResult(); | ||
112 | assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); | ||
113 | } | ||
114 | |||
115 | @WebSocket | ||
116 | public static class CloseImmediatelyTestClient extends WebSocketIntegrationTestClient { | ||
117 | @Override | ||
118 | protected void arrange(Session session, int responsesReceived) throws IOException { | ||
119 | session.close(); | ||
120 | } | ||
121 | } | ||
122 | |||
123 | @Test | ||
124 | void subProtocolNegotiationTest() { | ||
125 | startServer(null); | ||
126 | var clientSocket = new CloseImmediatelyTestClient(); | ||
127 | var session = connect(clientSocket, null, "<invalid sub-protocol>", XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1); | ||
128 | assertThat(session.getUpgradeResponse().getAcceptedSubProtocol(), | ||
129 | equalTo(XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1)); | ||
130 | clientSocket.waitForTestResult(); | ||
131 | assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); | ||
132 | } | ||
133 | |||
134 | @Test | ||
135 | void invalidJsonTest() { | ||
136 | startServer(null); | ||
137 | var clientSocket = new InvalidJsonTestClient(); | ||
138 | connect(clientSocket, null, XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1); | ||
139 | clientSocket.waitForTestResult(); | ||
140 | assertThat(clientSocket.getCloseStatusCode(), equalTo(XtextStatusCode.INVALID_JSON)); | ||
141 | } | ||
142 | |||
143 | @WebSocket | ||
144 | public static class InvalidJsonTestClient extends WebSocketIntegrationTestClient { | ||
145 | @Override | ||
146 | protected void arrange(Session session, int responsesReceived) throws IOException { | ||
147 | session.getRemote().sendString("<invalid json>"); | ||
148 | } | ||
149 | } | ||
150 | |||
151 | @ParameterizedTest(name = "Origin: {0}") | ||
152 | @ValueSource(strings = { "https://refinery.example", "https://refinery.example:443", "HTTPS://REFINERY.EXAMPLE" }) | ||
153 | void validOriginTest(String origin) { | ||
154 | startServer("https://refinery.example;https://refinery.example:443"); | ||
155 | var clientSocket = new CloseImmediatelyTestClient(); | ||
156 | connect(clientSocket, origin, XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1); | ||
157 | clientSocket.waitForTestResult(); | ||
158 | assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); | ||
159 | } | ||
160 | |||
161 | @Test | ||
162 | void invalidOriginTest() { | ||
163 | startServer("https://refinery.example;https://refinery.example:443"); | ||
164 | var clientSocket = new CloseImmediatelyTestClient(); | ||
165 | var exception = assertThrows(CompletionException.class, | ||
166 | () -> connect(clientSocket, "https://invalid.example", XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1)); | ||
167 | var innerException = exception.getCause(); | ||
168 | assertThat(innerException, instanceOf(UpgradeException.class)); | ||
169 | assertThat(((UpgradeException) innerException).getResponseStatusCode(), equalTo(HttpStatus.FORBIDDEN_403)); | ||
170 | } | ||
171 | |||
172 | private void startServer(String allowedOrigins) { | ||
173 | server = new Server(new InetSocketAddress(SERVER_PORT)); | ||
174 | var handler = new ServletContextHandler(); | ||
175 | var holder = new ServletHolder(ProblemWebSocketServlet.class); | ||
176 | if (allowedOrigins != null) { | ||
177 | holder.setInitParameter(ProblemWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM, allowedOrigins); | ||
178 | } | ||
179 | handler.addServlet(holder, SERVLET_URI); | ||
180 | JettyWebSocketServletContainerInitializer.configure(handler, null); | ||
181 | server.setHandler(handler); | ||
182 | try { | ||
183 | server.start(); | ||
184 | } catch (Exception e) { | ||
185 | throw new RuntimeException("Failed to start websocket server"); | ||
186 | } | ||
187 | } | ||
188 | |||
189 | private Session connect(Object webSocketClient, String origin, String... subProtocols) { | ||
190 | var upgradeRequest = new ClientUpgradeRequest(); | ||
191 | if (origin != null) { | ||
192 | upgradeRequest.setHeader(HttpHeader.ORIGIN.name(), origin); | ||
193 | } | ||
194 | upgradeRequest.setSubProtocols(subProtocols); | ||
195 | CompletableFuture<Session> sessionFuture; | ||
196 | try { | ||
197 | sessionFuture = client.connect(webSocketClient, URI.create("ws://localhost:" + SERVER_PORT + SERVLET_URI), | ||
198 | upgradeRequest); | ||
199 | } catch (IOException e) { | ||
200 | throw new AssertionError("Unexpected exception while connection to websocket", e); | ||
201 | } | ||
202 | return sessionFuture.join(); | ||
203 | } | ||
204 | } | ||
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java new file mode 100644 index 00000000..b70d0ed5 --- /dev/null +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java | |||
@@ -0,0 +1,42 @@ | |||
1 | package tools.refinery.language.web.tests; | ||
2 | |||
3 | import java.util.ArrayList; | ||
4 | import java.util.List; | ||
5 | import java.util.concurrent.ExecutorService; | ||
6 | |||
7 | import org.eclipse.xtext.ide.ExecutorServiceProvider; | ||
8 | |||
9 | import com.google.inject.Singleton; | ||
10 | |||
11 | @Singleton | ||
12 | public class AwaitTerminationExecutorServiceProvider extends ExecutorServiceProvider { | ||
13 | private List<RestartableCachedThreadPool> servicesToShutDown = new ArrayList<>(); | ||
14 | |||
15 | @Override | ||
16 | protected ExecutorService createInstance(String key) { | ||
17 | var instance = new RestartableCachedThreadPool(); | ||
18 | synchronized (servicesToShutDown) { | ||
19 | servicesToShutDown.add(instance); | ||
20 | } | ||
21 | return instance; | ||
22 | } | ||
23 | |||
24 | public void waitForAllTasksToFinish() { | ||
25 | synchronized (servicesToShutDown) { | ||
26 | for (var executorService : servicesToShutDown) { | ||
27 | executorService.waitForAllTasksToFinish(); | ||
28 | } | ||
29 | } | ||
30 | } | ||
31 | |||
32 | @Override | ||
33 | public void dispose() { | ||
34 | super.dispose(); | ||
35 | synchronized (servicesToShutDown) { | ||
36 | for (var executorService : servicesToShutDown) { | ||
37 | executorService.waitForTermination(); | ||
38 | } | ||
39 | servicesToShutDown.clear(); | ||
40 | } | ||
41 | } | ||
42 | } | ||
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java new file mode 100644 index 00000000..43c12faa --- /dev/null +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/ProblemWebInjectorProvider.java | |||
@@ -0,0 +1,47 @@ | |||
1 | package tools.refinery.language.web.tests; | ||
2 | |||
3 | import org.eclipse.xtext.ide.ExecutorServiceProvider; | ||
4 | import org.eclipse.xtext.util.DisposableRegistry; | ||
5 | import org.eclipse.xtext.util.Modules2; | ||
6 | |||
7 | import com.google.inject.Guice; | ||
8 | import com.google.inject.Injector; | ||
9 | |||
10 | import tools.refinery.language.ide.ProblemIdeModule; | ||
11 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
12 | import tools.refinery.language.web.ProblemWebModule; | ||
13 | import tools.refinery.language.web.ProblemWebSetup; | ||
14 | |||
15 | public class ProblemWebInjectorProvider extends ProblemInjectorProvider { | ||
16 | |||
17 | protected Injector internalCreateInjector() { | ||
18 | return new ProblemWebSetup() { | ||
19 | @Override | ||
20 | public Injector createInjector() { | ||
21 | return Guice.createInjector( | ||
22 | Modules2.mixin(createRuntimeModule(), new ProblemIdeModule(), createWebModule())); | ||
23 | } | ||
24 | }.createInjectorAndDoEMFRegistration(); | ||
25 | } | ||
26 | |||
27 | protected ProblemWebModule createWebModule() { | ||
28 | // Await termination of the executor service to avoid race conditions between | ||
29 | // the tasks in the service and the {@link | ||
30 | // org.eclipse.xtext.testing.extensions.InjectionExtension}. | ||
31 | return new ProblemWebModule() { | ||
32 | @SuppressWarnings("unused") | ||
33 | public Class<? extends ExecutorServiceProvider> bindExecutorServiceProvider() { | ||
34 | return AwaitTerminationExecutorServiceProvider.class; | ||
35 | } | ||
36 | }; | ||
37 | } | ||
38 | |||
39 | @Override | ||
40 | public void restoreRegistry() { | ||
41 | // Also make sure to dispose any IDisposable instances (that may depend on the | ||
42 | // global state) created by Xtext before restoring the global state. | ||
43 | var disposableRegistry = getInjector().getInstance(DisposableRegistry.class); | ||
44 | disposableRegistry.dispose(); | ||
45 | super.restoreRegistry(); | ||
46 | } | ||
47 | } | ||
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java new file mode 100644 index 00000000..1468273d --- /dev/null +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java | |||
@@ -0,0 +1,109 @@ | |||
1 | package tools.refinery.language.web.tests; | ||
2 | |||
3 | import java.util.Collection; | ||
4 | import java.util.List; | ||
5 | import java.util.concurrent.Callable; | ||
6 | import java.util.concurrent.ExecutionException; | ||
7 | import java.util.concurrent.ExecutorService; | ||
8 | import java.util.concurrent.Executors; | ||
9 | import java.util.concurrent.Future; | ||
10 | import java.util.concurrent.TimeUnit; | ||
11 | import java.util.concurrent.TimeoutException; | ||
12 | |||
13 | import org.slf4j.Logger; | ||
14 | import org.slf4j.LoggerFactory; | ||
15 | |||
16 | public class RestartableCachedThreadPool implements ExecutorService { | ||
17 | private static final Logger LOG = LoggerFactory.getLogger(RestartableCachedThreadPool.class); | ||
18 | |||
19 | private ExecutorService delegate; | ||
20 | |||
21 | public RestartableCachedThreadPool() { | ||
22 | delegate = createExecutorService(); | ||
23 | } | ||
24 | |||
25 | public void waitForAllTasksToFinish() { | ||
26 | delegate.shutdown(); | ||
27 | waitForTermination(); | ||
28 | delegate = createExecutorService(); | ||
29 | } | ||
30 | |||
31 | public void waitForTermination() { | ||
32 | try { | ||
33 | delegate.awaitTermination(1, TimeUnit.SECONDS); | ||
34 | } catch (InterruptedException e) { | ||
35 | LOG.warn("Interrupted while waiting for delegate executor to stop", e); | ||
36 | } | ||
37 | } | ||
38 | |||
39 | protected ExecutorService createExecutorService() { | ||
40 | return Executors.newCachedThreadPool(); | ||
41 | } | ||
42 | |||
43 | @Override | ||
44 | public boolean awaitTermination(long arg0, TimeUnit arg1) throws InterruptedException { | ||
45 | return delegate.awaitTermination(arg0, arg1); | ||
46 | } | ||
47 | |||
48 | @Override | ||
49 | public void execute(Runnable arg0) { | ||
50 | delegate.execute(arg0); | ||
51 | } | ||
52 | |||
53 | @Override | ||
54 | public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> arg0, long arg1, TimeUnit arg2) | ||
55 | throws InterruptedException { | ||
56 | return delegate.invokeAll(arg0, arg1, arg2); | ||
57 | } | ||
58 | |||
59 | @Override | ||
60 | public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> arg0) throws InterruptedException { | ||
61 | return delegate.invokeAll(arg0); | ||
62 | } | ||
63 | |||
64 | @Override | ||
65 | public <T> T invokeAny(Collection<? extends Callable<T>> arg0, long arg1, TimeUnit arg2) | ||
66 | throws InterruptedException, ExecutionException, TimeoutException { | ||
67 | return delegate.invokeAny(arg0, arg1, arg2); | ||
68 | } | ||
69 | |||
70 | @Override | ||
71 | public <T> T invokeAny(Collection<? extends Callable<T>> arg0) throws InterruptedException, ExecutionException { | ||
72 | return delegate.invokeAny(arg0); | ||
73 | } | ||
74 | |||
75 | @Override | ||
76 | public boolean isShutdown() { | ||
77 | return delegate.isShutdown(); | ||
78 | } | ||
79 | |||
80 | @Override | ||
81 | public boolean isTerminated() { | ||
82 | return delegate.isTerminated(); | ||
83 | } | ||
84 | |||
85 | @Override | ||
86 | public void shutdown() { | ||
87 | delegate.shutdown(); | ||
88 | } | ||
89 | |||
90 | @Override | ||
91 | public List<Runnable> shutdownNow() { | ||
92 | return delegate.shutdownNow(); | ||
93 | } | ||
94 | |||
95 | @Override | ||
96 | public <T> Future<T> submit(Callable<T> arg0) { | ||
97 | return delegate.submit(arg0); | ||
98 | } | ||
99 | |||
100 | @Override | ||
101 | public <T> Future<T> submit(Runnable arg0, T arg1) { | ||
102 | return delegate.submit(arg0, arg1); | ||
103 | } | ||
104 | |||
105 | @Override | ||
106 | public Future<?> submit(Runnable arg0) { | ||
107 | return delegate.submit(arg0); | ||
108 | } | ||
109 | } | ||
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java new file mode 100644 index 00000000..49464d27 --- /dev/null +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java | |||
@@ -0,0 +1,98 @@ | |||
1 | package tools.refinery.language.web.tests; | ||
2 | |||
3 | import static org.junit.jupiter.api.Assertions.fail; | ||
4 | |||
5 | import java.io.IOException; | ||
6 | import java.time.Duration; | ||
7 | import java.util.ArrayList; | ||
8 | import java.util.List; | ||
9 | |||
10 | import org.eclipse.jetty.websocket.api.Session; | ||
11 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; | ||
12 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; | ||
13 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; | ||
14 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; | ||
15 | |||
16 | public abstract class WebSocketIntegrationTestClient { | ||
17 | private static long TIMEOUT_MILLIS = Duration.ofSeconds(1).toMillis(); | ||
18 | |||
19 | private boolean finished = false; | ||
20 | |||
21 | private Object lock = new Object(); | ||
22 | |||
23 | private Throwable error; | ||
24 | |||
25 | private int closeStatusCode; | ||
26 | |||
27 | private List<String> responses = new ArrayList<>(); | ||
28 | |||
29 | public int getCloseStatusCode() { | ||
30 | return closeStatusCode; | ||
31 | } | ||
32 | |||
33 | public List<String> getResponses() { | ||
34 | return responses; | ||
35 | } | ||
36 | |||
37 | @OnWebSocketConnect | ||
38 | public void onConnect(Session session) { | ||
39 | arrangeAndCatchErrors(session); | ||
40 | } | ||
41 | |||
42 | private void arrangeAndCatchErrors(Session session) { | ||
43 | try { | ||
44 | arrange(session, responses.size()); | ||
45 | } catch (Exception e) { | ||
46 | finishedWithError(e); | ||
47 | } | ||
48 | } | ||
49 | |||
50 | protected abstract void arrange(Session session, int responsesReceived) throws IOException; | ||
51 | |||
52 | @OnWebSocketClose | ||
53 | public void onClose(int statusCode, String reason) { | ||
54 | closeStatusCode = statusCode; | ||
55 | testFinished(); | ||
56 | } | ||
57 | |||
58 | @OnWebSocketError | ||
59 | public void onError(Throwable error) { | ||
60 | finishedWithError(error); | ||
61 | } | ||
62 | |||
63 | @OnWebSocketMessage | ||
64 | public void onMessage(Session session, String message) { | ||
65 | responses.add(message); | ||
66 | arrangeAndCatchErrors(session); | ||
67 | } | ||
68 | |||
69 | private void finishedWithError(Throwable t) { | ||
70 | error = t; | ||
71 | testFinished(); | ||
72 | } | ||
73 | |||
74 | private void testFinished() { | ||
75 | synchronized (lock) { | ||
76 | finished = true; | ||
77 | lock.notify(); | ||
78 | } | ||
79 | } | ||
80 | |||
81 | public void waitForTestResult() { | ||
82 | synchronized (lock) { | ||
83 | if (!finished) { | ||
84 | try { | ||
85 | lock.wait(TIMEOUT_MILLIS); | ||
86 | } catch (InterruptedException e) { | ||
87 | fail("Unexpected InterruptedException", e); | ||
88 | } | ||
89 | } | ||
90 | } | ||
91 | if (!finished) { | ||
92 | fail("Test still not finished after timeout"); | ||
93 | } | ||
94 | if (error != null) { | ||
95 | fail("Unexpected exception in websocket thread", error); | ||
96 | } | ||
97 | } | ||
98 | } | ||
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java new file mode 100644 index 00000000..5b8fedba --- /dev/null +++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java | |||
@@ -0,0 +1,165 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | ||
2 | |||
3 | import static org.hamcrest.MatcherAssert.assertThat; | ||
4 | import static org.hamcrest.Matchers.equalTo; | ||
5 | import static org.hamcrest.Matchers.hasProperty; | ||
6 | import static org.hamcrest.Matchers.instanceOf; | ||
7 | import static org.mockito.Mockito.mock; | ||
8 | import static org.mockito.Mockito.times; | ||
9 | import static org.mockito.Mockito.verify; | ||
10 | |||
11 | import java.util.List; | ||
12 | import java.util.Map; | ||
13 | |||
14 | import org.eclipse.emf.common.util.URI; | ||
15 | import org.eclipse.xtext.resource.IResourceServiceProvider; | ||
16 | import org.eclipse.xtext.testing.InjectWith; | ||
17 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
18 | import org.eclipse.xtext.web.server.model.DocumentStateResult; | ||
19 | import org.eclipse.xtext.web.server.syntaxcoloring.HighlightingResult; | ||
20 | import org.eclipse.xtext.web.server.validation.ValidationResult; | ||
21 | import org.junit.jupiter.api.BeforeEach; | ||
22 | import org.junit.jupiter.api.Test; | ||
23 | import org.junit.jupiter.api.extension.ExtendWith; | ||
24 | import org.mockito.ArgumentCaptor; | ||
25 | import org.mockito.junit.jupiter.MockitoExtension; | ||
26 | |||
27 | import com.google.inject.Inject; | ||
28 | |||
29 | import tools.refinery.language.web.tests.AwaitTerminationExecutorServiceProvider; | ||
30 | import tools.refinery.language.web.tests.ProblemWebInjectorProvider; | ||
31 | import tools.refinery.language.web.xtext.server.ResponseHandler; | ||
32 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | ||
33 | import tools.refinery.language.web.xtext.server.TransactionExecutor; | ||
34 | import tools.refinery.language.web.xtext.server.message.XtextWebOkResponse; | ||
35 | import tools.refinery.language.web.xtext.server.message.XtextWebRequest; | ||
36 | import tools.refinery.language.web.xtext.server.message.XtextWebResponse; | ||
37 | |||
38 | @ExtendWith(MockitoExtension.class) | ||
39 | @ExtendWith(InjectionExtension.class) | ||
40 | @InjectWith(ProblemWebInjectorProvider.class) | ||
41 | class TransactionExecutorTest { | ||
42 | private static final String RESOURCE_NAME = "test.problem"; | ||
43 | |||
44 | private static final String PROBLEM_CONTENT_TYPE = "application/x-tools.refinery.problem"; | ||
45 | |||
46 | private static final String TEST_PROBLEM = """ | ||
47 | class Person { | ||
48 | Person[0..*] friend opposite friend | ||
49 | } | ||
50 | |||
51 | friend(a, b). | ||
52 | """; | ||
53 | |||
54 | private static final Map<String, String> UPDATE_FULL_TEXT_PARAMS = Map.of("resource", RESOURCE_NAME, "serviceType", | ||
55 | "update", "fullText", TEST_PROBLEM); | ||
56 | |||
57 | @Inject | ||
58 | private IResourceServiceProvider.Registry resourceServiceProviderRegistry; | ||
59 | |||
60 | @Inject | ||
61 | private AwaitTerminationExecutorServiceProvider executorServices; | ||
62 | |||
63 | private TransactionExecutor transactionExecutor; | ||
64 | |||
65 | @BeforeEach | ||
66 | void beforeEach() { | ||
67 | transactionExecutor = new TransactionExecutor(new SimpleSession(), resourceServiceProviderRegistry); | ||
68 | } | ||
69 | |||
70 | @Test | ||
71 | void updateFullTextTest() throws ResponseHandlerException { | ||
72 | var captor = newCaptor(); | ||
73 | var stateId = updateFullText(captor); | ||
74 | assertThatPrecomputedMessagesAreReceived(stateId, captor.getAllValues()); | ||
75 | } | ||
76 | |||
77 | @Test | ||
78 | void updateDeltaTextHighlightAndValidationChange() throws ResponseHandlerException { | ||
79 | var stateId = updateFullText(); | ||
80 | var responseHandler = sendRequestAndWaitForAllResponses( | ||
81 | new XtextWebRequest("bar", Map.of("resource", RESOURCE_NAME, "serviceType", "update", "requiredStateId", | ||
82 | stateId, "deltaText", "individual q.\nnode(q).\n<invalid text>\n", "deltaOffset", "0", "deltaReplaceLength", "0"))); | ||
83 | |||
84 | var captor = newCaptor(); | ||
85 | verify(responseHandler, times(3)).onResponse(captor.capture()); | ||
86 | var newStateId = getStateId("bar", captor.getAllValues().get(0)); | ||
87 | assertThatPrecomputedMessagesAreReceived(newStateId, captor.getAllValues()); | ||
88 | } | ||
89 | |||
90 | @Test | ||
91 | void updateDeltaTextHighlightChangeOnly() throws ResponseHandlerException { | ||
92 | var stateId = updateFullText(); | ||
93 | var responseHandler = sendRequestAndWaitForAllResponses( | ||
94 | new XtextWebRequest("bar", Map.of("resource", RESOURCE_NAME, "serviceType", "update", "requiredStateId", | ||
95 | stateId, "deltaText", "indiv q.\nnode(q).\n", "deltaOffset", "0", "deltaReplaceLength", "0"))); | ||
96 | |||
97 | var captor = newCaptor(); | ||
98 | verify(responseHandler, times(2)).onResponse(captor.capture()); | ||
99 | var newStateId = getStateId("bar", captor.getAllValues().get(0)); | ||
100 | assertHighlightingResponse(newStateId, captor.getAllValues().get(1)); | ||
101 | } | ||
102 | |||
103 | @Test | ||
104 | void fullTextWithoutResourceTest() throws ResponseHandlerException { | ||
105 | var resourceServiceProvider = resourceServiceProviderRegistry | ||
106 | .getResourceServiceProvider(URI.createFileURI(RESOURCE_NAME)); | ||
107 | resourceServiceProviderRegistry.getContentTypeToFactoryMap().put(PROBLEM_CONTENT_TYPE, resourceServiceProvider); | ||
108 | var responseHandler = sendRequestAndWaitForAllResponses(new XtextWebRequest("foo", | ||
109 | Map.of("contentType", PROBLEM_CONTENT_TYPE, "fullText", TEST_PROBLEM, "serviceType", "validate"))); | ||
110 | |||
111 | var captor = newCaptor(); | ||
112 | verify(responseHandler).onResponse(captor.capture()); | ||
113 | var response = captor.getValue(); | ||
114 | assertThat(response, hasProperty("id", equalTo("foo"))); | ||
115 | assertThat(response, hasProperty("responseData", instanceOf(ValidationResult.class))); | ||
116 | } | ||
117 | |||
118 | private ArgumentCaptor<XtextWebResponse> newCaptor() { | ||
119 | return ArgumentCaptor.forClass(XtextWebResponse.class); | ||
120 | } | ||
121 | |||
122 | private String updateFullText() throws ResponseHandlerException { | ||
123 | return updateFullText(newCaptor()); | ||
124 | } | ||
125 | |||
126 | private String updateFullText(ArgumentCaptor<XtextWebResponse> captor) throws ResponseHandlerException { | ||
127 | var responseHandler = sendRequestAndWaitForAllResponses(new XtextWebRequest("foo", UPDATE_FULL_TEXT_PARAMS)); | ||
128 | |||
129 | verify(responseHandler, times(3)).onResponse(captor.capture()); | ||
130 | return getStateId("foo", captor.getAllValues().get(0)); | ||
131 | } | ||
132 | |||
133 | private ResponseHandler sendRequestAndWaitForAllResponses(XtextWebRequest request) throws ResponseHandlerException { | ||
134 | var responseHandler = mock(ResponseHandler.class); | ||
135 | transactionExecutor.setResponseHandler(responseHandler); | ||
136 | transactionExecutor.handleRequest(request); | ||
137 | executorServices.waitForAllTasksToFinish(); | ||
138 | return responseHandler; | ||
139 | } | ||
140 | |||
141 | private String getStateId(String requestId, XtextWebResponse okResponse) { | ||
142 | assertThat(okResponse, hasProperty("id", equalTo(requestId))); | ||
143 | assertThat(okResponse, hasProperty("responseData", instanceOf(DocumentStateResult.class))); | ||
144 | return ((DocumentStateResult) ((XtextWebOkResponse) okResponse).getResponseData()).getStateId(); | ||
145 | } | ||
146 | |||
147 | private void assertThatPrecomputedMessagesAreReceived(String stateId, List<XtextWebResponse> responses) { | ||
148 | assertHighlightingResponse(stateId, responses.get(1)); | ||
149 | assertValidationResponse(stateId, responses.get(2)); | ||
150 | } | ||
151 | |||
152 | private void assertHighlightingResponse(String stateId, XtextWebResponse highlightingResponse) { | ||
153 | assertThat(highlightingResponse, hasProperty("resourceId", equalTo(RESOURCE_NAME))); | ||
154 | assertThat(highlightingResponse, hasProperty("stateId", equalTo(stateId))); | ||
155 | assertThat(highlightingResponse, hasProperty("service", equalTo("highlight"))); | ||
156 | assertThat(highlightingResponse, hasProperty("pushData", instanceOf(HighlightingResult.class))); | ||
157 | } | ||
158 | |||
159 | private void assertValidationResponse(String stateId, XtextWebResponse validationResponse) { | ||
160 | assertThat(validationResponse, hasProperty("resourceId", equalTo(RESOURCE_NAME))); | ||
161 | assertThat(validationResponse, hasProperty("stateId", equalTo(stateId))); | ||
162 | assertThat(validationResponse, hasProperty("service", equalTo("validate"))); | ||
163 | assertThat(validationResponse, hasProperty("pushData", instanceOf(ValidationResult.class))); | ||
164 | } | ||
165 | } | ||