aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/test
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-20 01:49:14 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-10-31 19:26:11 +0100
commit7732fedb5933bdc699cd1ef22a766397d5a701d2 (patch)
tree691b0461ee763c0060f901f306a7fd99d756fcce /language-web/src/test
parentfeat(web): batched xtext websocket prototype (diff)
downloadrefinery-7732fedb5933bdc699cd1ef22a766397d5a701d2.tar.gz
refinery-7732fedb5933bdc699cd1ef22a766397d5a701d2.tar.zst
refinery-7732fedb5933bdc699cd1ef22a766397d5a701d2.zip
feat(web): push precomputed service results
Diffstat (limited to 'language-web/src/test')
-rw-r--r--language-web/src/test/java/tools/refinery/language/web/xtext/servlet/AwaitTerminationExecutorServiceProvider.java26
-rw-r--r--language-web/src/test/java/tools/refinery/language/web/xtext/servlet/ProblemWebInjectorProvider.java2
-rw-r--r--language-web/src/test/java/tools/refinery/language/web/xtext/servlet/RestartableCachedThreadPool.java109
-rw-r--r--language-web/src/test/java/tools/refinery/language/web/xtext/servlet/TransactionExecutorTest.java148
4 files changed, 198 insertions, 87 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
index 08230335..25bcec37 100644
--- 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
@@ -3,7 +3,6 @@ package tools.refinery.language.web.xtext.servlet;
3import java.util.ArrayList; 3import java.util.ArrayList;
4import java.util.List; 4import java.util.List;
5import java.util.concurrent.ExecutorService; 5import java.util.concurrent.ExecutorService;
6import java.util.concurrent.TimeUnit;
7 6
8import org.eclipse.xtext.ide.ExecutorServiceProvider; 7import org.eclipse.xtext.ide.ExecutorServiceProvider;
9 8
@@ -11,24 +10,33 @@ import com.google.inject.Singleton;
11 10
12@Singleton 11@Singleton
13public class AwaitTerminationExecutorServiceProvider extends ExecutorServiceProvider { 12public class AwaitTerminationExecutorServiceProvider extends ExecutorServiceProvider {
14 private List<ExecutorService> servicesToShutDown = new ArrayList<>(); 13 private List<RestartableCachedThreadPool> servicesToShutDown = new ArrayList<>();
15 14
16 @Override 15 @Override
17 protected ExecutorService createInstance(String key) { 16 protected ExecutorService createInstance(String key) {
18 var instance = super.createInstance(key); 17 var instance = new RestartableCachedThreadPool();
19 servicesToShutDown.add(instance); 18 synchronized (servicesToShutDown) {
19 servicesToShutDown.add(instance);
20 }
20 return instance; 21 return instance;
21 } 22 }
22 23
24 public void waitForAllTasksToFinish() {
25 synchronized (servicesToShutDown) {
26 for (var executorService : servicesToShutDown) {
27 executorService.waitForAllTasksToFinish();
28 }
29 }
30 }
31
23 @Override 32 @Override
24 public void dispose() { 33 public void dispose() {
25 super.dispose(); 34 super.dispose();
26 for (var executorService : servicesToShutDown) { 35 synchronized (servicesToShutDown) {
27 try { 36 for (var executorService : servicesToShutDown) {
28 executorService.awaitTermination(1, TimeUnit.SECONDS); 37 executorService.waitForTermination();
29 } catch (InterruptedException e) {
30 // Continue normally.
31 } 38 }
39 servicesToShutDown.clear();
32 } 40 }
33 } 41 }
34} 42}
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
index 3493c9eb..a6d97c8b 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/xtext/servlet/ProblemWebInjectorProvider.java
@@ -26,7 +26,7 @@ public class ProblemWebInjectorProvider extends ProblemInjectorProvider {
26 26
27 protected ProblemWebModule createWebModule() { 27 protected ProblemWebModule createWebModule() {
28 // Await termination of the executor service to avoid race conditions between 28 // Await termination of the executor service to avoid race conditions between
29 // between the tasks in the service and the {@link 29 // the tasks in the service and the {@link
30 // org.eclipse.xtext.testing.extensions.InjectionExtension}. 30 // org.eclipse.xtext.testing.extensions.InjectionExtension}.
31 return new ProblemWebModule() { 31 return new ProblemWebModule() {
32 @SuppressWarnings("unused") 32 @SuppressWarnings("unused")
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
new file mode 100644
index 00000000..02ef38e2
--- /dev/null
+++ b/language-web/src/test/java/tools/refinery/language/web/xtext/servlet/RestartableCachedThreadPool.java
@@ -0,0 +1,109 @@
1package tools.refinery.language.web.xtext.servlet;
2
3import java.util.Collection;
4import java.util.List;
5import java.util.concurrent.Callable;
6import java.util.concurrent.ExecutionException;
7import java.util.concurrent.ExecutorService;
8import java.util.concurrent.Executors;
9import java.util.concurrent.Future;
10import java.util.concurrent.TimeUnit;
11import java.util.concurrent.TimeoutException;
12
13import org.slf4j.Logger;
14import org.slf4j.LoggerFactory;
15
16public 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/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 6ad82d7f..7f7c3e43 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
@@ -4,23 +4,19 @@ import static org.hamcrest.MatcherAssert.assertThat;
4import static org.hamcrest.Matchers.equalTo; 4import static org.hamcrest.Matchers.equalTo;
5import static org.hamcrest.Matchers.hasProperty; 5import static org.hamcrest.Matchers.hasProperty;
6import static org.hamcrest.Matchers.instanceOf; 6import static org.hamcrest.Matchers.instanceOf;
7import static org.junit.jupiter.api.Assertions.fail;
8import static org.mockito.Mockito.mock; 7import static org.mockito.Mockito.mock;
9import static org.mockito.Mockito.times; 8import static org.mockito.Mockito.times;
10import static org.mockito.Mockito.verify; 9import static org.mockito.Mockito.verify;
11 10
12import java.io.IOException;
13import java.util.List; 11import java.util.List;
14import java.util.Map; 12import java.util.Map;
15import java.util.UUID;
16 13
17import org.eclipse.xtext.resource.IResourceServiceProvider; 14import org.eclipse.xtext.resource.IResourceServiceProvider;
18import org.eclipse.xtext.testing.InjectWith; 15import org.eclipse.xtext.testing.InjectWith;
19import org.eclipse.xtext.testing.extensions.InjectionExtension; 16import org.eclipse.xtext.testing.extensions.InjectionExtension;
20import org.eclipse.xtext.web.server.ServiceConflictResult;
21import org.eclipse.xtext.web.server.model.DocumentStateResult; 17import org.eclipse.xtext.web.server.model.DocumentStateResult;
18import org.eclipse.xtext.web.server.syntaxcoloring.HighlightingResult;
22import org.eclipse.xtext.web.server.validation.ValidationResult; 19import org.eclipse.xtext.web.server.validation.ValidationResult;
23import org.hamcrest.Matcher;
24import org.junit.jupiter.api.BeforeEach; 20import org.junit.jupiter.api.BeforeEach;
25import org.junit.jupiter.api.Test; 21import org.junit.jupiter.api.Test;
26import org.junit.jupiter.api.extension.ExtendWith; 22import org.junit.jupiter.api.extension.ExtendWith;
@@ -29,123 +25,121 @@ import org.mockito.junit.jupiter.MockitoExtension;
29 25
30import com.google.inject.Inject; 26import com.google.inject.Inject;
31 27
28import tools.refinery.language.web.xtext.server.ResponseHandler;
29import tools.refinery.language.web.xtext.server.ResponseHandlerException;
30import tools.refinery.language.web.xtext.server.TransactionExecutor;
31import tools.refinery.language.web.xtext.server.message.XtextWebOkResponse;
32import tools.refinery.language.web.xtext.server.message.XtextWebRequest;
33import tools.refinery.language.web.xtext.server.message.XtextWebResponse;
34
32@ExtendWith(MockitoExtension.class) 35@ExtendWith(MockitoExtension.class)
33@ExtendWith(InjectionExtension.class) 36@ExtendWith(InjectionExtension.class)
34@InjectWith(ProblemWebInjectorProvider.class) 37@InjectWith(ProblemWebInjectorProvider.class)
35class TransactionExecutorTest { 38class TransactionExecutorTest {
36 private static final String RESOURCE_NAME = "test.problem"; 39 private static final String RESOURCE_NAME = "test.problem";
37 40
38 private static final String INVALID_STATE_ID = "<invalid_state>";
39
40 private static final String TEST_PROBLEM = """ 41 private static final String TEST_PROBLEM = """
41 class Person { 42 class Person {
42 Person friend[0..*] opposite friend 43 Person[0..*] friend opposite friend
43 } 44 }
44 45
45 friend(a, b). 46 friend(a, b).
46 """; 47 """;
47 48
48 private static final Map<String, String> UPDATE_FULL_TEXT_PARAMS = Map.of("serviceType", "update", "fullText", 49 private static final Map<String, String> UPDATE_FULL_TEXT_PARAMS = Map.of("resource", RESOURCE_NAME, "serviceType",
49 TEST_PROBLEM); 50 "update", "fullText", TEST_PROBLEM);
50
51 private static final Map<String, String> VALIDATE_PARAMS = Map.of("serviceType", "validate");
52 51
53 @Inject 52 @Inject
54 private IResourceServiceProvider.Registry resourceServiceProviderRegistry; 53 private IResourceServiceProvider.Registry resourceServiceProviderRegistry;
55 54
55 @Inject
56 private AwaitTerminationExecutorServiceProvider executorServices;
57
56 private TransactionExecutor transactionExecutor; 58 private TransactionExecutor transactionExecutor;
57 59
58 @BeforeEach 60 @BeforeEach
59 void beforeEach() { 61 void beforeEach() {
60 transactionExecutor = new TransactionExecutor(new SimpleSession(), resourceServiceProviderRegistry); 62 transactionExecutor = new TransactionExecutor(new SimpleSession(), resourceServiceProviderRegistry);
61 } 63 }
62
63 @Test
64 void emptyBatchTest() {
65 performBatchRequest(null);
66 }
67 64
68 @Test 65 @Test
69 void fullTextUpdateTest() { 66 void updateFullTextTest() throws ResponseHandlerException {
70 var response = performSingleRequest(null, UPDATE_FULL_TEXT_PARAMS); 67 var captor = newCaptor();
71 assertThat(response, hasResponseData(instanceOf(DocumentStateResult.class))); 68 var stateId = updateFullText(captor);
69 assertThatPrecomputedMessagesAreReceived(stateId, captor.getAllValues());
72 } 70 }
73 71
74 @Test 72 @Test
75 void validationAfterFullTextUpdateInSameBatchTest() { 73 void updateDeltaTextHighlightAndValidationChange() throws ResponseHandlerException {
76 var response = performBatchRequest(null, UPDATE_FULL_TEXT_PARAMS, VALIDATE_PARAMS).get(1); 74 var stateId = updateFullText();
77 assertThat(response, hasResponseData(instanceOf(ValidationResult.class))); 75 var responseHandler = sendRequestAndWaitForAllResponses(
76 new XtextWebRequest("bar", Map.of("resource", RESOURCE_NAME, "serviceType", "update", "requiredStateId",
77 stateId, "deltaText", "<invalid text>\n", "deltaOffset", "0", "deltaReplaceLength", "0")));
78
79 var captor = newCaptor();
80 verify(responseHandler, times(3)).onResponse(captor.capture());
81 var newStateId = getStateId("bar", captor.getAllValues().get(0));
82 assertThatPrecomputedMessagesAreReceived(newStateId, captor.getAllValues());
78 } 83 }
79 84
80 @Test 85 @Test
81 void validationAfterFullTextUpdateInDifferentBatchTest() { 86 void updateDeltaTextHighlightChangeOnly() throws ResponseHandlerException {
82 var stateId = updateFullText(); 87 var stateId = updateFullText();
83 var validateResponse = performSingleRequest(stateId, VALIDATE_PARAMS); 88 var responseHandler = sendRequestAndWaitForAllResponses(
84 assertThat(validateResponse, hasResponseData(instanceOf(ValidationResult.class))); 89 new XtextWebRequest("bar", Map.of("resource", RESOURCE_NAME, "serviceType", "update", "requiredStateId",
90 stateId, "deltaText", "class Vehicle.\n", "deltaOffset", "0", "deltaReplaceLength", "0")));
91
92 var captor = newCaptor();
93 verify(responseHandler, times(2)).onResponse(captor.capture());
94 var newStateId = getStateId("bar", captor.getAllValues().get(0));
95 assertHighlightingResponse(newStateId, captor.getAllValues().get(1));
85 } 96 }
86 97
87 @Test 98 private ArgumentCaptor<XtextWebResponse> newCaptor() {
88 void conflictTest() { 99 return ArgumentCaptor.forClass(XtextWebResponse.class);
89 updateFullText();
90 var response = performSingleRequest(INVALID_STATE_ID, VALIDATE_PARAMS);
91 assertThat(response, hasResponseData(instanceOf(ServiceConflictResult.class)));
92 } 100 }
93 101
94 @Test 102 private String updateFullText() throws ResponseHandlerException {
95 void transactionCancelledDueToConflictTest() { 103 return updateFullText(newCaptor());
96 updateFullText(); 104 }
97 var response = performBatchRequest(INVALID_STATE_ID, VALIDATE_PARAMS, VALIDATE_PARAMS).get(1); 105
98 assertThat(response, hasErrorKind(equalTo(XtextWebSocketErrorKind.TRANSACTION_CANCELLED))); 106 private String updateFullText(ArgumentCaptor<XtextWebResponse> captor) throws ResponseHandlerException {
107 var responseHandler = sendRequestAndWaitForAllResponses(new XtextWebRequest("foo", UPDATE_FULL_TEXT_PARAMS));
108
109 verify(responseHandler, times(3)).onResponse(captor.capture());
110 return getStateId("foo", captor.getAllValues().get(0));
99 } 111 }
100 112
101 @SafeVarargs 113 private ResponseHandler sendRequestAndWaitForAllResponses(XtextWebRequest request) throws ResponseHandlerException {
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); 114 var responseHandler = mock(ResponseHandler.class);
107 try { 115 transactionExecutor.setResponseHandler(responseHandler);
108 transactionExecutor.handleRequest(request, responseHandler); 116 transactionExecutor.handleRequest(request);
109 } catch (IOException e) { 117 executorServices.waitForAllTasksToFinish();
110 fail("Unexpected IOException", e); 118 return responseHandler;
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 } 119 }
128 120
129 private XtextWebSocketResponse performSingleRequest(String requiredStateId, Map<String, String> param) { 121 private String getStateId(String requestId, XtextWebResponse okResponse) {
130 return performBatchRequest(requiredStateId, param).get(0); 122 assertThat(okResponse, hasProperty("id", equalTo(requestId)));
123 assertThat(okResponse, hasProperty("responseData", instanceOf(DocumentStateResult.class)));
124 return ((DocumentStateResult) ((XtextWebOkResponse) okResponse).getResponseData()).getStateId();
131 } 125 }
132 126
133 private String updateFullText() { 127 private void assertThatPrecomputedMessagesAreReceived(String stateId, List<XtextWebResponse> responses) {
134 var updateResponse = (XtextWebSocketOkResponse) performSingleRequest(null, UPDATE_FULL_TEXT_PARAMS); 128 assertHighlightingResponse(stateId, responses.get(1));
135 var documentStateResult = (DocumentStateResult) updateResponse.getResponseData(); 129 assertValidationResponse(stateId, responses.get(2));
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 } 130 }
142 131
143 private static Matcher<XtextWebSocketResponse> hasResponseData(Matcher<?> responseDataMatcher) { 132 private void assertHighlightingResponse(String stateId, XtextWebResponse highlightingResponse) {
144 return hasProperty("responseData", responseDataMatcher); 133 assertThat(highlightingResponse, hasProperty("resourceId", equalTo(RESOURCE_NAME)));
134 assertThat(highlightingResponse, hasProperty("stateId", equalTo(stateId)));
135 assertThat(highlightingResponse, hasProperty("service", equalTo("highlighting")));
136 assertThat(highlightingResponse, hasProperty("pushData", instanceOf(HighlightingResult.class)));
145 } 137 }
146 138
147 private static Matcher<XtextWebSocketResponse> hasErrorKind( 139 private void assertValidationResponse(String stateId, XtextWebResponse validationResponse) {
148 Matcher<? extends XtextWebSocketErrorKind> errorKindMatcher) { 140 assertThat(validationResponse, hasProperty("resourceId", equalTo(RESOURCE_NAME)));
149 return hasProperty("errorKind", errorKindMatcher); 141 assertThat(validationResponse, hasProperty("stateId", equalTo(stateId)));
142 assertThat(validationResponse, hasProperty("service", equalTo("validation")));
143 assertThat(validationResponse, hasProperty("pushData", instanceOf(ValidationResult.class)));
150 } 144 }
151} 145}