diff options
author | Kristóf Marussy <kristof@marussy.com> | 2023-08-20 19:41:32 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2023-08-20 20:29:02 +0200 |
commit | a3f1e6872f4f768d14899a1e70bbdc14f32e478d (patch) | |
tree | b2daf0c81724f31ee190f5d63eb42988086dabf2 /subprojects/language-web | |
parent | fix: nullary model initialization (diff) | |
download | refinery-a3f1e6872f4f768d14899a1e70bbdc14f32e478d.tar.gz refinery-a3f1e6872f4f768d14899a1e70bbdc14f32e478d.tar.zst refinery-a3f1e6872f4f768d14899a1e70bbdc14f32e478d.zip |
feat: improve semantics error reporting
Also makes model seeds cancellable to reduce server load during semantic
analysis.
Diffstat (limited to 'subprojects/language-web')
9 files changed, 196 insertions, 28 deletions
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java new file mode 100644 index 00000000..aa14f39d --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/CancellableSeed.java | |||
@@ -0,0 +1,99 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import tools.refinery.store.map.AnyVersionedMap; | ||
9 | import tools.refinery.store.map.Cursor; | ||
10 | import tools.refinery.store.reasoning.representation.PartialSymbol; | ||
11 | import tools.refinery.store.reasoning.seed.ModelSeed; | ||
12 | import tools.refinery.store.reasoning.seed.Seed; | ||
13 | import tools.refinery.store.tuple.Tuple; | ||
14 | import tools.refinery.viatra.runtime.CancellationToken; | ||
15 | |||
16 | import java.util.Set; | ||
17 | |||
18 | class CancellableSeed<T> implements Seed<T> { | ||
19 | private final CancellationToken cancellationToken; | ||
20 | private final Seed<T> seed; | ||
21 | |||
22 | private CancellableSeed(CancellationToken cancellationToken, Seed<T> seed) { | ||
23 | this.cancellationToken = cancellationToken; | ||
24 | this.seed = seed; | ||
25 | } | ||
26 | |||
27 | @Override | ||
28 | public int arity() { | ||
29 | return seed.arity(); | ||
30 | } | ||
31 | |||
32 | @Override | ||
33 | public Class<T> valueType() { | ||
34 | return seed.valueType(); | ||
35 | } | ||
36 | |||
37 | @Override | ||
38 | public T reducedValue() { | ||
39 | return seed.reducedValue(); | ||
40 | } | ||
41 | |||
42 | @Override | ||
43 | public T get(Tuple key) { | ||
44 | return seed.get(key); | ||
45 | } | ||
46 | |||
47 | @Override | ||
48 | public Cursor<Tuple, T> getCursor(T defaultValue, int nodeCount) { | ||
49 | return new CancellableCursor<>(cancellationToken, seed.getCursor(defaultValue, nodeCount)); | ||
50 | } | ||
51 | |||
52 | public static ModelSeed wrap(CancellationToken cancellationToken, ModelSeed modelSeed) { | ||
53 | var builder = ModelSeed.builder(modelSeed.getNodeCount()); | ||
54 | for (var partialSymbol : modelSeed.getSeededSymbols()) { | ||
55 | wrap(cancellationToken, (PartialSymbol<?, ?>) partialSymbol, modelSeed, builder); | ||
56 | } | ||
57 | return builder.build(); | ||
58 | } | ||
59 | |||
60 | private static <A, C> void wrap(CancellationToken cancellationToken, PartialSymbol<A, C> partialSymbol, | ||
61 | ModelSeed originalModelSeed, ModelSeed.Builder builder) { | ||
62 | var originalSeed = originalModelSeed.getSeed(partialSymbol); | ||
63 | builder.seed(partialSymbol, new CancellableSeed<>(cancellationToken, originalSeed)); | ||
64 | } | ||
65 | |||
66 | private record CancellableCursor<T>(CancellationToken cancellationToken, Cursor<Tuple, T> cursor) | ||
67 | implements Cursor<Tuple, T> { | ||
68 | @Override | ||
69 | public Tuple getKey() { | ||
70 | return cursor.getKey(); | ||
71 | } | ||
72 | |||
73 | @Override | ||
74 | public T getValue() { | ||
75 | return cursor.getValue(); | ||
76 | } | ||
77 | |||
78 | @Override | ||
79 | public boolean isTerminated() { | ||
80 | return cursor.isTerminated(); | ||
81 | } | ||
82 | |||
83 | @Override | ||
84 | public boolean move() { | ||
85 | cancellationToken.checkCancelled(); | ||
86 | return cursor.move(); | ||
87 | } | ||
88 | |||
89 | @Override | ||
90 | public boolean isDirty() { | ||
91 | return cursor.isDirty(); | ||
92 | } | ||
93 | |||
94 | @Override | ||
95 | public Set<AnyVersionedMap> getDependingMaps() { | ||
96 | return cursor.getDependingMaps(); | ||
97 | } | ||
98 | } | ||
99 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsErrorResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsInternalErrorResult.java index ce34ef6c..ff592e93 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsErrorResult.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsInternalErrorResult.java | |||
@@ -5,5 +5,5 @@ | |||
5 | */ | 5 | */ |
6 | package tools.refinery.language.web.semantics; | 6 | package tools.refinery.language.web.semantics; |
7 | 7 | ||
8 | public record SemanticsErrorResult(String error) implements SemanticsResult { | 8 | public record SemanticsInternalErrorResult(String error) implements SemanticsResult { |
9 | } | 9 | } |
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java new file mode 100644 index 00000000..644bd179 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsIssuesResult.java | |||
@@ -0,0 +1,13 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import org.eclipse.xtext.web.server.validation.ValidationResult; | ||
9 | |||
10 | import java.util.List; | ||
11 | |||
12 | public record SemanticsIssuesResult(List<ValidationResult.Issue> issues) implements SemanticsResult { | ||
13 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java index 92639578..a2e19a2f 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsResult.java | |||
@@ -7,5 +7,6 @@ package tools.refinery.language.web.semantics; | |||
7 | 7 | ||
8 | import org.eclipse.xtext.web.server.IServiceResult; | 8 | import org.eclipse.xtext.web.server.IServiceResult; |
9 | 9 | ||
10 | public sealed interface SemanticsResult extends IServiceResult permits SemanticsSuccessResult, SemanticsErrorResult { | 10 | public sealed interface SemanticsResult extends IServiceResult permits SemanticsSuccessResult, |
11 | SemanticsInternalErrorResult, SemanticsIssuesResult { | ||
11 | } | 12 | } |
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java index 39191162..56b2cbc1 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java | |||
@@ -5,9 +5,11 @@ | |||
5 | */ | 5 | */ |
6 | package tools.refinery.language.web.semantics; | 6 | package tools.refinery.language.web.semantics; |
7 | 7 | ||
8 | import com.google.gson.JsonObject; | ||
8 | import com.google.inject.Inject; | 9 | import com.google.inject.Inject; |
9 | import com.google.inject.Provider; | 10 | import com.google.inject.Provider; |
10 | import com.google.inject.Singleton; | 11 | import com.google.inject.Singleton; |
12 | import org.eclipse.xtext.ide.ExecutorServiceProvider; | ||
11 | import org.eclipse.xtext.service.OperationCanceledManager; | 13 | import org.eclipse.xtext.service.OperationCanceledManager; |
12 | import org.eclipse.xtext.util.CancelIndicator; | 14 | import org.eclipse.xtext.util.CancelIndicator; |
13 | import org.eclipse.xtext.web.server.model.AbstractCachedService; | 15 | import org.eclipse.xtext.web.server.model.AbstractCachedService; |
@@ -19,6 +21,7 @@ import org.slf4j.LoggerFactory; | |||
19 | import tools.refinery.language.model.problem.Problem; | 21 | import tools.refinery.language.model.problem.Problem; |
20 | import tools.refinery.language.web.xtext.server.push.PushWebDocument; | 22 | import tools.refinery.language.web.xtext.server.push.PushWebDocument; |
21 | 23 | ||
24 | import java.util.List; | ||
22 | import java.util.concurrent.*; | 25 | import java.util.concurrent.*; |
23 | 26 | ||
24 | @Singleton | 27 | @Singleton |
@@ -34,7 +37,12 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> { | |||
34 | @Inject | 37 | @Inject |
35 | private ValidationService validationService; | 38 | private ValidationService validationService; |
36 | 39 | ||
37 | private final ExecutorService executorService = Executors.newCachedThreadPool(); | 40 | private ExecutorService executorService; |
41 | |||
42 | @Inject | ||
43 | public void setExecutorServiceProvider(ExecutorServiceProvider provider) { | ||
44 | executorService = provider.get(this.getClass().getName()); | ||
45 | } | ||
38 | 46 | ||
39 | @Override | 47 | @Override |
40 | public SemanticsResult compute(IXtextWebDocument doc, CancelIndicator cancelIndicator) { | 48 | public SemanticsResult compute(IXtextWebDocument doc, CancelIndicator cancelIndicator) { |
@@ -42,12 +50,15 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> { | |||
42 | if (LOG.isTraceEnabled()) { | 50 | if (LOG.isTraceEnabled()) { |
43 | start = System.currentTimeMillis(); | 51 | start = System.currentTimeMillis(); |
44 | } | 52 | } |
45 | var problem = getProblem(doc, cancelIndicator); | 53 | if (hasError(doc, cancelIndicator)) { |
46 | if (problem == null) { | ||
47 | return null; | 54 | return null; |
48 | } | 55 | } |
56 | var problem = getProblem(doc); | ||
57 | if (problem == null) { | ||
58 | return new SemanticsSuccessResult(List.of(), new JsonObject()); | ||
59 | } | ||
49 | var worker = workerProvider.get(); | 60 | var worker = workerProvider.get(); |
50 | worker.setProblem(problem,cancelIndicator); | 61 | worker.setProblem(problem, cancelIndicator); |
51 | var future = executorService.submit(worker); | 62 | var future = executorService.submit(worker); |
52 | SemanticsResult result = null; | 63 | SemanticsResult result = null; |
53 | try { | 64 | try { |
@@ -58,11 +69,19 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> { | |||
58 | Thread.currentThread().interrupt(); | 69 | Thread.currentThread().interrupt(); |
59 | } catch (ExecutionException e) { | 70 | } catch (ExecutionException e) { |
60 | operationCanceledManager.propagateAsErrorIfCancelException(e.getCause()); | 71 | operationCanceledManager.propagateAsErrorIfCancelException(e.getCause()); |
61 | throw new IllegalStateException(e); | 72 | LOG.debug("Error while computing semantics", e); |
73 | if (e.getCause() instanceof Error error) { | ||
74 | throw error; | ||
75 | } | ||
76 | String message = e.getMessage(); | ||
77 | if (message == null) { | ||
78 | message = "Partial interpretation error"; | ||
79 | } | ||
80 | return new SemanticsInternalErrorResult(message); | ||
62 | } catch (TimeoutException e) { | 81 | } catch (TimeoutException e) { |
63 | future.cancel(true); | 82 | future.cancel(true); |
64 | LOG.trace("Semantics service timeout", e); | 83 | LOG.trace("Semantics service timeout", e); |
65 | return new SemanticsErrorResult("Partial interpretation timed out"); | 84 | return new SemanticsInternalErrorResult("Partial interpretation timed out"); |
66 | } | 85 | } |
67 | if (LOG.isTraceEnabled()) { | 86 | if (LOG.isTraceEnabled()) { |
68 | long end = System.currentTimeMillis(); | 87 | long end = System.currentTimeMillis(); |
@@ -72,17 +91,17 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> { | |||
72 | return result; | 91 | return result; |
73 | } | 92 | } |
74 | 93 | ||
75 | @Nullable | 94 | private boolean hasError(IXtextWebDocument doc, CancelIndicator cancelIndicator) { |
76 | private Problem getProblem(IXtextWebDocument doc, CancelIndicator cancelIndicator) { | ||
77 | if (!(doc instanceof PushWebDocument pushDoc)) { | 95 | if (!(doc instanceof PushWebDocument pushDoc)) { |
78 | throw new IllegalArgumentException("Unexpected IXtextWebDocument: " + doc); | 96 | throw new IllegalArgumentException("Unexpected IXtextWebDocument: " + doc); |
79 | } | 97 | } |
80 | var validationResult = pushDoc.getCachedServiceResult(validationService, cancelIndicator, true); | 98 | var validationResult = pushDoc.getCachedServiceResult(validationService, cancelIndicator, true); |
81 | boolean hasError = validationResult.getIssues().stream() | 99 | return validationResult.getIssues().stream() |
82 | .anyMatch(issue -> "error".equals(issue.getSeverity())); | 100 | .anyMatch(issue -> "error".equals(issue.getSeverity())); |
83 | if (hasError) { | 101 | } |
84 | return null; | 102 | |
85 | } | 103 | @Nullable |
104 | private Problem getProblem(IXtextWebDocument doc) { | ||
86 | var contents = doc.getResource().getContents(); | 105 | var contents = doc.getResource().getContents(); |
87 | if (contents.isEmpty()) { | 106 | if (contents.isEmpty()) { |
88 | return null; | 107 | return null; |
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java index 25589260..43d0238c 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java | |||
@@ -8,13 +8,19 @@ package tools.refinery.language.web.semantics; | |||
8 | import com.google.gson.JsonArray; | 8 | import com.google.gson.JsonArray; |
9 | import com.google.gson.JsonObject; | 9 | import com.google.gson.JsonObject; |
10 | import com.google.inject.Inject; | 10 | import com.google.inject.Inject; |
11 | import org.eclipse.emf.common.util.Diagnostic; | ||
12 | import org.eclipse.emf.ecore.EObject; | ||
11 | import org.eclipse.xtext.service.OperationCanceledManager; | 13 | import org.eclipse.xtext.service.OperationCanceledManager; |
12 | import org.eclipse.xtext.util.CancelIndicator; | 14 | import org.eclipse.xtext.util.CancelIndicator; |
13 | import org.slf4j.Logger; | 15 | import org.eclipse.xtext.validation.CheckType; |
14 | import org.slf4j.LoggerFactory; | 16 | import org.eclipse.xtext.validation.FeatureBasedDiagnostic; |
17 | import org.eclipse.xtext.validation.IDiagnosticConverter; | ||
18 | import org.eclipse.xtext.validation.Issue; | ||
19 | import org.eclipse.xtext.web.server.validation.ValidationResult; | ||
15 | import tools.refinery.language.model.problem.Problem; | 20 | import tools.refinery.language.model.problem.Problem; |
16 | import tools.refinery.language.semantics.model.ModelInitializer; | 21 | import tools.refinery.language.semantics.model.ModelInitializer; |
17 | import tools.refinery.language.semantics.model.SemanticsUtils; | 22 | import tools.refinery.language.semantics.model.SemanticsUtils; |
23 | import tools.refinery.language.semantics.model.TracedException; | ||
18 | import tools.refinery.store.model.Model; | 24 | import tools.refinery.store.model.Model; |
19 | import tools.refinery.store.model.ModelStore; | 25 | import tools.refinery.store.model.ModelStore; |
20 | import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; | 26 | import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; |
@@ -22,17 +28,19 @@ import tools.refinery.store.reasoning.ReasoningAdapter; | |||
22 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; | 28 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; |
23 | import tools.refinery.store.reasoning.literal.Concreteness; | 29 | import tools.refinery.store.reasoning.literal.Concreteness; |
24 | import tools.refinery.store.reasoning.representation.PartialRelation; | 30 | import tools.refinery.store.reasoning.representation.PartialRelation; |
31 | import tools.refinery.store.reasoning.translator.TranslationException; | ||
25 | import tools.refinery.store.representation.TruthValue; | 32 | import tools.refinery.store.representation.TruthValue; |
26 | import tools.refinery.store.tuple.Tuple; | 33 | import tools.refinery.store.tuple.Tuple; |
27 | import tools.refinery.viatra.runtime.CancellationToken; | 34 | import tools.refinery.viatra.runtime.CancellationToken; |
28 | 35 | ||
36 | import java.util.ArrayList; | ||
29 | import java.util.Arrays; | 37 | import java.util.Arrays; |
30 | import java.util.List; | 38 | import java.util.List; |
31 | import java.util.TreeMap; | 39 | import java.util.TreeMap; |
32 | import java.util.concurrent.Callable; | 40 | import java.util.concurrent.Callable; |
33 | 41 | ||
34 | class SemanticsWorker implements Callable<SemanticsResult> { | 42 | class SemanticsWorker implements Callable<SemanticsResult> { |
35 | private static final Logger LOG = LoggerFactory.getLogger(SemanticsWorker.class); | 43 | private static final String DIAGNOSTIC_ID = "tools.refinery.language.semantics.SemanticError"; |
36 | 44 | ||
37 | @Inject | 45 | @Inject |
38 | private SemanticsUtils semanticsUtils; | 46 | private SemanticsUtils semanticsUtils; |
@@ -41,6 +49,9 @@ class SemanticsWorker implements Callable<SemanticsResult> { | |||
41 | private OperationCanceledManager operationCanceledManager; | 49 | private OperationCanceledManager operationCanceledManager; |
42 | 50 | ||
43 | @Inject | 51 | @Inject |
52 | private IDiagnosticConverter diagnosticConverter; | ||
53 | |||
54 | @Inject | ||
44 | private ModelInitializer initializer; | 55 | private ModelInitializer initializer; |
45 | 56 | ||
46 | private Problem problem; | 57 | private Problem problem; |
@@ -71,15 +82,17 @@ class SemanticsWorker implements Callable<SemanticsResult> { | |||
71 | cancellationToken.checkCancelled(); | 82 | cancellationToken.checkCancelled(); |
72 | var store = builder.build(); | 83 | var store = builder.build(); |
73 | cancellationToken.checkCancelled(); | 84 | cancellationToken.checkCancelled(); |
74 | var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); | 85 | var cancellableModelSeed = CancellableSeed.wrap(cancellationToken, modelSeed); |
86 | var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(cancellableModelSeed); | ||
75 | cancellationToken.checkCancelled(); | 87 | cancellationToken.checkCancelled(); |
76 | var partialInterpretation = getPartialInterpretation(initializer, model); | 88 | var partialInterpretation = getPartialInterpretation(initializer, model); |
77 | 89 | ||
78 | return new SemanticsSuccessResult(nodeTrace, partialInterpretation); | 90 | return new SemanticsSuccessResult(nodeTrace, partialInterpretation); |
79 | } catch (RuntimeException e) { | 91 | } catch (TracedException e) { |
80 | LOG.debug("Error while computing semantics", e); | 92 | return getTracedErrorResult(e.getSourceElement(), e.getMessage()); |
81 | var message = e.getMessage(); | 93 | } catch (TranslationException e) { |
82 | return new SemanticsErrorResult(message == null ? "Partial interpretation error" : e.getMessage()); | 94 | var sourceElement = initializer.getInverseTrace(e.getPartialSymbol()); |
95 | return getTracedErrorResult(sourceElement, e.getMessage()); | ||
83 | } | 96 | } |
84 | } | 97 | } |
85 | 98 | ||
@@ -130,4 +143,19 @@ class SemanticsWorker implements Callable<SemanticsResult> { | |||
130 | json.add(value.toString()); | 143 | json.add(value.toString()); |
131 | return json; | 144 | return json; |
132 | } | 145 | } |
146 | |||
147 | private SemanticsResult getTracedErrorResult(EObject sourceElement, String message) { | ||
148 | if (sourceElement == null || !problem.eResource().equals(sourceElement.eResource())) { | ||
149 | return new SemanticsInternalErrorResult(message); | ||
150 | } | ||
151 | var diagnostic = new FeatureBasedDiagnostic(Diagnostic.ERROR, message, sourceElement, null, 0, | ||
152 | CheckType.EXPENSIVE, DIAGNOSTIC_ID); | ||
153 | var xtextIssues = new ArrayList<Issue>(); | ||
154 | diagnosticConverter.convertValidatorDiagnostic(diagnostic, xtextIssues::add); | ||
155 | var issues = xtextIssues.stream() | ||
156 | .map(issue -> new ValidationResult.Issue(issue.getMessage(), "error", issue.getLineNumber(), | ||
157 | issue.getColumn(), issue.getOffset(), issue.getLength())) | ||
158 | .toList(); | ||
159 | return new SemanticsIssuesResult(issues); | ||
160 | } | ||
133 | } | 161 | } |
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java index 1542c694..2d43fb26 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java | |||
@@ -53,10 +53,9 @@ public class PushWebDocument extends XtextWebDocument { | |||
53 | public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName, | 53 | public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName, |
54 | CancelIndicator cancelIndicator, boolean logCacheMiss) { | 54 | CancelIndicator cancelIndicator, boolean logCacheMiss) { |
55 | var serviceClass = service.getClass(); | 55 | var serviceClass = service.getClass(); |
56 | var previousResult = precomputedServices.get(serviceClass); | ||
57 | var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); | 56 | var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); |
58 | precomputedServices.put(serviceClass, result); | 57 | precomputedServices.put(serviceClass, result); |
59 | if (result != null && !result.equals(previousResult)) { | 58 | if (result != null) { |
60 | notifyPrecomputationListeners(serviceName, result); | 59 | notifyPrecomputationListeners(serviceName, result); |
61 | } | 60 | } |
62 | } | 61 | } |
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 index 99ca5420..889a55cb 100644 --- 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 | |||
@@ -93,7 +93,7 @@ class ProblemWebSocketServletIntegrationTest { | |||
93 | clientSocket.waitForTestResult(); | 93 | clientSocket.waitForTestResult(); |
94 | assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); | 94 | assertThat(clientSocket.getCloseStatusCode(), equalTo(StatusCode.NORMAL)); |
95 | var responses = clientSocket.getResponses(); | 95 | var responses = clientSocket.getResponses(); |
96 | assertThat(responses, hasSize(7)); | 96 | assertThat(responses, hasSize(8)); |
97 | assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}")); | 97 | assertThat(responses.get(0), equalTo("{\"id\":\"foo\",\"response\":{\"stateId\":\"-80000000\"}}")); |
98 | assertThat(responses.get(1), startsWith( | 98 | assertThat(responses.get(1), startsWith( |
99 | "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\"," + | 99 | "{\"resource\":\"test.problem\",\"stateId\":\"-80000000\",\"service\":\"highlight\"," + |
@@ -108,7 +108,10 @@ class ProblemWebSocketServletIntegrationTest { | |||
108 | assertThat(responses.get(5), startsWith( | 108 | assertThat(responses.get(5), startsWith( |
109 | "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\"," + | 109 | "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"highlight\"," + |
110 | "\"push\":{\"regions\":[")); | 110 | "\"push\":{\"regions\":[")); |
111 | assertThat(responses.get(6), startsWith( | 111 | assertThat(responses.get(6), equalTo( |
112 | "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"validate\"," + | ||
113 | "\"push\":{\"issues\":[]}}")); | ||
114 | assertThat(responses.get(7), startsWith( | ||
112 | "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"semantics\"," + | 115 | "{\"resource\":\"test.problem\",\"stateId\":\"-7fffffff\",\"service\":\"semantics\"," + |
113 | "\"push\":{")); | 116 | "\"push\":{")); |
114 | } | 117 | } |
@@ -130,7 +133,7 @@ class ProblemWebSocketServletIntegrationTest { | |||
130 | "\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}", | 133 | "\"deltaOffset\":\"0\",\"deltaReplaceLength\":\"0\"}}", |
131 | Callback.NOOP | 134 | Callback.NOOP |
132 | ); | 135 | ); |
133 | case 7 -> session.close(); | 136 | case 8 -> session.close(); |
134 | } | 137 | } |
135 | } | 138 | } |
136 | } | 139 | } |
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 index b7142506..22ce1b47 100644 --- 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 | |||
@@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; | |||
18 | import org.junit.jupiter.api.extension.ExtendWith; | 18 | import org.junit.jupiter.api.extension.ExtendWith; |
19 | import org.mockito.ArgumentCaptor; | 19 | import org.mockito.ArgumentCaptor; |
20 | import org.mockito.junit.jupiter.MockitoExtension; | 20 | import org.mockito.junit.jupiter.MockitoExtension; |
21 | import tools.refinery.language.web.semantics.SemanticsService; | ||
21 | import tools.refinery.language.web.tests.AwaitTerminationExecutorServiceProvider; | 22 | import tools.refinery.language.web.tests.AwaitTerminationExecutorServiceProvider; |
22 | import tools.refinery.language.web.tests.ProblemWebInjectorProvider; | 23 | import tools.refinery.language.web.tests.ProblemWebInjectorProvider; |
23 | import tools.refinery.language.web.xtext.server.ResponseHandler; | 24 | import tools.refinery.language.web.xtext.server.ResponseHandler; |
@@ -59,11 +60,16 @@ class TransactionExecutorTest { | |||
59 | @Inject | 60 | @Inject |
60 | private AwaitTerminationExecutorServiceProvider executorServices; | 61 | private AwaitTerminationExecutorServiceProvider executorServices; |
61 | 62 | ||
63 | @Inject | ||
64 | private SemanticsService semanticsService; | ||
65 | |||
62 | private TransactionExecutor transactionExecutor; | 66 | private TransactionExecutor transactionExecutor; |
63 | 67 | ||
64 | @BeforeEach | 68 | @BeforeEach |
65 | void beforeEach() { | 69 | void beforeEach() { |
66 | transactionExecutor = new TransactionExecutor(new SimpleSession(), resourceServiceProviderRegistry); | 70 | transactionExecutor = new TransactionExecutor(new SimpleSession(), resourceServiceProviderRegistry); |
71 | // Manually re-create the semantics analysis thread pool if it was disposed by the previous test. | ||
72 | semanticsService.setExecutorServiceProvider(executorServices); | ||
67 | } | 73 | } |
68 | 74 | ||
69 | @Test | 75 | @Test |
@@ -95,7 +101,7 @@ class TransactionExecutorTest { | |||
95 | "0"))); | 101 | "0"))); |
96 | 102 | ||
97 | var captor = newCaptor(); | 103 | var captor = newCaptor(); |
98 | verify(responseHandler, times(3)).onResponse(captor.capture()); | 104 | verify(responseHandler, times(4)).onResponse(captor.capture()); |
99 | var newStateId = getStateId("bar", captor.getAllValues().get(0)); | 105 | var newStateId = getStateId("bar", captor.getAllValues().get(0)); |
100 | assertHighlightingResponse(newStateId, captor.getAllValues().get(1)); | 106 | assertHighlightingResponse(newStateId, captor.getAllValues().get(1)); |
101 | } | 107 | } |