diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-10-24 01:19:02 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-10-31 19:26:11 +0100 |
commit | aa205509820b526bf21dfedebfd798ded0d763e7 (patch) | |
tree | 6ec0178cc7bb245e2790e9807ad3184fc0033805 | |
parent | feat(web): push precomputed service results (diff) | |
download | refinery-aa205509820b526bf21dfedebfd798ded0d763e7.tar.gz refinery-aa205509820b526bf21dfedebfd798ded0d763e7.tar.zst refinery-aa205509820b526bf21dfedebfd798ded0d763e7.zip |
test(web): websockets fixes and tests
-rw-r--r-- | language-web/build.gradle | 1 | ||||
-rw-r--r-- | language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java | 2 | ||||
-rw-r--r-- | language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java | 4 | ||||
-rw-r--r-- | language-web/src/test/java/tools/refinery/language/web/ProblemWebInjectorProvider.java (renamed from language-web/src/test/java/tools/refinery/language/web/xtext/servlet/ProblemWebInjectorProvider.java) | 5 | ||||
-rw-r--r-- | language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java | 172 | ||||
-rw-r--r-- | language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java | 23 |
6 files changed, 200 insertions, 7 deletions
diff --git a/language-web/build.gradle b/language-web/build.gradle index 970db115..6aaf546b 100644 --- a/language-web/build.gradle +++ b/language-web/build.gradle | |||
@@ -14,6 +14,7 @@ dependencies { | |||
14 | implementation "org.slf4j:log4j-over-slf4j:${slf4JVersion}" | 14 | implementation "org.slf4j:log4j-over-slf4j:${slf4JVersion}" |
15 | testImplementation testFixtures(project(':refinery-language')) | 15 | testImplementation testFixtures(project(':refinery-language')) |
16 | testImplementation "org.eclipse.xtext:org.eclipse.xtext.testing:${xtextVersion}" | 16 | testImplementation "org.eclipse.xtext:org.eclipse.xtext.testing:${xtextVersion}" |
17 | testImplementation "org.eclipse.jetty.websocket:websocket-jetty-client:${jettyVersion}" | ||
17 | } | 18 | } |
18 | 19 | ||
19 | def generateXtextLanguage = project(':refinery-language').tasks.named('generateXtextLanguage') | 20 | def generateXtextLanguage = project(':refinery-language').tasks.named('generateXtextLanguage') |
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; | |||
18 | import org.eclipse.jetty.servlet.ServletContextHandler; | 18 | import org.eclipse.jetty.servlet.ServletContextHandler; |
19 | import org.eclipse.jetty.servlet.ServletHolder; | 19 | import org.eclipse.jetty.servlet.ServletHolder; |
20 | import org.eclipse.jetty.util.resource.Resource; | 20 | import org.eclipse.jetty.util.resource.Resource; |
21 | import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; | ||
21 | import org.slf4j.Logger; | 22 | import org.slf4j.Logger; |
22 | import org.slf4j.LoggerFactory; | 23 | import org.slf4j.LoggerFactory; |
23 | 24 | ||
@@ -73,6 +74,7 @@ public class ServerLauncher { | |||
73 | allowedOriginsString); | 74 | allowedOriginsString); |
74 | } | 75 | } |
75 | handler.addServlet(problemServletHolder, "/xtext-service/*"); | 76 | handler.addServlet(problemServletHolder, "/xtext-service/*"); |
77 | JettyWebSocketServletContainerInitializer.configure(handler, null); | ||
76 | } | 78 | } |
77 | 79 | ||
78 | private void addDefaultServlet(ServletContextHandler handler) { | 80 | 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 { | |||
58 | 58 | ||
59 | protected String getPrecomputedServiceName(AbstractCachedService<? extends IServiceResult> service) { | 59 | protected String getPrecomputedServiceName(AbstractCachedService<? extends IServiceResult> service) { |
60 | if (service instanceof ValidationService) { | 60 | if (service instanceof ValidationService) { |
61 | return "validation"; | 61 | return "validate"; |
62 | } | 62 | } |
63 | if (service instanceof HighlightingService) { | 63 | if (service instanceof HighlightingService) { |
64 | return "highlighting"; | 64 | return "highlight"; |
65 | } | 65 | } |
66 | throw new IllegalArgumentException("Unknown precomputed service: " + service); | 66 | throw new IllegalArgumentException("Unknown precomputed service: " + service); |
67 | } | 67 | } |
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/ProblemWebInjectorProvider.java index a6d97c8b..2db4590e 100644 --- a/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/ProblemWebInjectorProvider.java +++ b/language-web/src/test/java/tools/refinery/language/web/ProblemWebInjectorProvider.java | |||
@@ -1,4 +1,4 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | 1 | package tools.refinery.language.web; |
2 | 2 | ||
3 | import org.eclipse.xtext.ide.ExecutorServiceProvider; | 3 | import org.eclipse.xtext.ide.ExecutorServiceProvider; |
4 | import org.eclipse.xtext.util.DisposableRegistry; | 4 | import org.eclipse.xtext.util.DisposableRegistry; |
@@ -9,8 +9,7 @@ import com.google.inject.Injector; | |||
9 | 9 | ||
10 | import tools.refinery.language.ide.ProblemIdeModule; | 10 | import tools.refinery.language.ide.ProblemIdeModule; |
11 | import tools.refinery.language.tests.ProblemInjectorProvider; | 11 | import tools.refinery.language.tests.ProblemInjectorProvider; |
12 | import tools.refinery.language.web.ProblemWebModule; | 12 | import tools.refinery.language.web.xtext.servlet.AwaitTerminationExecutorServiceProvider; |
13 | import tools.refinery.language.web.ProblemWebSetup; | ||
14 | 13 | ||
15 | public class ProblemWebInjectorProvider extends ProblemInjectorProvider { | 14 | public class ProblemWebInjectorProvider extends ProblemInjectorProvider { |
16 | 15 | ||
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 @@ | |||
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.startsWith; | ||
7 | import static org.junit.jupiter.api.Assertions.fail; | ||
8 | |||
9 | import java.io.IOException; | ||
10 | import java.net.InetSocketAddress; | ||
11 | import java.net.URI; | ||
12 | import java.time.Duration; | ||
13 | import java.util.ArrayList; | ||
14 | import java.util.List; | ||
15 | |||
16 | import org.eclipse.jetty.server.Server; | ||
17 | import org.eclipse.jetty.servlet.ServletContextHandler; | ||
18 | import org.eclipse.jetty.websocket.api.Session; | ||
19 | import org.eclipse.jetty.websocket.api.StatusCode; | ||
20 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; | ||
21 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; | ||
22 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; | ||
23 | import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; | ||
24 | import org.eclipse.jetty.websocket.api.annotations.WebSocket; | ||
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 | |||
34 | import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; | ||
35 | |||
36 | class ProblemWebSocketServletIntegrationTest { | ||
37 | private static int SERVER_PORT = 28080; | ||
38 | |||
39 | private static long TIMEOUT_MILLIS = Duration.ofSeconds(1).toMillis(); | ||
40 | |||
41 | private GlobalStateMemento stateBeforeInjectorCreation; | ||
42 | |||
43 | private Server server; | ||
44 | |||
45 | private WebSocketClient client; | ||
46 | |||
47 | @BeforeEach | ||
48 | void startServer() throws Exception { | ||
49 | stateBeforeInjectorCreation = GlobalRegistries.makeCopyOfGlobalState(); | ||
50 | server = new Server(new InetSocketAddress(SERVER_PORT)); | ||
51 | var handler = new ServletContextHandler(); | ||
52 | handler.addServlet(ProblemWebSocketServlet.class, "/xtext-service/*"); | ||
53 | JettyWebSocketServletContainerInitializer.configure(handler, null); | ||
54 | server.setHandler(handler); | ||
55 | server.start(); | ||
56 | client = new WebSocketClient(); | ||
57 | client.start(); | ||
58 | } | ||
59 | |||
60 | @AfterEach | ||
61 | void stopServer() throws Exception { | ||
62 | client.stop(); | ||
63 | server.stop(); | ||
64 | stateBeforeInjectorCreation.restoreGlobalState(); | ||
65 | } | ||
66 | |||
67 | @Test | ||
68 | void updateTest() throws IOException { | ||
69 | var clientSocket = new UpdateTestClient(); | ||
70 | var upgradeRequest = new ClientUpgradeRequest(); | ||
71 | upgradeRequest.setSubProtocols(XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1); | ||
72 | var sessionFuture = client.connect(clientSocket, URI.create("ws://localhost:" + SERVER_PORT + "/xtext-service"), | ||
73 | upgradeRequest); | ||
74 | var session = sessionFuture.join(); | ||
75 | assertThat(session.getUpgradeResponse().getAcceptedSubProtocol(), | ||
76 | equalTo(XtextWebSocketServlet.XTEXT_SUBPROTOCOL_V1)); | ||
77 | clientSocket.waitForTestResult(); | ||
78 | } | ||
79 | |||
80 | @WebSocket | ||
81 | public static class UpdateTestClient { | ||
82 | private boolean finished = false; | ||
83 | |||
84 | private Object lock = new Object(); | ||
85 | |||
86 | private Throwable error; | ||
87 | |||
88 | private int closeStatusCode; | ||
89 | |||
90 | private String closeReason; | ||
91 | |||
92 | private List<String> responses = new ArrayList<>(); | ||
93 | |||
94 | @OnWebSocketConnect | ||
95 | public void onConnect(Session session) { | ||
96 | try { | ||
97 | session.getRemote().sendString( | ||
98 | "{\"id\":\"foo\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"fullText\":\"class Person.\n\"}}"); | ||
99 | } catch (IOException e) { | ||
100 | finishedWithError(e); | ||
101 | } | ||
102 | } | ||
103 | |||
104 | @OnWebSocketClose | ||
105 | public void onClose(int statusCode, String reason) { | ||
106 | closeStatusCode = statusCode; | ||
107 | closeReason = reason; | ||
108 | testFinished(); | ||
109 | } | ||
110 | |||
111 | @OnWebSocketError | ||
112 | public void onError(Throwable error) { | ||
113 | finishedWithError(error); | ||
114 | } | ||
115 | |||
116 | @OnWebSocketMessage | ||
117 | public void onMessage(Session session, String message) { | ||
118 | try { | ||
119 | responses.add(message); | ||
120 | switch (responses.size()) { | ||
121 | case 3 -> session.getRemote().sendString( | ||
122 | "{\"id\":\"bar\",\"request\":{\"resource\":\"test.problem\",\"serviceType\":\"update\",\"requiredStateId\":\"-80000000\",\"deltaText\":\"class Car.\n\",\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}"); | ||
123 | case 5 -> session.close(); | ||
124 | } | ||
125 | } catch (IOException e) { | ||
126 | finishedWithError(e); | ||
127 | } | ||
128 | } | ||
129 | |||
130 | private void finishedWithError(Throwable t) { | ||
131 | error = t; | ||
132 | testFinished(); | ||
133 | } | ||
134 | |||
135 | private void testFinished() { | ||
136 | synchronized (lock) { | ||
137 | finished = true; | ||
138 | lock.notify(); | ||
139 | } | ||
140 | } | ||
141 | |||
142 | public void waitForTestResult() { | ||
143 | synchronized (lock) { | ||
144 | if (!finished) { | ||
145 | try { | ||
146 | lock.wait(TIMEOUT_MILLIS); | ||
147 | } catch (InterruptedException e) { | ||
148 | fail("Unexpected InterruptedException", e); | ||
149 | } | ||
150 | } | ||
151 | } | ||
152 | if (!finished) { | ||
153 | fail("Test still not finished after timeout"); | ||
154 | } | ||
155 | if (error != null) { | ||
156 | fail("Unexpected exception in websocket thread", error); | ||
157 | } | ||
158 | if (closeStatusCode != StatusCode.NORMAL) { | ||
159 | fail("Abnormal close status " + closeStatusCode + ": " + closeReason); | ||
160 | } | ||
161 | assertThat(responses, hasSize(5)); | ||
162 | assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}")); | ||
163 | assertThat(responses.get(1), startsWith( | ||
164 | "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\",\"push\":{\"regions\":[")); | ||
165 | assertThat(responses.get(2), equalTo( | ||
166 | "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"validate\",\"push\":{\"issues\":[]}}")); | ||
167 | assertThat(responses.get(3), equalTo("{\"id\":\"bar\",\"response\":{\"stateId\":\"-7fffffff\"}}")); | ||
168 | assertThat(responses.get(4), startsWith( | ||
169 | "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\",\"push\":{\"regions\":[")); | ||
170 | } | ||
171 | } | ||
172 | } | ||
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; | |||
11 | import java.util.List; | 11 | import java.util.List; |
12 | import java.util.Map; | 12 | import java.util.Map; |
13 | 13 | ||
14 | import org.eclipse.emf.common.util.URI; | ||
14 | import org.eclipse.xtext.resource.IResourceServiceProvider; | 15 | import org.eclipse.xtext.resource.IResourceServiceProvider; |
15 | import org.eclipse.xtext.testing.InjectWith; | 16 | import org.eclipse.xtext.testing.InjectWith; |
16 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | 17 | import org.eclipse.xtext.testing.extensions.InjectionExtension; |
@@ -25,6 +26,7 @@ import org.mockito.junit.jupiter.MockitoExtension; | |||
25 | 26 | ||
26 | import com.google.inject.Inject; | 27 | import com.google.inject.Inject; |
27 | 28 | ||
29 | import tools.refinery.language.web.ProblemWebInjectorProvider; | ||
28 | import tools.refinery.language.web.xtext.server.ResponseHandler; | 30 | import tools.refinery.language.web.xtext.server.ResponseHandler; |
29 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | 31 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; |
30 | import tools.refinery.language.web.xtext.server.TransactionExecutor; | 32 | import tools.refinery.language.web.xtext.server.TransactionExecutor; |
@@ -38,6 +40,8 @@ import tools.refinery.language.web.xtext.server.message.XtextWebResponse; | |||
38 | class TransactionExecutorTest { | 40 | class TransactionExecutorTest { |
39 | private static final String RESOURCE_NAME = "test.problem"; | 41 | private static final String RESOURCE_NAME = "test.problem"; |
40 | 42 | ||
43 | private static final String PROBLEM_CONTENT_TYPE = "application/x-tools.refinery.problem"; | ||
44 | |||
41 | private static final String TEST_PROBLEM = """ | 45 | private static final String TEST_PROBLEM = """ |
42 | class Person { | 46 | class Person { |
43 | Person[0..*] friend opposite friend | 47 | Person[0..*] friend opposite friend |
@@ -95,6 +99,21 @@ class TransactionExecutorTest { | |||
95 | assertHighlightingResponse(newStateId, captor.getAllValues().get(1)); | 99 | assertHighlightingResponse(newStateId, captor.getAllValues().get(1)); |
96 | } | 100 | } |
97 | 101 | ||
102 | @Test | ||
103 | void fullTextWithoutResourceTest() throws ResponseHandlerException { | ||
104 | var resourceServiceProvider = resourceServiceProviderRegistry | ||
105 | .getResourceServiceProvider(URI.createFileURI(RESOURCE_NAME)); | ||
106 | resourceServiceProviderRegistry.getContentTypeToFactoryMap().put(PROBLEM_CONTENT_TYPE, resourceServiceProvider); | ||
107 | var responseHandler = sendRequestAndWaitForAllResponses(new XtextWebRequest("foo", | ||
108 | Map.of("contentType", PROBLEM_CONTENT_TYPE, "fullText", TEST_PROBLEM, "serviceType", "validate"))); | ||
109 | |||
110 | var captor = newCaptor(); | ||
111 | verify(responseHandler).onResponse(captor.capture()); | ||
112 | var response = captor.getValue(); | ||
113 | assertThat(response, hasProperty("id", equalTo("foo"))); | ||
114 | assertThat(response, hasProperty("responseData", instanceOf(ValidationResult.class))); | ||
115 | } | ||
116 | |||
98 | private ArgumentCaptor<XtextWebResponse> newCaptor() { | 117 | private ArgumentCaptor<XtextWebResponse> newCaptor() { |
99 | return ArgumentCaptor.forClass(XtextWebResponse.class); | 118 | return ArgumentCaptor.forClass(XtextWebResponse.class); |
100 | } | 119 | } |
@@ -132,14 +151,14 @@ class TransactionExecutorTest { | |||
132 | private void assertHighlightingResponse(String stateId, XtextWebResponse highlightingResponse) { | 151 | private void assertHighlightingResponse(String stateId, XtextWebResponse highlightingResponse) { |
133 | assertThat(highlightingResponse, hasProperty("resourceId", equalTo(RESOURCE_NAME))); | 152 | assertThat(highlightingResponse, hasProperty("resourceId", equalTo(RESOURCE_NAME))); |
134 | assertThat(highlightingResponse, hasProperty("stateId", equalTo(stateId))); | 153 | assertThat(highlightingResponse, hasProperty("stateId", equalTo(stateId))); |
135 | assertThat(highlightingResponse, hasProperty("service", equalTo("highlighting"))); | 154 | assertThat(highlightingResponse, hasProperty("service", equalTo("highlight"))); |
136 | assertThat(highlightingResponse, hasProperty("pushData", instanceOf(HighlightingResult.class))); | 155 | assertThat(highlightingResponse, hasProperty("pushData", instanceOf(HighlightingResult.class))); |
137 | } | 156 | } |
138 | 157 | ||
139 | private void assertValidationResponse(String stateId, XtextWebResponse validationResponse) { | 158 | private void assertValidationResponse(String stateId, XtextWebResponse validationResponse) { |
140 | assertThat(validationResponse, hasProperty("resourceId", equalTo(RESOURCE_NAME))); | 159 | assertThat(validationResponse, hasProperty("resourceId", equalTo(RESOURCE_NAME))); |
141 | assertThat(validationResponse, hasProperty("stateId", equalTo(stateId))); | 160 | assertThat(validationResponse, hasProperty("stateId", equalTo(stateId))); |
142 | assertThat(validationResponse, hasProperty("service", equalTo("validation"))); | 161 | assertThat(validationResponse, hasProperty("service", equalTo("validate"))); |
143 | assertThat(validationResponse, hasProperty("pushData", instanceOf(ValidationResult.class))); | 162 | assertThat(validationResponse, hasProperty("pushData", instanceOf(ValidationResult.class))); |
144 | } | 163 | } |
145 | } | 164 | } |