diff options
author | Kristóf Marussy <kristof@marussy.com> | 2021-10-19 00:49:31 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2021-10-31 19:26:11 +0100 |
commit | ac09055140a5c30e73e9ada16986ef60c38c2138 (patch) | |
tree | 251e7554359fd3437e0d2aeb3b91545c940a2f10 /language-web/src/test/java | |
parent | feat(web): batch operations for websocket protocol (diff) | |
download | refinery-ac09055140a5c30e73e9ada16986ef60c38c2138.tar.gz refinery-ac09055140a5c30e73e9ada16986ef60c38c2138.tar.zst refinery-ac09055140a5c30e73e9ada16986ef60c38c2138.zip |
feat(web): batched xtext websocket prototype
Diffstat (limited to 'language-web/src/test/java')
3 files changed, 232 insertions, 0 deletions
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 new file mode 100644 index 00000000..08230335 --- /dev/null +++ b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/AwaitTerminationExecutorServiceProvider.java | |||
@@ -0,0 +1,34 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | ||
2 | |||
3 | import java.util.ArrayList; | ||
4 | import java.util.List; | ||
5 | import java.util.concurrent.ExecutorService; | ||
6 | import java.util.concurrent.TimeUnit; | ||
7 | |||
8 | import org.eclipse.xtext.ide.ExecutorServiceProvider; | ||
9 | |||
10 | import com.google.inject.Singleton; | ||
11 | |||
12 | @Singleton | ||
13 | public class AwaitTerminationExecutorServiceProvider extends ExecutorServiceProvider { | ||
14 | private List<ExecutorService> servicesToShutDown = new ArrayList<>(); | ||
15 | |||
16 | @Override | ||
17 | protected ExecutorService createInstance(String key) { | ||
18 | var instance = super.createInstance(key); | ||
19 | servicesToShutDown.add(instance); | ||
20 | return instance; | ||
21 | } | ||
22 | |||
23 | @Override | ||
24 | public void dispose() { | ||
25 | super.dispose(); | ||
26 | for (var executorService : servicesToShutDown) { | ||
27 | try { | ||
28 | executorService.awaitTermination(1, TimeUnit.SECONDS); | ||
29 | } catch (InterruptedException e) { | ||
30 | // Continue normally. | ||
31 | } | ||
32 | } | ||
33 | } | ||
34 | } | ||
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 new file mode 100644 index 00000000..3493c9eb --- /dev/null +++ b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/ProblemWebInjectorProvider.java | |||
@@ -0,0 +1,47 @@ | |||
1 | package tools.refinery.language.web.xtext.servlet; | ||
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 | // between 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/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 new file mode 100644 index 00000000..6ad82d7f --- /dev/null +++ b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java | |||
@@ -0,0 +1,151 @@ | |||
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.junit.jupiter.api.Assertions.fail; | ||
8 | import static org.mockito.Mockito.mock; | ||
9 | import static org.mockito.Mockito.times; | ||
10 | import static org.mockito.Mockito.verify; | ||
11 | |||
12 | import java.io.IOException; | ||
13 | import java.util.List; | ||
14 | import java.util.Map; | ||
15 | import java.util.UUID; | ||
16 | |||
17 | import org.eclipse.xtext.resource.IResourceServiceProvider; | ||
18 | import org.eclipse.xtext.testing.InjectWith; | ||
19 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
20 | import org.eclipse.xtext.web.server.ServiceConflictResult; | ||
21 | import org.eclipse.xtext.web.server.model.DocumentStateResult; | ||
22 | import org.eclipse.xtext.web.server.validation.ValidationResult; | ||
23 | import org.hamcrest.Matcher; | ||
24 | import org.junit.jupiter.api.BeforeEach; | ||
25 | import org.junit.jupiter.api.Test; | ||
26 | import org.junit.jupiter.api.extension.ExtendWith; | ||
27 | import org.mockito.ArgumentCaptor; | ||
28 | import org.mockito.junit.jupiter.MockitoExtension; | ||
29 | |||
30 | import com.google.inject.Inject; | ||
31 | |||
32 | @ExtendWith(MockitoExtension.class) | ||
33 | @ExtendWith(InjectionExtension.class) | ||
34 | @InjectWith(ProblemWebInjectorProvider.class) | ||
35 | class TransactionExecutorTest { | ||
36 | private static final String RESOURCE_NAME = "test.problem"; | ||
37 | |||
38 | private static final String INVALID_STATE_ID = "<invalid_state>"; | ||
39 | |||
40 | private static final String TEST_PROBLEM = """ | ||
41 | class Person { | ||
42 | Person friend[0..*] opposite friend | ||
43 | } | ||
44 | |||
45 | friend(a, b). | ||
46 | """; | ||
47 | |||
48 | private static final Map<String, String> UPDATE_FULL_TEXT_PARAMS = Map.of("serviceType", "update", "fullText", | ||
49 | TEST_PROBLEM); | ||
50 | |||
51 | private static final Map<String, String> VALIDATE_PARAMS = Map.of("serviceType", "validate"); | ||
52 | |||
53 | @Inject | ||
54 | private IResourceServiceProvider.Registry resourceServiceProviderRegistry; | ||
55 | |||
56 | private TransactionExecutor transactionExecutor; | ||
57 | |||
58 | @BeforeEach | ||
59 | void beforeEach() { | ||
60 | transactionExecutor = new TransactionExecutor(new SimpleSession(), resourceServiceProviderRegistry); | ||
61 | } | ||
62 | |||
63 | @Test | ||
64 | void emptyBatchTest() { | ||
65 | performBatchRequest(null); | ||
66 | } | ||
67 | |||
68 | @Test | ||
69 | void fullTextUpdateTest() { | ||
70 | var response = performSingleRequest(null, UPDATE_FULL_TEXT_PARAMS); | ||
71 | assertThat(response, hasResponseData(instanceOf(DocumentStateResult.class))); | ||
72 | } | ||
73 | |||
74 | @Test | ||
75 | void validationAfterFullTextUpdateInSameBatchTest() { | ||
76 | var response = performBatchRequest(null, UPDATE_FULL_TEXT_PARAMS, VALIDATE_PARAMS).get(1); | ||
77 | assertThat(response, hasResponseData(instanceOf(ValidationResult.class))); | ||
78 | } | ||
79 | |||
80 | @Test | ||
81 | void validationAfterFullTextUpdateInDifferentBatchTest() { | ||
82 | var stateId = updateFullText(); | ||
83 | var validateResponse = performSingleRequest(stateId, VALIDATE_PARAMS); | ||
84 | assertThat(validateResponse, hasResponseData(instanceOf(ValidationResult.class))); | ||
85 | } | ||
86 | |||
87 | @Test | ||
88 | void conflictTest() { | ||
89 | updateFullText(); | ||
90 | var response = performSingleRequest(INVALID_STATE_ID, VALIDATE_PARAMS); | ||
91 | assertThat(response, hasResponseData(instanceOf(ServiceConflictResult.class))); | ||
92 | } | ||
93 | |||
94 | @Test | ||
95 | void transactionCancelledDueToConflictTest() { | ||
96 | updateFullText(); | ||
97 | var response = performBatchRequest(INVALID_STATE_ID, VALIDATE_PARAMS, VALIDATE_PARAMS).get(1); | ||
98 | assertThat(response, hasErrorKind(equalTo(XtextWebSocketErrorKind.TRANSACTION_CANCELLED))); | ||
99 | } | ||
100 | |||
101 | @SafeVarargs | ||
102 | private List<XtextWebSocketResponse> performBatchRequest(String requiredStateId, Map<String, String>... params) { | ||
103 | var id = UUID.randomUUID().toString(); | ||
104 | var request = new XtextWebSocketRequest(id, RESOURCE_NAME, null, requiredStateId, List.of(params)); | ||
105 | |||
106 | var responseHandler = mock(ResponseHandler.class); | ||
107 | try { | ||
108 | transactionExecutor.handleRequest(request, responseHandler); | ||
109 | } catch (IOException e) { | ||
110 | fail("Unexpected IOException", e); | ||
111 | } | ||
112 | |||
113 | var captor = ArgumentCaptor.forClass(XtextWebSocketResponse.class); | ||
114 | int nParams = params.length; | ||
115 | try { | ||
116 | verify(responseHandler, times(nParams)).onResponse(captor.capture()); | ||
117 | } catch (IOException e) { | ||
118 | throw new RuntimeException("Mockito threw unexcepted exception", e); | ||
119 | } | ||
120 | var allResponses = captor.getAllValues(); | ||
121 | for (int i = 0; i < nParams; i++) { | ||
122 | var response = allResponses.get(i); | ||
123 | assertThat(response, hasProperty("id", equalTo(id))); | ||
124 | assertThat(response, hasProperty("index", equalTo(i))); | ||
125 | } | ||
126 | return allResponses; | ||
127 | } | ||
128 | |||
129 | private XtextWebSocketResponse performSingleRequest(String requiredStateId, Map<String, String> param) { | ||
130 | return performBatchRequest(requiredStateId, param).get(0); | ||
131 | } | ||
132 | |||
133 | private String updateFullText() { | ||
134 | var updateResponse = (XtextWebSocketOkResponse) performSingleRequest(null, UPDATE_FULL_TEXT_PARAMS); | ||
135 | var documentStateResult = (DocumentStateResult) updateResponse.getResponseData(); | ||
136 | var stateId = documentStateResult.getStateId(); | ||
137 | if (INVALID_STATE_ID.equals(stateId)) { | ||
138 | throw new RuntimeException("Service returned unexpected stateId: " + stateId); | ||
139 | } | ||
140 | return stateId; | ||
141 | } | ||
142 | |||
143 | private static Matcher<XtextWebSocketResponse> hasResponseData(Matcher<?> responseDataMatcher) { | ||
144 | return hasProperty("responseData", responseDataMatcher); | ||
145 | } | ||
146 | |||
147 | private static Matcher<XtextWebSocketResponse> hasErrorKind( | ||
148 | Matcher<? extends XtextWebSocketErrorKind> errorKindMatcher) { | ||
149 | return hasProperty("errorKind", errorKindMatcher); | ||
150 | } | ||
151 | } | ||